では 、前のブログ記事は、我々は、ApacheKafka®のための一度だけのセマンティクスを導入しました。その投稿では、さまざまなメッセージ配信セマンティクスを取り上げ、べき等プロデューサー、トランザクション、およびKafka Streamsの1回だけ処理されるセマンティクスを紹介しました。次に、中断したところからピックアップして、Apache Kafkaのトランザクションについて詳しく説明します。このドキュメントの目的は、Apache KafkaでトランザクションAPIを効果的に使用するために必要な主な概念を読者に理解してもらうことです。
トランザクションAPIが設計された主なユースケース、Kafkaのトランザクションセマンティクス、JavaクライアントのトランザクションAPIの詳細、実装の興味深い側面、そして最後に、APIを使用する際の重要な考慮事項について説明します。
このブログ投稿は、トランザクションの使用に関する詳細についてのチュートリアルを意図したものではありません。また、設計の要点についても詳しく説明しません。代わりに、JavaDocsまたは設計ドキュメントにリンクし、必要に応じて、さらに深く読みたい読者に適しています。
読者は、トピック、パーティション、ログオフセット、Kafkaベースのアプリケーションでのブローカーとクライアントの役割など、Kafkaの基本的な概念に精通している必要があります。JavaのKafkaクライアントに精通していることも役立ちます。
なぜトランザクション?
Kafkaのトランザクションは、主に、読み取りと書き込みがKafkaトピックなどの非同期データストリームとの間で行われる「読み取りプロセス書き込み」パターンを示すアプリケーション向けに設計されています。このようなアプリケーションは、ストリーム処理アプリケーションとして広く知られています。
第一世代のストリーム処理アプリケーションは、不正確な処理を許容できます。たとえば、Webページインプレッションのストリームを消費し、Webページごとのビューの合計数を生成するアプリケーションは、数のエラーを許容できます。
しかし、これらのアプリケーションの人気とともに、より強力なセマンティクスを持つストリーム処理アプリケーションの需要が高まっています。たとえば、一部の金融機関は、ストリーム処理アプリケーションを使用して、ユーザーアカウントの借方と貸方を処理します。これらの状況では、処理のエラーは許容されません。例外なく、すべてのメッセージを1回だけ処理する必要があります。
より正式には、ストリーム処理アプリケーションがメッセージA を消費し 、B = F(A)のような メッセージBを生成 する場合、 1回だけの処理は、B が正常に生成された 場合にのみAが消費され たと見なされ 、その逆の場合も同様です。
少なくとも1回の配信セマンティクスに設定されたバニラKafkaプロデューサーとコンシューマーを使用すると、ストリーム処理アプリケーションは、次の方法でセマンティクスを一度だけ処理すると失われる可能性があります。
- producer.send()は 、内部再試行が原因でメッセージBの重複した書き込みを引き起こす可能性があります 。これはべき等プロデューサーによって対処されており、この投稿の残りの部分の焦点ではありません。
- 入力メッセージAを再処理すると 、重複した B メッセージが出力に書き込まれ、1回だけの処理のセマンティクスに違反する可能性があります。ストリーム処理アプリケーション がBの書き込み後、A を消費済みとマークする前に クラッシュすると、再処理が発生する可能性があります 。したがって、再開すると、Aを 再度消費 し、Bを 再度書き込み 、重複を引き起こします。
- 最後に、分散環境では、アプリケーションがクラッシュするか、さらに悪いことに、システムの他の部分への接続が一時的に失われます。通常、失われたと見なされたインスタンスを置き換えるために、新しいインスタンスが自動的に開始されます。このプロセスを通じて、同じ入力トピックを処理し、同じ出力トピックに書き込む複数のインスタンスが存在する可能性があります。これにより、出力が重複し、1回だけ処理されるセマンティクスに違反します。これを「ゾンビインスタンス」の問題と呼びます。
2番目と3番目の問題を解決するために、KafkaでトランザクションAPIを設計しました。トランザクションは、これらのサイクルをアトミックにし、ゾンビフェンシングを容易にすることで、読み取りプロセスと書き込みサイクルで1回限りの処理を可能にします。
トランザクションセマンティクス
アトミックなマルチパーティション書き込み
トランザクションにより、複数のKafkaトピックおよびパーティションへのアトミックな書き込みが可能になります。トランザクションに含まれるすべてのメッセージが正常に書き込まれるか、どれも書き込まれません。たとえば、処理中にエラーが発生すると、トランザクションが中止される可能性 があります。その場合、トランザクションからのメッセージは、コンシューマが読み取ることができません。これにより、アトミックな読み取り/処理/書き込みサイクルがどのように可能になるかを見ていきます。
最初に、アトミックな読み取りプロセス書き込みサイクルの意味を考えてみましょう。一言で言えば、それは、もしアプリケーションがメッセージを消費することを意味 Aを オフセットに X いくつかのトピックパーティションの TP0、 およびメッセージの書き込み Bを トピックパーティションに TP1 メッセージにいくつかの処理を行った後に A ように B = F(A) 、次いで、読み取りプロセスと書き込みサイクルは、メッセージAとBが正常に消費およびパブリッシュされたと見なされるか、またはまったく見なされない場合にのみアトミックです。
ここで、メッセージ A は 、オフセットX が消費済みとしてマークされている場合にのみ、 トピックパーティションtp0から消費されたと見なさ れます。オフセットを消費済みとしてマークすることを、オフセットのコミットと呼び ます。 Kafkaでは、offsets topicと呼ばれる内部のKafkaトピックに書き込むことにより、オフセットコミットを記録し ます。メッセージは、そのオフセットがオフセットトピックにコミットされている場合にのみ消費されたと見なされます。
したがって、オフセットコミットはKafkaトピックへの別の書き込みであり、メッセージはそのオフセットがコミットされた場合にのみ消費されると見なされるため、複数のトピックおよびパーティションにわたるアトミック書き込みでも、アトミックな読み取りプロセス書き込みサイクルが可能になります。オフセットのコミット オフセットトピックへのXとtp1へ のメッセージB の書き込み は、単一のトランザクションの一部になるため、アトミックになります。
ゾンビフェンシング
各トランザクションプロデューサーにtransactional.idと呼ばれる一意の識別子を割り当てることを要求することで、ゾンビインスタンスの問題を解決します 。これは、プロセスの再起動時に同じプロデューサーインスタンスを識別するために使用されます。
APIでは、トランザクションプロデューサーの最初のオペレーションは、そのtransactional.idをKafkaクラスターに明示的に登録する必要があります。その場合、Kafkaブローカーは、指定されたtransactional.idを使用して開いているトランザクションをチェックし、それらを完了します。また 、transactional.idに関連付けられているエポックを増分します 。エポックは、transactional.idごとに保存される内部メタデータです。
エポックがバンプされると、同じtransactional.idと古いエポックを持つすべてのプロデューサーはゾンビと見なされ、フェンスされます。それらのプロデューサーからの将来のトランザクション書き込みは拒否されます。
トランザクションメッセージの読み取り
ここで、トランザクションの一部として書き込まれたメッセージを読み取るときに提供される保証に注意を向けましょう。
Kafkaコンシューマーは、トランザクションが実際にコミットされた場合にのみ、トランザクションメッセージをアプリケーションに配信します。言い換えると、コンシューマは、開いているトランザクションの一部であるトランザクションメッセージを配信せず、中止されたトランザクションの一部であるメッセージも配信しません。
上記の保証がアトミック読み取りに達していないことは注目に値します。特に、Kafkaコンシューマーを使用してトピックからのメッセージを消費する場合、アプリケーションはこれらのメッセージがトランザクションの一部として書き込まれたかどうかを認識しないため、トランザクションがいつ開始または終了するかがわかりません。さらに、特定のコンシューマーがトランザクションの一部であるすべてのパーティションにサブスクライブすることは保証されておらず、これを発見する方法がないため、単一のトランザクションの一部であったすべてのメッセージが最終的に消費されることを保証するのは困難です。一人の消費者によって。
つまり、Kafkaは、最終的には非トランザクションメッセージまたはコミットされたトランザクションメッセージのみをコンシューマーが配信することを保証します。開いているトランザクションからのメッセージを保留し、中止されたトランザクションからのメッセージを除外します。
JavaのトランザクションAPI
トランザクション機能は、主にサーバー側およびプロトコルレベルの機能であり、それをサポートするクライアントライブラリで使用できます。KafkaのトランザクションAPIを使用するJavaで記述された「読み取りプロセス書き込み」アプリケーションは、次のようになります。
1行目から5行目では、transactional.id構成を指定し、それをinitTransactions APIに登録することにより、プロデューサーをセットアップしてい ます。producer.initTransactions() が返された後、 同じtransactional.idを持つプロデューサーの別のインスタンスによって開始されたトランザクションは閉じられ、フェンスされます。
7-10行目では、KafkaConsumerが非トランザクションメッセージまたはコミットされたトランザクションメッセージのみを入力トピックから読み取るように指定しています。ストリーム処理アプリケーションは通常、複数の読み取りプロセス書き込みステージでデータを処理し、各ステージは前のステージの出力を入力として使用します。read_committed モードを指定することで、 すべてのステージで1回だけ処理を実行できます。
14〜21行目は、読み取り-処理-書き込みループのコアを示しています。レコードを消費し、トランザクションを開始し、消費されたレコードを処理し、処理されたレコードを出力トピックに書き込み、消費されたオフセットをオフセットトピックに送信し、最後にトランザクションをコミットします。上記の保証により、オフセットと出力レコードがアトミックユニットとしてコミットされることがわかります。
トランザクションの仕組み
このセクションでは、上記で紹介したトランザクションAPIによって導入された新しいコンポーネントと新しいデータフローの概要を示します。このテーマをより徹底的に扱うには、元の設計ドキュメントを読むか 、トランザクションが導入された カフカサミットの講演をご覧ください 。
以下のコンテンツの目的は、トランザクションを使用するアプリケーションをデバッグするとき、またはトランザクションを調整してパフォーマンスを向上させるときに、メンタルモデルを提供することです。
トランザクションコーディネーターとトランザクションログ
Kafka 0.11.0のトランザクションAPIで導入されたコンポーネントは、上の図の右側にあるトランザクションコーディネーターとトランザクションログです。
トランザクションコーディネーターは、すべてのKafkaブローカー内で実行されるモジュールです。トランザクションログは内部のカフカトピックです。各コーディネーターは、トランザクションログ内のパーティションのサブセットを所有しています。そのブローカーがリーダーであるパーティション。
すべてのtransactional.idは、単純なハッシュ関数を介してトランザクションログの特定のパーティションにマップされます。つまり、1人のコーディネーターが特定のtransactional.idを所有しています。
このように、Kafkaの堅実なレプリケーションプロトコルとリーダー選出プロセスを活用して、トランザクションコーディネーターが常に利用可能であり、すべてのトランザクション状態が永続的に保存されるようにします。
トランザクションログに は、トランザクションの実際のメッセージではなく、トランザクションの最新の状態のみが保存されることに注意して ください。メッセージは、実際のトピックパーティションにのみ保存されます。トランザクションは、「進行中」、「コミットの準備」、「完了」などのさまざまな状態になる可能性があります。トランザクションログに保存されるのは、この状態と関連するメタデータです。
データフロー
高レベルでは、データフローは4つの異なるタイプに分類できます。
A:プロデューサーとトランザクションコーディネーターの相互作用
トランザクションを実行するとき、プロデューサーは次の時点でトランザクションコーディネーターに要求を行います。
- initTransactions APIは、transactional.idをコーディネーターに登録します。この時点で、コーディネーターはそのtransactional.idで保留中のトランザクションをすべて閉じ、エポックをバンプしてゾンビを隔離します。これは、プロデューサーセッションごとに1回だけ発生します。
- プロデューサーがトランザクションで初めてパーティションにデータを送信しようとすると、最初にパーティションがコーディネーターに登録されます。
- アプリケーションがcommitTransaction または abortTransactionを 呼び出すと 、2フェーズコミットプロトコルを開始する要求がコーディネーターに送信されます。
B:コーディネーターとトランザクションログの相互作用
トランザクションが進行すると、プロデューサーは上記のリクエストを送信して、コーディネーターのトランザクションの状態を更新します。トランザクションコーディネーターは、所有する各トランザクションの状態をメモリに保持し、その状態をトランザクションログに書き込みます(これは3つの方法でレプリケートされるため、永続的です)。
トランザクションコーディネーターは、トランザクションログに対して読み取りと書き込みを行う唯一のコンポーネントです。特定のブローカーに障害が発生すると、新しいコーディネーターが、死んだブローカーが所有するトランザクションログパーティションのリーダーとして選出され、着信パーティションからメッセージを読み取り、それらのパーティションのトランザクションのメモリ内状態を再構築します。
C:プロデューサーが対象のトピックパーティションにデータを書き込む
コーディネーターとのトランザクションで新しいパーティションを登録した後、プロデューサーは通常どおり実際のパーティションにデータを送信します。これはまったく同じ producer.send フローですが、プロデューサがフェンスされていないことを確認するための追加の検証がいくつかあります。
D:トピックとパーティションの相互作用のコーディネーター
プロデューサーがコミット(またはアボート)を開始した後、コーディネーターは2フェーズコミットプロトコルを開始します。
最初のフェーズでは、コーディネーターはその内部状態を「prepare_commit」に更新し、トランザクションログでこの状態を更新します。これが完了すると、トランザクションは何があってもコミットされることが保証されます。
次に、コーディネーターはフェーズ2を開始します。フェーズ2では 、トランザクションの一部であるトピックパーティションにトランザクションコミットマーカーを書き込みます 。
これらの トランザクションのマーカーは、 アプリケーションに公開されていませんが、中に消費者によって使用されている READ_COMMITTED 中止されたトランザクションからのメッセージをフィルタリングするモードと、開いているトランザクション(すなわち、ログに記録されているが、あるものは持っていないの一部であるメッセージを返さないために それらに関連付けられたトランザクションマーカー )。
マーカーが書き込まれると、トランザクションコーディネーターはトランザクションに「完了」のマークを付け、プロデューサーは次のトランザクションを開始できます。
実際の取引
トランザクションのセマンティクスとそれらがどのように機能するかを理解したので、トランザクションを活用するアプリケーションの作成の実際的な側面に注意を向けます。
transactional.idを選択する方法
transactional.idは、ゾンビの防御に大きな役割を果たします。ただし、プロデューサーセッション全体で一貫した識別子を維持し、ゾンビを適切に隔離することは、少し注意が必要です。
ゾンビを適切にフェンシングするための鍵は、指定されたtransactional.idに対して、読み取りプロセスと書き込みサイクルの入力トピックとパーティションが常に同じになるようにすることです。これが当てはまらない場合、トランザクションによって提供されるフェンシングを通じて一部のメッセージがリークする可能性があります。
たとえば、分散ストリーム処理アプリケーションで、トピックパーティション tp0 が元々transactional.id T0によって処理されたとし ます。 その後のある時点で、transactional.id T1 を使用して別のプロデューサーにマップできる場合、 T0 と T1の間にフェンシングはありません 。 そのため、tp0 からのメッセージが再処理される可能性があり、 1回だけの処理の保証に違反します。
実際には、入力パーティションとtransactional.idsの間のマッピングを外部ストアに格納するか、静的なエンコードを行う必要があります。Kafka Streamsは、この問題を解決するために後者のアプローチを選択します。
トランザクションの実行方法と調整方法
トランザクションプロデューサーのパフォーマンス
トランザクションの実行方法に注意を向けましょう。
まず、トランザクションは中程度の書き込み増幅のみを引き起こします。追加書き込みの原因は次のとおりです。
- トランザクションごとに、パーティションをコーディネーターに登録するための追加のRPCがありました。これらはバッチ処理されるため、トランザクション内のパーティションよりも少ないRPCになります。
- トランザクションを完了するとき、トランザクションに参加している各パーティションに1つのトランザクションマーカーを書き込む必要があります。この場合も、トランザクションコーディネーターは、同じブローカーにバインドされたすべてのマーカーを単一のRPCにバッチ処理するため、RPCオーバーヘッドをそこに保存します。ただし、トランザクション内の各パーティションへの追加の書き込みを1つ回避することはできません。
- 最後に、状態の変更をトランザクションログに書き込みます。これには、トランザクションに追加されたパーティションのバッチごとの書き込み、「prepare_commit」状態、および「complete_commit」状態が含まれます。
ご覧のとおり、オーバーヘッドは トランザクションの一部として書き込まれたメッセージの 数とは関係ありません。したがって、スループットを高くするための鍵は、トランザクションごとに多数のメッセージを含めることです。
実際には、最大スループットで1KBレコードを生成するプロデューサーの場合、100ミリ秒ごとにメッセージをコミットしても、スループットは3%低下するだけです。メッセージが小さいか、トランザクションのコミット間隔が短いと、パフォーマンスがさらに低下します。
トランザクション期間を増やす際の主なトレードオフは、エンドツーエンドのレイテンシが増えることです。トランザクションメッセージを読み取るコンシューマは、開いているトランザクションの一部であるメッセージを配信しないことを思い出してください。そのため、コミットの間隔が長くなるほど、アプリケーションの待機時間が長くなり、エンドツーエンドのレイテンシが増加します。
トランザクションコンシューマーのパフォーマンス
トランザクションコンシューマは、プロデューサよりもはるかに単純です。
- 中止されたトランザクションに属するメッセージを除外します。
- 開いているトランザクションの一部であるトランザクションメッセージを返しません。
したがって、トランザクションコンシューマは、read_committed モードでトランザクションメッセージを読み取るときにスループットの低下を示しません 。これの主な理由は、トランザクションメッセージを読み取るときにコピーの読み取りを保持しないためです。
さらに、消費者はトランザクションが完了するのを待つためにバッファリングを行う必要はありません。代わりに、ブローカーはオープントランザクションを含むオフセットに進むことを許可しません。
したがって、消費者は非常に軽量で効率的です。興味のある読者は、このドキュメントでコンシューマデザインの詳細について学ぶことができ ます。
参考文献
Apache Kafkaのトランザクションの表面をスクラッチしたところです。幸い、設計の詳細のほとんどすべてがオンラインで文書化されています。関連ドキュメントは次のとおりです。
- オリジナルのKafka KIP:これは、データフローの詳細とパブリックインターフェイスの概要、特にトランザクションに伴う構成オプションを提供します。
- 元の設計ドキュメント:気弱な人のためではなく、これは、ソースコード以外の場所で、各トランザクションRPCの処理方法、トランザクションログの維持方法、トランザクションデータのパージ方法などを確認するための決定的な場所です。
- KafkaProducer javadocs:新しいAPIの使用方法を学ぶのに最適な場所です。ページの最初の例とsendメソッドのドキュメントは、出発点として適しています。
結論
この投稿では、Apache KafkaでのトランザクションAPIの主要な設計目標について学び、トランザクションAPIのセマンティクスを理解し、APIが実際にどのように機能するかについての概要を説明しました。
読み取りプロセスと書き込みサイクルを考えると、この投稿では主に読み取りパスと書き込みパスを扱い、処理自体はブラックボックスでした。真実は、トランザクションAPIだけを使用して保証することが1回きりの処理を不可能にする処理段階で実行できることがたくさんあるということです。たとえば、処理が他のストレージシステムに影響を与える場合、ここで説明するAPIは、1回だけの処理を保証するには不十分です。
Kafka Streamsフレームワークは、ここで説明するトランザクションAPIを使用してバリューチェーンを上に移動し、処理中に特定の追加の状態ストアを更新するものであっても、さまざまなストリーム処理アプリケーションに1回だけ処理を提供します。
今後のブログ投稿では、Kafka Streamsが1度だけ処理するセマンティクスを提供する方法と、それを利用するアプリケーションを作成する方法について説明します。
最後に、上記のAPIの実装に関する詳細に飢えている人のために、ここで説明するトランザクションAPIの基礎となるより興味深いソリューションのいくつかをカバーする別のフォローアップブログ投稿があります。
もっと興味がありますか?
詳細については、次のリソースをご覧ください。
- Confluent Platformをダウンロードでき ます
- ドキュメントを読む
- Confluentプロフェッショナルサービス は、導入に関するアドバイスと支援を提供します
- Confluentトレーニング は、Confluentプラットフォームの開発と展開のためにチームを準備することができます