TL

分散ログ(Kafka系)のパーティションと順序保証

Kafkaで「どこまで順序が保たれ、どこからは保たれないか」を正しく見切るために。追記専用ログ、パーティション内全順序、コンシューマグループのリバランス、ISRレプリケーションによる耐久性を原理から解きほぐします。

応用Kafka分散ログパーティション順序保証レプリケーションメッセージング最終更新: 2026-06-21
TL;DR要点だけ先に
  • 1.分散ログは不変の追記専用ログをキーでパーティションに分割した構造で、全順序が保証されるのはパーティション内だけ。トピック全体の順序は保証されない。
  • 2.同じキーは同じパーティションに必ず割り当てられるため、順序を守りたい単位(例:口座ID)をキーにすればその単位内の順序は守られる。並列度はパーティション数で上限が決まる。
  • 3.耐久性はISR(同期レプリカ集合)への複製で担保され、acks=allかつmin.insync.replicasを満たした書き込みだけがコミット済みとして読める。オフセットコミットの順序が再処理と欠落を分ける。

追記専用ログ:そもそも何を保存しているのか

Kafka系の分散ログ(commit log)の中核は、驚くほど単純なデータ構造です。末尾にしか書けず、書いたものは書き換えも削除もしない不変の列——追記専用ログ(append-only log)です。各レコードは書き込まれた順に連続した整数の オフセット(offset) を割り当てられます。0, 1, 2, … と単調増加し、欠番も飛びもありません。

パーティション0:  [off=0][off=1][off=2][off=3][off=4] ← ここに追記
                   古い                          新しい

この「不変・追記のみ・連番オフセット」という制約こそが、性能と順序保証の源泉です。ランダム書き込みが無いのでディスクへはシーケンシャル書き込みになり、HDDでも高スループットが出ます。読み出しもオフセットを起点にした連続読みなのでページキャッシュと相性がよく、ブローカは多くの場合データをユーザー空間にコピーせず sendfile でネットワークへ流せます。そして順序はオフセットの大小そのものなので、別途ソートやタイムスタンプ比較を要しません。オフセットが小さいレコードは必ず先に書かれた、という事実が順序の定義になります。

ログはキューではなく「保持される台帳」

従来のメッセージキューは「読んだら消える」モデルですが、分散ログは読んでも消えないのが本質的な違いです。レコードは保持ポリシ(時間 or サイズ)で期限切れになるまで残り、コンシューマは「自分がどこまで読んだか」を示すオフセットを各自が持ちます。だから同じログを複数の独立した読み手が、別々の速度で、別々の地点から読めます。再生(replay)も「オフセットを巻き戻すだけ」で済みます。

パーティション:全順序が保たれる単位

単一ログでは1台のブローカの容量と帯域が上限になります。これを破るのが パーティション(partition) です。1つのトピックを複数のパーティションに水平分割し、それぞれを別ブローカに置く。これでスループットと容量はパーティション数に比例してスケールします。

ここで順序保証の最重要原則が出ます。オフセットはパーティション単位で振られ、全順序(total order)が保証されるのは1つのパーティション内だけです。トピック全体としての順序は存在しません。

トピック T(3パーティション)
  P0: [a0][a1][a2]
  P1: [b0][b1]
  P2: [c0][c1][c2][c3]

P0内では a0 → a1 → a2 の順序が厳密に保証されます。しかし P0 の a1 と P1 の b0 のどちらが先に起きたかは、ログ構造としては定義されていません。複数パーティションにまたがるイベント間の前後関係を知りたければ、論理クロックなど別の仕組みが要ります(/devops/logical-clocks/)。「分散ログは順序を保証する」という言い方は不正確で、正しくは「パーティション内の全順序を保証する」です。

キーによる分配:順序を守りたい単位を設計する

では、順序を守りたいレコード群を確実に同じパーティションへ集めるにはどうするか。鍵は メッセージキー(partition key) です。プロデューサがレコードにキーを付けると、ブローカ(実際にはプロデューサ側のパーティショナ)が次のように分配先を決めます。

partition = hash(key) mod パーティション数      # キー有り:決定的
partition = ラウンドロビン or sticky             # キー無し:負荷分散優先

キーのハッシュをパーティション数で割った余りで宛先が決まるため、同じキーは(パーティション数が変わらない限り)常に同じパーティションに行きます。したがって「順序を保ちたい単位」をキーにするのが設計の要諦です。

  • 口座残高を順に適用したい → キー = 口座ID
  • 同一ユーザーのイベントを順序通りに処理したい → キー = ユーザーID
  • 同一デバイスのテレメトリを時系列で並べたい → キー = デバイスID

こうすれば、その口座/ユーザー/デバイスに属するレコードは1パーティションに集まり、その単位内では厳密な順序が保証されます。異なる口座どうしの順序は不要なので、それらは別パーティションに散って並列処理できる——「グローバルな順序は要らない、ローカルな順序だけ要る」という業務の性質を、キー設計でそのままパーティション構造に写すわけです。

