1 ポイント 投稿者 GN⁺ 2024-11-14 | 1件のコメント | WhatsAppで共有
  • Kafka互換ストリーミングシステム Bufstream 0.1.0〜0.1.3 の検証で、Bufstream自体の可用性問題2件と安全性問題3件が見つかり、0.1.3時点で5件すべて修正された
  • テストは Java Kafka Client 3.8.0 と既存の Kafka/Redpanda Jepsen テストをベースに行われ、acks = allenable.idempotence = trueenable.auto.commit = falseread_committed などの 安全性優先設定 を使用した
  • Bufstreamの問題には、コンシューマー・プロデューサーの停止、不正な offset 0 応答、トランザクションコミットの消失、fetch API の応答サイズフィルタリングバグによる 承認済み書き込みの消失 が含まれる
  • 調査の過程で、Kafka Java client と Kafka トランザクションプロトコルでも Consumer.close() の無期限ブロック、予測不可能な consumer offset、aborted read・lost write・torn transaction の問題が明らかになった
  • Jepsenは、Kafka トランザクションプロトコルがクライアント要求順序とトランザクション番号を明示的に保証していないため、公式 Java client の使用時には Kafka および Kafka互換システムのトランザクション安全性が損なわれうると見ている

Bufstreamの構造と検証範囲

  • Kafka は複製・シャーディングされた append-only ログを提供するストリーミングシステムであり、Bufstream はクラウド環境での データガバナンス とコスト効率を重視した Kafka 代替実装である
  • Bufstreamは Kafka のように topic と partition を提供し、標準 Kafka client と動作する
    • producer は producer.send() で record を append する
    • consumer は consumer.assign() または consumer.subscribe() で partition にバインドされた後、consumer.poll() で record を読む
    • consumer group は topic 集合の record 処理を分担する
  • Buf Schema Registry と連携すると、Protocol Buffer record を検査して、record 検証、field-level access control、他システムとのデータ形式変換をサポートできる
  • Kafka がローカルディスクと独自の複製プロトコルを使うのとは異なり、Bufstream はデータを object storage に直接書き込む
    • object storage の複製トラフィックのコスト構造を活用してコスト削減を狙う
    • Bufstream node は stateless auto-scaled VM として動作できる
  • 3つのサブシステムで Bufstream を構成する
    • agent: Kafka API を提供する stateless サービス
    • object store: record chunk を保存し、reader に提供する
    • coordination service: 現在は etcd を使用し、どの chunk が commit されたかと record の順序を決定する
  • 2024年10月時点で Bufstream は一部顧客にのみ展開されており、ドキュメントでは「Apache Kafka の drop-in replacement」と Kafka transactions および exactly-once semantics との互換性を打ち出していたが、具体的な安全性の主張は多くなかった

クライアント設定とトランザクション前提

  • Jepsenは Kafka互換システムの従来テストと同様に、より安全な動作を得るため client 設定を調整した
  • Producer設定

    • デフォルトの acks = all を使用する
    • Bufstream では acks = 0 にすると storage の待機なしで書き込みを承認できるため、commit 済み書き込みを失う可能性がある
    • acks = 1acks = all は、Bufstream が durable persist を確信するまで block する
    • Kafka producer の自動リトライで重複 append を防ぐため、デフォルト値の enable.idempotence = true を使用する
  • Consumer設定

    • auto-commit がデータ損失につながりうるという文書があるため、概ね enable.auto.commit = false を使用する
    • committed offset がないとき、デフォルトの auto.offset.reset は最新 offset から始まるため、at-least-once delivery を保証しない
    • consumer がログ全体を観測できるよう auto.offset.reset = earliest を使用する
    • Kafka トランザクションは、producer が送った record 集合と、consumer が poll した partition ごとの最大 offset map で構成される
    • transaction が commit されたときにのみ、送信された record は durable になり、read_committed consumer に最終的に見えるようになり、committed offset も transaction で指定された offset 以上へ進む
    • transaction が commit されなければ committed offset は進まず、書き込み可視性は consumer 設定によって変わることがある
    • read_uncommitted consumer が abort された transaction の値を読む現象は aborted read(G1a) に分類される
    • Kafka の文書では read_committed が G1a を防ぎ、transaction のすべての書き込みが見えるか何も見えないかという性質をある程度保証するとされるが、Jepsen の Kafka・Redpanda・Bufstream テストでは write cycle(G0類似現象)と一部の G1c 形態が観測された

