BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ ポッドキャスト Lily Maraと信頼性の高いKafkaデータ処理パイプラインを構築する

Lily Maraと信頼性の高いKafkaデータ処理パイプラインを構築する

今日の回では、Thomas Betts氏がカリフォルニア州サンマテオにあるOneSignalのエンジニアリングマネージャー、Lily Mara氏に話を聞いた。
彼女は、OneSignalの他のエンジニアリングチームが使用する社内サービスを担当するインフラサービスチームを管理している。信頼性の高いKafkaデータ処理パイプラインの構築方法について議論する。OneSignalは、RustのKafkaコンシューマーに支持されている最大スループットとなるHTTPエンドポイントのパフォーマンスと保守性を、パイプライン処理を非同期システムにすることで改善した。同期式から非同期式への移行は、運用コストを簡素化できるが、新たな課題も発生する。

キーポイント

  • OneSignalの主要なパイプラインは、HTTPハンドラーで要求を受け取り、Postgresに入れ込むことで単純化させる仕組みだ。この動作を同期式で行うことは想定されていない

  • Postgresは負荷に圧倒されてしまうため、より慎重に負荷をコントロールするためにKafkaが導入された

  • HTTPハンドラーはGo用のKafkaプロデューサーを使用したが、OneSignal社はGoコンシューマーを彼らが好む言語であるRustで書かなければならなかった

  • 安全かつ確実にスケールアウトするには、作業負荷のシャーディングとパーティショニングについて考慮する必要がある。OneSignalは、顧客のアプリIDをシャーディングとKafkaパーティショニング戦略の重要な要素とみなしており、例えば顧客メッセージを受信した順番に並べることに役立てている。

  • 同期処理から非同期処理に移行することで、サービスの健全性を測定するための新しいメトリクスが必要になった。そのうちの2つが、メッセージ・ラグ(Kafkaで処理待ちになっているメッセージの数)とタイム・ラグ(メッセージが受信されてから処理されるまでの時間)だった

トランスクリプト

導入

Thomas Betts氏: 皆さん、こんにちは。今日の配信に入る前に、InfoQの国際ソフトウェア開発会議,Qcon,が10月2日から6日までサンフランシスコに戻ってくることをお知らせしたいと思います。

Qconでは、革新的な上級ソフトウェア開発者が、現在の課題に対処するために新たなパターンやプラクティスを適用した、実際の技術的ソリューションについて語ります。詳しくはqconsf.comをご覧ください。会場でお会いできることを楽しみにしています。

こんにちは、InfoQ配信へようこそ。Thomas Bettsです。今日はLily Maraと一緒にお送りします。Lilyはカリフォルニア州サンマテオにあるOneSignalのエンジニアリング・マネージャーです。彼女は、OneSignalのエンジニアリングチームが使用する社内サービスを担当するインフラサービスチームを管理しています。

Lilyは、Rustコードを徐々に追加することで既存のソフトウェアシステムのパフォーマンスを向上させる、Manning Publicationsの早期アクセス本Refactoring to Rustの著者です。

今日は、信頼性の高いKafkaデータ処理パイプラインの構築についてお話します。OneSignalは、RustのKafkaコンシューマに支持されている、もっともスループットの高いHTTPエンドポイントのパフォーマンスと保守性を、パイプライン処理を非同期システムにすることで改善しました。

同期から非同期への移行は、運用コストを簡素化できますが、新たな課題をもたらします。Lily、InfoQポッドキャストへようこそ。

Lily Mara氏: ありがとう、Thomas。この話題について多くの方へお話しできる機会を得られることが本当にうれしいです。

OneSignalのメッセージング1:06

Thomas Betts氏: あなたはQCon New Yorkで講演し、講演のタイトルは、少し長くなりますが、"コンテンション、アップタイム、レイテンシーを重視した信頼性の高いKafkaデータ処理パイプラインの構築方法"でした。注目すべき点がたくさんありますね。

早速講演のすべてを取り上げ、一つずつ掘り下げていきたいところですが、一旦話を前に戻させてください。OneSignalをよく知らない人のために、まずはOneSignalが何をする会社なのか、そしてあなたが特に最適化しようとしていたデータ処理パイプラインは何なのか、ということから始めていきましょう。

Lily Mara氏: ええ、わかりました。またトークのタイトルは私が決めたのではないということも言っておきたいです。トラックホストが、私がもう少し洒落た題をつけるまでの一時的な保留として入れてくれたのだと思いますが、私は結局それをしなかったし、また題があまりに長かったので、発表資料のタイトルスライドに収めることができませんでした。だから、私の発表資料には "The One About Kafka "とだけ書いたのです。

