HN 日本語サマリー

← 一覧へ戻る
プログラミング

SQLiteがあれば十分

SQLite Is All You Need (dbpro.app)

39 pointsby upmostly49 コメント

要約

この記事では、多くの開発者が習慣で利用しているPostgresのような重いデータベースの代わりに、SQLiteを本番環境のアプリケーションに活用できる可能性を探求しています。5万ユーザー、100万投稿を持つソーシャルネットワークをSQLiteで構築し、そのパフォーマンスベンチマーク、設定、限界について詳細に解説しています。結論として、多くのユースケースにおいてSQLite(WALモード)で十分であり、過剰なデータベース設定は不要であると主張しています。

全文翻訳

先週、Evan Hahn氏の「Prefer STRICT tables in SQLite」を読み、考えさせられました。SQLiteを、実際の型付けされた本番データを格納するデータベースとして扱っている人がいるのです。そこで当然の疑問が湧き上がります。それは、どこまでそれが通用するのか?ということです。私たちはそれを突き止めました。SQLite上にソーシャルネットワークを構築し、5万人のユーザーと100万件の投稿を単一の343MBのファイルに収め、Node APIの背後に配置し、Evan氏のアドバイスに従って全てのテーブルをSTRICTにし、数字がそれ以上良くならなくなるまで負荷をかけました。この記事は、その結果として得られたベンチマーク、設定、コード、そして限界についてです。要するに、1つのファイル、1つのプロセスで、アプリケーションの中で最も負荷の高いページがラップトップ上で1日3億1500万リクエストを処理しました。あなたが構築しているほとんどのアプリケーションにとって、WALモードのSQLiteで十分であり、習慣で起動したPostgresコンテナは必要なかったのです。議論ではなく証拠を見たい場合は、直接ベンチマークに進んでください。限界もここにあり、それは現実のものです。 私たちが構築したもの ソーシャルアプリは、ハードなクエリを避けられないため、良いストレステストになります。ホームタイムラインは、あなたが投稿したものと私がフォローしているものを結合し、時間順に並べ替え、いいねの数を数える必要があります。最初の要求でキャッシュで回避することはできませんし、グラフが成長するにつれて遅くなります。そこで、Chirp、1つのファイルに存在するソーシャルネットワークです。 テーブル 行 ユーザー 50,000 投稿 1,000,000 フォロー 2,498,799 いいね 4,999,764 合計 343 MB 1つのファイルに 全てのテーブルはSTRICTであり、これについては後述します。バックエンド全体は、隣にあるchirp.dbと通信する単一のNodeプロセスです。データベースサーバーなし、接続文字列なし、ポート5432なし、コンテナなし。本番環境の設定全体は、5つのpragmaです。 JAVASCRIPT const db = new Database('chirp.db'); db.pragma('journal_mode = WAL'); // リーダーとライターがお互いをブロックしないように db.pragma('synchronous = NORMAL'); // コミットごとではなくチェックポイントでfsync db.pragma('busy_timeout = 5000'); // 書き込みロックを待つのではなく、スローする db.pragma('foreign_keys = ON'); // デフォルトでオフ、これは人々を驚かせる db.pragma('cache_size = -64000'); // 64MBのページキャッシュ これが全体のセットアップです。プロビジョニングするステップはありません。 数字 以下は全て、Apple M1ラップトップ(8コア、16GB RAM)で、Node 22とbetter-sqlite3(SQLite 3.49.2)を使って実行されました。サーバーではありません。ラップトップです。 まず、実際のHTTP経由のエンドツーエンドのパフォーマンスです。実際のNodeサーバー、実際のソケット、実際のJSONシリアライゼーション、50の同時接続、相手側はautocannonです。これはスタック全体であり、データベース単体ではありません。全ての数値は4回の実行の中央値であり、実行ごとのばらつきは約10%です。 エンドポイント req/s 50p99 エラー GET /post/:id (ポイントリード) 51,427 0ms 1ms 0 GET /u/:handle (プロフィール) 47,776 0ms 2ms 0 GET /timeline/:id (重いもの) 3,543 13ms 27ms 0 Mixed: 95% タイムラインリード、5% 書き込み 3,654 13ms 27ms 0 最後の行をしばらく見てください。これはアプリケーションの中で最も負荷の高いクエリであり、250万件のフォローと100万件の投稿を結合し、全ての結果に対するいいねの数を数え、ライブ書き込みと並行して、単一のNodeプロセスからラップトップ上で処理されています。秒間3,654リクエストは、1日あたり3億1500万リクエストです。最も負荷の高いエンドポイントで。ポイントリードは44億を処理するでしょう。もしあなたの製品が1日あたり3億1500万件のタイムラインロードを処理しているなら、あなたはトップ0.01%におり、すでに自分が誰であるかを知っているでしょう。それ以外の人々は、ファイルに収まるワークロードのためにコネクションプーラーについて議論しています。 HTTPレイヤーの下では、生のクエリの数値は以下のようになります。各ベンチマークは、新しいプロセスで、データベースの新しいコピーに対して3回実行され、中央値を取ります。 クエリ ops/s 50p99 ポイントリード (IDによる投稿) 232,011 0.004ms 0.007ms プロフィールページ (2つの集計) 175,850 0.005ms 0.007ms ホームタイムライン (20投稿 + いいね数) 4,247 0.232ms 0.337ms 投稿の挿入 (各トランザクション) 23,459 0.012ms 0.089ms 投稿へのいいね (各トランザクション) 12,618 0.016ms 0.243ms 投稿の挿入 (バッチ処理、トランザクションあたり100件) 32,217 行/秒 そこでの全ての書き込みは、実際のコミットされた耐久性のあるトランザクションです。バッチでも、バッファでも、キューでもありません。秒間2万3千件のコミットされたトランザクションが、ラップトップ上で、外部キーが有効な状態で実行されています。 WALが重要な部分です SQLiteに対する古い反論は、ロックがかかることです。1人のライターがデータベースを占有し、他の全員が待ちます。その反論はロールバックジャーナルに関するもので、2010年以来ほとんどのアプリで間違ったデフォルトとなっています。Write-Ahead Logging (WAL) は、問題の形状を変えます。ライターはページをインプレースで変更する代わりにログに追記するため、書き込みが進行中でもリーダーは最後にコミットされたスナップショットを読み続けます。リーダーはライターをブロックしません。ライターはリーダーをブロックしません。 私たちはそれを主張するのではなく、テストしました。7つのリーダー・スレッドがタイムラインクエリを実行します。まず単独で、次に現実的な毎秒1,000件の書き込みを行うライターの隣で、次にフルスピードで動作するライターの隣で。違いがわかるように、WALモードと古いロールバックジャーナルモードで同じテストを実行しました。 journal_mode = WAL シナリオ reads/s 99p worst read SQLITE_BUSY リーダーのみ 17,581 1.53ms 7ms 0 リーダー + 毎秒1,000件の書き込み 2,792 4.40ms 17ms 0 リーダー + フルスピードのライター (毎秒14,838件の書き込み) 2,854 5.16ms 30ms 0 journal_mode = DELETE (ロールバックジャーナル、人々が覚えているもの) シナリオ reads/s 99p worst read SQLITE_BUSY リーダーのみ 18,439 1.23ms 6ms 0 リーダー + 毎秒1,000件の書き込み 497 133.85ms 794ms 0 リーダー + フルスピードのライター (毎秒2,806件の書き込み) 227 586.02ms 1,762ms 0 同じクエリ、同じデータ、同じマシンです。1つのpragmaの違いです。毎秒1,000件の書き込みを実行するライターがいる場合、WALは読み取りスループットの5.6倍を提供し、99パーセンタイルを4.40msに保ちます。ロールバックジャーナルはp99で133msに低下し、個々の読み取りは800ミリ秒近く停止します。これがSQLiteについて人々が不満を言うものであり、それは10年前のデータベースです。WALの下では、全てのシナリオで、SQLITE_BUSYエラーはゼロでした。「少数」ではなく、ゼロです。 数字が止まる場所 この記事に良い表だけが含まれていたら、信頼すべきではありません。SQLiteを不利にしない発見をここに示します。何かが書き込んでいると、読み取りのスケーリングは停止します。ライターがいない7つのリーダー・スレッドは、毎秒17,581件の読み取りを実行します。毎秒1,000件の書き込みしか行わないライターを追加すると、読み取りは2,792件に低下します。これは6.3倍の低下であり、毎秒14,000件の書き込みに増やしても意味のある悪化は見られません。これは書き込み量ではなく、キャッシュの無効化の問題であることを示しています。それらのリーダーは、256MBのメモリマップドウィンドウとウォームなページキャッシュから速度を得ていました。コミットごとにマップされたページが無効化されるため、リーダーは実際のI/Oと再検証にフォールバックします。 設定をスキャンして確認しました。mmapをオフにすると、読み取り専用の数値は毎秒17,069件から毎秒6,034件に低下し、同時ライターからのペナルティはほとんど消えます。17,581という数値は読み取り専用の成果です。正直な混合ワークロードの数値は、マシンあたり約2,800件の最も負荷の高いクエリの読み取り/秒であり、これが私たちが議論の基盤としたものです。 1人のライター、グローバルに。 SQLiteはデータベース全体に対して単一の書き込みロックを取ります。書き込みは並列実行されず、キューイングされます。秒間23,000件のコミットされたトランザクションで、そのキューは速く空になりますが、それはキューであり、いくらハードウェアを増やしても2つのキューにはなりません。1台のマシンです。フェイルオーバーはありません。ボックスが故障した場合、復旧するまでダウンしており、復旧時間はファイルのリストアにかかる時間です。多くの製品にとって、これは運用上のシンプルさに対する完全に許容できるトレードオフです。一部の製品にとってはそうではなく、規制産業でSLA(サービスレベルアグリーメント)がある場合は、どちらの選択肢かを知っているでしょう。 Postgresに手を伸ばすべき時 同じ行に対して複数のライターが競合する場合、リードレプリカや自動フェイルオーバーが必要な場合、数億行に対する実際の分析エンジンが必要な場合、またはチームが拡張機能のエコシステムを本当に必要とする場合です。それらは正当な理由です。「いつかスケールするかもしれない」は理由の一つではなく、これらのコンテナのほとんどが存在する理由です。 「しかしラップトップでテストしたのでしょう?」 公平です。M1は2026年の基準では高速なマシンではありませんが、$6のVPSでもありません。そして、これらの数字がApple Silicon上でしか存在しないのであれば、議論全体が成り立たなくなります。私たちはボックスをレンタルして再実行しなかったため、以下は推定値です。