著者:xiewang、IEGオペレーターTencent開発エンジニア
序文
遅延キューは、私たちが頻繁に連絡し、日常の開発プロセスで使用する必要がある技術的なソリューションです。少し前にビジネス要件を作成したときに、遅延メッセージキューの使用を必要とする需要シナリオにも遭遇したため、インターネット上の一連のさまざまな遅延キュー実装スキームについても調査しました。ここに要約を示します。要約を示します。共有する。
遅延キューの定義
まず第一に、誰もがキューのデータ構造に精通していると思います。それはファーストインファーストアウトのデータ構造です。通常のキュー内の要素は順序付けられ、最初にキューに入った要素が最初に取り出されて消費されます。
遅延キューと通常キューの最大の違いは、その遅延属性です。通常キューの要素は、先着順でキューに入る順に処理されますが、遅延キューの要素は、キューに入るときに指定されます。指定された時間が経過した後に処理できることを望んでいることを示す遅延時間。ある意味で、遅延キューの構造はキューのようなものではなく、時間で重み付けされた順序付けられたヒープ構造のようなものです。
アプリケーションシナリオ
ビジネスニーズの開発時に遭遇した使用シナリオは次のとおりです。ユーザーはアップルトでさまざまなWeChatまたはQQテンプレートメッセージをサブスクライブでき、製品の学生は指定された時間になると、アップルトの管理側で新しいメッセージプッシュプランを作成できます。ノードの時点で、テンプレートメッセージをサブスクライブしているすべてのユーザーに対してメッセージプッシュが実行されます。
それが単一の小さなプログラムのみを提供する場合、それは時限タスクである可能性があり、手動の時限実行でさえこの要求を完了するために最も便利で最速である可能性がありますが、すべてのメッセージサブスクリプションモジュールサービスを抽象化したいと考えていますビジネスで使用する場合は、この時点で一般的なシステムソリューションが必要であり、この時点で遅延キューが必要です。
上記で遭遇した一般的な要件に加えて、遅延キューのアプリケーションシナリオは、実際には次のような非常に広範囲です。
ユーザーが15分以内に支払いを怠った場合、新しく作成された注文は自動的にキャンセルされます。
会社の会議予約システムは、会議が正常に予約された後、会議が始まる30分前に会議を予約したすべてのユーザーに通知します。
安全チケットが24時間以上処理されない場合、会社のWeChatグループが自動的にプルされ、責任者に通知されます。
ユーザーがテイクアウトを注文した後、タイムアウト時間の10分前に、タイムアウトが間もなく終了することをテイクアウトブラザーに通知します。
データ量が比較的少なく、適時性の要件がそれほど高くないシナリオの場合、比較的簡単な方法は、データベース内のすべてのデータを毎秒ポーリングするなど、データベースをポーリングし、私が社内の会社である場合など、期限切れのデータをすべて処理することです。会議予約システムの開発者として、システム全体のデータ量がそれほど多くなく、会議開始の30分前に通知することと29分前に通知することの違いが大きくないため、このソリューションを使用する可能性があります。
ただし、処理する必要のあるデータの量が比較的多い場合、15分以内に支払われないタオバオのすべての新規注文の自動タイムアウトなど、リアルタイムの要件は比較的高く、その規模は100万、さらには1,000万にもなります。現時点で、データベースをポーリングする場合上司に殴られて死にたい場合は、そうでない場合は、運用および保守のクラスメートに殴られて死ぬ可能性があります。
このシナリオでは、今日の主人公である遅延キューを使用する必要があります。遅延キューは、遅延が必要な多数のメッセージを処理するための効率的なソリューションを提供します。言うまでもありませんが、いくつかの一般的な遅延キューソリューションとそれぞれの長所と短所を見てみましょう。
実行計画
Redis ZSet
Redisには順序付けられたデータ構造ZSetのセットがあり、ZSetの各要素には対応するスコアがあり、ZSetのすべての要素はスコアに従ってソートされていることがわかっています。
次に、RedisのZSetを使用して、次の操作で遅延キューを実装できます。
ZADD KEY timestamp task
エンキュー操作:、処理する必要のあるタスクを追加し、必要に応じてスコアとしてZSetに追加します。Redis ZAdd時間の複雑さはO(logN)
、N
ZSet内の要素の数であるため、比較的効率的なエンキュー操作を行うことができます。ZREANGEBYSCORE
最小要素スコアクエリZSetのメソッドによるプロセスのタイミング(たとえば毎秒)なので、特定の操作は次のようになりZRANGEBYSCORE KEY -inf +inf limit 0 1 WITHSCORES
ます。クエリ結果には2つの状況があります。a。クエリによって取得されたスコアが現在のタイムスタンプ以下であり、タスクが実行される時間に達し、タスクが非同期で処理されることを示します。
b。クエリによって取得されたスコアが現在のタイムスタンプよりも大きい。クエリ操作でスコアが最小の要素が取り出されたため、ZSet内のすべてのタスクが実行に必要な時間に達していないことを意味し、クエリは1秒間スリープした後も続行されます。
同様に、ZSetの要素数である
ZRANGEBYSCORE
操作の時間の複雑さは、クエリの要素数として、操作が比較的効率的であることを定期的にチェックします。O(logN + M)
N
M
これは、インターネットからのRedis実装遅延キューバックエンドアーキテクチャのセットです。これは、元のRedis ZSet実装に対して一連の最適化を実行し、システム全体をより安定して堅牢にし、高い同時実行シナリオに対応できるようにします。のスケーラビリティは非常に優れたアーキテクチャ設計であり、全体的なアーキテクチャ図は次のとおりです。
そのコアデザインのアイデア:
遅延メッセージタスクをハッシュアルゴリズムを介して異なるRedisキーにルーティングすることには、2つの大きな利点があります。
a。KEYがより多くの遅延メッセージを格納する場合、エンキュー操作とクエリ操作速度が遅くなるという問題を回避します(両方の操作の時間の複雑さは両方です
O(logN)
)。b。システムの水平方向のスケーラビリティが向上します。データ量が急激に増加した場合、データ量の増加に抵抗するためにRedisキーの数を増やすことで、システム全体をすばやく拡張できます。
各Redisキーは、イベントプロセスと呼ばれる対応する処理プロセスを確立します。キーは、上記の手順2で説明したZRANGEBYSCOREメソッドを介してポーリングされ、処理される遅延メッセージがあるかどうかがチェックされます。
すべてのイベントプロセスはメッセージの配信のみを担当し、特定のビジネスロジックは追加のメッセージキューを介して非同期に処理されます。そうすることの利点も明らかです。
a。一方で、イベントプロセスはメッセージの配信のみを担当するため、メッセージの処理速度は非常に速く、複雑なビジネスロジックのためにメッセージが蓄積される可能性はほとんどありません。
b。一方、追加のメッセージキューを使用すると、メッセージ処理のスケーラビリティが向上します。コンシューマープロセスの数を増やすことで、システム全体のメッセージ処理機能を拡張できます。
イベントプロセスは、Zookeeperを採用して、メインの単一プロセス展開方法を選択し、イベントプロセスがダウンした後のRedisキーへのメッセージの蓄積を回避します。Zookeeperのリーダーホストがダウンすると、Zookeeperは自動的に新しいリーダーホストを選択して、Redisキーのメッセージを処理します。
上記の説明から、Redis Zsetを介して遅延キューを実装することは、迅速に実装できるより直感的なソリューションであることがわかります。また、Redis独自の永続性を利用して永続性を実現できます。Redisクラスターを使用して高い同時実行性と高い可用性をサポートすることは、遅延キューの実装に適したソリューションです。
RabbitMQ
RabbitMQ自体は、遅延キューを直接サポートしていません。遅延キューの効果を実現するには、RabbitMQのTTLおよびデッドレターキュー機能に依存しています。まず、RabbitMQのデッドレターキューとTTL機能について理解しましょう。
デッドレターキュー
デッドレターキューは、実際にはRabbitMQのメッセージ処理メカニズムです。RabbmitMQがメッセージを生成および消費している場合、メッセージは次の状況で「デッドレター」になります。
メッセージは拒否され
basic.reject/ basic.nack
、再投稿されませんrequeue=false
メッセージはタイムアウト後に消費されません。つまり、TTLが期限切れになります。
メッセージキューが最大長に達する
メッセージがデッドレターになると、Dead-Letter-Exchangeに再配信され、バインドルールに従ってDead-Letter-Exchangeが対応するデッドレターキューに転送され、キューを監視してメッセージが送信されます。再消費します。
TTLを生きるメッセージ時間
TTL(Time-To-Live)は、RabbitMQの高度な機能であり、メッセージの最大存続期間をミリ秒単位で表します。TTLで設定された時間内にメッセージが消費されない場合、メッセージはデッドレターになり、前述のデッドレターキューに入ります。
メッセージのTTL属性を設定するには、2つの方法があります。1つは、キューの作成時にキュー全体のTTL有効期限を直接設定する方法です。キューに入るすべてのメッセージは、均一な有効期限に設定されます。メッセージの有効期限が切れると、 、すぐに破棄され、デッドレターキューに入ります。参照コードは次のとおりです。
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-message-ttl", 6000);
channel.queueDeclare(queueName, durable, exclusive, autoDelete, args);
この方法は、遅延キューの遅延時間が固定値の場合に適しています。
別の方法は、単一のメッセージを設定することです。参照コードは次のとおりです。メッセージは6秒の有効期限で設定されます。
AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder();
builder.expiration("6000");
AMQP.BasicProperties properties = builder.build();
channel.basicPublish(exchangeName, routingKey, mandatory, properties, "msg content".getBytes());
メッセージごとに異なる遅延時間を設定する必要がある場合、上記のキューのTTL設定ではニーズを満たすことができないため、単一のメッセージに対してこのTTL設定を使用する必要があります。
ただし、TTLをこのように設定すると、RabbitMQは最初のメッセージの有効期限が切れているかどうかのみをチェックするため、メッセージが時間どおりに終了しない場合があることに注意してください。たとえば、この場合、最初のメッセージが20秒のTTLで設定され、2番目のメッセージが10秒のTTLで設定されている場合、RabbitMQは、最初のメッセージが期限切れになるまで待機してから、2番目のメッセージを期限切れにします。
この問題を解決する方法も非常に簡単です。RabbitMQのプラグインをインストールするだけです。
https://www.rabbitmq.com/community-plugins.html
このプラグインをインストールした後、設定されたTTLに従ってすべてのメッセージが期限切れになる可能性があります。
RabbitMQは遅延キューを実装します
さて、RabbitMQのデッドレターキューとTTLの2つの機能を導入した後、遅延キューの実装からわずか1ステップです。
スマートリーダーは、TTLが遅延キュー内のメッセージを遅延させる時間ではないことを発見したかもしれませんか?遅延が必要なメッセージの遅延時間にTTLを設定し、それをRabbitMQの通常のキューに配信し、決して消費しない場合、TTL時間の後、メッセージは自動的にデッドレターキューに配信されます。この時点でコンシューマープロセスを使用して、デッドレターキュー内のメッセージをリアルタイムで消費します。これは、遅延キューの効果を実現するだけではありません。
RabbitMQを使用して遅延キューを実装する全体的なプロセスは、次の図から直感的に確認できます。
RabbitMQを使用して遅延キューを実装すると、信頼性の高いメッセージ送信、信頼性の高いメッセージ配信、デッドレターキューなど、RabbitMQのいくつかの特性をうまく利用して、メッセージが少なくとも1回消費され、正しく処理されなかったメッセージが破棄されないようにすることができます。さらに、RabbitMQクラスターの特性により、単一の障害点の問題を非常にうまく解決でき、単一ノードの障害によって遅延キューが使用できなくなったり、メッセージが失われたりすることはありません。
TimeWheel
TimeWheelタイムホイールアルゴリズムは、遅延キューを実装するための独創的で効率的なアルゴリズムであり、Netty、Zookeeper、Kafkaなどのさまざまなフレームワークで使用されます。
タイムホイール
タイムホイールには、タイムホイールが現在指している時間を示すダイヤルポインターがあります。時間が経つにつれて、ポインターは進み続け、対応する位置で遅延タスクリストを処理します。
遅延タスクを追加する
タイムホイールのサイズは固定されており、タイムホイールの各要素は双方向の循環リンクリストO(1)
であるため、の時間の複雑さの下で遅延タスクをタイムホイールに追加できます。
たとえば、下の図に示すように、このようなタイムホイールがあります。ダイヤルポインタが現在の時刻2を指している場合、3秒の遅延で新しいタスクを追加する必要があります。タイムホイール内の遅延タスクの位置を5としてすばやく計算できます。 、およびタスクリストの最後の位置5に追加されます。
多層タイムホイール
これまでのところすべてが素晴らしいですが、注意深い学生は、上のタイムホイールのサイズが12秒だけに固定されていることに気付いたかもしれません。この時点で200秒遅らせる必要があるタスクがある場合、どうすればよいですか?タイムホイール全体のサイズを直接拡大しますか?これは明らかに望ましくありません。この方法では、非常に大きなタイムホイールを維持する必要があり、メモリが受け入れられず、基になるアレイが大きくなるとアドレス指定効率が低下し、パフォーマンスに影響を与えるためです。
この目的のために、カフカは多層タイムホイールの概念を導入しました。実際、多層タイムホイールの概念は、機械式時計の時、分、秒針の概念と非常によく似ており、秒針だけでは現在の時刻を表現できない場合は、分針と秒針を組み合わせて表示します。同様に、タスクの期限が現在のタイムホイールで示される時間範囲を超えると、次の図に示すように、タスクは上部のタイムホイールに追加しようとします。
たとえば、200秒の遅延で遅延メッセージを追加する必要があり、タイムホイールの最初のレイヤーが表すことができる時間範囲を超えていることがわかった場合、引き続き上部のタイムホイールを調べて、それを時間の2番目のレイヤーに追加する必要があります。ホイール200/12 = 17の位置で、17も2番目のタイムホイールの表現範囲を超えていることがわかりました。引き続き検索して、3番目のタイムホイールの17/12 = 2に追加する必要があります。ポジション。
Kafkaのタイムホイールアルゴリズムに遅延タスクを追加し、タイムホイールを押してスクロールするコアプロセスは次のとおりです。バケットはタイムホイールの遅延タスクキューであり、Kafkaによって導入されたDelayQueueは、ほとんどのバケットが空であるために発生する非効率的なタイムホイールスクロールの問題を解決します。
タイムホイールによって実装された遅延キューは、多数のタスクの効率的なトリガーをサポートできます。また、Kafkaのタイムホイールアルゴリズムの実装では、DelayQueueも導入され、DelayQueueを使用してタイムホイールのスクロールをプッシュし、遅延タスクの追加と削除をタイムホイールに配置します。この設計により、遅延キュー全体が大幅に改善されます。効果。
総括する
遅延キューは、私たちの日常の開発で広く使用されています。この記事では、遅延キューを実装するための3つの異なるソリューションを紹介します。3つのソリューションには独自の特徴があります。たとえば、Redisの実装は最も理解しやすく、すばやく実装できますが、結局のところ、Redisはこれはメモリに基づいています。データ永続化ソリューションはありますが、それでもデータが失われる可能性があります。信頼性の高いメッセージ送信、信頼性の高いメッセージ配信、デッドレターキューなどのRabbitMQ独自の機能により、RabbitMQの実装により、メッセージが少なくとも1回消費され、正しく処理されなかったメッセージが破棄されないようにして、メッセージの信頼性を高めることができます。保証されています。最後に、カフカのタイムホイールアルゴリズムは、個人的には3つの実装の中で最も理解しにくいと感じていますが、非常に独創的な実装でもあります。最後に、上記のコンテンツが、独自の遅延キューを実装するときにいくつかのアイデアを提供するのに役立つことを願っています。
私たちのビデオ番号をフォローすることを歓迎します:Tencentプログラマー