テスト設計

  • Jepsenは Bufstream 0.1.0 から 0.1.3 までと複数の release candidate build をテストした
  • テストハーネスは Bufstream test harnessJepsen testing library、Java Kafka Client 3.8.0 を使用した
  • 実行環境

    • Debian Bookworm node を 3〜5台、LXC container と EC2 VM の両方で使用した
    • etcd 用 node を1台、Minio 用 node を1台、残りを Bufstream agent として使用した
    • producer、consumer、admin client は bootstrap_servers に単一 node のみを入れて初期化したが、smart client discovery は防がなかった
  • 主な安全設定

    • auto-commit false
    • acks = all
    • retries 1,000
    • idempotence enabled
    • isolation level read_committed
    • auto_offset_reset = earliest
    • サーバー側の自動 topic 作成は無効化
    • 障害注入には process pause(SIGSTOP)、crash(SIGKILL)、clock skew(clock_settime)、network partition(iptables)を含む
    • Bufstream は agent、object store、coordination service に分かれているため、特定サブシステムだけを対象に障害を注入できる Jepsen ツールを新たに作成した
    • たとえば Bufstream node だけを crash させたり、etcd coordinator だけを pause させたりする形で、組み合わせを時間に応じて変化させた

QueueワークロードとAbortワークロード

  • QueueワークロードはKafkaのデータモデルに合わせて安全性を分析する
    • 各logical processはproducer、consumer、admin clientを実行する
    • numeric keyは特定のtopic-partitionを識別する
    • keyは指数分布の頻度で選択され、一部のkeyには頻繁に、一部のkeyにはまれにアクセスされる
  • 3種類の基本operationを使用する
    • crash: logical processを終了し、新しいclientに置き換える
    • subscribe または assign: consumerがpollするtopicまたはpartitionの集合を変更する
    • txn, poll, send: pollまたはsend micro-operationのシーケンスを実行する
  • non-transactionalワークロードでは、各sendまたはpollは正確に1つのmicro-operationだけを含む
  • transactionalワークロードでは、複数のmicro-operationをKafka transactionで包む
  • 分析では、keyごとのoffset-to-value mappingを作成したうえでエラーを検出する
    • 同じoffsetで複数のvalueが見つかれば inconsistent offset
    • 同じvalueが複数のoffsetで見つかれば duplicate error
    • 承認されたrecordがまったく観測されなければ lost または unseen
    • abortされたoperationが送信したvalueをpollが返した場合は aborted read
    • transactionが自分自身の書き込みを観測するかどうかも検査する
  • main testの後には障害を解消し、final reads 段階に入る
    • 各processはすべてのtopic-partitionをoffset 0から読み、既知の最大written offsetまでpollする
    • final readsがtimeoutし、承認されたrecordがなお観測されなければ unseen に分類する
  • Abortワークロードは、transaction abort後のpoll offsetの挙動を追跡するために追加された
    • topicは単一のpartition、process、producer、consumerに制限する
    • transactionがrecordをpollした後に意図的にabortし、その後のpoll offsetを advance、rewind、rewind-further、other に分類する

