Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

SidekiqからKarafkaへ:Honeybadgerが実現したシームレスなバックグラウンドジョブの移行

Webアプリケーションをスケールさせることを考えるとき、バックグラウンド処理はその中核を担う要素のひとつです。基本的な仕組みはシンプルです。クライアントがWebアプリケーションにリクエストを送ると、アプリはその処理の中で時間のかかるタスクを複数実行します。クライアントにより速くレスポンスを返すために、アプリは重い処理をバックグラウンド処理システムへジョブとして投入(エンキュー)します。あとはバックグラウンド処理システムが、計算処理やI/O操作といった重い作業をすべて引き受けてくれます。バックグラウンドジョブを効果的に活用することは、Webアプリケーションをスケールさせる上で最も重要な構成要素のひとつなのです。

Rails開発者にとって幸いなことに、選択できる優れたライブラリは複数あります。それぞれに長所・短所があり、バックエンドのデータストアも異なります。こうしたライブラリを活用すれば、重い処理を簡単にオフロードでき、アプリケーションのレスポンスは高速化し、少ないリソースでより多くのユーザーに対応できるようになります。

最近まで、私たちはHoneybadgerのバックグラウンド処理の大部分にSidekiqを使用していました。Sidekiqは、快適で高速なユーザー体験の維持と、大量に取り込むデータをさばく堅牢なパイプラインの構築において、大きな役割を果たしてくれました。

Sidekiqのどこが課題だったのか?

高い信頼性を誇るRedisにも、いくつかの制約があります。これまで私たちは、エラートラッキングエンドポイントへのデータ取り込みを含む、すべてのジョブ処理にSidekiqを使っていました。エラートラフィックは変動が激しく、キュー内のジョブ数が一瞬で10倍や20倍になることも珍しくありませんでした。その結果、オートスケーリングでワーカーが追いつくまで大きなバックログが溜まってしまうことがありました。しかし、それ以上に深刻だったのが、ElastiCacheクラスタのメモリ枯渇です。

ElastiCacheクラスタには大量のジョブトラフィックが流れていたため、下流の処理が少しでも遅延すると、クラスタがメモリ不足に陥るリスクがありました。キュー以外のデータ用には別クラスタを用意していましたが、時間とともに一部の非キューデータがプライマリクラスタ側にも混在するようになり、メモリ不足エラー発生時にはそれらが退避(eviction)される可能性がありました。ただ、より重大な問題は、クラスタのメモリが枯渇すると新しいジョブを受け付けられなくなることでした。

もうひとつの問題は、Redis/ElastiCacheがデフォルトでvolatile-lruという退避ポリシーを採用している点です。このポリシーでは、メモリ使用量が高くなった際に、TTLが設定されたデータのうち最も使われていないものから順に削除されます。実際、Honeybadgerでもメモリ使用量が想定を超えて高くなり、Redisが意図せずキャッシュのクリアを始めてしまった事象がありました。幸い、退避されたデータは再生成可能なもの(TTLが設定されていた理由です)だったため、恒久的なデータの損失はありませんでした。

それでも、「もし同様の事象が起きて、再構築できないデータを失ったらどうなるのか?」「顧客データを決して失わないことをどう保証できるのか?」という問いに向き合う必要がありました。顧客のエラーデータを扱うことは私たちのビジネスの根幹です。だからこそ、私たちにはデータ損失に強いシステムが必要でした。

データ取り込み基盤としてKafkaを採用

Kafkaは、スケーラビリティと耐障害性の両方を備えた分散型イベントパイプラインです。最近リリースしたInsightsでは、イベントデータを処理するインフラとしてKafkaを構築しており、そこで多くの経験を積むことができました。その経験を踏まえ、同じ技術スタックでエラー取り込みデータも処理したいと考えるようになりました。Kafkaを採用することで、冗長化されたストレージ、優れた拡張性、そしてより手頃なコストを実現できると考えたのです。

Insights向けにすでに自前のAWS MSKクラスタを運用していたため、インフラとオートスケーリングの仕組みは整っていました。つまり、トピックをいくつか作成し、Sidekiqワーカーと同じコードを実行するコンシューマを用意すればよかったのです。コンセプト自体は非常にシンプルだったので、Kafkaコンシューマの細かなチューニングに注力できました。

SidekiqからKarafkaへの移行

Honeybadgerは「majestic monolith(荘厳なモノリス)」として設計されており、Karafkaはそのアーキテクチャを維持するのに役立っています。実は、Insightsデータの一部処理ですでにKarafkaを使用していたため、新しいコンシューマをいくつか追加するだけの簡単な作業でした。

KarafkaとSidekiqの主な違いのひとつは、ジョブの取得方法です。Karafkaではジョブがバッチ化され、1回のコンシューマ実行でまとめて処理されます。コンシューマ内ではメッセージの配列を反復処理し、Sidekiqワーカーのコードをインラインで実行できます。

class NoticeConsumer < ApplicationConsumer
  def consume
    messages.each do |message|
      NoticeWorker.new.perform(message.payload)
    end
  end
end

考慮が必要だったもうひとつの違いは、エラーハンドリングの仕組みです。Sidekiqでは各ジョブがアトミックであるため、ワーカー自身がリトライや失敗時のコールバックでエラーを処理します。一方、Kafkaのバッチ処理では、エラー対応の選択肢がより多くあります。中でも特筆すべきは、Karafkaが提供する「Dead Letter Queue(DLQ)」という仕組みです。これにより、バッチ単位または個別メッセージ単位でエラーハンドリングを指定できます。

dead_letter_queue(
  topic: "ingestion.errors.dead",
  max_retries: 5,
  independent: true
)

