
2026/08/10 0:36
すべての高速書き込みでワークロードはどこか別の場所に移動します。
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
書き込み遅延とデータの永続性の核心的な違いは、ストレージメディアそのものではなく、システムが成功を認識する正確な場所にあります。
fdatasync() のようなコマンド名を観察しただけでは不十分であり、操作がメモリへのコピー後、ローカル SSD への同期後、あるいはデータをリモートに複製した後で完了を確認するかを特定する必要があります。ローカルでの同期はホストクラッシュには耐えられますが、ディスクが故障すれば失敗します。一方、リモートのレプリケーションはネットワーク遅延を追加し、障害発生可能なポイントを拡大します。ハードウェアの違いのみでは説明できない巨大な速度差——最大 50 倍——は、これらの認識ポイントによって生じることがあり、NVMe デバイス同士でも当てはまります。現代的なアーキテクチャはますます関心の分離を進め、不変データについてはサービスが永続性を担うオブジェクトストレージを使用し、データベースのバージョン管理と回復制御の下で永続性管理を扱うためのローカルログ(例:WAL)を使用しています。開発者は明示的にリスクプロファイルを選択する必要があります:書き込みのバッチ化はスループットを向上させますが、待機時間を増やし、フラッシュタイムアウト時にバッチ全体の失敗のリスクを高めます。1 つのフラッシュを多数の書き込みで共有することはパフォーマンスを最適化しますが、バッチ全体に対する単一障害点を導入します。究極的には、産業界は PUT などの操作のビッグ O 計算量は経路に関わらず一定である一方、隠れた定数時間での作業量は認識ポイントによって劇的に異なり、速度とクラッシュやデバイスの喪失に対する耐性を決定することを認識する必要があります。最後に、条件付き書き込み(例:If-None-Match)のような技術は、分散オブジェクトストレージ環境内の競合条件を処理する上で不可欠です。本文
ストレージ設計における「書き込みの成功」と耐久性のトレードオフ
すべてのストレージシステムは、クライアントに対して「書き込みが成功した」と報告する前に、どのレベルでデータを永続化する必要があるかという決断を迫られます。この決断が遅延と耐久性(障害耐性)に直接影響を与えます。
1. 書き込みの成功条件と障害耐性
システムは以下のいずれかの戦略を選択します。各選択肢は、低遅延と耐久性という要素のバランスを変化させます。
- メモリのみ
- バイトデータをメモリにコピーした時点で即座に応答します(最も高速)。
- 欠点: メマクラッシュやマシンクラッシュが発生すると、未フラッシュのデータは失われます。
- ローカル SSD(同期)
を完了させ、ホスト上の NVMe SSD に物理的に書き込みを完了してから応答します。fdatasync()- 利点: プロセスやカーネルのクラッシュには耐性があります。
- 欠点: その HDD/SSD 自体やホストマシンの消失には耐えません。
- リモートストレージ / レプリケーション
- ネットワークボリュームへのコピー、オブジェクトストレージへの PUT、または他のデータベースサーバーへの WAL コピーを完了させるまで待機します。
- 利点: ネットワーク時間とコピー時間を要するため遅延が増加しますが、ホスト消失や一部のサーバー障害に耐性があります。
重要: システムコール(syscall)の名前が同一であっても、その背後で待機する遅延の種類や、システムが受け入れられる障害の範囲は大きく異なります。
2. 新しいストレージ設計:オブジェクトストレージと WAL の役割
従来のデータベースはローカル NVMe SSD を事前ログ(WAL)やキャッシュとして利用し、データを LSM-Tree で整理してきました。これに対し、オブジェクトストレージを中心とした設計は以下の特長があります。
- 不変のファイル: 一度 PUT 成功すれば、データを変更せずに保持できます。
- 計算リソースのスケーラビリティ: ストレージサービス自体がスケーリング可能で、共有ディスクページのランダムな更新を回避できます。
- 責任の分断:
- オブジェクトストレージ: クライアント PUT が成功すれば、永続化のコピー管理はストレージサービス側に委譲されます。
- データベース: バージョン管理(有効なバージョンの決定)、他のマシンへの決定伝播、読み込み高速化、古いデータ削除、クラッシュ復旧などの責任を担います。
コードと用語の定義
本稿における用語定義は以下の通りです:
- クライアント PUT: キー・バリューリクエストを送信する操作(例:HTTP リクエスト)。
- オブジェクト PUT: オブジェクトストレージへの HTTP リクエスト送信。
これらを分離することで、以下のような 1 つの書き込みの流れを追うことが可能になります。
クライアント PUT ↓ メモリ → ローカル SSD → リモートストレージ → 多数の DB サーバー
高速なベンチマーク数値を分析する際、以下の点を理解することが重要です:
- そのコストを負担しているのは誰か?
- 成功しても何が失われる可能性があるか?
- システムは未完成のクリーンアップまでどの程度耐えられるか?
3. オペレーションのコストと追加専用ストア
追加専用キーバリューストアにおける主要なオペレーションのコストは以下の通りです。
コスト詳細(Big O)
| 操作 | コスト計算式 | 具体的な処理内容 |
|---|---|---|
| GET | | インデックスルックアップ 1 回、読み込み 1 回 |
| PUT | | ハッシュ化 1 回、WAL 追加 1 回、共有 1 回 |
| DELETE | | 削除記録の追加 1 回 |
注目すべき点:
- 通常の要求では全保存オブジェクトを走査する必要がありません。
- インデックスが既に「キー」→「最新値(またはディスク位置)」のマッピングを持っているため、高速です。
は古いバイトを即座に消去せず、「無効」という事実だけを記録する軽量な処理です。DELETE
4. クライアント PUT の最適化:ローカル SSD への到達経路
データベースホストに直結した NVMe SSD(ホストローカル)を使用する場合、書き込みの永続化ポイントを慎重に選ぶ必要があります。
NVMe の注意点
- NVMe はインターフェース名称であり、ストレージの所在地や障害耐性を示しません。
- リモートブロックボリュームでも NVMe デバイスとして現れますが、これはネットワークを跨ぐため、遅延数値に**認識ポイント(acknowledgment point)**が含まれているか確認が必要です。
待機タイミングの違い
- メモリコピーのみ: タイマー停止後即座に応答。
- ローカル SSD 同期完了後:
完了後に応答。fdatasync() - リモートボリューム認識後: ネットワーク経由で書き込みが認識された後に応答。
ローカル WAL との遅延比較例
| サービス | 特性 | 備考 |
|---|---|---|
| S3 Express One Zone | 一貫した単一桁のミリ秒オーダー遅延 | S3 Standard より最大 10 倍高速(AWS) |
| Turso (4KB PUT) | 平均 6.4 ms / p99 7 ms | オブジェクトストレージ基準値 |
ローカル | 約 1 ms | ローカルパスはオブジェクト比で約 6 倍高速 |
| 理想の極小遅延 | 約 0.1 ms | ローカルパスが約 64 倍高速になる場合 |
重要: 50 倍以上の性能差は真実ですが、これは特定のデバイスの特性だけでなく、「どの障害に耐えるか」という設計方針の違いによるものです。
処理フロー:クライアント PUT の通過経路
書き込みが「成功」と見なされるまでの典型的なカーネルとデバイスの流れ(ローカル WAL 使用時)は以下の通りです。
C["client PUT"] --> E subgraph H["DATABASE HOST"] E["storage engine"] -->|"WAL を記述"| K["カーネルページキャッシュ"] K -->|"fdatasync でページを記述<br/>デバイスキャッシュをフラッシュ"| D["ホストローカル SSD"] end K -.->|"write() はここで戻ることができる"| V["メモリのみ<br/>永続化されていない"] D --> S["成功"]
コールは WAL がページキャッシュに到達した時点で返せますが、**「成功」**とは定義されません。write()- Linux では
でデータをコピーし、ディルティ(汚染)状態のページを後でデバイスに書き込みます。プロセス終了によるキャッシュ破棄はありませんが、カーネルクラッシュやマシン損失では到達していないデータは失われます。write()
はファイルデータと必要なメタデータの両方を同期します。fdatasync()
やO_DIRECT
はパフォーマンスを向上させますが、未同期書き込みの永続化という根本的な性質は変えません。io_uring
接続切断時のリスク
fdatasync() が完了し WAL はコミット済みでも、ネットワークが切断された直後などにはクライアントから成功応答が届かない場合があります。
- 再試行: クライアントがオペレーション ID を付与して再試行することは可能ですが、異なるバイト列で同じ ID を再利用すると失敗します。
- 高速化の影響: 高速ハードウェアはこのウィンドウを縮小しますが、曖昧さを完全に排除することはできません。
5. スループット向上:1 つのフラッシュで多数の書き込みをカバー
各クライアント PUT 後に
fdatasync() を呼ぶ方法は実装が容易ですが、スループットはデバイスの最大フラッシュ回数によって制限されます(例:秒間約 1,000 フラッシュ)。
これを改善するには、WAL ライターが追加記録をバッチ化し、固定のバイト数・回数の後に一度にフラッシュを行う仕組みを導入します。
バッチ化フロー
複数の書き込みを 1 つの
fdatasync() で共有し、最終的な位置で一括処理を行います。
A["PUT A"] --> G B["PUT B"] --> G C["PUT C"] --> G D["PUT D"] --> G subgraph H["SAME DATABASE HOST"] G["WAL バッチを開く"] --> O["バイト数、回数、時間で閉じる"] O --> F["1 回の fdatasync()"] F --> SSD["ホストローカル SSD"] N["次の WAL バッチ"] end SSD --> S["成功<br/>A, B, C, D"] E["PUT E"] -.->|"閉鎖後に来る"| N
- メリット:
〜PUT A
は 1 つのフラッシュを共有して即座に成功を受けます。スループットが向上します。D - 注意点:
がバッチ終了後に到着する場合、次のフラッシュまで待機する必要があります。PUT E- 最初のリクエストは最も長く待機します。
- フラッシュエラーはバッチ全体に影響します。
バッチ化の制限事項
- メモリエキス: スローなデバイスでは、保留中の書き込みキューが利用可能なメモリを消費し尽くす可能性があります。
- 遅延増大: 負荷が高い状況で
してバッチを埋めようとすると、スループットへの寄与は薄く、単に遅延が増加するだけです。sleep - デバイスのコスト: デバイス書き込みサイズが大きいほど IOPS は必要です(例:256 KiB で 1 GiB/s を動かすなら約 4,096 IOPS が必要)。
6. ホストを超えてデータを永続化する戦略
ローカル SSD での
fdatasync() 完了後も、データが「真に永続された」とは限りません。最新の WAL がホスト消滅前にリモートにコピーされていれば安全ですが、そうでなければデータ損失のリスクがあります。
選択肢ごとの成功ポイントと耐性
各ストレージオプションには「安全に成功として返せる」タイミング(認識ポイント)が異なります。
| 成功を待つ対象 | 遅延に含まれるもの | 耐える障害 (Failover Point) |
|---|---|---|
| ページキャッシュへのコピー | メモリコピーのみ | プロセス終了は OK。 カーネルクラッシュ・マシン損失は NG。 |
ローカル SSD への | SSD 書き込み・キャッシュフラッシュ | プロセス・カーネルクラッシュ OK。 SSD 又はホストの消失は NG。 |
永続ネットワークボリュームへの | ネットワーク要求+コピー処理 | DB ホスト喪失 OK。 保護範囲はボリュームサービスに依存。 |
| オブジェクト PUT 成功後 | HTTP 要求+オブジェクトサービスコピー | ライター・DB ホスト喪失 OK。 保護範囲はオブジェクトサービスに依存。 |
| 3 台サーバー内の 2 台への WAL コピー | ネットワーク転送+ディスク同期 | 1 サーバー喪失 OK。 修復前の 2 サーバー同時喪失は NG。 |
ウィンドウ管理:アップロードを後回しにするか?
- ローカル
を待つ: パスが短く高速ですが、データ損失ウィンドウが「ホスト消失」まで広がります。fdatasync() - オブジェクト PUT を完了させる: ウィンドウは「サービス障害」に縮まりますが、各書き込みにネットワーク要求が含まれます。
- バッチ化することで要求数は減りますが、バッチ内の最初の書き込みはバッチ埋まるまで待機します。
7. WAL の複製と協調性
WAL を複数のサーバー(リーダーとフォロワー)に複製すると、システムはステートフルになり、障害耐性が向上します。
複製 WAL フロー
3 ノード構成の場合、クライアントはリーダー宛てに PUT を送ります。リーダーはローカル WAL に追加後、2 つのフォロワーにコピーを送信し、永続コピーが 2 つ達成された時点で成功を返却します。
C["client PUT"] --> L subgraph R["3-ノード複製 WAL"] L["リーダー<br/>WAL + ローカル SSD"] F1["最初に完了するフォロワー<br/>WAL + ローカル SSD"] F2["他のフォロワー<br/>WAL + ローカル SSD"] L -->|"複製"| F1 L -->|"複製"| F2 end L --> Q["最初の 2 つの永続コピー"] F1 --> Q F2 -.->|"後に完了する可能性がある"| Q Q --> S["成功"]
- 耐性: 1 つのサーバー喪失には耐えます。
- 遅延: コミットには全てネットワーク転送と、多数性を形成するための遅いデバイスの同期が含まれます。
- 障害ドメイン: 異なる障害ドメインに配置されたレプリカは、単なるダウンタイムより大きな障害にも耐えられます。
ライター選出の責任分担
- システム内部で自動的にリーダー選出(エレクト)を行う必要はありません(データが重要だから)。
- 別のサービスがライターの割り当てを行い、番号を管理します。
- ストレージエンジンは、古いライターの書き込みを拒絶するだけの役割に留まります。
- サーバー間通信不能時でも、自分自身でライター選出とコミットログの保存を行う必要があります。
Raft アルゴリズムなどでは、リーダーは現在のテーム(term)、投票数、ログ位置を送信し、多数がディスク上のログを保存した時点でコミットされます。RAM だけの認識(Acknowledgment)に依存せず、必ずディスクへの記述を行います。
8. 高速読み取りとクリーンアップの必要性
WAL を再実行できることは、GET を安価にするだけでありません。すべての読み取りで WAL を走査するとコストが履歴と共に成長するため、以下の最適化が必要です。
- インデックス維持: エンジンは「キー」→「最新値/ディスク位置」のマッピングを常に更新します。
- ハッシュインデックス: 順序不要な高速ルックアップが可能。
- B ツリー: 順序付き読み取り・範囲スキャンをサポート(ただし、深さ増加に伴うオーバーヘッドあり)。
- LSM-Tree: メモリとソート済みファイルに記述し、マージで再記述します。
クリーンアップ(DELETE)の仕組み:
の DELETE は即座に返ります(古いデータは物理的に削除せず)。O(key bytes)- 真正な解放には後からファイルインデックスを読み込み、代替ファイルにデータをコピーし、不要なファイルを削除するプロセスが必要です。
9. Big O の真偽:成長と耐久性
Big O 記法は「データ量が増えたときにコストがどう増えるか」のみを示しますが、「どのタイミングで永続化するかが異なる」という違いを隠しています。
| リクエストパス | ビッグ O (成長項) | 成功までに必須だが Big O で隠れた作業 |
|---|---|---|
| ローカル WAL | | 1 回の WAL 追加+ローカル の共有分 |
| オブジェクトストレージ | | ネットワークラウンドトリップ+サービスによる認識 (バッチ化で共有可能) |
| 複製 WAL | | ネットワーク転送+多数性確保のための永続追加 |
決断が必要な領域(テスト対象)
- どのパスを「成功」とみなすか(
のタイミング)。fdatasync() - タイムアウトの定義。
- クラッシュ時にどうデータを失うべきか(選択不可な問題ではない)。
- 未完成の作業(アップロード、複製ログなど)をどこまで許容するか。
これらの判断は、システムがメモリ→ローカル SSD→リモートストレージ→レプリカという流れを追跡することで初めて明確になります。
結論: より高速なパスはより少ない障害に耐えますが、成功を遅らせることで耐久性を増やすことができます。GET, PUT, DELETE の数値だけを見てシステム評価を行うのは誤りであり、背後にある永続化戦略を理解する必要があります。