生命を模倣するアルゴリズム

大学で最初にコンピューターサイエンスコースを受けて以来、私は現実世界でアルゴリズムがどのように機能するかに興味がありました。現実の世界で起こることを考えると、それを模倣するアルゴリズムを思いつくことができます。私は特に、食料品店、交通機関、空港などで行列に並んでいるときにこれを行います。列に並んで退屈なときが、キューイング理論を熟考する絶好の機会になることがわかりました。

10 年以上前、Amazon フルフィルメントセンターで 1 日働きました。棚からアイテムを取り出し、アイテムをあるボックスから別のボックスに移動し、ビンを動かすアルゴリズムの手ほどきを受けました。他の多くの人々と並んで仕事をしていると、本質的に驚くほど組織化された物理的なマージソートの一部であることが美しいと感じました。

キューイング理論では、キューが短いときの動作はどちらかというと面白くありません。結局、キューが短ければ、誰もが幸せなのです。キューへのバックログが発生し、イベントへの列がドアから出て角を曲がったときに初めて、人々はスループットと優先順位付けについて考え始めます。

この記事では、キューバックログシナリオに対処するために Amazon で使用している戦略について説明します。それはキューを迅速にドレインし、ワークロードに優先順位を付けるための設計アプローチです。最も重要なこととして、そもそもキューのバックログが蓄積するのを防ぐ方法を説明します。前半では、バックログにつながるシナリオについて説明し、後半では、バックログを回避したり、それらを適切に処理するために Amazon で使用する多くのアプローチについて説明します。

キューの二重の性質

キューは、信頼性の高い非同期システムを構築するための強力なツールです。キューを使用すると、あるシステムが別のシステムからのメッセージを受け入れ、長時間の停止、サーバー障害、または依存システムの問題が発生した場合でも、メッセージが完全に処理されるまでメッセージを保持できます。障害が発生したときにメッセージをドロップするのではなく、キューはメッセージが正常に処理されるまでメッセージを再試行します。最終的に、キューはシステムの耐久性と可用性を向上させますが、再試行によりレイテンシーを時折増加させます。
 
Amazon では、キューを利用する多くの非同期システムを構築しています。これらのシステムの一部は、長時間かかる可能性があり、amazon.com での注文の履行など、世界中を物理的に移動するワークフローを処理しています。他のシステムは、無視できないほどの時間を要する可能性があるステップを調整しています。たとえば、Amazon RDS は EC2 インスタンスをリクエストし、それらが起動するのを待ってから、データベースを設定しています。他のシステムはバッチ処理を利用しています。たとえば、CloudWatch のメトリクスとログの取り込みに関係するシステムは、大量のデータを取り込み、それをまとめてチャンクに「フラット化」しています。
 
メッセージを非同期で処理するためのキューの利点は簡単にわかるかもしれませんが、キューを使用するリスクはより微妙です。長年にわたって、可用性の向上を目的としたキューイングが逆効果になる可能性があることを発見しました。実際、停止後の回復時間が劇的に増加する可能性があります。
 
キューベースのシステムでは、処理は停止するがメッセージが届き続けると、メッセージの負債が大量のバックログに蓄積され、処理時間が長くなる可能性があります。結果が役立つには作業が遅すぎる可能性があり、基本的にキューイングが防止することを意図した可用性のヒットを引き起こします。
 
別の言い方をすれば、キューベースのシステムには 2 つの動作モード、またはバイモーダル動作があります。キューにバックログがない場合、システムのレイテンシーは低く、システムは高速モードです。しかし、障害または予期しない負荷パターンが原因で到着率が処理率を超えると、より不吉な動作モードにすぐに変わります。このモードでは、エンドツーエンドのレイテンシーがますます長くなり、バックログを処理して高速モードに戻るまでにかなりの時間がかかる場合があります。

キューベースのシステム