私が講演を行ってきたカンファレンスではちょっとひねったタイトルを付けていたのですが、QConでは万人に内容が伝わるような具体的なタイトルが好まれるみたいですね。

私が付けた中でひねった題のものは「A Kafkaesque Series of Events」です。というのもOnesignalで起こった、まるでカフカの小説みたいに混乱するステップやデバッグ、インシデントを語るイベントだったからです。

OneSignalは顧客向けのメッセージング会社です。私たちは、ウェブサイトビルダー、アプリビルダー、マーケティング担当者、開発者である顧客が、自社のユーザーベースとより効果的にコンタクトできるようにしています。

つまり、世界で起きているライブイベントに関する情報をリアルタイムで送ることです。自分の興味やニーズに合わせてカスタマイズしたプロモーションを知らせることも可能です。ウェブサイト上で商品が入ったままのカートがあることを知らせ、再度訪問して見てもらうこともできます。

オープンエンドなプラットフォームにおいて、顧客はうちの開発者が考えるよりよっぽどクリエイティブなので、顧客がどのようにわが社の製品を使うのか全てを推測しきることは不可能です。

Kafkaの前に、OneSignalはRailsモノリスを持っていた 03:08

Thomas Betts氏: しかし、そのメッセージングシステムはOneSignalでコミュニケーション方法を提供しようとする上で間違いなく重要な要素を担っています。だからこそコミュニケーション方法の提供にKafkaを導入したのでしょうか?それともKafkaを導入する前に別のシステムを使っていたのでしょうか?

Lily Mara氏: Kafkaを使う前は、OneSignalは単一のRailsモノリスで、その内部ですべてが完結していました。HTTP処理はRailsで行い、非同期ジョブはSidekiqに送って処理していました。

当初から、私たちは常にある程度の非同期処理をしていたと思います。なぜなら、私たちが提供していた当初の製品の方向性は――その後より広範囲に拡張しましたが――大規模なユーザー基盤のもと、基盤自体またはその一部に大規模なメッセージングキャンペーンを送るというものでした。

皆さんに知っていただきたいのですが、この動作をHTTPハンドラーで同期式で行うことはできません。なぜなら、HTTPハンドラーで変数処理をやりたくないからです。やりたいことは、Postgresにちょっとしたメタデータを突っ込んで、Sidekiqのジョブを実行することただ一つなのです。

そこで、そのSidekiqジョブにキューイングを適用して、データベースを圧迫しないようにしました。その後、通知配信を処理するキャパシティができたら、Postgresデータベースを使用して、通知が送信されるすべてのユーザーを列挙し、メッセージを配信できます。

これが非同期処理への最初の進出でした。

しかし、今日お話ししたいのは、OneSignalの数年後、一見シンプルな、しかしより高スループットのHTTPエンドポイントのうちの一つに問題が発生し始めた点です。

このエンドポイントは、各デバイスのレコード、iPhoneにインストールされた各アプリ、ウェブサイトの各ユーザプロファイルのような、データベースの各サブスクリプションのプロパティをお客様が更新するためのものを指します。私たちの顧客は、キーバリューペアや他の関連するメタデータを、世の中にあるすべてのサブスクリプションに保存できます。

そして、顧客からHTTPリクエストを受けるたびに、そのリクエストからメタデータを取り出し、直接Postgresに突っ込んでいました。CRUD APIはある程度の規模までは問題なく機能しますが、規模をどんどん大きくしていくと、更新やリクエストがPostgresを圧迫し始める時が来ます。

Postgresは素晴らしいデータベースですが、確かにいくつかの制限があり、私たちはかなり頻繁にそれにぶつかり始めました。

なぜ非同期処理が必要だったのか05:39

Thomas Betts氏: 入力ストリームの話ですが、処理しきれないほどのリクエストを受け取っていたようですね。非同期処理が遅すぎるのではなく、1秒間に何千、何百万というリクエストがあって、それがPostgresに過負荷をかけていたのですね?

Lily Mara氏: その通りです。今はまだ、この特定のエンドポイントに非同期処理を導入していない段階です。つまり、これはすべてHTTPハンドラ内で同期的に行われているのです。

その時点で、同期的な作業をキューイングに追加して、Postgresにかかる並行処理やPostgresにかかる負荷をより慎重に制御したいと考えました。

HTTPハンドラでの同期的なワークロードをKafka製品に変更し、それをRust Kafkaコンシューマが受け取り、データの変更をPostgresデータベースに適用します。

