KafkaのExactly-Onceセマンティクスを徹底解明:包括的なガイド
冪等プロデューサー、トランザクション、read_committedコンシューマー、オフセットコミットを用いたKafkaのExactly-Onceセマンティクスを理解します。
KafkaのExactly-Onceセマンティクスを解き明かす:包括的ガイド
KafkaのExactly-Onceセマンティクスは、プロデューサーがリトライしたり、ブローカーがフェイルオーバーしたり、アプリケーションが再起動した際に、ストリーム処理パイプラインが重複した出力レコードを生成するのを防ぐことができます。この保証は強力ですが、その言葉が示すほど広範囲ではありません。Kafkaは、Kafkaへの書き込みと消費されたオフセットをトランザクションにすることができます。外部のデータベース、支払いゲートウェイ、HTTP APIを自動的にExactly-Onceにすることはできません。
重複した出力が高コストであったり、クリーンアップが困難な場合(在庫調整、口座残高イベント、他のサービスが消費する派生状態トピックなど)に、Exactly-Onceセマンティクスを使用してください。
配信保証を平易な言葉で
Kafkaアプリケーションは通常、3つの配信モデルについて説明します。
- At-most-once:アプリケーションはレコードを失う可能性がありますが、同じレコードを2回処理することはありません。これは、処理が完了する前にオフセットがコミットされた場合に発生する可能性があります。
- At-least-once:アプリケーションはレコードを失うべきではありませんが、リトライや再起動後に同じレコードを複数回処理する可能性があります。
- Exactly-once:Kafkaの読み取り-処理-書き込みループが、出力レコードと消費されたオフセットを1つのトランザクションとしてコミットします。
最後のポイントが鍵です。Exactly-Onceセマンティクスは、アプリケーションがKafkaから読み取り、結果をKafkaに書き戻し、同じトランザクション内でオフセットをコミットする場合に最も強力です。
冪等プロデューサー
冪等プロデューサーは、プロデューサーのリトライによって引き起こされる重複書き込みを防ぎます。KafkaはプロデューサーにIDを割り当て、プロデューサーとパーティションごとにシーケンス番号を追跡します。ブローカーがすでにバッチを受け入れ、その後リトライを受け取った場合、再度追加する代わりに重複を拒否できます。
現在のKafkaクライアントでは、競合するプロデューサー設定を構成しない場合、冪等性はデフォルトで有効になります。明示的に設定することもできます:
enable.idempotence=true
acks=all
acks=allは、リーダーが書き込みを確認する前にすべての同期レプリカを待つことを意味します。冪等性は、互換性のあるリトライとインフライトリクエストの設定にも依存するため、クライアントバージョンでの影響を理解していない限り、プロデューサーの信頼性設定を上書きしないでください。
冪等性はプロデューサーのリトライを保護しますが、処理ワークフロー全体をアトミックにするわけではありません。アプリケーションがあるトピックから消費し、別のトピックに生成する場合、出力とオフセットコミットを結びつけるためにトランザクションが必要です。
Kafkaトランザクション
トランザクションにより、1つのプロデューサーが複数の書き込みをアトミックユニットにグループ化できます。プロデューサーには安定したtransactional.idが必要です。
transactional.id=inventory-adjuster-0
enable.idempotence=true
acks=all
典型的なトランザクションフローは次のとおりです:
- アプリケーション起動時にトランザクションを初期化します。
- トランザクションを開始します。
- 入力トピックからレコードを消費します。
- 出力レコードを生成します。
- 消費されたオフセットをトランザクションに送信します。
- トランザクションをコミットするか、失敗した場合は中止します。
コミット前にプロセスがクラッシュした場合、Kafkaは未コミットの出力をread_committedコンシューマーに公開しません。再起動時に、アプリケーションは同じ入力レコードを再度読み取り、1つのコミットされた結果を生成できます。
重要なコンシューマー設定
トランザクション出力を読み取るコンシューマーは、次を使用する必要があります:
isolation.level=read_committed
enable.auto.commit=false
read_committedは、中止されたトランザクションからのレコードを隠します。enable.auto.commit=falseは、コンシューマーがトランザクション外でオフセットをコミットするのを防ぎます。
プロパティ名が重要です。Kafkaのコンシューマー設定はenable.auto.commitであり、auto.commit.enableではありません。
手動のコンシューマー-プロデューサーアプリケーションの場合、オフセットコミットはプロデューサートランザクションの一部である必要があります。Javaクライアントでは、トランザクショナルプロデューサーAPIを使用することを意味し、コミット前にトランザクションにオフセットを送信することを含みます。
具体的なシナリオ
ordersトピックとinventory-events出力トピックを想像してください。サービスは注文を読み取り、SKUを確認し、在庫控除イベントを書き込みます。
トランザクションがない場合、出力を書き込んだ後、入力オフセットをコミットする前にクラッシュすると、再起動後に重複した控除が作成される可能性があります。トランザクションがある場合、出力イベントと入力オフセットコミットは一緒に成功するか失敗します。再起動により注文が再読み取りされる可能性がありますが、ダウンストリームのread_committedコンシューマーには1つのコミットされた在庫イベントのみが表示されます。
覚えておくべき制限
KafkaのExactly-Onceセマンティクスは、特別に設計しない限り、Kafka外部の副作用をカバーしません。同じサービスがPostgreSQLに書き込んだり、課金APIを呼び出したりする場合、その外部副作用には独自の冪等性キー、一意制約、トランザクション戦略、またはアウトボックスパターンが必要です。
トランザクションは調整オーバーヘッドも追加します。重複が許容される単純なログ取り込みの場合、冪等プロデューサーとat-least-onceコンシューマーで十分な場合があります。
実用的なチェックリスト
アプリケーションインスタンスまたはタスクごとに安定したtransactional.idを使用します。2つのライブプロデューサーが同時に同じトランザクションIDを使用しないようにします。
トランザクション出力のコンシューマーをread_committedに設定します。トランザクション処理ループで自動オフセットコミットを無効にします。
トランザクションを短く保ちます。大規模なトランザクションはレイテンシを増加させ、リカバリを遅くする可能性があります。
外部システムは個別に扱います。KafkaはKafkaの状態を保護できますが、データベースへの書き込みには依然として冪等な設計が必要です。
有用なポイント:Exactly-Onceセマンティクスは魔法のスイッチではありません。これらは、KafkaからKafkaへのストリーム処理に最適なプロデューサー、コンシューマー、トランザクションの選択肢のセットです。