パーティション数の変更は順序とハッシュ分配を壊す

hash(key) mod N の N(パーティション数)を後から増やすと、同じキーの宛先パーティションが変わります。増設前後で同一キーのレコードが別パーティションに分かれ、増設をまたいだ順序保証が崩れます。Kafkaがパーティション数の「増加のみ可・減少不可」としているのもこのためで、減らすと既存データの再配置が必要になり整合が壊れます。順序が重要なトピックは、最初から十分なパーティション数を見積もるか、再配置時の順序断絶を許容できる設計にしておきます。なお同じ余りハッシュの弱点を緩和する手法が一貫性のあるハッシュ(/devops/consistent-hashing-load-balancing/)です。

キーを付けない場合、ブローカは負荷分散を優先してレコードを各パーティションへ散らします(ラウンドロビンや、バッチ効率を上げる sticky 戦略)。この場合、順序保証は一切ありません。順序が要るかどうかでキーの有無を決める、というのが第一の分岐点です。

コンシューマグループとリバランス:並列度はパーティション数で頭打ち

読み手側の単位が コンシューマグループ(consumer group) です。同じ group.id を持つコンシューマ群がグループを成し、1つのパーティションはグループ内のちょうど1コンシューマだけに割り当てられます。この「1パーティション=1消費者」という排他割り当てが、コンシューマ側でも順序を守る仕掛けです。複数の消費者が同一パーティションを同時に読むと順序処理が崩れるため、それを禁じています。

グループG(3コンシューマ), トピックT(4パーティション)
  C1 ← P0, P3
  C2 ← P1
  C3 ← P2

この割り当てから重要な帰結が出ます。グループ内の有効な並列度はパーティション数で上限が決まるということです。パーティションが4つなら、5人目のコンシューマを足しても割り当てるパーティションが無く、遊休します。スループットを上げるための消費者の増設は、パーティション数までしか効きません。並列度を上げたければ、まずパーティションを増やす必要があります。

コンシューマの増減・離脱・障害が起きると、割り当てを組み直す リバランス(rebalance) が走ります。グループコーディネータ(ブローカの1つ)が新しい割り当てを計算し、各コンシューマへ配ります。

リバランスのstop-the-worldと重複処理

古典的な(eager な)リバランスでは、全コンシューマが一旦すべてのパーティションを手放してから割り当て直すため、その間グループ全体の消費が止まります(stop-the-world)。コンシューマが多いほど、また割り当て直しが頻発するほど、消費停止が積み重なります。これを緩和するのが**協調的リバランス(cooperative / incremental)**で、影響を受けるパーティションだけを段階的に移し、無関係なパーティションの消費は止めません。

さらに厄介なのが重複処理です。リバランスの瞬間、あるパーティションが C2 から C3 へ移ったとき、C2 が直前まで処理したぶんのオフセットをコミットし切れていないと、C3 はコミット済み地点から読み直すため、未コミットの差分が再処理されます。リバランスは「重複が定常的に発生しうる点」なので、処理はべき等であることが前提になります(/devops/idempotency-exactly-once/)。

オフセットコミット:どこまで読んだかの記録が再処理と欠落を分ける

コンシューマは「自分がどこまで処理したか」を コミット済みオフセット(committed offset) としてブローカ(内部トピック __consumer_offsets)に記録します。次回の読み出しやリバランス後の引き継ぎは、この記録された地点から再開します。ここでコミットの順序が、障害時に重複(再処理)か欠落(取りこぼし)かを決定づけます。

パターンA(at-least-once): 処理 → 成功 → オフセットをコミット
  処理後・コミット前に落ちると、再開時に同じレコードを再処理(重複)。欠落はしない。

パターンB(at-most-once): オフセットをコミット → 処理
  コミット後・処理前に落ちると、そのレコードは二度と読まれない(欠落)。重複はしない。

業務系の多くは欠落が致命的なので、処理を確定してからコミットする(パターンA、at-least-once) を選び、そこで生じる重複を受信側のべき等処理で吸収します。「自動コミット(一定間隔で勝手にコミット)」は実装が楽ですが、処理の完了とコミットのタイミングがずれ、落ち方によって重複も欠落も起こりうるため、順序と正確性が要る系では手動コミットで処理境界に揃えるのが定石です。

観点処理→コミット(at-least-once)コミット→処理(at-most-once)
障害時の挙動未コミット分を再処理(重複あり)コミット済み未処理分が欠落
失ってはいけないデータ向く(欠落しない)不向き(欠落しうる)
前提となる消費側の性質べき等処理が必須重複を嫌う・欠落許容な系
代表的な用途決済・注文・在庫の取り込みメトリクスのサンプリング等

ISRとレプリケーション:書いたものが消えない仕組み

最後が耐久性です。各パーティションは複数のブローカに複製され、そのうち1つが リーダー(leader)、残りが フォロワー(follower) になります。プロデューサとコンシューマはリーダーとだけやり取りし、フォロワーはリーダーのログを追いかけてコピーします。リーダーが載るブローカが落ちたら、フォロワーの中から新リーダーを選びます(リーダー選出の原理と分断時の注意は /devops/leader-election-split-brain/)。

