グループコミットとfsyncバッチングの内部
コミットが遅いのは fsync が遅いから。複数トランザクションの WAL フラッシュを束ねて同期回数を減らすグループコミットで、レイテンシとスループットのトレードオフを内部から解き明かします。
- 1.コミットのレイテンシ下限は WAL を fsync し切る時間で決まり、これは ストレージへの物理同期で 1 回あたり数百マイクロ秒〜ミリ秒級と高コスト。コミットごとに律儀に fsync すると、その逆数がスループット上限になる。
- 2.グループコミットは短い待機窓で到着したコミットを 1 回の fsync にまとめ、同期回数を 1/N に削減する。リーダー 1 スレッドが代表でフラッシュし、残りは flushedLSN が自分のコミット LSN に届くのを待つ。
- 3.待機窓を伸ばすほど 1 回あたりの束ねが増えてスループットは上がるが、各コミットのレイテンシは悪化する。低負荷では待っても無駄なので、負荷に応じて窓を適応させるのが定石。
コミットのレイテンシは fsync が決める
WAL(先行書き込みログ)を使う DBMS では、トランザクションのコミットを確定させる最後の一歩は「コミットログレコードまでを永続ストレージへ確実に書く」ことです。これは OS のページキャッシュを実デバイスまで届ける fsync(または fdatasync)の呼び出しで実現します。
問題は fsync の物理的なコストです。回転ディスクなら 1 回数ミリ秒、SSD でも数十〜数百マイクロ秒、停電保護付き書き込みキャッシュ(バッテリバックアップ)が無ければさらに遅くなります。ここで素朴に「コミットのたびに 1 回 fsync」を守ると、1 つのコミットが終わるまで次の fsync を開始できない直列実行になり、スループットの上限はおおむね次式に張り付きます。
最大コミット率 ≒ 1 / (fsync 1 回のレイテンシ)
例: fsync = 5 ms なら 最大 200 コミット/秒(並行数をいくら上げても)
並行トランザクションを 1000 本走らせても、fsync が直列なら全員がこの細い口を通ることになります。これがコミットのボトルネックの正体です。
fdatasync はファイルのデータ部分だけを同期し、サイズ変更を伴わないメタデータ更新(mtime など)を省けます。WAL は追記でファイルサイズが伸びるため一見メタデータ同期が要りそうですが、多くの実装は WAL ファイルを事前確保(preallocate)してサイズを固定し、fdatasync で済むようにします。これだけでメタデータ用の余分なディスク往復を 1 回減らせます。
グループコミットの基本原理
fsync は「呼んだ時点までにバッファへ書かれた全データ」をまとめて永続化します。つまり 1 回の fsync の中に、複数トランザクションのコミットログを同時に含められます。ここがグループコミット(group commit)の出発点です。
短い時間窓の間に到着したコミットのログをログバッファ上で連結し、まとめて 1 回の fsync で永続化します。N 件束ねれば fsync 回数は 1/N になり、同じハードウェアでもコミット率が N 倍近くまで伸びます。
個別コミット: T1→fsync T2→fsync T3→fsync (fsync 3 回・直列)
グループコミット: [T1,T2,T3 のコミットログを連結] → fsync 1 回で全員確定
重要なのは、束ねても永続性が一切緩まない点です。各トランザクションは「自分のコミット LSN まで flushedLSN(永続化済みログの末尾)が進んだ」ことを確認してからクライアントへ完了応答します。1 回の fsync が複数のコミット LSN をまとめて追い越すので、全員が安全に確定します。
リーダー・フォロワー方式の実装
実装の中核は「誰が fsync を呼ぶか」の調停です。代表的なのがリーダー・フォロワー方式(leader/follower、または slot 方式)です。
| 役割 | やること |
|---|---|
| リーダー | 最初にコミット待ち行列へ入ったスレッド。後続が溜まるのを少し待ってから、自分を含む全員ぶんのコミットログを 1 回の fsync でフラッシュする |
| フォロワー | リーダー確定後に行列へ加わったスレッド。自分ではフラッシュせず、リーダーの fsync 完了を待つだけ |
擬似コードで書くとこうなります。地の文の波括弧を避けるため、ブロックはコードとして示します。
commit(txn):
enqueue(txn) # コミット待ち行列に登録
if first_in_queue(txn): # 自分がリーダー
wait_briefly() # 後続が溜まる窓を開ける(任意)
batch = drain_queue() # 今いる全員を取り込む
write_log(batch) # コミットログをバッファへ連結
fsync() # 1 回の物理同期
publish flushedLSN = max_lsn(batch)
wake_followers(batch)
else: # 自分はフォロワー
wait_until(flushedLSN >= txn.commit_lsn)
return committed
この方式の利点は、fsync が走っている間に到着したコミットが自然に次のバッチへ吸い込まれることです。fsync のレイテンシそのものが「束ねる窓」として機能するため、明示的な待機をゼロにしても、負荷が高いほど勝手にバッチが大きくなる自己調整が効きます。これをパイプライン化したのが MySQL の binary log group commit で、flush・sync・commit の 3 ステージを別々のキューで回し、各ステージのリーダーが順送りに次のバッチを処理します。
最も洗練された実装は「わざと待つ」必要すらありません。リーダーが fsync している区間に来たコミットを次バッチにためるだけで、スループットが上がるほどバッチサイズが自動で増える。これを暗黙的グループコミット(implicit group commit)と呼びます。明示的な待機窓は、負荷が低くてこの自然なバッチが効かない領域を補うための追加策と捉えると整理しやすいです。
レイテンシとスループットのトレードオフ
グループコミットの設計判断は、つまるところ「リーダーがどれだけ後続を待つか」に集約されます。待機窓を W、平均到着率を λ とすると、1 バッチに集まる件数はおおよそ λ × W です。
- 窓
Wを大きくする → バッチが大きくfsync回数が減りスループット↑。ただし各トランザクションは平均W/2程度の追加待ちを背負うのでコミットレイテンシ↑。 - 窓
Wを小さく(あるいは 0)→ レイテンシは最小だが、低負荷ではほとんど束ねられずfsyncを打ちまくることになりスループット↓。
ここに非自明な点があります。低負荷で固定の待機窓を入れるのは純粋な損です。後続が来ないのに W だけ待ち、結局 1 件で fsync するので、レイテンシだけ悪化して何も束ねられません。逆に高負荷では、暗黙的グループコミットだけで十分大きなバッチが形成され、明示的な待機はほぼ不要になります。だから実用実装は待機窓を負荷に応じて適応させます。
| 負荷状況 | 良い戦略 | 理由 |
|---|---|---|
| 低並行(コミットがまばら) | 待機窓を 0 に近づける | 束ねる相手がいない。待つだけレイテンシ損 |
| 高並行(コミットが密) | fsync 中の自然なバッチに任せる | 明示待機なしでもバッチが十分大きくなる |
| 中程度 | 短い適応窓を入れる | わずかな待ちで束ね効率が一気に上がる領域 |
MySQL の binlog_group_commit_sync_delay / ..._sync_no_delay_count、PostgreSQL の commit_delay / commit_siblings は、まさにこの「窓をどれだけ開け、何件溜まったら待たずに飛ばすか」を露出したパラメータです。commit_siblings は「他にアクティブなトランザクションがこれ以上いるときだけ待つ」という条件で、低負荷での無駄待ちを避ける適応の一形態です。
commit_delay の適正値はストレージの fsync レイテンシに強く依存します。停電保護付き書き込みキャッシュを持つストレージでは fsync がマイクロ秒級に下がり、待つ意味がほぼ消えるため小さく(または 0)すべきです。逆に fsync が高コストな環境ほど待って束ねる価値が出ます。ベンチマーク無しの一律設定は危険で、レイテンシ目標とスループット目標のどちらを優先するかで符号が変わります。
永続性をどこまで譲るか
グループコミットは永続性を保ったままスループットを上げる手法ですが、隣には「永続性を一部諦めて速くする」設定があり、混同しがちなので切り分けます。
- グループコミット: 各コミットは必ず自分の
fsync完了を待つ。永続性は完全。束ねるだけ。 - 非同期コミット(asynchronous commit): クライアントへの応答を
fsync完了より前に返す。クラッシュ時に直近の確定済みコミットが失われ得る(ただし WAL ルールにより DB の論理的整合性自体は壊れない)。
両者は直交します。同期コミットのままグループコミットで束ねるのが基本戦略で、レイテンシ要件がさらに厳しい一部処理だけ非同期コミットに落とす、という使い分けが現実的です。
グループコミットは「fsync が本当にデバイスへ着地する」という前提の上に立ちます。ストレージやコントローラが揮発キャッシュを持ち、fsync 完了を返しても実体は未着地、というケースでは束ねた全員ぶんのコミットが一度に飛びます。書き込みキャッシュは無効化するか、停電保護付きであることを確認してください。これは WAL 単体でも同じ落とし穴です。
「並行数を上げてもコミット率が頭打ちになるのはなぜか」――答えは fsync の直列レイテンシ。「ではどう破るか」――グループコミットで複数コミットを 1 回の fsync に束ね、fsync 回数を 1/N にする。「副作用は」――待機窓を入れると個々のレイテンシが悪化するためトレードオフであり、低負荷では待たない適応が要る、と三段で答えられると強いです。
まとめ
コミットの速さを最終的に縛るのは fsync の物理レイテンシであり、コミットごとに 1 回呼ぶ素朴な実装ではその逆数がスループット上限になります。グループコミットは複数トランザクションのコミットログを 1 回の fsync に束ねてこの上限を押し上げる手法で、永続性は一切緩めません。鍵はリーダー・フォロワーによる fsync の調停と、fsync 実行中に来たコミットを次バッチへ吸い込む暗黙のバッチ化です。残るのは待機窓のチューニング――束ねる量とレイテンシのトレードオフであり、負荷に応じて窓を適応させるのが定石です。レプリケーションの同期モード(同期・準同期・非同期レプリケーション)やコネクションプーリングと組み合わせて、初めてコミット経路全体のスループットが設計可能になります。
データベース Article
グループコミットとfsyncバッチングの内部を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
グループコミット
比較で見る軸
難易度: advanced / カテゴリ: データベース / タグ数: 5
導入後に効く点
グループコミットは短い待機窓で到着したコミットを 1 回の fsync にまとめ、同期回数を 1/N に削減する。リーダー 1 スレッドが代表でフラッシュし、残りは flushedLSN が自分のコミット LSN に届くのを待つ。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- データベース
- タグ数
- 5
判断チェックリスト
- 自社の用途が「グループコミット / fsync」に近いか確認する。
- 強みである「コミットのレイテンシ下限は WAL を fsync し切る時間で決まり、これは ストレージへの物理同期で 1 回あたり数百マイクロ秒〜ミリ秒級と高コスト。コミットごとに律儀に fsync すると、その逆数がスループット上限になる。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。