この記事でキューベースのシステムを説明するために、次の 2 つの AWS のサービスが内部でどのように機能するかについて触れます。1 つ目の AWS Lambda は、実行するインフラストラクチャを心配することなく、イベントに応じてコードを実行するサービスです。2 つ目の AWS IoT Core は、接続されたデバイスがクラウドアプリケーションやその他のデバイスと簡単かつ安全にやり取りできるようにするマネージドサービスです。

AWS Lambda を使用して、関数コードをアップロードし、次の 2 つの方法のいずれかで関数を呼び出します。

• 同期: 関数の出力が HTTP レスポンスで返されます
• 非同期: HTTP レスポンスがすぐに返され、関数が実行され、舞台裏で再試行されます

Lambda は、サーバー障害が発生した場合でも関数が実行されることを確認するため、リクエストを保存する永続キューが必要です。永続キューでは、関数が初めて失敗した場合にリクエストを再駆動できます。

AWS IoT Core を使用すると、デバイスとアプリケーションが接続され、PubSub メッセージトピックをサブスクライブできます。デバイスまたはアプリケーションがメッセージを発行すると、サブスクリプションが一致するアプリケーションが、メッセージの独自のコピーを受け取ります。制限された IoT デバイスは、サブスクライブされたすべてのデバイス、アプリケーション、およびシステムがコピーを受信することを保証するために待って限られたリソースを消費することを望まないため、この PubSub メッセージングの多くは非同期に行われます。別のデバイスが関心のあるメッセージを発行したとき、サブスクライブしたデバイスがオフラインである可能性があるため、これは特に重要です。オフラインデバイスが再接続すると、最初に速度が回復し、その後メッセージが配信されます (再接続後のメッセージ配信を管理するためのシステムのコーディングについては、AWS IoT 開発者ガイドの「MQTT 永続的セッション」を参照してください)。これを実現するために、さまざまな種類の永続性と非同期処理が舞台裏で行われます。

このようなキューベースのシステムは、多くの場合、永続キューを使用して実装されます。SQS は耐久性があり、スケーラブルな少なくとも 1 回のメッセージ配信セマンティクスを提供するため、Lambda や IoT を含む Amazon チームは、スケーラブルな非同期システムを構築する際にそれを定期的に使用しています。キューベースのシステムでは、あるコンポーネントがメッセージをキューに入れてデータを生成し、別のコンポーネントが定期的にメッセージを要求し、メッセージを処理し、完了したら最終的に削除することでそのデータを消費します。

非同期システムの障害

AWS Lambda では、関数の呼び出しが通常よりも遅い場合 (依存関係のためなど) 、または一時的に失敗した場合でも、データは失われず、Lambda は関数を再試行します。Lambda は起動の呼び出しをキューに入れ、関数が再び動作を開始すると、Lambda は関数のバックログを処理します。ここで、バックログを処理して通常に戻るまでにかかる時間を考えてみましょう。

メッセージの処理中に 1 時間の停止が発生したシステムを想像してみてください。所定のレートと処理能力に関係なく、停止から回復するには、回復後の 1 時間はシステムの能力を 2 倍にする必要があります。実際には、システムは、特に Lambda のような弾力性のあるサービスの場合、使用可能な容量が 2 倍以上になる可能性があり、回復がより速く行われる可能性があります。一方、関数が相互作用する他のシステムは、バックログを処理する際の処理の大幅な増加に対応する準備ができていない可能性があります。これが発生すると、追いつくのにさらに時間がかかる可能性があります。非同期サービスは、停止中にバックログを蓄積し、停止中にリクエストをドロップするが回復時間は速い同期サービスとは異なり、回復時間が長くなります。

長年にわたり、キューイングについて考えるとき、レイテンシーは非同期システムにとって重要ではないと考えたくなることがありました。非同期システムは、多くの場合、耐久性のために、またはレイテンシーから発信者をすぐに分離するために構築されます。ただし、実際には、処理時間が重要であり、多くの場合、非同期システムでも 1 秒以下のレイテンシーが期待されています。永続性のためにキューが導入されると、バックログに直面してこのような高い処理レイテンシーを引き起こすトレードオフを見逃しがちです。非同期システムの隠れたリスクは、大量のバックログを処理することです。