これに伴うエンジニアリング上の興味深い課題がたくさんありましたが、特にその当時は、RustでKafkaコンシューマーを書いている人がまったくいなかったのです。

私たちはRdKafkaライブラリのスタックをベースに使用していました。RdKafka用のRustラッパー・ライブラリがあり、私たちはそれを多用していましたが、"別のKafkaメッセージをください"とのような基本的な命令を超えた、高レベルのKafkaコンシューマーを書くための制御機能はあまりありませんでした。

そこで、コンシューマー内の同時実行を制御するためのライブラリを独自に開発し、当初は社内でKafka Frameworkと呼んでいました。おそらく、私たちと同じように無謀にも、Rustで本番用のKafkaコンシューマーを書きたいという人がいると思うので、これをオープンソース化する話もあったのですが、今のところ実現できていません。

なぜOneSignalはRustでKafkaコンシューマーを書くことにしたのか 07:23

Thomas Betts氏: Rustがまだ存在していなかった時期に、どのような理由でRustを採用したのでしょうか?他の選択肢も検討したのですか?それとも全てのプラットフォームにRustを使うつもりでいたのでしょうか?

Lily Mara氏: 私たちはかなり早い段階でRustに大きな賭けをしました。OneSignalのテクノロジーの中核のひとつがOnePushで、これは私たちのすべての配信を支えるシステムです。

私たちがそのことをエンジニアリング・ブログで公に書いたのは、2016年だったと思います。Rustの1.0リリースから約1年後、もしかしたら1年も経っていないかもしれません。

OneSignalでは、Rustを本番環境に導入するのがかなり早かったのですが、本番環境に導入してからのランタイムの速さ、オーバーヘッドの少なさ、オーバーヘッドが少ないことによる運用コストの低さ、そして率直に言って開発のしやすさの全てにとても満足しています。

Rustは習得が難しいという話はよく聞きますが、ある程度のスキルを身につければ、開発で使うには非常に生産的な言語だと思います。

他の言語では心配しなければならない多くのバグが、Rustでは考える必要がない。皆さんにとって本当に便利なことです。

少なくとも私に報告する人は皆、主にRustの開発者であり、現時点ではGoもある程度やっていますが、Rustの経験がまったくない人を、かなり熟達したRust開発者にまで育て上げました。Rustの経験がまったくない人たちを、かなり熟達したRust開発者にまで育て上げました。ですから、Rustで活躍するために何が必要かを見てきましたし、それは正直なところ、インターネット上でのRustの習得難度の評判よりもずっと簡単だと思います。

技術スタック: Go、Kafka、Rust、Postgres 09:06

Thomas Betts氏: Rustを採用したわけですが、今のスタックにはどのように組み込んだのですか?HTTPエンドポイントが、Railsのモノリス上にありましたが、それらを変更したのですか?

Lily Mara氏: その時点で、特定のエンドポイントのHTTP処理はすでにRailsから移行していました。2018年のことですが、私たちはもっともスループットの高いHTTPエンドポイントのいくつかを取り出し、実際にそれらをGo HTTPサーバーに移しました。

それらをRust HTTPサーバーに置きたかったのですが、その時点では製品として世に出せる品質だと思えるRust HTTPライブラリーがありませんでした。私たちがHTTPライブラリに求めていたような機能セットを備えたものがなかったのです。

今はだいぶ状況が変わっていると思いますが、5年前の話です。ライブラリーはIronだったと思うし、他のライブラリーが何だったか覚えていないけど、RustでのHTTPはかなり複雑な話でした。

同時実行を管理するようなものを書くのと比べると、"HTTPサーバー・ライブラリを自分で書け"と言われても、求められるエンジニアリングのレベルは非常に高く、私たちはそれを引き受ける準備ができていませんでした。

Thomas Betts氏: 想像に難くありませんね。HTTPサーバーにはGoを使っていますが、Rustのコードにはどうやってたどり着いたのですか?それはスタックのどこにあるのですか?

Lily Mara氏: 基本的に今日あるものはすべて、HTTPレイヤーにRubyとGoを使っています。顧客データを直接扱うレイヤーの下には、RustとGoが混在しています。

将来的には、新しいサービスをRustだけで開発する予定です。ですから、OneSignalのコードにあるものはすべてRustで書かれており、古いものはGoで書かれています。

Thomas Betts氏: Go HTTPがRustサービスを呼び出して、「このリクエストを処理してください」と言うだけで、HTTPのエンドポイントでは処理能力などはあまり持たないのですよね?