Bufstreamで見つかった5つの問題

  • 停滞するコンシューマー (#1)

    • 0.1.0から0.1.3-rc.8まで、final read段階が頻繁に停止
    • consumer.poll() は即座に空の結果を返したが、ログには承認済みのrecordが数千件残っていた
    • この状態は数十秒から1時間以上続いた
    • あるテストでは、最初の120秒間で承認済みのrecord 691件を送信し、final reads開始時点で40件がどのpollerからも観測されなかった
    • その後1時間以上 consumer.poll() が結果を返さず、テストはtimeoutした
    • 原因は、再起動したBufstream nodeが last stable offset と high watermark の 古いキャッシュ値 を返し得たことにあった
    • 一部のclient libraryは、それより後ろにrecordがないと判断してstallし、Bufstreamは起動時にcacheをrefreshするよう0.1.3-rc.6でpatchを適用した
  • 停滞するプロデューサーとコンシューマー (#2)

    • 0.1.3-rc.6でも、coordinator、storage、Bufstream nodeに対するpause、crash、partitionの後に unseen write 問題が引き続き観察された
    • 一部のケースでは、coordinatorのpause後、すべてのBufstream nodeが稼働しているにもかかわらず、clientが InitProducerId を待ち続けてtimeoutする状態に入った
    • 別のケースでは、listOffsetsnode ... being disconnected または timed out waiting for a node assignment で失敗し、poll は完了しても結果を返さなかった
    • Bufstream nodeをkillしてからrestartすると問題は解消した
    • 原因は etcd lease に関連していた
    • Bufstream agentは、アクティブなagentの追跡に etcd leases を使用している
    • 短いpauseやpartitionのため、etcdがagent leaseに紐づくkeyを削除したが、その削除updateがagentに届かない場合があった
    • agentは自分がleaseを失ったことを認識しない状態になった
    • Bufstreamチームは追加のpolling logicを入れ、0.1.3-rc.8で unseen write はおおむね解消された
  • 不正なゼロoffset (#3)

    • 0.1.0から0.1.3-rc.2まで、送信したvalueに offset 0 が割り当てられた後、実際にはより大きいoffsetで現れることがあった
    • これは、offset 0 がすでにかなり前に割り当てられていた場合でも発生した
    • senderだけがoffset 0を観測し、pollerはより大きいoffsetを観測した
    • 単一のBufstream nodeとetcd processのpauseを入れた2分間のテストでは、6件のwriteが offset 0 を受け取った後、より大きいoffsetに現れた
    • 原因は、Bufstreamのerror responseに必要なfieldが欠けていたことだった
    • Bufstreamがetcdにlog commitリクエストを送り、etcdがそれを処理しても、pauseやpartitionのためBufstreamがresponse待ちでtimeoutすることがあった
    • Bufstreamはclientにerror codeを返したが、送信済みrecordのoffsetをerror signalである -1 に設定しなかった
    • Java Kafka clientはこれをoffset 0 の成功応答として解釈した
    • Bufstream test suiteが使用していた Franz-go はこのmessageをerrorとして解釈したため、この問題はテストでは表面化しなかった
    • Bufstreamは0.1.3-rc.6で修正し、その後Jepsenでは再観測されていない
  • トランザクション書き込みの消失 (#4)

    • 0.1.2では、commitされたtransactionの一部recordが消え、再び観測されないwrite lossが頻繁に発生した
    • あるテストでは、100秒間と6,761件のwrite transactionのあいだに、commitされたtransactionが書いたrecord 240件が失われた
    • 例では、key 5 の value 141 は offset 274 に正常に書き込まれたと返されたが、すべての consumer.poll() がそのoffsetを飛ばした
    • 原因は、0.1.2で追加された concurrency safety mechanism のbugだった
    • このmechanismは、Kafka transaction protocolの冪等性不足を緩和するため、producer epoch内の各transactionに一意の番号を付与する
    • transaction number tracking logicのbugにより、複数のepochにまたがって複数のtransactionがcommitされると、一部のcommitが誤って無視された
    • commitされたように見えたtransactionが実際にはabortされたり、その逆になったりする可能性があった
    • Jepsenはtransaction timeoutを1秒という低い値に設定していたため、このbugを発見できた
    • Bufstreamは0.1.2 release後数時間以内に問題を把握し、顧客のupgradeを止めたため、顧客が0.1.2へupgradeすることはなかった
    • 修正は0.1.3-rc2に含まれている
  • Server-side filteringによるlost writes (#5)

    • 0.1.3-rc.8では、Bufstream processやcoordinatorのpause、両者間のpartitionのような小規模な障害の後に、短いwrite loss windowが頻繁に現れた
    • data lossはtransactionの使用有無に関係なく発生した
    • ある5分間のテストでは、16,770件のrecordのうち22件がacknowledgeされたが、どのconsumerもpollできなかった
    • 一部のrecordはしばらくpollerに見えていたが、後になってpollから消えることもあった
    • 原因は、人気のあるKafka web GUIのbugを回避するために0.1.3-rc.8で追加された fetch API response size制限logic だった
    • filtering logicのbugが遅延したconsumerにrecordを隠し、write lossのように見せていた
    • Bufstreamは0.1.3-rc.12で修正した

Kafka JavaクライアントとKafkaプロトコルの問題

  • KIP-588: 誤解を招く ProducerFencedException

    • テスト中に ProducerFencedException: There is a newer producer with the same transactionalId which fences the current one. エラーが頻繁に発生した
    • すべてのproducerに一意のtransactional IDを与えるテストでもこのエラーが現れ、原因の特定に時間がかかった
    • KIP-588 では、transaction timeoutでも ProducerFencedException が投げられる可能性があると記されている
    • Kafka Javaクライアントはほとんどのtimeoutに専用の TimeoutException を使うが、このケースでは ProducerFencedException を投げる
    • 実際には競合するproducerが存在しないのに、エラーメッセージは2つ目のproducerインスタンスが存在すると示している
    • KIP-588は2年間オープンのままで、JepsenはKafkaチームにエラーメッセージの変更を勧告している
  • KAFKA-17734: Consumer.close() が無期限にblockする可能性

    • BufstreamとKafkaの両方のテストで、Javaクライアントのbugにより数時間おきにテストが停止した
    • Consumer.close() はデフォルトでnetwork IOでblockする
    • close() のtimeout parameterは無期限blockを防ぐはずだが、機能していなかった
    • 別threadから consumer.wakeup() を呼び出して、IOにstuckしたconsumerをinterruptする方法も効果がなかった
    • Jepsenは、長時間稼働するプログラムはnetwork error時にもclient、connection、thread、memoryのようなresourceを妥当な時間内に解放できるべきだと考え、KAFKA-17734 を登録した
  • KAFKA-17582: transaction失敗後のconsumer offsetが予測不能

    • Kafkaの公式文書では、transaction commitの失敗時にconsumer offsetがどうあるべきかについてほとんど説明していない
    • ConfluentのKafka design documentationでは、transactionがabortされるとconsumer positionは以前の値に戻るとしているが、実際のJavaクライアントは常にそう動作するわけではない
    • Abort workloadの結果、healthyなclusterでもabort後の挙動は予測が難しかった
    • ほとんどのtransaction pairはさらに先のoffsetへadvanceした
    • 一部は以前のoffsetへrewindした
    • すべてのrewindはrebalance eventと関連しており、すべてのadvanceにはrebalanceがなかった
    • Kafka側の回答によれば、この挙動はintentionalである
    • consumerは継続してadvanceする
    • rebalanceが発生すると、committed offsetに応じて任意の地点までrewindされることがある
    • ユーザーはtransaction abort時にconsumer positionを手動でrewindする必要がある
    • Jepsenは KAFKA-17582 を起票し、この挙動の文書化と、transaction abort時のデフォルトrewind変更の検討を提案した
    • Queue workloadもconsumerを明示的にrewindするよう修正された
  • KAFKA-17754: write loss、aborted read、torn transaction

    • Bufstream 0.1.0〜0.1.3では、Bufstream processのpause、coordinatorのpause、crash、network partitionだけでaborted read、lost write、atomicity violationが観測された
    • 分析はKafka transaction protocolの根本的な欠陥へとつながった
    • 例では、clientは一意のtransactional ID jt1234 でtransactionを実行し、EndTxncommitted = false を送ってabortしたが、15回の poll() 呼び出しでabortされたtransactionのwriteが観測された
    • 同じtransactionの別のwriteは、どのpollerからも観測されなかった
    • packet captureとBufstream logを合わせて見ると、原因は遅延したcommit messageだった
    • 数transaction前に送られたcommit EndTxn があるnodeで遅れて処理された
    • clientはすでに次のtransaction群を進めていた
    • 遅延したcommitが現在のtransactionに適用され、そのtransactionの前半だけがcommitされ、残りは別transactionのように扱われてabortされた
    • Kafka protocolは、clientが複数のTCP connectionと複数のnodeにrequestを送れるよう設計されているが、同じclientのrequest順序を定めるsequence numberがない
    • transaction numberの概念もないため、serverはcommitまたはabort messageを受け取っても、clientがどのtransactionを終わらせようとしていたのか分からない
    • その結果、次のような状況が起こりうる
      • commitされたように見えるtransactionが実際にはabortされる
      • abortされたtransactionが実際にはcommitされる
      • transactionの一部のwriteだけが保持され、一部は失われる torn transaction が発生する
    • 公式Java Kafkaクライアントはtimeoutをretryableとして扱い、複数の EndTxn messageを自動送信することがあるため、ユーザーがtransactionごとにcommitまたはabortを1回しか呼び出していなくても問題が起こりうる
    • JepsenはKafkaでもprocess pauseによりaborted readとtorn transactionを観測し、KAFKA-17754 を起票した
    • Kafkaのengineerたちは、KIP-890 がこの問題を修正できる可能性があると見ている
    • KIP-890は、transactionごとにproducer epochを上げる方式でtransaction protocolを変更する
    • serverが以前のepoch messageを拒否するため、過去のtransactionのcommit messageが後続transactionに入り込むのを防げる
    • Bufstreamは0.1.3でetcd revisionをlogical clockとして使い、頻度を減らすmechanismを追加したが、clientとBufstreamの間でのreorderまでは防げない
    • Jepsenは0.1.3でもaborted read、lost write、torn transactionを継続して観測しており、client側の解決が必要だと見ている

全体結果の要約

  • Bufstream自体の問題5件はすべて修正済み
    • #1: lagging highest stable offset により consumer が stuck する問題。障害は不要で、0.1.3-rc.6で修正
    • #2: etcd lease expiry により producer/consumer が stuck する問題。pause が必要で、0.1.3-rc.8で修正
    • #3: spurious zero offsets。pause が必要で、0.1.3-rc.6で修正
    • #4: lost transaction writes。障害は不要で、0.1.3-rc.2で修正
    • #5: server-side filtering により lost writes。pause が必要で、0.1.3-rc.12で修正
  • Kafka関連の問題は依然として残っている
    • KIP-588: transaction timeout 時の誤った error message、未解決
    • KAFKA-17734: ConsumerClient.close() が無期限に block する可能性がある、未解決
    • KAFKA-17582: transaction 失敗後の consumer offset が予測不能、未解決
    • KAFKA-17754: write loss、aborted read、torn transaction、未解決
  • Jepsenは、実験的な安全性検証は bug の存在は証明できても、不在は証明できないと注意を促している
  • 特に KAFKA-17754 のため、Bufstreamに他の write loss の事例があるかどうかを判断しにくいとみている

Bufstreamユーザーと運用への推奨

  • 公式Java Kafka clientで Bufstream transaction を利用しているユーザーは、現時点では transaction が安全でない可能性を考慮すべきである
    • abort された transaction が実際には commit される可能性がある
    • commit された transaction が実際には abort される可能性がある
    • transaction が途中で分断され、効果の一部だけが保持される可能性がある
  • Bufstreamは Franz-go client のほうがこの問題に対して脆弱性が低いとみているが、Jepsenは本作業と同様の手法で Franz-go をテストしていない
  • 他の client は脆弱かもしれないし、そうでないかもしれない
  • Bufstream 0.1.3 より前の利用者は次の問題に遭遇する可能性がある
    • producer.send() が実際の offset ではなく 0 offset を誤って返す
    • client が stuck する metastable availability issue
  • Jepsenは 0.1.3への upgrade を推奨している
  • Bufstreamの全体アーキテクチャは sound に見えると評価している
    • etcd のような coordination service で immutable data chunk の順序を決める方式は、OLTP や streaming system に先例のある比較的シンプルなアプローチである
  • 運用面では2つの改善が推奨されている
    • startup 時に storage の shared file 要求が失敗すると cluster が crash する可能性があるため retry の追加を勧め、Bufstreamは retry layer を追加した
    • dependency が unavailable なとき、agent が即座に終了するのではなく、継続実行して backpressure と system status を提供し、より穏やかに recover する方向が推奨されている
  • 0.1.3時点で Bufstream は etcd に対する追加の retry logic を導入しているが、online 状態を保つには依然として constant supervision が必要である
  • ユーザーは process supervisor があり、長期 outage 中でも諦めずに動作するかをテストすべきである

Kafka transaction の文書化とプロトコル修正の必要性

  • Kafka公式ドキュメントは transaction についてほとんど触れておらず、ユーザーは曖昧で相反する複数の source を組み合わせる必要がある
  • Jepsenは Kafka チームに対し、transaction semantics を明確に整理した中央ドキュメントを作るよう推奨し、KAFKA-17671に言及している
  • その文書には少なくとも次の点を明記すべきである
    • consumer がいつ monotonically increasing offset を観測するのか
    • consumer がいつ acknowledge 済み record をスキップしうるのか
    • rebalance が transaction の途中に影響しうるのか
    • producer write offset がいつ単調増加するのか
    • G0、G1a、G1b、G1c、fractured read、自己 transaction write read がいつ合法なのか
    • abort された transaction の後、poll() の戻り値と offset が何を意味するのか
    • transaction error、abort 中の error、rewind 中の error をどう扱うべきか
  • Confluent の文書は Kafka のデフォルトが at-least-once delivery を提供すると繰り返し述べているが、Jepsenはこれは事実ではないようだと指摘している
    • auto.offset.reset = latest は未処理の record を「committed」であるかのようにしてしまう可能性がある
    • Confluent の offset management 文書も、デフォルトの auto-commit では crash 時に message progress を失うリスクがあると述べている
    • transaction abort 時に consumer が rewind されるという文書も実際とは異なる
  • Jepsenは Kafka transaction protocol は根本的に修正されるべきだとみている
    • protocol は ordered reliable delivery を暗黙に前提としているが、実際には process pause、network unreliability、non-zero latency、複数の TCP socket 間での unordered delivery が存在する
    • Kafka protocol は複数の node と TCP socket に message を分散させ、client は message を自動で retry する
    • 同じ client message の順序を復元するための sequence number や、transaction 対象を確認するための transaction number が存在しない
  • KIP-890 は、transaction commit ごとに epoch を上げることで、より厳密な順序を保証しようとしている
  • client library も、message が acknowledge されないときに producer を re-initialize して epoch を上げる方法で支援できる
  • Java Kafka Client 3.8.0 はこの問題に対して脆弱である
  • Jepsenは Franz-go が timeout 時に re-initialize を行うことで問題を緩和または防止できる可能性があるとみているが、他の client library は調査していない

今後の作業

  • 多くのユーザーは transaction を直接扱うよりも Kafka Streams API の「exactly-once semantics」に依存しているため、今後は Streams application の正確性を調査できる
  • Jepsen は KAFKA-17754 を調査している間に Kafka でも unseen write に遭遇したが、時間の制約で分析できなかった
    • unseen write は hanging transaction、stuck consumer、data loss の兆候である可能性がある
    • 遅延した Produce message が将来の transaction に入り、transaction guarantee に違反しうるかどうかも疑問として残っている
    • Kafka Java Client が request timeout 時に sequence number を再利用し、write が acknowledge されたにもかかわらず静かに discard される可能性も疑っている
  • rebalance event が発生すると consumer position が前後に動くことがあるが、その規則は不明確である
  • Kafka が意図した動作を文書化すれば、Jepsen はそれを検証したいと考えている
  • Jepsen は random process であるため、まれな anomaly を探索しにくいと説明している
    • 一度だけ発生する問題は debugging と reproduction が非常に難しい
  • Bufstream は deterministic hypervisor と simulated network 上で分散システム全体を実行する Antithesis も使用している
    • Jepsen の workload generation と history checking を Antithesis の deterministic で replayable な environment と組み合わせれば、テストの再現性を高められる

1件のコメント

 
GN⁺ 2024-11-14
Hacker Newsのコメント
  • KAFKA-17754 のような issue を調べていて、Kafka に 不可視の書き込みまで見つかったのなら、Jepsen が Kafka をもう一度深く掘り下げる時期に来ているように思う
    最後の調査は 2013 年(https://aphyr.com/posts/293-call-me-maybe-kafka、Kafka 0.8 ベータ)で、今は Kafka 自体で複数の問題を見つけ始めている段階に見える
    「書き込みが確認されたのに、黙って捨てられることがある」というのはかなり怖い

    • Kafka の分析はぜひやってみたい :-)
  • デフォルト値 enable.auto.commit=true で、Kafka コンシューマーがアプリケーションで実際に処理されたかどうかに関係なく オフセットをコミットできるという部分にはとても驚いた
    自動コミットをそんなふうに理解したことはないし、そういうデフォルトなら筋が通らないと思う
    ドキュメントの説明はあまり明確ではないが、全体としては処理が終わった場合にだけオフセットがコミットされる、と読めた
    自動コミット間隔の調整は、少なくとも 1 回処理(at-least-once)で期待するように、メッセージ喪失ではなく重複処理の時間窓を小さくするのに役立つものだと理解していた

    • 少し驚きだし、ドキュメントがこの点をうまく説明できていないというのには同意する
      明示的にコミットしない限り、Kafka にはメッセージが処理されたかどうかを知る方法がない
      Kafka は、渡したメッセージが即座に処理されたと仮定する
      自動コミットは、アイスクリームコーンを手渡してすぐ振り返り、相手が食べたと仮定するようなものだ。受け取った瞬間に落として、一口も食べられない人もいる
    • 要点は、メッセージが Kafka クライアントに正常に配信されたからといって、アプリケーションが処理したという意味ではないこと
      その保証が欲しいなら、明示的に 確認応答する必要がある
      例えばメッセージをデータベースに書くだけなら、メッセージがクライアントのハンドラーコールバックに入った瞬間に確認済みとして扱われる
      しかし実際には、DB への挿入が成功した後で確認されてほしい可能性が高い
      DB がネットワーク、Kubernetes、ファイアウォール設定などの理由で到達不能になり、その最中にエンジニアが再起動を試みてクライアントが落ちると、未処理のメッセージが発生しやすい
    • この機能は 高性能な状況のためのものだと理解している
      別のシステムが失敗したかどうかを判定でき、この機能で上限位置を動かして再処理を減らせる
      ただしタイミングが合って障害が起きると、再起動後にすでに処理した一部を再び受け取る可能性があると考えるべきだ
      問題は、自動コミットの前にこうした処理がない場合
      読む限り、処理のかなり後でコミットされるよう意図されているようだが、自動コミットでありながら、自動コミット時点の数ミリ秒前までの項目だけをコミットすべきという点は矛盾のようにも見える
    • この機能が存在する理由は、ある程度正当化できる。同期的な単一スレッドのコンシューマー向けに設計され、おおむね poll を呼び出した後にメッセージを永続的に処理するループを想定している
      混乱しやすい点は、自動コミットのチェックがタイムアウト後に非同期で起きるのではなく、次の poll 呼び出し時に起きることだ
      したがって、再び poll を呼び出す前にメッセージを永続的に処理せず、保存するだけの場合、例えば非同期処理・遅延・キューなどを使う場合にだけ、書き込みを落とし得るはずだ
      これは Java クライアントライブラリの文書化された挙動(https://kafka.apache.org/32/javadoc/org/apache/kafka/clients...)に基づくもので、現在の実装が実際にそうかどうかは別の話だ
      Kafka プロトコルは高レベルと低レベルの間に挟まっており、どちらもうまくこなせていない
      自動コミットは単純なアプリケーションを作りやすくするための高レベル機能だが、期待された使い方をしなければ当然失敗し得る
      今ではエンドユーザーは Kafka クライアントを直接使うより、細部を正しく処理してくれる高レベル実装を使うべきだと思う。データ用途ならストリーム処理エンジン、アプリケーション用途なら継続実行エンジンのようなものだ
  • 製品ページ(https://buf.build/product/bufstream)を見ると、「AWS や GCP の VPC 内でのみ実行され、外部に連絡しない」という説明と、「圧縮前 GiB あたり $0.002」という 従量課金がどう両立するのか気になる
    まさか事業全体を名誉システムで運営しているわけではないだろう

    • 紹介に「2024 年 10 月時点で Bufstream は選定された顧客にのみデプロイされている」とあるので、名誉システムもあり得ると思う
      もちろん悪用のリスクはあるが、特定の顧客を引き寄せるための価値ある妥協かもしれない
    • プログラムはオープンソースか、そうでないかのどちらかだ
      ソースが公開されていないなら、「外部に連絡しない」という主張は絶対に信じるべきではない
  • 「Kafka のトランザクションプロトコルは根本的に壊れており、改訂されるべきだ」とは痛烈に聞こえる
    それでもいつも通り、調査と文章は素晴らしい

  • Kyle が NATS JetStream を検討したことがあるのか気になる。どう考えるのか興味がある

    • まだ検討していないが、要望した人は初めてではない
      何人かは、これは……何と言うべきか……面白いだろうと提案していた :-)
  • bufstream の GitHub プロジェクトが見つからないのだが、どこにあるのか気になる

    • あっ、すみません。もう修正されているはず
    • Web サイトで https://github.com/bufbuild/buf を見つけた
    • bufstream 自体はオープンソースではなさそうだが、望んでいるものに近いかもしれない https://github.com/bufbuild/bufstream-demo がある
      ただし不思議なことに、これにもライセンスがない
  • 関連するブログ記事とドキュメントを読んでみると、Kafka の「正確に 1 回の配信」は、ワーカーがトピック 1 から読み、トピック 2 に書き込み、両方のトピックが同じ論理的な Kafka システム内にある 読み取り・処理・書き込み作業の性質として定義されているように見える
    もしそうなら、これはトランザクションと呼んだほうがよいのではないかと思う

    • Kafka も実際にこれを トランザクションと呼んでいる
      ただし「正確に 1 回」を見る方法は 2 つある
      1 つはデータベーストランザクションのように、効果が重複したり消えたりしてはならないという意味
      もう 1 つは、トピック・パーティションをまたぐメッセージ関係に関するデータフローグラフの性質に近く、ACID の一貫性にもう少し近い
      直列化可能トランザクションシステムが特定のドメインレベルの一貫性を保証するのと同じように、トランザクションを使ってそのデータフロー上の性質に到達できる
      例えば直列化可能性は、各トランザクションを個別に見たときに保たれる不変条件が、並行実行履歴でも保たれることを保証する
      Kafka はそのような形で「正確に 1 回のセマンティクス」に到達しようとしていると見なせる
  • https://www.warpstream.com/ と混同してはいけない

    • その通り。WarpStream はトランザクションにも対応していない
  • 正誤表: 「Transactions may observe none, part, or all」は「Consumers may observe none, part, or all」であるべきだと思う

    • どちらも正しいが、明確さのために トランザクションと書いた
      トランザクション外のコンシューマーのセマンティクスはもっと曖昧だ
      このワークロードのすべての読み取りはトランザクションの文脈で行われ、トランザクションオフセットコミットの経路を通る
  • このソフトウェアがどこで使われるのか気になる。計測? ブラックボックス?

    • Jepsen は、自分が開発しているデータベースをテストしていると知らなければ泣いてしまうようなツールだ
      もちろん喜びの涙だ。Jepsen の注目を受けること自体が一つの成果だからだ
    • Kafka クローンだ。Kafka はおおむね 耐久性のあるキュー