可用性とレイテンシーの測定方法

レイテンシーと可用性のトレードオフに関するこの議論は、非同期サービスのレイテンシーと可用性に関する目標をどのように測定し設定したらよいか、という興味深い質問を投げかけます。 プロデューサーの観点からエラー率を測定すると、可用性の状況の一部はわかりますが、それほど多くはありません。プロデューサーの可用性は、使用しているシステムのキューの可用性に比例します。したがって、SQS に基づいて構築すると、プロデューサーの可用性は SQS の可用性と一致します。

一方、コンシューマー側の目線でシステムの可用性を見た場合、それが実際よりも悪く見える場合があり得ます。なぜなら、障害が起きた場合は再試行が行われ、次の段階で処理に成功するということがあるためです。

また、可用性の観測は、デッドレターキュー (DLQ) を通じても行えます。再試行中に届いたメッセージは、ドロップされるか DLQ 内に置かれます。DLQ とは単純に独立した 1 つのキューであり、処理できないメッセージを、後で行う調査や介入のために格納しておく用途に使います。ドロップされたメッセージまたは DLQ メッセージのレートは、可用性の良い測定値ですが、問題の検出が遅すぎる場合があります。DLQ ボリュームについて警告するのは良い考えですが、DLQ 情報が到着するのが遅すぎて、問題を検出するためだけにそれを使用することはできません。

レイテンシーはどうでしょうか? 繰り返しになりますが、プロデューサーが観測した待機時間は、キューサービス自体の待機時間を反映しています。したがって、キュー内のメッセージの経過時間の測定に重点を置きます。これにより、システムが遅れている場合や、頻繁にエラーが発生して再試行を引き起こしている場合をすばやく検出できます。SQS などのサービスは、各メッセージがキューに到達したときのタイムスタンプを提供します。タイムスタンプ情報を使用すると、メッセージをキューから取り出すたびに、システムがどれだけ遅れているかを記録し、メトリクスを生成できます。

ただし、レイテンシーの問題はもう少し微妙な場合があります。結局のところ、バックログは予期されるものであり、実際、一部のメッセージでは問題ありません。たとえば、AWS IoT では、デバイスがオフラインになるか、メッセージの読み取りが遅くなることが予想される場合があります。これは、多くの IoT デバイスが低電力で、インターネット接続にむらがあるためです。AWS IoT Core のオペレーターとして、デバイスがオフラインになったり、メッセージをゆっくり読んだりすることを選択したことによって生じる予想される小さなバックログと、予期しないシステム全体のバックログとの違いを見分ける必要があります。

AWS IoT では、AgeOfFirstAttempt という別のメトリクスでサービスを計測しました。この測定は、メッセージのエンキュー時間を差し引いたものになりましたが、これは、AWS IoT が初めてメッセージをデバイスに配信しようとした場合に限ります。これにより、デバイスがバックアップされたときに、デバイスがメッセージを再試行したりキューに入れたりすることで汚染されないクリーンなメトリクスが得られます。メトリクスをさらにクリーンにするために、2 番目のメトリクス AgeOfFirstSubscriberFirstAttempt を発行します。AWS IoT のような PubSub システムでは、特定のトピックにサブスクライブできるデバイスまたはアプリケーションの数に実質的な制限はないため、1 つのデバイスに送信する場合よりも百万台のデバイスにメッセージを送信する場合の方がレイテンシーは長くなります。安定したメトリクスを得るために、そのトピックの最初のサブスクライバーにメッセージを発行する最初の試行でタイマーメトリクスを発行します。そして、残りのメッセージの発行に関するシステムの進捗を測定するためのその他のメトリクスがあります。