たくさんのリクエストが入ってきて、RustをKafkaに入れるとすごく速くなるとおっしゃいましたよね。どこからどこへリクエストが引き継がれるのか知りたいのですが?

Lily Mara氏: (以下Lily):Go HTTPサーバーを用意して、基本的にはApache Kafkaにイベントを入れ込みます。そしてその後、Rust KafkaのコンシューマーがKafkaからイベントを取り出します。つまり、この2つは互いに直接作用することはありません。Kafkaを橋渡しするようなイメージです。

Thomas Betts氏: なるほど。GoはそのままKafkaに入り、コンシューマーがKafkaから取り出すというのが、あなたがRustで書いたことなんですね?

Lily Mara氏: その通りです。

Thomas Betts氏: Kafkaから取り出すのは、Kafkaプロデューサーではなかったのですね。GoのKafkaプロデューサーとRustのKafkaコンシューマーが混同していたんです。そしてまた、そのGo HTTPサーバーで行う処理は最小限であり、あとは非同期でKafkaに突っ込んで終わります。

Rust Kafkaコンシューマー 11:43 3

Thomas Betts氏: それで、Rustの運用はどうなっていて、並行処理はどうなっているのですか?

Lily Mara氏: このシステムは多かれ少なかれ、Kafkaからイベントデータを取り出し、残りのライブラリが理解できる構造体にデシリアライズし、Postgresの更新コールをPostgresに送信します。並行処理の話は多くの段階を経ているのです。

もちろん、プロセスごとに一度に1つのイベントを処理するにはデータ処理が重くなりすぎます。Kafkaにはすでに並行処理のサポートが組み込まれています。Kafkaにはパーティションというデータ構造があり、基本的にKafka内のトピックの各パーティションは独立したイベントのストリームです。

つまり、Kafka内のトピックの各パーティションは、それぞれ独立したイベントのストリームなのです。これが、並行処理の最初のレイヤーのようなものです。この特定のデータストリームは、720のパーティションに分割されています。そのため、理論的には、並行処理サポートを追加しなくとも、720のイベントを同時に処理し、720のPostgres更新コールを同時に送信できるのです。

しかし、それでは十分な同時実行性が得られないことがわかりました。 なぜならそれ以上のイベントがあったからです。そこで私たちは、各パーティション内で同時並行処理を行う戦略を立てました。

そして、各パーティションにいくつものワーカースレッドを用意し、Kafkaに一度にいくつものイベントを要求し、それらのイベントを様々なワーカースレッドにバラバラに振分け、同時に処理するのです。

しかし、同じサブスクリプションの更新を並行に、またはバラバラに処理したくないという問題がありました。

例えば、私のiPhoneに2つのプロパティのアップデートがあった場合、1つは8から10に設定され、もう1つは8から20に設定されています。HTTPレイヤーでこれらのアップデートを受信した順番が10と20だった場合、20の後に10のデータを取り込むことになってはいけません。私たちは、顧客から送られてきた最新のものをデータベースに保存したいのです。

なぜ顧客がそのようなアップデートを送ってくるのかは、私たちには関係ありません。ただ、顧客から送られてきたものを正確に反映させたいだけなのです。

Thomas Betts氏: そうですね。顧客があなたのシステムで何をしようとするのかはコントロールできませんから。

Lily Mara氏: 確かにそうですね。

Thomas Betts氏: 少なくとも、顧客の行動を制御できない問題になる可能性があることは認めるのですよね。

Lily Mara氏: ええ、その通りです。この問題をどう解決したものか、私たちは考えました。無制限の同時実行はできません。そこで、各イベントのプロパティとして、サブスクリプションID(Postgresでいうところの行ID)を使用することにしました。

それをハッシュ化し、ワーカースレッドごとにメモリ上にキューを作ります。つまり、各ワーカー・スレッド(パーティションごとに4つのワーカー・スレッドがあるかもしれません)は、4つのワーカー・スレッドのそれぞれに専用のキューを持つことになります。

これは仮想キューのようなもので、Kafkaのパーティションが本当のキューといえるでしょう。それをあまりいじくり回すことはできない。しかし、ワーカースレッドごとに、同じワーカースレッドIDにハッシュするイベントだけを含むKafkaパーティション上のビューのような仮想キューがあります。

つまり、Postgresの同じ行、同じサブスクリプションを対象とする2つの更新があった場合、それらは同じワーカースレッドによって処理されることになり、同じキューに置かれることになります。それはHTTPレイヤーで受け取った順に処理されます。

