KafkaとRuby、Sidekiqで紡ぐラブストーリー
成長を続けるオールインワンAPMとして、私たちは増え続けるトラフィックにAppSignalが対応できるよう、多大な時間をかけて取り組んでいます。普段はその手法について語ることはあまりありません。私たちのブログにはRubyの内部動作やElixirでの突拍子もない実験に関する記事は数多くありますが、AppSignalそのものを動かしている仕組みについてはほとんど触れてこなかったからです。
しかし今回は、ここ数年でスタックに加えてきた大きな変更点のいくつかをご紹介したいと思います。おかげで、毎月私たちに送られてくる数百億件ものリクエストをリアルタイムで(しかも余裕をもって)処理できるようになりました。今日はそのスケーリングの経験を活かし、私たち自身のスタックについて解説することで、読者の皆さんのお役に立てれば幸いです。
標準的なRails構成から、よりカスタムな構成へ
AppSignalは、かなり標準的なRails構成からスタートしました。RailsアプリがAPIエンドポイント経由でデータを収集し、Sidekiqジョブを作成してバックグラウンドで処理するという仕組みです。
しばらく運用した後、速度を稼ぐためにRails APIをRackミドルウェアに置き換え、さらにその後、Sidekiq互換のジョブをRedisにプッシュするGo製のWebサーバーへと置き換えました。
アプリケーションの状態とカウンター更新
この構成は長い間うまく機能していましたが、やがてデータベースがクエリ量に追いつけなくなる問題に直面するようになりました。この時点で、私たちはすでに数百億件規模のリクエストを処理していました。主な原因は、正しいカウンターをインクリメントし、適切なドキュメントを更新するために、各Sidekiqプロセスがアプリケーション全体の状態をデータベースから取得する必要があったことです。
データをローカルキャッシュすることである程度緩和はできましたが、構成がラウンドロビン方式だったため、ペイロードがどのサーバーに届くかわからない以上、各サーバーが全データの完全なキャッシュを持つ必要がありました。経験していたデータの増加率から考えて、この構成では将来破綻すると気づいたのです。
そこで登場したのがKafka
より良いデータ処理の方法を探す中で、私たちはKafkaをデータ処理パイプラインとして採用しました。データベース内でメトリクスを集約する代わりに、現在はKafkaのプロセッサ上でメトリクスを集約しています。目標は、集約されたデータをフラッシュ(書き出し)する時まで、Kafkaパイプラインがデータベースへ一切クエリを発行しないことです。これにより、ペイロードごとのクエリ数は最大10回の読み書きから、パイプライン末端の書き込み1回だけへと削減されました。
Kafkaメッセージごとにキーを指定しており、Kafkaは同じキーを持つメッセージが必ず同じパーティションに振り分けられることを保証します。このパーティションは同一サーバーによって消費されます。私たちはアプリケーションIDをメッセージキーとして使っているため、サーバー上に全顧客分のキャッシュを持つ必要はなく、そのサーバーがKafkaから受け取るアプリケーションのデータだけをキャッシュすればよくなりました。
Kafkaは素晴らしいシステムで、私たちは2年かけて移行を完了させました。現在ではほぼすべての処理がRustで書かれ、Kafka経由で実行されています。ただし、通知の送信など、データベースへの負荷が高いタスクにはRubyの方が適している部分も残っています。そのため、KafkaからRailsスタックへデータを渡す何らかの仕組みが必要になりました。
KafkaとRuby/Railsをつなぐ
移行を始めた当時、Ruby向けのKafka gemはいくつか存在しましたが、当時最新だった0.10.x系のKafkaに対応しているものはなく、大半はメンテナンスも停止していました。
私たちは独自gemを作ることを検討しました(最終的には実際に作っています)。詳細については別の記事で触れる予定です。しかし、優れたドライバーがあるだけでは要件の一部しか満たせません。Rubyでデータを消費してタスクを実行し、古いワーカーがクラッシュした際に新しいワーカーを起動する仕組みも必要でした。
最終的に、私たちはまったく異なるソリューションに行き着きました。KafkaスタックはRustで構築されており、sidekiq_outトピックを消費し、RedisにSidekiq互換ジョブを作成する小さなバイナリを書いたのです。このバイナリをワーカーマシンにデプロイすれば、Rails内部から行うのと同じように新しいジョブをSidekiqへ投入できます。
このバイナリにはいくつかのオプションがあります。たとえばRedis内のデータ量を制限し、閾値が解消されるまでKafkaトピックの消費を停止する機能です。これにより、バックログが発生しても、KafkaからのデータがすべてワーカーのRedisメモリに流れ込むことはなくなります。
Ruby側から見ると、Railsで生成されたジョブとKafkaからやってくるジョブの間に、まったく違いはありません。おかげで、Kafkaについて一切知らなくても、Kafkaからデータを受け取ってRailsで処理する——通知の送信やデータベースの更新など——新しいワーカーをプロトタイプできます。
また、新しいRubyコードをデプロイすることなくKafkaへの切り替えと切り戻しができたため、移行作業も格段に楽になりました。テストも非常に簡単です。ローカルにKafka環境一式を構築しなくても、テストスイート内で簡単にジョブを生成し、Rubyに消費させることができました。
すべての社内メッセージはProtobufで定義しています。そのため、テストさえ通れば、ワーカーがKafkaからのジョブを正しく処理できるとかなり確信を持てます。
結局、このソリューションは多くの時間と労力を節約し、Rubyチームの生活をずっとシンプルなものにしてくれました。
メリットとデメリット
あらゆる技術選定と同様に、この構成にもメリットとデメリットがあります。
メリット
- Ruby側の変更が不要で、API互換性を維持できる
- デプロイも切り戻しも簡単
- KafkaとRubyの間の切り替えが容易
- リミッターを使えばRedisがメッセージで溢れることがなく、メッセージをKafka側に保持することでサーバーのメモリを節約できる
- キー付きメッセージのおかげで、水平スケーリングにより各サーバーのキャッシュを小さくできる
デメリット
- サーバーが消費するパーティションに属するアプリケーションの全データキャッシュに、各Sidekiqスレッドがアクセスできる必要がある問題は依然として残る(例:Memcached)
- サーバー上で別プロセスを動かす必要がある
- RustプロセッサはメッセージをRedisにフラッシュした時点でオフセットをコミットします。つまりメッセージがRedisに存在することは保証されますが、Rubyによって処理されることは保証されません。そのため、サーバークラッシュ時に、Redisに残ったまま未処理のメッセージが失われる可能性があります
SidekiqとKafka
処理パイプラインのKafkaへの移行において、Sidekiqは私たちにとって非常に大きな助けとなりました。現在ではほぼ完全にSidekiqから離れ、すべてを自社開発のKafkaドライバーで直接処理していますが、その話はまた別の記事で。
ハッピーエンドでラブストーリーは幕を閉じます。パフォーマンスとスケーリングに関する視点、そしてAppSignalをスケールさせた私たちの経験を楽しんでいただけたなら幸いです。スタックを巡って私たちが下した意思決定の記録が、皆さんの役に立つことを願っています。
ブログの他の記事もぜひご覧ください。また、フォローしていただければ、Kafka構成に関する次回の記事の公開を見逃しません。そしてもし、「本当に開発者のための、開発者による」オールインワンAPMをお探しなら、ぜひ私たちに会いに来てください。
-
RuboCopで実現するRubyコードのリントと自動フォーマット徹底ガイド
リンティング(Lint)とは、ソースコードに含まれるプログラム上の誤りやスタイル違反を自動的に検出する仕組みです。このチェックは「リンター」と呼ばれる静的コード解析ツールによって行われます。一方、コードフォーマッターは、あらかじめ設定されたルールセットにソースコードを厳密に準拠させることに特化したツールです。一般的にリンターは違反を報告するだけで、実際の修正はプログラマーの手に委ねられます。それに対してコードフォーマッターは、ルールを直接ソースコードに適用し、フォーマットのミスを自動的に修正してくれます。 プロジェクトで一貫性のあるコードスタイルを実現するには、通常リンティング用とフォーマッ
-
Rubyでのログ出力をマスターする:LoggerとLogrageの使い方徹底解説
Rubyでのログ出力入門:LoggerとLogrageの使い方 ロギングは、アプリケーション開発において最も重要なタスクの一つです。ログは以下のような場面で活用されます。 アプリ内部で何が起きているかを把握したいとき アプリケーションを監視したいとき 特定のデータに関するメトリクスを収集したいとき 新しいプログラミング言語を学ぶ際、最初に選ばれるのはその言語がネイティブに備えているロギング機構でしょう。標準機能は通常、扱いやすく、ドキュメントも充実しており、コミュニティでも広く使われています。 ただし、ログデータの内容や扱い方は、企業の方針、ビジネスの性質、アプリケーションの種類に