AgeOfFirstAttempt メトリクスは、システム全体の問題の早期警告として機能します。これは、メッセージをより遅く読むことを選択しているデバイスからのノイズを除去することが大きな理由です。特筆すべき点は、AWS IoT のようなシステムには、これよりもはるかに多くのメトリクスが装備されていることです。ただし、レイテンシー関連のすべてのメトリクスが利用可能なため、再試行のレイテンシーとは別に最初の試行のレイテンシーを分類する戦略が、Amazon 全体で一般的に使用されています。

非同期システムの待機時間と可用性の測定は困難です。また、リクエストはサーバー間で跳ね返り、各システムの外部で遅延する可能性があるため、デバッグも難しい場合があります。分散トレースを支援するために、キューに入れられたメッセージの中でリクエスト ID を伝播して、つなぎ合わせることができます。通常、X-Ray のようなシステムもこれを支援するために使用しています。

マルチテナント非同期システムのバックログ

多くの非同期システムはマルチテナントであり、多くの異なる顧客に代わって作業を処理しています。これにより、レイテンシーと可用性の管理に複雑な側面が加わります。マルチテナンシーの利点は、複数のフリートを個別に運用する必要があるという運用上のオーバーヘッドを節約し、より高いリソース使用率で複合ワークロードを実行できることです。ただし、顧客は、他の顧客のワークロードに関係なく、予測可能なレイテンシーと高可用性を備えた独自のシングルテナントシステムのように動作することを期待しています。

AWS のサービスは、発信者がメッセージを入れるための内部キューを直接公開しません。代わりに、軽量 API を実装して発信者を認証し、キューに入れる前に各メッセージに発信者情報を追加します。これは、前述の Lambda アーキテクチャに似ています。関数を非同期的に呼び出すと、Lambda は Lambda の内部キューを直接公開するのではなく、Lambda が所有するキューにメッセージを入れてすぐに戻ります。

この軽量 API により、公平性調整を追加することもできます。顧客のワークロードが他の顧客に影響を与えないように、マルチテナントシステムの公平性は重要です。AWS が公平性を実装する一般的な方法は、バーストに対してある程度の柔軟性を持たせながら、顧客ごとにレートベースの制限を設定することです。SQS 自体など、当社のシステムの多くでは、顧客が有機的に成長するにつれて顧客ごとの制限を増やしています。制限は、予期しないスパイクのガードレールとして機能し、舞台裏でプロビジョニングを調整する時間を確保できます。

非同期システムの公平性は、同期システムの調整と同じように機能します。ただし、非同期システムでは、大量のバックログがすぐに蓄積される可能性があるため、さらに重要だと考えています。

例として、非同期システムに十分なノイズのある近隣からの保護が組み込まれていない場合にどうなるかを考えてみてください。システムの 1 人の顧客が突然スロットルを切ってトラフィックを急増させ、システム全体のバックログを生成した場合、オペレーターが関与し、何が起こっているかを把握し、問題を軽減するのに 30 分程度かかる場合があります。その 30 分間に、システムのプロデューサー側が適切にスケーリングし、すべてのメッセージをキューに入れられた可能性があります。ただし、キューに入れられたメッセージの量が、コンシューマー側がスケーリングした容量の 10 倍である場合、システムがバックログを処理して回復するのに 300 分かかることを意味します。短時間の負荷スパイクでも数時間の復旧時間が発生する可能性があるため、数時間の停止が発生してしまいます。

実際には、AWS のシステムには、キューのバックログによる悪影響を最小限に抑える、または防ぐための多数の補正要素があります。たとえば、Auto Scaling は、負荷が増加したときの問題を軽減するのに役立ちます。ただし、複数のレイヤーで信頼性のあるシステムを設計するのに役立つため、補正要因を考慮せずに、キューイングの影響のみを調べると役立ちます。 大きなキューバックログと長い復旧時間を回避するのに役立つことがわかったいくつかの設計パターンを次に示します。