パーティショニングの扱い方 15:18

Thomas Betts氏: 最初のレイヤーはパーティションで、次に仮想キューがあると言いましたね。どのようにパーティショニングしているのですか?

720のパーティションがあるとのことですが、それが最大だとおっしゃいましたね。その最大値を用い、それをどう分割しているのですか?あるデータがある場所に行き、あるデータが別の場所に行くということがないように、パーティションを選択するための論理的な構造があるのでしょうか?

Lily Mara氏: もちろんです。Kafkaで使用できるパーティションの最大数は720ではありません。最大数はわかりませんが、最大値ではないことは確かです。誰も本番で使いたがらないような巨大なものだと思います。

私たちが720を選んだのは、たくさんの約数があるからです。Kafkaコンシューマを起動すると、Kafkaは各コンシューマに消費するパーティション数を割り当てます。

基本的には、コンシューマーごとに偶数のパーティションに簡単に分割できるパーティション数が必要でした。また、質問の後半にあるように、各データベースには偶数のパーティションを割り当てる必要があります。

その結果、私たちのパーティショニング戦略は、Postgresのデータベースに合わせた特定のパーティションにメッセージを分割することに行きつきました。私たちのシステムでは内部でにいくつかのPostgresデータベースが、顧客のアプリIDやデータセットIDに基づいてシャーディングされています。

アプリIDの最初のバイトによって、どのデータベースシャードに常駐するかが決まると思います。そのため、パフォーマンス上の問題やインシデントが発生した場合、次のようなお知らせをステータスページに更新するでしょう。「ステータスページでDで始まるアプリIDをお持ちのお客様は、特定のPostgresデータベースで問題が発生しているため、現在パフォーマンス上の問題が発生している可能性があります」。

どのKafkaパーティションに割り当てられるかを決定するために、これを使います。つまり、ある顧客のアプリでは、すべてのKafkaイベントが常に同じKafkaパーティションに送られるのです。

そして、同じPostgresサーバーに送られるデータセットは、同じKafkaパーティションに置かれるか、少し異なるKafkaパーティションに置かれます。必ずしも同じであることを保証するものではありません。

Thomas Betts氏: これはカスタマー・アプリIDの一部です。つまり、それは送られてくるリクエストの中にあるわけです「このデータを更新したい」というHTTPリクエストが来たら、「そのデータをKafkaのどこに送ればいいのかわかる」キーを持つようになるわけですが、それがPostgresのシャーディングにも対応するわけですね?

Lily Mara氏: その通りです。

Thomas Betts氏: わかりました。では、このシステムが、データがどのように構造化されているかというデータレベルの設計に組み込まれているのですね?

Lily Mara氏: その通りです。結局のところ、私たちの会社が直面している問題のほとんどは、Postgresのスケーリングの問題です。そのため、私たちのシステムの信頼性を高めていくにつれ、その作業の多くは、特定のアクセスがどのPostgresサーバーに送られるかに基づいてそれらを順に並べる必要があることを、より多くのシステムに認識させることに集約されていきました。

Thomas Betts氏: 複雑なパーティショニングの多くをカバーしていると思うし、かなりうまく説明してくれたと思います。少なくとも、今は理解できます。

同期と非同期のメトリクスの変更 18:24

Thomas Betts氏: このデータ処理で同期から非同期のワークフローに移行すると、測定方法が変わります。以前はパフォーマンスに問題があったことと思われます。

パフォーマンスが向上したことをどうやって確認するのか、また、非同期式になったためにシステムの状態をどのように測定しなければならないのでしょうか。サーバーに負荷がかかったり、CPUが急上昇したりはしていませんが、健全な状態なのでしょうか?

Lily Mara氏: ええ、確かに。これは私たちのメトリクスに対する考え方を大きく変えました。システムに対して行うべきモニタリングの範囲が、劇的に広がりました。

大量のHTTPトラフィックを処理する同期型システムの場合、各リクエストの処理時間や成功数、エラー数を気にしますよね。

もちろん単純なことではないので、簡単だとは言いたくありませんが、この3つを監視し、レスポンスタイムを短縮し、エラーを減らすことで、かなり効果的に保守できます。

しかし、このような非同期システムに移行したとたんに、気にするメトリクスがこの3つだけになってしまったら、更新処理を行うことがなくなってしまうかもしれません。