ここで核になるのが ISR(In-Sync Replicas:同期レプリカ集合) です。ISRとは「リーダーに十分追いついている(遅延が一定以内の)レプリカの集合」で、リーダー自身も含みます。追従が遅れたフォロワーはISRから外れ、追いつけば戻ります。

パーティションP0:  leader=B1,  followers=B2,B3
  B2 が追従中・B3 が遅延 →  ISR = {B1, B2}      ← B3 は同期外れ

耐久性の保証はこのISRと2つのパラメータで決まります。

  • acks=all:プロデューサは「ISR内の全レプリカが書き込み終えた」ことを確認してから成功とみなす。acks=1(リーダーだけ)なら、リーダーが書いた直後・複製前に落ちるとそのレコードは失われうる。acks=0 は確認すらしない。
  • min.insync.replicas:書き込みを受け付けるためにISRに最低限必要なレプリカ数。例えば3レプリカでこれを2に設定すると、ISRが2未満(=1台しか同期していない)になった時点で、acks=all の書き込みは拒否されます(エラーを返す)。

acks=allmin.insync.replicas を組み合わせる狙いは明快です。コミット済みと認めるレコードは、必ず複数台に書かれている状態を保証する。だから1台が落ちても、そのレコードを持つレプリカが他に残っており、データは失われません。逆に min.insync.replicas を満たせないほどレプリカが減ったら、書き込みを止めてでも耐久性の不変条件を守る——可用性より一貫性・耐久性を優先する選択です。これはCAP的なトレードオフそのもので、整合性モデルの議論(/devops/consistency-models/)と地続きです。

そしてコンシューマが読めるのはコミット済み(high watermark まで)のレコードだけです。high watermark は「ISR全員が複製し終えた最大オフセット」を指し、ここを超えた未複製のレコードはまだ読めません。これにより、後でリーダーが落ちて新リーダーに切り替わっても、一度読めたレコードが「無かったこと」にならない——読み手から見た順序と内容の一貫性が保たれます。

設計レビュー・試験の頻出論点

頻出の確認点は次の通り。(1) 順序が保証されるのはパーティション内だけで、トピック全体では保証されない。(2) 順序単位はキーで決まり、hash(key) mod N ゆえパーティション数変更で同一キーの宛先が変わり順序が割れる。(3) グループの並列度はパーティション数が上限で、消費者を増やしてもそれ以上は遊ぶ。(4) リバランスとコミット順序の組で**重複(処理→コミット)か欠落(コミット→処理)**が決まる。(5) 耐久性は acks=all と min.insync.replicas の両立で「複数台に書かれたものだけコミット済み」を保証し、読めるのは high watermark まで。「acks=all なら絶対消えない」ではなく「ISRが min.insync.replicas を満たす範囲で」という条件付きである点が落とし穴。

まとめ

  • 分散ログは不変・追記専用・連番オフセットのログで、オフセットの大小がそのまま順序の定義になる。読んでも消えず、複数の読み手が各自のオフセットで独立に読める。
  • 全順序が保証されるのはパーティション内だけ。トピック全体の順序は存在しない。順序を守りたい単位をキーにすると、同じキーが同一パーティションに集まりその単位内の順序が守られる。
  • コンシューマグループでは1パーティション=1消費者。よって並列度はパーティション数で頭打ち。リバランスは消費停止と重複処理を伴うため、消費はべき等が前提。
  • オフセットコミットの順序が、処理→コミットなら重複(at-least-once)、コミット→処理なら欠落(at-most-once)を決める。
  • 耐久性は ISRへの複製+acks=all+min.insync.replicas で「複数台に書かれたものだけコミット済み」を保証し、コンシューマは high watermark までしか読めない。レプリカが足りなければ書き込みを止めてでも耐久性を守る。

DevOps/インフラ Article

分散ログ(Kafka系)のパーティションと順序保証を実務で読む

TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。

解決すること

Kafka

比較で見る軸

難易度: advanced / カテゴリ: DevOps/インフラ / タグ数: 6

導入後に効く点

同じキーは同じパーティションに必ず割り当てられるため、順序を守りたい単位(例:口座ID)をキーにすればその単位内の順序は守られる。並列度はパーティション数で上限が決まる。

先に潰すリスク

用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。

数字・仕様の読み方
難易度
advanced
カテゴリ
DevOps/インフラ
タグ数
6

判断チェックリスト

  • 自社の用途が「Kafka / 分散ログ」に近いか確認する。
  • 強みである「分散ログは不変の追記専用ログをキーでパーティションに分割した構造で、全順序が保証されるのはパーティション内だけ。トピック全体の順序は保証されない。」が本当に評価軸になるか確認する。
  • 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
  • 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
  • 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

Kafka分散ログパーティション順序保証レプリケーションKafka分散ログパーティション