すべてのレイヤーでの保護は、非同期システムで重要である。 同期システムはバックログを蓄積する傾向がないため、フロントドアスロットリングとアドミッションコントロールでそれらを保護します。非同期システムでは、システムの各コンポーネントが過負荷から自身を保護し、1 つのワークロードがリソースの不公平なシェアを消費しないようにする必要があります。フロントドアアドミッションコントロールを回避するワークロードが常に存在するため、サービスが過負荷にならないようにするには、ベルト、サスペンダー、ポケットプロテクターが必要です。
複数のキューを使用すると、トラフィックのシェーピングに役立つ。 いくつかの点で、単一のキューとマルチテナンシーは互いに対立しています。作業が共有キューに入れられるまでに、1 つのワークロードを別のワークロードから分離するのは困難です。
リアルタイムシステムは多くの場合 FIFO 風のキューで実装されるが、LIFO 風の動作を好む。 お客様から、バックログに直面した場合、新鮮なデータがすぐに処理されるのを好むと聞いています。停電や急増中に蓄積されたデータは、容量が利用可能になったときに処理できます。

復元力のあるマルチテナント非同期システムを作成するための Amazon の戦略

Amazon のシステムがマルチテナントの非同期システムをワークロードの変化に対して回復力のあるものにするために使用するパターンがいくつかあります。これらには多くの手法がありますが、Amazon 全体で使用する多くのシステムもあり、それぞれ独自のライブ状態と耐久性の要件があります。次のセクションでは、私たちが使用するパターンのいくつかと、AWS のお客様がシステムで使用するパターンを説明します。

ワークロードを個別のキューに分離する

一部のシステムでは、すべての顧客で 1 つのキューを共有する代わりに、各顧客に独自のキューを割り当てています。サービスはすべてのキューのポーリングにリソースを費やす必要があるため、各顧客またはワークロードにキューを追加することは必ずしも費用対効果が高いとはいえません。けれども、少数の顧客や隣接システムがあるシステムでは、この単純なソリューションが役立ちます。一方、システムに数多くの顧客がいる場合、別々のキューが扱いにくくなる可能性があります。たとえば、AWS IoT は、世の中のすべての IoT デバイスに個別のキューを使用しているわけではありません。その場合、ポーリングコストは適切に調整されません。

シャッフルシャーディング

AWS Lambda は、Lambda の顧客ごとに個別のキューをポーリングするとコストがかかりすぎるシステムの例です。ただし、キューが 1 つしかない場合、この記事で説明した問題のいくつかが発生する可能性があります。したがって、1 つのキューを使用するのではなく、AWS Lambda は固定数のキューをプロビジョニングし、各顧客を少数のキューにハッシュします。メッセージをキューに入れる前に、ターゲットとなるキューの中でメッセージが最も少ないものを確認し、そのキューに入れます。ある顧客のワークロードが増加すると、マップされたキューでバックログが発生しますが、他のワークロードはそのキューから自動的にルーティングされます。魅力的なリソースの分離を構築するために多数のキューを必要としません。これは、Lambda に組み込まれている多くの保護の 1 つにすぎませんが、Amazon の他のサービスでも使用されている手法です。

過剰なトラフィックを別のキューに並べる

ある意味、バックログがキューに蓄積されてからトラフィックに優先順位を付けるのでは遅すぎます。ただし、メッセージの処理に比較的費用がかかる場合や時間がかかる場合は、メッセージを別のスピルオーバーキューに移動できるようにする価値があります。Amazon の一部のシステムでは、コンシューマーサービスが分散スロットルを実装し、設定されたレートを超えた顧客のメッセージをデキューすると、過剰なメッセージを個別のスピルオーバーキューにエンキューし、プライマリキューからメッセージを削除します。システムは、リソースが利用可能になるとすぐに、スピルオーバーキュー内のメッセージを処理します。本質的に、これは優先度キューに近似します。同様のロジックがプロデューサー側で実装されることもあります。このように、システムが単一のワークロードから大量のリクエストを受け入れる場合、そのワークロードはホットパスキュー内の他のワークロードを混雑させません。

古いトラフィックを別のキューに並べる