HTTPリクエストのデータを実際に適用するのが12時間後かもしれない状況では、HTTPレスポンスが成功してもあまり意味がありません。もちろん、12時間後に更新が行われることは避けたいのですが、これが現在追跡しなければならないメトリクスの性質なのです。

私たちが最初に注目したのは、メッセージラグでした。Kafkaに残っていてまだ処理されていないメッセージの数です。通常、システムが正常であれば、これはゼロに近く一時は数十万件に達することもありますが、すぐにゼロになるまで処理されます。これが通常の、名目的な運用です。

状態が悪くなり始めると、数千万から、まれに数億のメッセージがKafkaで処理待ちの状態になることもあります。

これは良いことではありません。なぜか処理能力が不足していて、処理可能と思っていたメッセージが大量に残存しているため、システムが健全でないことを示しています。

このような現象が発生する理由は様々です。Postgresサーバがダウンしているのかもしれませんし、私たちのようなケースでは、Postgressに大量のデータを一括更新した顧客がいるのかもしれません。というのも、全てのメッセージがPostgresの単一の行に対してキューイングされ、すべてのワーカースレッドが何もしていない状態になり、同時実行ロジックが壊れる可能性があります。。

しかし、なぜ大量のメッセージが遅延しているのかということの本質的な原因は、ある意味幅広いです。Kafkaの中にあるメッセージの数というのは、どのようなメトリクスなのかという話です。

最近、有用な指標として注目しているのが、タイムラグです。なぜなら、Kafkaに入るすべてのメッセージには、このメッセージがKafkaのキューに追加された時刻を示すタイムスタンプがあるからです。そこで最近、コンシューマー側で最近処理されたメッセージのタイムスタンプを報告するメトリックを追加しました。

これにより、私たちは"今は午後2時30分だが、最後に処理されたメッセージは、実際は正午にKafkaに送られている"というような事象を発見することができるようになりました。では、なぜこのメッセージが生成されてから消費されるまでに2時間半もあるのでしょうか?これはつまり、私たちが現実に追いついていない可能性が高いので、更新する必要があるということです。

このメトリックについては少し混乱を招く可能性があります。というのも、通常の状態では、タイムスタンプは常にかなり安定した状態で刻々と上がっていくからです。タイムスタンプは、基本的に常に現実に追いついているのです。

今は2:30で、最近処理されたメッセージも2:30であれば、つじつまが合います。そして、もしプロダクションレートが上昇し、Kafkaコンシューマーが処理しきれないほどのHTTPリクエストが一定期間入ってきた場合、処理スピードは少し低下するでしょう。現実に追いついていないことになり、2時30分になっても午後2時からのメッセージを処理していることになります。

しかし、次のようなパターンがあると、ちょっと混乱が起こります。例えば、通常1秒間に2000通のメッセージが送信されているとしましょう。それが1分間だけ、1秒間に2万メッセージに急増したとします。異常なトラフィックが短時間に大量に発生し、Kafkaに大量のメッセージがエンキューされたとします。

つまり、2時から2時10分までの10分間で、約1200万通のメッセージが送られるということであり、それらはKafka内で処理待ちをすることになります。1秒間に2万通のメッセージの処理は不可能です。4000通なら処理できるかもしれません。定常状態の受信メッセージ数より少し多く処理できますが、現実に遅れをとることになります。そしてその遅延は、やがて恒常的なものになっていきます。午後2:00に始まったにもかかわらず2:03分までしか処理されていないということが起こりえます。

メッセージのレイテンシーのグラフを見るだけならまだしも、Kafkaに置かれているメッセージの数を表示するグラフを見ると、Kafkaに置かれているメッセージの数が大きく増えているように見えるからです。

しかし今、メッセージ数のグラフは本当に劇的に急降下し始めています。今、私たちはそれらのメッセージを処理し、正常に戻り始めているからです。

しかし、時間レイテンシはどんどん悪化していき、なんとか処理を間に合わせるまで悪化していきます。しかし、この時間レイテンシは、実際の時間が直近に処理されたKafkaメッセージの時間からどれだけ離れているかを測定するもので、現実からどんどん遅れていくのです。

そのため、このような計測をするのは少しわかりにくいかもしれませんが、システムのリアルタイム性を判断する上では少し役に立つことがわかりました。

Kafkaに保存されているメッセージの数は、最初のベースラインの健全性の指標としては素晴らしいのですが、現実からどれくらい遅れているのかという質問に答えるには適していません。もしかしたら2000万通のメッセージが滞留していても、それは2分間だけの可能性があります。それが本当に問題なのか?私にはわかりません。

