Kafkaの内部構造
分析基盤のイベント収集やCDCの土台であるKafkaを、なぜ速く壊れにくいのか原理から腑に落とせます。分散コミットログ、パーティション、ISRとリーダー選出、オフセットと順序保証、ログ保持とコンパクションを内部動作から解きほぐします。
- 1.Kafkaの本体は追記専用の分散コミットログである。トピックは複数パーティションに分割され、各パーティションは末尾追記のログファイル(セグメント列+疎なインデックス)として実装される。書き込みは常に末尾追記なのでシーケンシャルIOになり、ページキャッシュとゼロコピー送出でディスクベースながら高スループットを出す。
- 2.各パーティションはリーダー1つとフォロワー数個に複製され、追随できているレプリカ集合ISRだけがコミット対象になる。プロデューサのacksとブローカのmin.insync.replicasの組でどこまで永続化を待つかが決まり、リーダー障害時はISR内から新リーダーを選びハイウォーターマーク以下だけを可視化するのでデータを失わない。
- 3.順序保証はパーティション内に閉じる。全順序はパーティション単位でしか成り立たず、同一キーを同一パーティションへ写すことで実務上の順序を担保する。オフセットは消費側が管理し、保持は時間・サイズによる削除か、キー単位で最新値だけ残すログコンパクションのいずれかで、後者はCDCの最新状態テーブルの土台になる。
Kafkaは何なのか:分析基盤の背骨としての分散ログ
Kafkaを「メッセージキュー」と捉えると本質を外す。中身は追記専用の分散コミットログ(distributed commit log) であり、分析基盤ではイベント収集・CDCの配送・ストリーム処理の入出力を束ねる背骨として使われる。キューが「読んだら消える」のに対し、Kafkaは書き込みを一定期間そのまま保持し、複数の消費者が各自の速度で何度でも読み返せる。この「消えないログ」という性質こそが、同じイベント列をリアルタイム集計・バッチ再処理・監査に同時供給できる理由だ。
なぜログという単純な構造がビッグデータ規模で効くのか。ログは末尾追記しかしないため、ディスクへの書き込みが常にシーケンシャルになる。ランダムIOを避けられるので、SSDでもHDDでも高いスループットが出る。読み出しも「あるオフセットから順に前方へ」なので、OSのページキャッシュに素直に乗る。Kafkaはこの単純さを、分散・複製・保持の三点で拡張したものだと捉えると全体像が掴める。
Kafkaの決定的な価値は、消費が非破壊で任意の過去位置から再生(replay) できる点にある。消費者はオフセットを戻すだけで過去のイベント列を再処理できる。これはストリーム一本でバッチ相当を賄うKappa的な設計(/data-engineering/batch-vs-stream-processing/)の前提であり、障害復旧・バックフィル・新しい集計ロジックの過去適用を、上流DBに再問い合わせせず実現する。分析基盤では「イベントの真実の系列を一定期間保持する層」としてKafkaが置かれる。
トピックとパーティション:並列性の単位
トピック(topic)は論理的なイベントの分類名にすぎず、物理的な実体はパーティション(partition) だ。1つのトピックはN個のパーティションに分割され、各パーティションが独立した1本のログになる。このパーティションこそが並列性・順序・複製すべての単位である。
- 並列性:パーティション数がその トピックの最大並列度を決める。プロデューサは複数パーティションへ同時に書け、コンシューマグループ内では1パーティションを高々1コンシューマが担当するため、消費側の並列度はパーティション数で頭打ちになる。
- 配置:どのレコードがどのパーティションへ行くかは、キーのハッシュで決まる(キーがなければラウンドロビン等)。
partition = hash(key) mod Nが基本形で、これはデータレイクのバケッティングと同じ発想だ(/data-engineering/partitioning-bucketing/)。
Topic "orders"(partitions=3)
P0: [off0][off1][off2][off3]... ← 末尾追記、オフセットは単調増加
P1: [off0][off1][off2]...
P2: [off0][off1][off2][off3][off4]...
各レコードは partition = hash(key) mod 3 で振り分けられる
オフセットは「パーティション内で一意」な連番(トピック全体では一意ではない)
ここで最重要の非対称性が生まれる。オフセットはパーティション内でのみ意味を持つ連番であり、トピック全体を貫く通し番号ではない。つまり順序も並列度もパーティション境界で切れる。パーティション数を増やせばスループットは伸びるが、後述の順序保証は緩む——このトレードオフが設計の核になる。
パーティション数を増やすと、hash(key) mod N の N が変わるため同一キーの行き先が変わる。増やした後に流れるレコードは新しいパーティションへ入りうるので、増設時点をまたいで同一キーの順序が乱れる可能性がある。さらにKafkaはパーティションの削減を通常サポートしない(オフセットの連続性が壊れるため)。分析基盤では将来のスループットを見越してやや多めに切っておくのが定石だが、過剰なパーティションはメタデータとオープンファイル数を増やし、リーダー選出やリバランスを重くする。「多すぎず・減らせない」を前提に初期設計する。
パーティションの中身:セグメントと疎なインデックス
1パーティションは単一の巨大ファイルではなく、セグメント(segment) と呼ぶ固定サイズ級のファイル列で構成される。末尾のアクティブセグメントにのみ追記し、一定サイズ/時間で切り替えて新しいセグメントを開く。保持ポリシによる削除はセグメント単位で行うため、古い1本を丸ごと消せて効率的だ。
各セグメントには本体のログに加え、疎なインデックス(sparse index) が付く。全レコードではなく一定バイトごとに「オフセット→ファイル内位置」を記録する索引で、消費者が「オフセット12345から読みたい」と言えば、インデックスで近傍位置へジャンプし、そこから前方スキャンで目的のレコードに達する。全件を索引しないのでインデックスは小さく、ページキャッシュに収まりやすい。
セグメント(例:baseOffset=10000)
.log 本体(レコード群、末尾追記)
.index 疎なオフセット索引 offset→物理位置(数KBごと)
.timeindex 時刻索引 timestamp→offset(時刻ベース検索・保持判定用)
検索: 目的offset → .indexで直近下限を引く → .logをそこから前方スキャン
高スループットの決め手はこの構造とOSの協調にある。書き込みはページキャッシュ経由でシーケンシャルに落ち、消費者への送出はゼロコピー(sendfile) でカーネル内でファイルからソケットへ直送される。Kafkaプロセスがレコードをユーザ空間へコピーし直さないため、CPUとメモリ帯域を節約できる。「JVMヒープではなくOSのページキャッシュを主記憶として使う」のがKafkaの流儀で、だからブローカには大きなRAMが効く。
リーダー・フォロワーとISR:壊れないための複製
各パーティションは複製(replication factor)され、レプリカの1つがリーダー(leader)、残りがフォロワー(follower) になる。読み書きは原則リーダーだけが受ける。フォロワーはリーダーからログを継続的にフェッチして自分のログへ複製する、いわば内蔵の消費者だ。
中核概念がISR(In-Sync Replicas:同期レプリカ集合) である。ISRとは「リーダーに十分追随できているレプリカの集合」で、リーダー自身を含む。フォロワーが遅延しすぎる(一定時間フェッチが滞る)とISRから外され、追いついたら戻る。レコードがコミット済み=ISRの全レプリカに複製済みと定義され、コミットされたレコードだけが消費者に見える。この可視化の境界がハイウォーターマーク(high watermark, HW) だ。
リーダー P0 ログ末尾(LEO=Log End Offset)= 108 ← 受理済みの最新
ISR = {leader, followerA, followerB}
followerA が複製済みの末尾 = 105
followerB が複製済みの末尾 = 106
ハイウォーターマーク HW = min(ISR各レプリカの複製済み末尾) = 105
→ 消費者に見えるのは offset 105 まで(106〜108は未コミットで不可視)
障害時の挙動がこの設計の勘所だ。リーダーが落ちると、コントローラ(クラスタ制御役)がISR内の別レプリカを新リーダーに昇格させる。新リーダーはHW以下(=全ISRが持つと保証された範囲)だけを有効とみなすので、コミット済みデータは失われない。逆にHWより先(未コミット)は消費者に見えていないため、消えても配信保証を破らない。
どこまで待って「書けた」とするかは、プロデューサの acks とブローカの min.insync.replicas の組で決まる。acks=0 はリーダーの受理も待たず投げっぱなし(最速・喪失あり)。acks=1 はリーダーのローカル書き込みだけ待つ(リーダーが複製前に落ちると喪失しうる)。acks=all はISR全員への複製を待つが、これだけでは不十分だ。ISRが1(リーダーだけ)に縮んでいると acks=all でも実質1重書き込みになる。そこで min.insync.replicas=2 を併用し、ISRが2未満なら書き込みを拒否(例外)させる。acks=all かつ min.insync.replicas=2(replication factor 3) が、可用性と耐久性のバランスとして分析基盤の標準構成になる。
ISR内に生存レプリカが1つも残らない極限状況で、ISR外の遅れたレプリカをリーダーに昇格させる設定が unclean leader election だ。これを許すと、HWまで追いついていないレプリカが正になり、コミット済みだったレコードが消える(可用性のためにデータ耐久性を犠牲にする)。既定は無効で、分析基盤の真実系列を預けるトピックでは無効のままにするのが原則。可用性が絶対要件のときだけ、喪失を承知で有効化する。
オフセットとコンシューマグループ:誰がどこまで読んだか
キューと違い、消費の進捗はブローカではなく消費側が管理する。各コンシューマは自分が処理し終えたパーティション内の位置をオフセットとして持ち、これをコミットする。コミット先は内部トピック __consumer_offsets(これ自体がログでありコンパクションされる)で、消費者が再起動しても続きから読める。
複数の消費者はコンシューマグループ(consumer group) を組む。グループ内では、トピックの各パーティションがちょうど1つのコンシューマに割り当てられる。これにより「グループ全体で各レコードを一度だけ処理」しつつ、パーティション数まで水平スケールできる。別グループは互いに独立なので、同じトピックを複数グループが別々のオフセットで並行消費できる——これがリアルタイム集計と監査を同居させられる仕組みだ。
Topic orders(P0,P1,P2)
group=analytics: C1←{P0,P1} C2←{P2} (2消費者で3パーティションを分担)
group=audit: C1←{P0,P1,P2} (別グループ、独自オフセットで全部読む)
制約: 1グループ内で 1パーティション ↔ 高々1コンシューマ
→ グループの実効並列度 ≦ パーティション数(余ったコンシューマは遊ぶ)
割り当ての再計算がリバランス(rebalance) だ。コンシューマの参加・離脱・障害を検知すると、グループコーディネータがパーティションを割り当て直す。素朴なリバランスはstop-the-worldで、全員が一旦手を止めて再割り当てを待つため消費が途切れる。これを緩和するのが協調的(cooperative/incremental)リバランスで、動かす必要のあるパーティションだけを段階的に移し、無関係なコンシューマは処理を続けられる。
「処理」と「オフセットコミット」は別操作なので、その順序が保証レベルを決める(CDCがログ位置を保存する話と同型:/data-engineering/cdc-log-based/)。処理してからコミットすれば、途中で落ちても未コミット分を再取得できるが重複しうる(at-least-once)。コミットしてから処理すれば重複はないが処理前に落ちると欠落する(at-most-once)。真のexactly-onceは、Kafkaトランザクション(プロデューサの transactional.id)で「消費オフセットのコミット」と「出力の書き込み」を1つのアトミックな単位にまとめ、消費側を isolation.level=read_committed にして初めて成立する(/data-engineering/exactly-once-streaming/)。
順序保証とスループット:切り離せないトレードオフ
Kafkaの順序保証は明快だが限定的だ。全順序が成り立つのは1パーティション内だけ。パーティション内ではオフセット順=書き込み順が保証されるが、異なるパーティション間には全順序が存在しない。トピック全体の順序を保証したければパーティションを1本にするしかなく、その瞬間に並列度を失いスループットが頭打ちになる。
| スコープ | 順序保証 | スループットへの含意 |
|---|---|---|
| 同一パーティション内 | オフセット順=書き込み順の全順序 | 1パーティションの処理能力が上限 |
| 同一キー(キー付き) | 常に順序を維持できる | 同一キーは同一パーティションへ集まるため、キーの偏りがスキューを生む |
| 異なるパーティション間 | 全順序は保証されない | 並列に書き・読みできるためスループットが伸びる |
| トピック全体で全順序 | パーティション1本なら可能 | 並列度1に落ち、スループットが最小になる |
実務の定石はキーで順序の粒度を設計することだ。順序が要るのは通常「同一エンティティ内」——ある注文ID、あるユーザ内での出来事の順序——であって、無関係なエンティティ間の全順序は要らない。だから順序を保ちたい単位をキーにすれば、hash(key) mod N で同一キーが同一パーティションに集まり、その内部で順序が守られる。異なるキー同士は別パーティションで並列に流れ、スループットが出る。順序の必要範囲を最小のキーに畳むことが、順序と並列性を両立させる唯一の道だ。
もう一つスループットを支えるのがバッチングと圧縮である。プロデューサはレコードを即送らず、linger.ms の間だけ溜めてまとめて送る。1リクエストあたりのレコード数が増えるほど1件あたりの固定コスト(ネットワーク往復・ブローカ処理)が薄まり、圧縮(gzip/lz4/zstd等)もバッチ単位でかけると効率が上がる。レイテンシ(linger を短く)とスループット(linger を長くまとめる) はここでも取引関係にある。ただしプロデューサ内でリトライと並行送信を許すと再送で順序が乱れうるため、順序厳守なら冪等プロデューサ(enable.idempotence=true)を使う——これはシーケンス番号でブローカ側の重複排除と順序維持を保証する。
ログ保持とコンパクション:捨て方の二系統
「消えないログ」といっても無限には貯められない。保持(retention)の戦略は二系統あり、トピックごとに選ぶ。ここは分析基盤の設計判断そのものになる。
系統は次のように分岐する。
- 削除(delete)方式:時間(
retention.ms、例:7日)またはサイズ(retention.bytes)の閾値を超えた古いセグメントごと丸ごと消す。イベントストリームの標準で、「直近N日ぶんのイベント列」を保持する用途に向く。何が書かれていたかを問わず、古さだけで捨てる。 - コンパクション(compact)方式:時間ではなくキーで間引く。同一キーについて最新の値だけを残し、それより古い同一キーのレコードを回収する。結果として「各キーの現在値」がログとして残り続ける。これは実質的にキー・バリューの最新状態テーブルをログで表現したものだ。
delete方式(時間軸で捨てる)
[k=A,v=1][k=B,v=2][k=A,v=3][k=B,v=4]... → 7日超のセグメントを丸ごと削除
compact方式(キー最新だけ残す)
圧縮前: [A:1][B:2][A:3][C:5][B:4][A:9]
圧縮後: ................[C:5][B:4][A:9] ← 各キーの最新のみ生存
tombstone: [A:null] を書くと A はコンパクションで最終的に物理削除される
コンパクションの威力は、ログを畳めば最新状態テーブルが得られる点にある。CDCで業務DBの変更を主キー付きで流し込み、コンパクションを効かせれば、Kafka上に「各行の現在値」を保つ複製テーブルが増分で維持できる(/data-engineering/cdc-log-based/)。削除はtombstone(値がnullのレコード)で表現し、コンパクションがこれを見て該当キーを最終的に物理削除する。__consumer_offsets がコンパクション対象なのも同じ理屈で、必要なのは各グループ×パーティションの最新オフセットだけだからだ。
押さえどころ。(1) Kafkaの実体は追記専用の分散コミットログで、消費は非破壊・再生可能。(2) パーティションが並列・順序・複製の単位で、オフセットはパーティション内でのみ一意。(3) 複製はリーダー+フォロワー、コミットはISR全員への複製で定義され、可視境界がハイウォーターマーク。障害時はISR内から昇格するのでコミット済みは失われない。(4) 耐久性は acks=all × min.insync.replicas=2(RF=3) で決まり、unclean leader election を許すと喪失。(5) 順序はパーティション内のみ全順序、キーで粒度を設計してスループットと両立。(6) 保持はdelete(時間・サイズ) と compact(キー最新のみ) の二系統で、コンパクションは最新状態テーブルを表現しCDCの土台になる。よくある誤りは「トピック全体で順序が保証される」「acks=all だけで安全」の二つ。
まとめ
- Kafkaは追記専用の分散コミットログであり、分析基盤ではイベント収集・CDC配送・ストリーム入出力を束ねる背骨になる。消費が非破壊で任意位置から再生できるため、リアルタイムとバッチ再処理を同じイベント列で賄える。
- 物理実体はパーティション。並列・順序・複製すべての単位で、
hash(key) mod Nで振り分けられ、オフセットはパーティション内でのみ一意。各パーティションはセグメント列+疎なインデックスで実装され、シーケンシャルIO・ページキャッシュ・ゼロコピーで高スループットを出す。 - 複製はリーダー+フォロワーで、コミットはISR全レプリカへの複製、可視境界がハイウォーターマーク。障害時はISR内から昇格するのでコミット済みは失われない。耐久性は
acks=all×min.insync.replicasの組で決まる。 - 消費の進捗は消費側がオフセットで管理し、コンシューマグループで1パーティション↔高々1コンシューマに割り当てて水平スケールする。実効並列度はパーティション数が上限。オフセットコミットの順序が配信保証(at-least/at-most-once)を左右する。
- 順序の全順序はパーティション内のみ。トピック全体の順序は並列度1と引き換え。順序が要る単位をキーに畳むことでスループットと両立させる。バッチング・圧縮・冪等プロデューサがスループットと順序を支える。
- 保持はdelete(時間・サイズで丸ごと削除) と compact(キーの最新値のみ残す) の二系統。コンパクションは最新状態テーブルをログで表現し、tombstoneで削除を伝え、CDCの複製テーブルの土台になる。
データ工学 Article
Kafkaの内部構造を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
Kafka
比較で見る軸
難易度: advanced / カテゴリ: データ工学 / タグ数: 6
導入後に効く点
各パーティションはリーダー1つとフォロワー数個に複製され、追随できているレプリカ集合ISRだけがコミット対象になる。プロデューサのacksとブローカのmin.insync.replicasの組でどこまで永続化を待つかが決まり、リーダー障害時はISR内から新リーダーを選びハイウォーターマーク以下だけを可視化するのでデータを失わない。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- データ工学
- タグ数
- 6
判断チェックリスト
- 自社の用途が「Kafka / 分散コミットログ」に近いか確認する。
- 強みである「Kafkaの本体は追記専用の分散コミットログである。トピックは複数パーティションに分割され、各パーティションは末尾追記のログファイル(セグメント列+疎なインデックス)として実装される。書き込みは常に末尾追記なのでシーケンシャルIOになり、ページキャッシュとゼロコピー送出でディスクベースながら高スループットを出す。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。