Karafkaのコンシューマが個別のメッセージの処理に失敗すると、最大5回まで再処理を試みます。5回目でも失敗した場合、そのメッセージは指定されたトピックに送られます。independent: trueオプションを指定すると、失敗したメッセージのみをDLQに送るようになり、バッチ全体が対象になることはありません。

Karafkaの監視とスケーリング

実際に運用してみると、Karafkaコンシューマの監視とスケーリングはかなり複雑であることがわかりました。AWS/MSK側とKarafka側の両方で追跡できる項目が多数あり、調整可能なノブもたくさんあります。コードの挙動とデータフローの特性に対して、注意深く目を配る必要があります。

AWS CloudWatchではさまざまな項目を監視していますが、その中でも特に注目しているKafka固有のメトリクスを紹介します。

  • SumOffsetLag — 指定したトピックとコンシューマグループについて、全パーティションのオフセットラグの合計値。
  • EstimatedMaxTimeLag — 指定したトピックとコンシューマグループについて、全パーティションが現在のオフセットまで追いつくまでにかかる推定時間。

Karafka自身も優れた計測機能(instrumentation)を提供していますが、このデータは自分で公開・保存する必要があります。

  • processing_lag — 処理された各バッチで取得できる値。Kafkaからメッセージを取り出してから処理を開始するまでにかかった時間を示します。
  • consumption_lagprocessing_lagと似ていますが、こちらはバッチの最後のメッセージがKafkaシステムに入ってから、コンシューマが処理を開始するまでの時間です。
  • duration — コンシューマがバッチ全体の処理に要した時間。

また、SidekiqプロセスのスケーリングとKarafkaコンシューマのスケーリングは、かなり性質が異なることも判明しました。Sidekiqの場合は、Redisインスタンスが許容できる範囲でプロセスを増やせば並列度を上げられます。ところがKafkaでは、トピックのパーティションごとに最大1プロセスという制約があります。経験則として、Kafkaコンシューマは複数のパーティションを担当できるため、予定している数よりも多めにパーティションを用意しておくのがおすすめです。

さらに覚えておきたいのは、Kafkaコンシューマのスケールアップ/スケールダウンは非常に時間のかかる操作になり得るという点です。コンシューマグループへの参加・脱退が発生すると、グループはリバランス(再均衡)を行い、パーティションの再割り当てが必要になります。再割り当ての間、コンシューマはメッセージの処理を停止します。sticky-cooperativeアサインメントである程度緩和できますが、基本的にはリソースを多めに確保し、リバランス自体を避けるのが賢明です。

現在、私たちはSumOffsetLagをスケーリング指標のひとつとして監視しています。重要な注意点として、リバランス中はこのメトリクスがレポートされません。つまり、リバランス期間中はこの値が完了まで大幅に増加し続けることになります。これもまた、スケーリング操作は最小限にとどめるべき理由のひとつです。

HoneybadgerにおけるKarafkaの今後

Kafka/Karafka構成を100%稼働させて1か月以上が経ちましたが、その成果には十分満足しています。それでも、万が一の際にはボタンひとつでSidekiqにフォールバックできる状態を維持しているのも心強い点です。これにより、どちらかのシステムでメンテナンス作業が必要になった場合でも、さらなる耐障害性を確保できます。

SidekiqからKarafkaへの移行を通じて、KafkaとKarafkaの扱い方についても多くを学びました。まだ最新版のHoneybadger gemにアップデートしていない方は、ぜひチェックしてみてください!karafkaプラグインに新機能を追加しました。Insightsを有効にすると、gemが重要な統計情報の追跡を開始し、Kafkaシステム全体の健全性をより把握しやすくなります。

さらに、このデータを可視化する「Insights Karafkaダッシュボード」も用意しました。Kafkaコンシューマの挙動をより深く理解するのに役立ちます。Karafkaダッシュボードを利用するには、プラグインでメトリクスを有効にする必要があります。honeybadger.ymlに以下の設定を追加してください。

karafka:
  insights:
    metrics: true

ダッシュボードの表示イメージは以下の通りです。

SidekiqからKarafkaへ:Honeybadgerが実現したシームレスなバックグラウンドジョブの移行

お客様がこのデータを活用して、自社のKafkaシステムをどう改善していくのか、楽しみにしています。SidekiqからKarafkaへの移行方法や、Kafkaの活用全般についてご質問があれば、お気軽にお問い合わせください!

  1. Rubyのputs・print・pの違いとは?出力メソッドの使い分けを徹底解説

    Rubyには、画面に何かを出力するための方法がいくつかあります。中でも特によく使われるのが、以下の3つのメソッドです。 puts print p しかし、これらは一体どう違うのでしょうか?そしてどんな場面でどれを使えばよいのか?この記事では、それぞれの特徴と使い分けのポイントを詳しく解説します。 改行なしで出力する方法 ユーザーに見せるために画面へ何かを出力したいときは、通常putsを使用します。 例: puts Hello there! putsは、実行するたびにメッセージの末尾へ自動的に改行を追加します。 もし改行したくない場合は、printを使いましょう。 例: print 1

  2. JSON Web Token(JWT)認証で Ruby アプリを安全に強化する方法

    ユーザーが関わるWebアプリケーションでは、当然ながらユーザーのデータを保護し、安全に管理する必要があります。 Webアプリケーションのセキュリティ確保にはさまざまな側面がありますが、本記事では、その中でも「JSON Web Token(JWT)を使った認証」と、Ruby on Railsフレームワークとの組み合わせに焦点を当てて解説します。 それでは始めましょう! JSON Web Token(JWT)とは? JSON Web Tokenは、Internet Engineering Task Force(IETF)が定義したインターネット標準であり、「2者間で受け渡されるクレーム(主張)を表