Thomas Betts氏: ええ、私はデフォルトの応答が、キューの長さはゼロに近いはずだというのが気に入っています。これは基礎的な質問ですが、キューが健全であることをどうやって知るのですか?Kafkaは一般的なキューよりもはるかに大きいですが、待機しているメッセージの数という点では同じです。

タイムラグはあなたに一つの指標と、実に初歩的な問題を示しているようですね。すなわち、"問題があることはわかるが、なぜ問題があるのかはわからない"ということです。そして、タイムラグもバックエンドの問題です。

生成側と処理側の構築ですが、その両方について、あなたは掘り下げて分析を始めたかったのですね。次に何をしましたか?さらにメトリクスを追加する必要がありましたか?ダッシュボードを追加する必要がありましたか?トラブルシューティングの次のステップは何でしたか?

Lily Mara氏: ええ、もちろんです。この問題を解決するために、分散トレーシングは非常に有効な方法でした。OneSignalではHoneycombを使用し、OpenTelemetryを私たちのサービスのあらゆるところで使用しています。

OpenTelemetryは、パフォーマンスの問題やバグ、動作の問題など、会社全体、技術スタック全体のあらゆる問題を追跡するのに非常に役立っています。私たちは、Rubyコード、Goコード、RustコードからOpenTelemetryデータを生成しており、それは私たちにとって本当に本当に貴重なものです。

OpenTelemetryとHoneycomb 26:29

Thomas Betts氏: OpenTelemetryのデータはKafkaに送られるメッセージの中にあります。だからそのリクエストからデータが入ってきて、常にデータを保持し続けています。1分後だろうが2時間半後だろうが、それを引き出すと同じトレースが残っているのです。

Honeycombを見れば、"このトレースは2時間半実行された"とわかります。そして、どのコードがどの時間に実行されたかを分析し、"このコードは1時間待機し、処理に1秒かからなかった"とわかるようになります。そうでしょう?

Lily Mara氏: 実際、私たちは生成を処理と結びつけて考えてはいません。それはかなり問題があるとわかりました。一時期、非常に楽観的だったころに試したのですが、いくつかの理由で問題があるとわかりました。

そのひとつが、トレースデータのサンプリングにRefineryを使っていることです。RefineryはHoneycombによって作られたツールで、多かれ少なかれ、トレース内のすべてのスパンのデータを保存し、それを使ってすべてのトレースの関連度スコアのようなものを決定し、それを使ってトレースをHoneycombに送るべきかを決定します。

ストレージ・サーバーに送るトレースには、それぞれ保存費用がかかります。だから、すべてを保存するかどうかを判断しなければなりません。もちろん、すべてを保存する余裕はないのです。

そのため、もしプロデュース・トレースとコンシューマ・トレースを一致させようとすれば、リファイナリーでそのデータを数時間にわたって保持しなければならない問題があります。このシステムは、それよりもはるかに短時間のものを想定して設計されている可能性があります。 そしてそれは問題です。 そのシステムは、それよりもはるかに寿命が短いもののために設計されており、実際にはそれほどうまく機能しません。

私たちが見つけた、もう少しうまくいく方法のひとつは、スパンリンクです。これが OpenTelemetry の仕様の一部なのか、Honeycomb 固有のものなのかはわかりませんが、、たとえ二つのスパンリンクが同じトレースのものでなくとも、基本的にはあるスパンから別のスパンへリンクできます。

つまり、処理スパンから、生成されたトレースのルートへリンクできます。これは、これら2つのデータをマッピングするのに役立ちますが、一般的には、これら2つの事柄をリンクすることは、それほど価値はありません。

HTTPトレースとKafkaコンシューマ・トレースをリンクさせるのは便利だと思うかもしれませんが、一般的に、Kafkaには独自の問題やメトリクス、懸念事項があり、それはHTTP処理側にも直接関わる問題です。

"Kafkaのキューに2時間半も滞留していたことがわかれば便利だ"と思うかもしれませんが、処理スパンに"生成されたタイムスタンプと処理された時間の差"を記録するフィールドが追加されるだけです。

つまり、この2つをリンクさせることで得られる付加価値は、実はあまりないのです。

消費者側と生産者側を別々に測定する 29:20

Thomas Betts氏: 参考になりました。つまり、「ああ、これは明らかに、リクエストが出されてから処理されるまでに保存しておきたいメッセージの構造だ」と考える私のような素人にも、わかりやすく説明していただきました。

そして、消費者側に問題がある場合、それは生産者側の問題とは異なるので、分けて考えるということですね。

