Kafka はイベントを再生可能なパーティションログとして保存します。これを理解するには、2つの独立した問題から始めることができます。レコードはどこに保存されているか、読者はどこまで読んだかです。前者はブローカーのログとレプリケーション機構が担当し、後者はコンシューマーの位置とコンシューマーグループが担当します。
本稿では、Kafka 4.1 ドキュメントの通常トピック、レプリケーションログ、コンシューマーグループを基準とします。具体的なクライアントのデフォルト値やクラスター機能は、デプロイバージョンに応じて確認してください。Share group は、本稿のパーティション排他的消費モデルには含まれません。
ログが複数の独立した読者に適している理由
注文サービスが順に3つのイベント、すなわち「作成」「支払い」「出荷」を生成すると仮定します。在庫サービスは在庫に関心を持ち、集計サービスは売上高に関心を持ち、監査サービスは完全な履歴を保持する必要があります。これらは速度が異なり、それぞれ再起動する可能性があります。
「ある読者が読んだら」イベントを削除すると、他の読者は独立して進捗できなくなります。ログモデルは保持ポリシーと消費進捗を分離します。イベントはログに残り続け、各読者は独自の位置を維持します。コンシューマーは復元時にカーソルを移動するだけでよく、プロデューサーに履歴全体を再送信させる必要はありません。
オフセットはパーティション内の位置識別子であり、グローバルなメッセージIDでも、「残りメッセージ数」でもありません。ログ圧縮やトランザクション制御レコードなどの要因により、可視なビジネスレコードのオフセットは連続しない場合があります。
Topic、Partition、Replica の役割分担
| 概念 | 役割 | 混同してはならない境界 |
| Topic | イベントストリームに名前を付け、保持などのポリシーを構成する | 1つのトピックには複数の独立した順序付きパーティションを持つことができる |
| Partition | 追記とソートの基本単位 | パーティション間にデフォルトの全順序はない |
| Replica | 特定のパーティションのコピーを保存する | コピーを増やすのは主に冗長性を高め、書き込みシャードを増やすこととは異なる |
| Broker | 複数のパーティションコピーをホストし、リクエストに応答する | 1台のブローカーは通常、異なるパーティションのリーダー/フォロラーの役割を同時に担う |
| Controller | クラスターのメタデータとリーダー権限の変更を管理する | コントロールプレーンのメタデータ合意は、各ビジネスレコードがコントローラーログに書き込まれることを意味しない |
パーティション数は、データがいくつの書き込みおよび消費ユニットに分散されるかを決定します。レプリカ数は、同じデータがいくつのコピーとして保存されるかを決定します。両方ともリソースを消費しますが、解決する問題は異なります。KRaft コントロールプレーンはメタデータ管理を担当し、データパーティションは独自のレプリケーションパスを持ちます。Kafka KRaft を参照してください。
1つのレコードの書き込みパス
クライアントのバッチ処理はネットワークとプロトコルのオーバーヘッドを薄めることができますが、バッチ処理を待つことでレイテンシが増加する可能性があります。圧縮は通常バッチに対して適用されます。パーティション数が増えると、トラフィックがより小さなバッチに分割される可能性があります。したがって、「パーティション数が多いほど速い」とは限りません。ホットスポット、バッチサイズ、ファイルディスクリプタ、レプリケーション接続、リカバリコストも考慮する必要があります。
同じビジネスエンティティを順序立てて処理する必要があるストリームでは、order_id をキーとしてよく使用します。順序の保証は、パーティショナー、キーのシリアライズ、パーティション数が一貫していることに依存します。パーティション数を変更するとマッピングが変わり、同じキーの新旧レコードが異なるパーティションに属する可能性があります。移行時には順序を個別に考慮する必要があります。
キーなしのレコードの割り当てはクライアントの実装に依存し、一律に1件ごとのラウンドロビンとして記述することはできません。Java クライアントのバッチ処理とパーティショニング戦略については、プロデューサー設定 を参照してください。同じキーは、順序付けが行われるパーティションを特定するだけです。複数の独立したプロデューサー間のビジネス上の前後関係は、アプリケーション側で構築する必要があります。
レプリケーション、ISR、および確認条件
リーダーは書き込みを受け付け、フォロワーは継続的にプルします。ISR(In-Sync Replicas)は、追いついていると見なされるレプリカの集合であり、リーダーを含みます。レプリカが集合に残るかどうかは、レプリケーションの進捗状況と関連する設定によります。
| 設定 | プロデューサーが成功を受け取るタイミング | 理解すべき代償 |
acks=0 | ブローカーの確認を待たない | 送信成功は、ブローカーが受信したことを証明しない |
acks=1 | リーダーが書き込みを受け入れた後 | リーダー障害時に、まだレプリケートされていないデータが失われる可能性がある |
acks=all | 現在の ISR のレプリケーション確認要件を満たした場合 | 確認待ちとレプリカの可用性が結合されている |
min.insync.replicas は acks=all と組み合わせて、書き込みを受け入れるための最小 ISR 条件を設定します。これは「2 に設定すると任意の2つのレプリカだけを待つ」という意味ではありません。例えば、レプリケーション係数が3で最小 ISR が2の場合:ISR が1つのレプリカだけになったとき、システムはこの種の書き込みを拒否し、単一レプリカでの成功を十分な冗長性の成功として偽装しません。詳細なセマンティクスについては Kafka Replication を参照してください。
「複数のノードにレプリケートする」と「各メッセージが各ディスク上で fsync を実行済みである」を区別する必要があります。確認ポリシーは、ログのフラッシュ、フェイルオーバードメイン、電源、リカバリポリシーを離れて、絶対的な永続性として単独で解釈することはできません。遅延レプリカをリーダーとして昇格させるリカバリポリシーは、確認済みレコードの損失を許容して可用性を得ることもあります。
3つの位置:末尾、レプリケーションコミット、トランザクション可視性
Kafka の読み書き位置は同じ数字ではありません。以下では「次の位置」を境界とし、概念的に簡略化します。
- LEO(Log End Offset):あるレプリカのログ末尾の次の位置。レプリカごとに異なる場合があります。
- HW(High Watermark):レプリケーションコミット境界。通常の読み取りでは、コミットされていないレプリケーション末尾を超えて読み取ることはできません。
- LSO(Last Stable Offset):トランザクション安定境界。
read_committed での読み取りはこれによって制限され、中止されたトランザクションがスキップされます。
この区別は、以下の現象を説明できます。ディスク上のログはすでに成長していますが、コンシューマーは後続のレコードを一時的に見ることができません。その理由は、レプリケーションの進捗状況や、前面の未完了のトランザクションのいずれかです。「ファイルにこのメッセージがあるかどうか」だけを見ても、それがコンシューマーに対して可視であるかどうかを判断することはできません。
同じ HW なのに、なぜ2つの分離レベルで異なるレコードが読めるか
図の位置を引き続き使用します。10、11 は通常のレコード、トランザクション T は 12 から始まり、13 はまた通常のレコードです。HW=14 は、これらの位置がすでにレプリケーションコミット範囲に入ったことを示していますが、T はまだ完了していません。read_uncommitted は 10、11、12、13 を返すことができますが、read_committed は 10、11 しか返すことができません。13 は T に属していませんが、前面のトランザクション安定境界を超えることはできません。
以下では、境界を入力として可視性フィルタリングをシミュレートします。トランザクションの状態 committed または aborted は、完了フラグが安定条件に達したことを示しており、プロデューサーがコミットリクエストをただ送信しただけではありません。モデルはブローカーのレプリケーションとトランザクション調整プロセスを省略しています。
records = [(10, None), (11, None), (12, "T"), (13, None)]
def visible(records, hw, transactions, committed_only):
first_open = [first for first, status in transactions.values()
if status == "open"]
lso = min([hw] + first_open)
boundary = lso if committed_only else hw
return [offset for offset, tx in records
if offset < boundary
and (not committed_only or tx is None
or transactions[tx][1] == "committed")]
assert visible(records, 14, {"T": (12, "open")}, False) == [10, 11, 12, 13]
assert visible(records, 14, {"T": (12, "open")}, True) == [10, 11]
assert visible(records, 15, {"T": (12, "committed")}, True) == [10, 11, 12, 13]
assert visible(records, 15, {"T": (12, "aborted")}, True) == [10, 11, 13]
assert visible(records, 11, {"T": (12, "open")}, True) == [10]
assert visible([], 0, {}, True) == []
最後の2つの完了シナリオでは、オフセット 14 にトランザクション完了フラグが格納され、HW がすでに 15 に到達していると仮定しています。フラグはビジネスレコードとしてモデルに含めません。T が中止になると、12 はフィルタリングされ、13 は可視性を回復するため、読み取り結果にギャップが生じます。このギャップは、コンシューマーが処理すべきレコードを見落としたことを意味するものではありません。
ここでは LSO を一貫して排他的な上限として扱う。読めるのは offset が LSO 未満のレコードであり、未完了のトランザクションが 12 から始まるなら境界は 12 である。read_committed での endOffsets() は min(HW, 最も早い未完了トランザクションの開始 offset) を返す。KafkaConsumer の公式仕様を参照。したがって、LSO を基準とする lag が 0 でも、未完了のトランザクションによって、すでに複製済みの末尾まで読めない場合がある。
トランザクション安定境界(LSO)の定義
LSO は排他的上限として統一されています。オフセットが LSO より小さいレコードを読み取ります。未完了のトランザクションが 12 から始まる場合、境界は 12 です。read_committed モードでの endOffsets() は min(HW, 最も早い未完了トランザクションの起点) を返します。詳細は KafkaConsumer のトランザクション読み取りと末尾位置の定義 を参照してください。これはまた、LSO に対するラグがゼロであることだけを見ても、レプリケーションログの末尾にトランザクションによってブロックされた蓄積がないことを証明できない理由も説明しています。
レコードはすでにレプリケートされているのに、なぜまだ読めないのか?
「トランザクションのコミットを要求する」と「コミットフラグがすでに安定している」を区別する必要があります。以下では3つの独立した履歴を比較します。完了履歴にはオフセット 14 にフラグがありますが、HW が 15 に到達して初めて、トランザクションが安定して終了したと見なされます。すべての可視性境界は排他的上限です。中止後のギャップはフィルタリングによるもので、コンシューマーが決して配信されないオフセット 12 を待つべきではありません。
図を準備しています
レコードはすでにレプリケートされているのに、なぜまだ読めないのか?read_committed は排他的な LSO によって制限されます。未完了のトランザクションは後続の通常のレコードをブロックします。中止後、トランザクションレコードはフィルタリングされ、後続の通常のレコードは可視性を回復します。
{"id":"kafka-transaction-visibility","title":"レコードはすでにレプリケートされているのに、なぜまだ読めないのか?","summary":"read_committed は排他的な LSO によって制限されます。未完了のトランザクションは後続の通常のレコードをブロックします。中止後、トランザクションレコードはフィルタリングされ、後続の通常のレコードは可視性を回復します。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"presets\" role=\"group\" data-i18n-label=\"mode\"><button data-mode=\"open\" data-i18n=\"open\"></button><button data-mode=\"committed\" data-i18n=\"committed\"></button><button data-mode=\"aborted\" data-i18n=\"aborted\"></button></div><div class=\"control\"><label for=\"hw\" data-i18n=\"hw\"></label><output for=\"hw\" id=\"hw-value\"></output><input id=\"hw\" type=\"range\" min=\"10\" max=\"15\" value=\"14\" step=\"1\"></div><div class=\"metrics\"><div><span data-i18n=\"leo\"></span><strong id=\"leo\"></strong></div><div><span data-i18n=\"lso\"></span><strong id=\"lso\"></strong></div></div><div class=\"track-row\"><span data-i18n=\"uncommitted\"></span><div id=\"raw\" class=\"track\"></div></div><div class=\"track-row\"><span data-i18n=\"visible\"></span><div id=\"visible\" class=\"track\"></div></div><div id=\"marker\" class=\"formula\"></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"waiting\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"done\"></p></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n\n.track .cell{min-height:58px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let mode='open';function draw(){const leo=mode==='open'?14:15;let hw=Math.min(leo,+$('#hw').value);$('#hw').value=hw;set('hw-value',hw);const stable=mode!=='open'&&hw===15,lso=stable?hw:Math.min(hw,12);set('leo',leo);set('lso',lso);const rows=[10,11,12,13];for(const [id,ok]of [['raw',x=>x<hw],['visible',x=>x<lso&&!(x===12&&mode==='aborted'&&stable)]])cells(id,rows.map(x=>({text:x+(x===12?' · T':''),cls:ok(x)?'on':'empty'})));$('#visible').dataset.offsets=rows.filter(x=>x<lso&&!(x===12&&mode==='aborted'&&stable)).join(',');set('marker','14: '+(mode==='open'?'—':mode.toUpperCase())+'; HW='+hw);set('explanation',t(stable?'done':'waiting'));pressed(mode);}document.querySelectorAll('[data-mode]').forEach(e=>e.onclick=()=>{mode=e.dataset.mode;draw();});$('#hw').oninput=draw;draw();","audio":false,"strings":{"aborted":"中止フラグを追記","committed":"コミットフラグを追記","done":"完了フラグはすでに安定しています。コミットは 12 を保持し、中止は 12 をフィルタリングし、通常のレコード 13 は可視です。","heading":"レコードはすでにレプリケートされているのに、なぜまだ読めないのか?","hw":"レプリケーション境界 HW","intro":"10、11 は通常のレコード、12 はトランザクション T に属し、13 はまた通常のレコードです。独立したトランザクション履歴を選択し、レプリケーション境界 HW を変更します。","leo":"LEO","lso":"LSO","mode":"独立した履歴を比較","open":"未決定","reset":"リセット","scope":"タブを切り替えて異なる実行を比較します。実際のトランザクションが終状態を変更することはできません。単一パーティションの固定ログ。open 時 LEO=14;完了フラグは 14 に追記され、HW=15 でなければ安定完了と見なされません。フラグはビジネスデータとして返されません。実際のブローカー調整は省略。","scopeLabel":"モデルの範囲と規約","uncommitted":"read_uncommitted","visible":"read_committed","waiting":"完了フラグはまだ安定していません。LSO はトランザクションの起点 12 を超えることはできません。"}}
消費位置とコミット済み位置
1回の poll() は複数のレコードを返し、クライアントの読み取り位置は随之進みますが、ビジネス処理は必ずしも完了していません。コミット済みオフセットは障害復旧時の進捗の約束であり、これらは分離されるべきです。
通常のコンシューマーグループでは、1つのパーティションは同時に1つのメンバーに割り当てられ、異なるグループは同じパーティションを独立して読み取ることができます。クライアントはオフセットに従って読み取りますが、非同期ワースレッドが同じ順序で完了することを自動的に保証するものではありません。引き継ぎプロセスと並列コミットルールについては、コンシューマーグループとコラボレーション を参照してください。
保持(Retention)とログ圧縮(Log Compaction)
2つのクリーンアップ方法は、2つのデータ用途に対応しています。
| 方法 | 注目点 | 表現に適したもの |
| 時間またはサイズに基づいて古いログセグメントを削除 | 履歴をどのくらい保持するか | 期限付きのイベント再生 |
| キーに基づいて古いバージョンを圧縮 | 各キーの最新状態 | 変更ログから状態を再構築 |
圧縮はバックグラウンドで行われ、読み取り時に各キーが1回だけ出現することが保証されるわけでも、残りのレコードの順序が入れ替えられたり再番号付けされたりするわけでもありません。削除状態は通常、tombstone(墓石レコード)によって表現され、対応する保持期間を過ぎると、tombstone 自体もクリーンアップされる可能性があります。遅れすぎた読者は、残りのログだけでは正しい削除履歴を復元できない場合があります。Log Compaction には、これらの保証の詳細が説明されています。
したがって、再生可能かどうかは保持の境界にかかり、状態を再構築できるかどうかは圧縮キーと削除セマンティクスにかかります。イベント監査と最新状態の同期は、「Kafka に存在する」というだけでは同じニーズとして見なすことはできません。
削除マークが消えた後、空のデータベースからの再構築と古いスナップショットからの継続は等価ではない
ログが順に 0:A=1、1:B=7、2:A=2、3:B=null であると仮定します。最後のレコードで B が削除されます。以下では、「すべての古いバージョンがすでにクリーンアップされた」結果をシミュレートしますが、実際のクリーナーの非同期スケジューリングは意図的にシミュレートしません。
log = [(0, "A", 1), (1, "B", 7), (2, "A", 2), (3, "B", None)]
def compact_snapshot(records, keep_tombstones):
latest = {}
for record in records:
latest[record[1]] = record
return sorted((record for record in latest.values()
if keep_tombstones or record[2] is not None),
key=lambda record: record[0])
def replay(initial, records):
state = dict(initial)
for _, key, value in records:
if value is None:
state.pop(key, None)
else:
state[key] = value
return state
with_delete = compact_snapshot(log, True)
without_delete = compact_snapshot(log, False)
assert with_delete == [(2, "A", 2), (3, "B", None)]
assert without_delete == [(2, "A", 2)] assert replay({}, log) == replay({}, without_delete) == {"A": 2}
stale_snapshot = {"A": 1, "B": 7}
assert replay(stale_snapshot, with_delete) == {"A": 2}
assert replay(stale_snapshot, without_delete) == {"A": 2, "B": 7}
古いスナップショット内の B は、ログに「B がない」からといって自動的に削除されません。コンシューマーは「B は一度も変更されていない」と「B は削除されたが、削除の証拠が期限切れになった」を区別できません。したがって、復元プロトコルでは、スナップショットに対応するオフセット、増分ログの利用可能な範囲、および削除の保持範囲を一緒に合意する必要があります。単にブローカーに接続できるかどうかを確認するだけでは不十分です。
ここで空のデータベースからの再構築が正しい結果を得られるのは、入力が固定された完全な圧縮後の集合であるためです。実際の再構築中、クリーナーは引き続き実行されています。読者が B の古い値を先に読み取り、その後クリーンアップされる tombstone を読む時間がなかった場合、誤った状態を保持する可能性があります。したがって、空のデータベースから開始する場合は、読み取り進捗と削除の保持条件を満たす必要があり、モデルの等式を無限時間の復旧の保証として扱うことはできません。関連する条件については Log Compaction の保証 を参照してください。
アーキテクチャの推論:注文フローの役割分担
合理的な分割は、注文 ID でパーティション分けし、同じ注文のイベントにパーティション内順序を持たせることです。在庫グループと集計グループは各自が進行状況をコミットします。監査ニーズは履歴の保持期間を決定し、レプリケーションと確認ポリシーは障害時の書き込みの約束を決定します。
ある大口顧客がトラフィックの半分を生成する場合、そのキーはホットスポットを形成する可能性があります。コンシューマーを増やしても、排他的なパーティションを自動的に分割することはできません。より細かいキーを使用すると並列度が向上しますが、順序を保つ必要があるビジネスの範囲を再定義する必要があります。パフォーマンスのトレードオフは、設定を単に大きくするだけでなく、ビジネスの関係性から生じます。
参照
キーワード:Kafka、partition、replica、ISR、acks、offset、HW、LSO、retention、log compaction