過剰なトラフィックを回避するのと同様に、古いトラフィックも回避できます。メッセージをデキューすると、メッセージの古さを確認できます。単に経過時間を記録するのではなく、情報を使用して、ライブキューに追いついた後にのみ処理するバックログキューにメッセージを移動するかどうかを決定できます。大量のデータを取り込む負荷が急増し、遅れる場合、トラフィックのキューをデキューおよび再エンキューできる限り、そのトラフィックの波を別のキューに一時的に追加できます。これにより、単にバックログを順番に処理する場合よりも、コンシューマーのリソースが解放され、新鮮なメッセージをすばやく処理できます。これは、LIFO の順序を概算する 1 つの方法です。

古いメッセージのドロップ (メッセージの存続時間)

一部のシステムは、非常に古いメッセージがドロップされるのを許容できます。たとえば、一部のシステムはシステムへのデルタを迅速に処理しますが、定期的に完全同期も実行します。これらの定期的な同期システムは、しばしば反エントロピースイーパーと呼ばれます。この場合、古いキューに入れられたトラフィックを横に並べる代わりに、最新のスイープの前にトラフィックが入った場合、それを安くドロップできます。

ワークロードごとのスレッド (およびその他のリソース) の制限

同期サービスと同様に、1 つのワークロードがスレッドの公平なシェアを超えて使用しないように非同期システムを設計します。まだ話していない AWS IoT の 1 つの側面に、ルールエンジンがあります。お客様は、デバイスから顧客所有の Amazon Elasticsearch クラスター、Kinesis Stream などにメッセージをルーティングするように AWS IoT を設定できます。これらの顧客所有のリソースへのレイテンシーが遅くなるが、着信メッセージレートが一定のままである場合、システムの同時実行性の量は増加します。また、システムが処理できる同時実行性の量はいつでも制限されるため、ルールエンジンは、1 つのワークロードが同時実行性関連リソースの公平なシェアを超えて消費するのを防ぎます。

実行中の力はリトルの法則 によって記述されます。この法則は、システムの同時実行性が、到着率に各リクエストの平均待機時間を掛けた値に等しいと説明します。たとえば、サーバーが平均 100 ミリ秒で 1 秒あたり 100 メッセージを処理していた場合、平均 10 スレッドを消費します。レイテンシーが突然 10 秒に急増すると、突然 1,000 スレッドが使用され (平均して、実際にはもっと長くなる可能性があります) 、スレッドプールを簡単に使い果たす可能性があります。

ルールエンジンは、これを防ぐためにいくつかの手法を使用しています。ノンブロッキング I/O を使用してスレッドの枯渇を回避しますが、特定のサーバーが持つ作業量には他の制限があります (たとえば、クライアントが接続をかき回し、依存関係がタイムアウトになったときのメモリ、ファイル) 。使用できる 2 番目の並行性ガードは、任意の時点で単一のワークロードに使用できる並行性の量を測定および制限するセマフォです。ルールエンジンは、レートベースの公平性制限も使用します。ただし、ワークロードが時間とともに変化することは完全に正常であるため、ルールエンジンはワークロードの変化に合わせて制限を時間とともに自動的にスケーリングします。また、ルールエンジンはキューベースであるため、IoT デバイスと、舞台裏でのリソースおよびセーフガード制限の Auto Scaling との間のバッファーとして機能します。

Amazon のサービス全体で、1 つのワークロードが使用可能なすべてのスレッドを消費することを回避するために、ワークロードごとに個別のスレッドプールを使用します。また、各ワークロードで AtomicInteger を使用して、各ワークロードで許可される同時実行を制限し、レートベースのリソースを分離するためのレートベースの調整アプローチを使用します。

バックプレッシャーをアップストリームに送信する