Kafkaへの取り込みに問題があって遅いのか、KafkaからPostgresへの取り込みに問題があるのか。この2つを別々の問題として取り組むのです。コンシューマの問題ではありません。

Lily Mara氏: ええ、もちろんです。HTTPトラフィックを分析し、どのような形状のトラフィックなのか、各顧客が一定時間の間にどれだけのリクエストを送ってきているのかを判断したい場合、サードパーティの監視ツールを使うのではなく、Kafkaにあるデータを見ればいいのです。なぜなら、Kafkaはすでにイベントの記録場所であり、基本的に私たちのワイヤーを渡ってくるHTTPリクエストをすべて保存しているからです。

そのため、ストリームを検査し、それに基づいてフィルタリングできるツールを社内で開発しました。基本的には、新しいものを導入するための調査があるたびに、新しい基準を追加しています。しかし、私たちはこのストリームをデバッグに、あるいはデバッグではないかもしれませんが、顧客が送ってくるHTTPトラフィックのパターンを調査するために、非常に広範囲に使用してきました。

Kafkaストリームを直接見て、どのようなデータが入ってきているのかを判断したり、ログやメトリクス、Honeycombのような監視ツールを見て、コンシューマがなぜそのような動作をしているのかを判断したりします。

それらは時に同じ答えを持っています:コンシューマーがそのように動作しているのは、私たちが奇妙なトラフィック・パターンを受け取ったからです。しかし、コンシューマのバグ、ネットワークの問題、あるいはツールに流入したトラフィック・パターンとは無関係なPostgresの問題のために、コンシューマがそのように動作している場合もよくあります。

現状と今後の計画31:13

Thomas Betts氏: システムの現状はどうですか?これを非同期にするという当初のニーズはすべて解決したのでしょうか?それともまだ進行中で、少しずつ微調整をしているのでしょうか?

Lily Mara氏: 計画が本当に完了し、そこから撤退することが可能かはわかりません。私たちのシステムは非常に安定しており、特に本番でのRustアプリケーションの安定性には満足しています。

Rustアプリケーションは非常に安定しているため、本番環境にデプロイしても、次に変更を加えなければならなくなったときに、ライブラリのバージョン更新が非常に遅れていることに気づきます。これらは本当に素晴らしい稼働率を誇っています。

ですから、システムの信頼性は非常に高いのですが、お客様のトラフィックパターンが変化するにつれて、それに合わせてシステムも変化させていかなければなりません。私たちは、さまざまな方法で同時接続を可能にする新しい戦略を模索しています。

私たちは現在、データをさまざまな方法で見る必要がある機能を追加しながら模索しています。また、並行処理戦略やデータ・パーティショニング戦略など、こういったものもすべて変えています。

Thomas Betts氏: ええ、とても基本的なことですね。もっと平行に。もっと分割して。どうすればいいんでしょう。でも、どうやってそれらすべてを考えなければいけないのでしょう。

興味深いことがたくさんあります。また、今後もいろいろな変化があれば、またお話を聞かせてください。他に現在取り組んでいることはありますか?次に計画していることは何でしょうか?

Lily Mara氏: 現在、経験豊富な開発者へ主要な開発言語としてRustに移行する方法を教えるコースをO'Reillyに提案しています。今、業界全体でRustへの関心が高まっていると思います。

マイクロソフトのような組織は、Windowsの中核部分をRustで書き直すと言っています。なぜなら現在の中核言語であるC言語やC++には、脆弱性があると言われているためです。

昨年、NISTの勧告があったと思います。NISTか国防総省のどちらかが、内部開発はメモリ安全な言語で行うよう勧告したとか、そんな感じです。

また、Rustに注目している技術業界の大手企業も数多くあると思います。Rustは習得がおそろしく大変だと言われているので、不安に思っている人も多いと思います。

Rustにはオーナーシップシステムと呼ばれる機能があります。それはいったい何なのか?このコースのゴールは、すでに他の言語で経験を積んでいる人たちに、Rustは比較的簡単で、Rustのプロフェッショナルな開発に応用できることを理解してもらうことです。

Thomas Betts氏: 素晴らしいですね。Lily Mara、あらためて、今日はInfoQ配信に参加してくれてありがとうございました。

Lily Mara氏: どうもありがとう。Thomas。お話できてよかったです。

Thomas Betts氏: これからもまた別の回に参加してくださいね。

Lily Mara氏: もちろんです。

メンション

作者について

Previous podcasts

この記事に星をつける

おすすめ度
スタイル

特集コンテンツ一覧

BT