非同期メッセージの処理の遅延

Indeed では、求職者にとっての最善を常に考えています。求職者が仕事に応募する際には、採用される全てのチャンスを掴んで欲しいと考えています。企業の採用選考中に、Indeedに掲載された求人に対する応募書類が処理待ちだったせいで、仕事の機会を逃してしまうということは、求職者にとって受け入れがたいことなのです。Indeedの応募書類担当チームは、応募書類の処理時間のサービスレベル目標 ( service level objectives ) を管理しています。私たちは、応募書類の処理、そしてそのシステムの大規模化にあたって、改善策を常に考えているのです。

当初 Indeed では、自社のアグリゲーションエンジン内に RabbitMQ を取り込み、毎日処理する求人情報の量に対応していました。この成功を踏まえ、私たちは求職者からの応募書類を処理するパイプラインなどの他のシステムに RabbitMQ を組み込むことにしました。現在では、このパイプラインは一日に150万件以上の応募書類の処理を担当しています。その過程の中で、担当チームはこの組込に関して、以下に述べるようなresilience patterns(障害発生時の回復パターン)を複数実装する必要がありました。

  • メッセージが作られてから消費されるまでの追跡
  • 予期されたエラーが発生時のメッセージ処理の遅延 
  • 完全には処理することのできないメッセージをデッドレターキューに送信

遅延キューの実装

遅延キューは、一定の時間メッセージを取って置いておくことで、メッセージ処理を先延ばしてくれます。このパターンを実装した理由を理解する上で、多くのメッセージングシステムの複数の主要な挙動を、まず考える必要があります。Rabbit MQの特徴:

  • 最低一回 (at least once ) の送信を保証(一部のメッセージは複数回送信される可能性あり)
  • acknowledgement (ack) やnegative acknowledgement (nack) 、またはメッセージを再度キューに置くのを許可
  • 再度メッセージをキューに置く場合は、キューの終わりではなく先頭にメッセージを配置

担当チームは、主に三番目の項目に対処するために遅延キュー (delay queue) を実装しました。Rabbit MQ がキューの先頭にメッセージを再度配置するので、コンシューマは直前に送信に失敗したメッセージを、おそらく次に処理することになります。これは、少量のメッセージしかない場合には問題にはなりませんが、処理不可能なメッセージの数がコンシューマスレッドの数を超えていくにつれ、重大な問題が発生します。コンシューマは、キューの先頭で処理不可能のメッセージのグループを通り抜けることができないため、クラスターの中にメッセージがたまっていってしまいます。

キューのサイズ
時間(24時間)

図1 クラスター内にたまっていくメッセージ

その仕組み

デッドレターキューなどのメカニズムはメッセージ処理の遅延を可能にする一方、システムを正常な状態に戻すために、そのメカニズムは、しばし人間による判断を必要とします。遅延キューのパターンはシステムが処理を続行できるようにします。くわえて、ファーストレスポンダー(就業時間内に本番環境の問題に即時対応するエンジニア)、サイト・リライアビリティ・エンジニア(SRE)、オペレーションチームの作業を減らしました。次の図は、エラーが発生した際のコンシューマ処理の選択肢を説明しています。

図2 非同期メッセージがコンシューマに渡っていくまでの仕組み

コンシューマがエラーにあい、メッセージを処理できない場合、エンジニアはコンシューマにそのメッセージを再度キューに置かせるか、遅延したキュー内に置かせるか、またはデッドレターキューに送信させるか選択する必要があります。その際、エンジニアは以下の項目を確認し、判断をします。

エラーは予期できたか?

システムに、再度発生することがなさそうな予期せぬエラーが発生した場合、メッセージは再度キューに置きます。こうすることで、システムがもう一度メッセージを処理できるようにします。再度メッセージをキューに置く作業は、以下のような状況で役立ちます。

  • サービスの通信中のネットワーク中断
  • トランザクションの取り消しや、ロックの取得に失敗することで起きるデータベースのオペレーションの不具合発生

依存するシステムは処理に追いつく時間を必要としているか?

再度処理実行前に少し時間をあける必要がありそうなエラーがシステムに発生した場合、メッセージを遅延させます。これはダウンストリームのシステムが回復できるようにするため、次にメッセージを処理しようとする際には、成功する可能性が高くなります。メッセージの遅延は以下のような状況に対応するのに便利です。

  • データベースの複製の際におきる時差の問題
  • 結果整合性(eventual consistency)が求められるシステムでの一貫性の問題

メッセージは処理不可能そうか?

もしメッセージが処理不可能な場合、デッドレターキューに送信します。それから、エンジニアは、メッセージを確認し、削除するか再度キューに手動で置く前に精査します。デッドレターキューは、システムが以下のような状況にある場合、役立ちます。

  • 不足している情報が含まれたメッセージを待っている
  • メッセージ再処理を試みる前に依存したリソースの人間による確認を必要している

エスカレーションポリシー

システムの障害発生時の回復力をさらに高めるため、エスカレーションポリシーを、1)エラーは予期できたか?2)依存するシステムは処理に追いつく時間を必要としているか?3)メッセージは処理不可能そうか?という前述の三つの選択肢の中で設定するのも良いでしょう。Indeedでは以下のように対応しました。

  • システムがキューにn回メッセージを置き直したら、そのメッセージを遅延させる。
  • もしこのメッセージがさらにm回遅延されたら、デッドレターキューに送る。

こういった種類のポリシーを持つことで、ファーストレスポンダー、SRE、オペレーションチームの作業を削減しました。日々、さらに増え続ける候補者の応募書類を処理する中で、私たちはその処理システムを拡張することができました。