ワークロードがコンシューマーが追いつかない不合理なバックログを引き起こしている場合、当社のシステムの多くは、プロデューサーでの作業をより積極的に拒否し始めます。ワークロードの 1 日分のバックログを構築するのは簡単です。そのワークロードが分離されている場合でも、偶然であり、チャーンに費用がかかる可能性があります。このアプローチの実装は、ワークロードのキューの深さをときどき測定し (ワークロードが独自のキューにあると仮定) 、バックログサイズに比例してインバウンドスロットル制限を (逆に) スケーリングするのと同じくらい簡単です。

複数のワークロードで SQS キューを共有する場合、このアプローチは扱いにくくなります。キュー内のメッセージ数を返す SQS API がありますが、特定の属性を持つキュー内のメッセージ数を返すことができる API はありません。それでもキューの深さを測定し、それに応じてバックプレッシャーを適用することはできますが、たまたま同じキューを共有している無実のワークロードに不当にバックプレッシャーをかけることになりかねません。Amazon MQ のような他のシステムでは、きめ細かいバックログの可視性があります。

バックプレッシャーは、Amazon のすべてのシステムに適しているわけではありません。たとえば、amazon.com の注文処理を実行するシステムでは、バックログが蓄積している場合でも、新しい注文を受け入れるのを妨げるのではなく、注文を受け入れたい傾向があります。しかし、もちろんこれには、舞台裏で多くの優先順位付けが伴うため、最も緊急の注文が最初に処理されます。

遅延キューを使用して後まで作業を先送りにする

特定のワークロードのスループットを下げる必要があるというシステムの感覚がある場合、そのワークロードでバックオフ戦略を使用しようとします。これを実装するために、メッセージの配信を後まで遅らせる SQS 機能をよく使用します。メッセージを処理し、後で保存することにした場合、そのメッセージを別のサージキューに再エンキューすることがありますが、遅延パラメータを設定して、メッセージが遅延キューに数分間隠されるようにします。これにより、システムは代わりに最新のデータを処理できるようになります。

あまりにも多くの処理中のメッセージを避ける

SQS のような一部のキューサービスには、キューのコンシューマーに配信できる処理中のメッセージの数に制限があります。これは、キューに入れることができるメッセージの数 (実際的な制限はありません) とは異なりますが、コンシューマーフリートが一度に処理しているメッセージの数です。システムがメッセージをデキューしても、削除に失敗すると、この数は増加する可能性があります。たとえば、メッセージの処理中にコードが例外をキャッチできず、メッセージの削除を忘れるバグがありました。このような場合、メッセージは、メッセージの VisibilityTimeout の SQS の観点から、処理中のままになります。エラー処理とオーバーロード戦略を設計するとき、これらの制限を念頭に置いて、余分なメッセージを表示したままにするのではなく、別のキューに移動することを好みます。

SQS FIFO キューにも同様の、しかし微妙な制限があります。SQS FIFO を使用すると、システムは特定のメッセージグループに対してメッセージを順番に消費しますが、異なるグループのメッセージは任意の順序で処理されます。したがって、1 つのメッセージグループで小さなバックログを作成した場合、他のグループのメッセージを処理し続けます。ただし、SQS FIFO は、最新の未処理の 20,000 件のメッセージのみをポーリングします。そのため、メッセージグループのサブセットに 20,000 件を超える未処理のメッセージがある場合、新しいメッセージを持つ他のメッセージグループは不足します。

処理できないメッセージにデッドレターキューを使用する

処理できないメッセージは、システムの過負荷につながる可能性があります。システムが処理できないメッセージをキューに入れる場合 (おそらく入力検証エッジケースをトリガーするため) 、SQS は、これらのメッセージをデッドレターキュー (DLQ) 機能を備えた別のキューに自動的に移動することで助けます。このキューにメッセージがある場合、警告が表示されます。これは、修正が必要なバグがあることを意味します。DLQ の利点は、バグが修正された後にメッセージを再処理できることです。

ワークロードごとのポーリングスレッドで追加のバッファーを確保する

ワークロードが、定常状態でもポーリングスレッドが常にビジーになるほど十分なスループットを駆動している場合、システムはトラフィックの急増を吸収するためのバッファーがないポイントに達している可能性があります。この状態では、着信トラフィックの小さなスパイクにより、未処理のバックログが大量に発生し、レイテンシーが長くなります。このようなバーストを吸収するために、ポーリングスレッドで追加のバッファを計画します。1 つの測定は、空の応答をもたらすポーリング試行の回数を追跡することです。ポーリングの試行ごとにもう 1 つのメッセージを取得している場合、ポーリングスレッドの数が適切であるか、着信トラフィックに対応するのに十分でない可能性があります。

ハートビートの長時間メッセージ

システムが SQS メッセージを処理するとき、SQS は、システムがクラッシュしたと想定する前にメッセージの処理を完了し、別のコンシューマーにメッセージを配信して再試行するために一定の時間をそのシステムに与えます。コードが実行を続け、この期限を忘れた場合、同じメッセージを複数回並行して配信できます。最初のプロセッサはタイムアウト後もメッセージを送り続けていますが、2 番目のプロセッサはそれを拾い上げ、タイムアウトを過ぎて同様に追い出し、3 番目のプロセッサなども同様に処理します。カスケードブラウンアウトの可能性があるため、メッセージの有効期限が切れたときに作業を停止するメッセージ処理ロジックを実装するか、SQS にまだ作業中であることを思い出させるためにそのメッセージのハートビートを続行します。このコンセプトは、リーダー選挙のリースに似ています。

これは潜在的な問題です。なぜなら、データベースへのクエリに時間がかかったり、サーバーが処理しきれないほどの作業を行ったりするため、過負荷時にシステムのレイテンシーが増加する可能性が高いためです。システムのレイテンシーが VisibilityTimeout のしきい値を超えると、既に過負荷のサービスが本質的に fork-bomb 自体になります。

クロスホストデバッグを計画する

分散システムの障害を理解することはすでに困難です。計測に関する関連記事では、キューの深さを定期的に記録することから、「トレース ID」を伝播し、X-Ray と統合することまで、非同期システムを計測するためのいくつかのアプローチについて説明しています。または、システムに単純な SQS キューを超える複雑な非同期ワークフローがある場合、ワークフローの可視性を提供し、分散デバッグを簡素化する、Step Functions などの別の非同期ワークフローサービスをよく使用します。

まとめ

非同期システムでは、レイテンシーについて考えることがいかに重要かを忘れがちです。結局のところ、非同期システムは、信頼できる再試行を実行するためのキューが先頭にあるため、時間がかかる場合があります。ただし、過負荷と障害のシナリオでは、サービスが適切な時間内に回復できない、巨大な克服できないバックログが蓄積する可能性があります。これらのバックログは、1 つのワークロードまたは顧客が予想外に高い割合でキューに登録すること、処理するために予測されるよりも高価になるワークロード、またはレイテンシーまたは依存関係の障害に起因する可能性があります。

非同期システムを構築する場合、これらのバックログシナリオに焦点を合わせて予測し、優先順位付け、サイドライン、バックプレッシャーなどの手法を使用してそれらを最小限に抑える必要があります。

参考文献

キューイング理論
リトルの法則
アムダールの法則
• Little A Proof for the Queuing Formula: L = λW, Case Western, 1961
• McKenney, Stochastic Fairness Queuing, IBM, 1990
• Nichols and Jacobson, Controlling Queue Delay, PARC, 2011

著者について

David Yanacek は、AWS Lambda に取り組むシニアプリンシパルエンジニアです。2006 年から Amazon のソフトウェア開発者で、以前は Amazon DynamoDB と AWS IoT、内部のウェブサービスフレームワーク、フリート運用自動化システムにも取り組んでいました。David の職場でのお気に入りの活動の 1 つは、ログ分析を実行し、運用メトリクスをふるいにかけて、システムを徐々にスムーズに実行する方法を見つけることです。

分散システムのリーダー選挙 運用の可視性を高めるために分散システムを装備する