web-dev-qa-db-ja.com

例によるRabbitMQ:複数のスレッド、チャネル、およびキュー

RabbitMQ's Java API docs を読んだだけで、非常に有益でわかりやすいことがわかりました。発行用の簡単なChannelを設定する方法の例/ consumptionは非常に簡単に理解できますが、非常に単純/基本的な例であり、重要な質問が残されました:1+ Channelsを設定するにはどうすればよいですか複数のキューとの間でパブリッシュ/消費しますか?

loggingsecurity_eventscustomer_ordersの3つのキューを持つRabbitMQサーバーがあるとします。したがって、3つのキューすべてに対してパブリッシュ/消費するための単一のChannelが必要であるか、3つの個別のChannelsがあり、それぞれが単一のキュー専用です。

さらに、RabbitMQのベストプラクティスでは、コンシューマスレッドごとに1 Channelを設定する必要があります。この例では、security_eventsはコンシューマスレッドを1つだけ使用しても問題ありませんが、loggingcustomer_orderは両方ともボリュームを処理するために5つのスレッドを必要とします。だから、私が正しく理解していれば、それは私たちが必要とすることを意味しますか:

  • 1 Channelおよびsecurity_eventsとの間でパブリッシュ/コンシュームするための1つのコンシューマスレッド。そして
  • 5 Channelsおよびloggingとの間でパブリッシュ/コンシュームするための5つのコンシューマスレッド。そして
  • 5 Channelsおよびcustomer_ordersとの間でパブリッシュ/コンシュームするための5つのコンシューマスレッド

ここで私の理解が誤っている場合、私を修正することから始めてください。いずれにせよ、戦闘に疲れたRabbitMQのベテランが、ここで私の要件を満たすパブリッシャー/コンシューマーをセットアップするためのまともなコード例で「点をつなぐ」のを助けてくれますか? 前もって感謝します!

55
user1768830

最初の理解にはいくつかの問題があると思います。率直に言って、私は次を見て少し驚いています:both need 5 threads to handle the volume。その正確な番号が必要だとどうやって特定しましたか? 5つのスレッドで十分であるという保証はありますか?

RabbitMQは調整および時間テストされているため、適切な設計と効率的なメッセージ処理がすべてです。

問題を確認して、適切な解決策を見つけてみましょう。ところで、メッセージキュー自体は、本当に優れたソリューションがあるという保証を提供しません。何をしているのかを理解し、さらに追加のテストを行う必要があります。

間違いなくご存知のように、多くのレイアウトが可能です。

enter image description here

Bを使用して、1プロデューサーNコンシューマーの問題。スループットがとても心配なので。ご想像のとおり、RabbitMQの動作は非常に良好です( source )。 prefetchCountに注意してください。後で対処します。

enter image description here

したがって、十分なスループットを確保するには、メッセージ処理ロジックが適切な場所である可能性があります。当然、メッセージを処理する必要があるたびに新しいスレッドにまたがることができますが、最終的にそのようなアプローチはシステムを殺します。基本的に、取得するレイテンシーの大きいスレッドが多くなります(必要に応じて Amdahlの法則 を確認できます)。

enter image description here

アムダールの法則の説明 を参照)

ヒント#1:スレッドに注意し、ThreadPoolsを使用する( details

スレッドプールは、Runnableオブジェクト(ワークキュー)のコレクションおよび実行中のスレッドの接続として説明できます。これらのスレッドは常に実行されており、新しい作業の作業クエリをチェックしています。実行する新しい作業がある場合、このRunnableを実行します。 Threadクラス自体がメソッドを提供します。 execute(Runnable r)は、新しいRunnableオブジェクトを作業キューに追加します。

public class Main {
  private static final int NTHREDS = 10;

  public static void main(String[] args) {
    ExecutorService executor = Executors.newFixedThreadPool(NTHREDS);
    for (int i = 0; i < 500; i++) {
      Runnable worker = new MyRunnable(10000000L + i);
      executor.execute(worker);
    }
    // This will make the executor accept no new threads
    // and finish all existing threads in the queue
    executor.shutdown();
    // Wait until all threads are finish
    executor.awaitTermination();
    System.out.println("Finished all threads");
  }
} 

ヒント2:メッセージ処理のオーバーヘッドに注意してください

これは明らかな最適化手法です。小さくて簡単に処理できるメッセージを送信する可能性があります。全体のアプローチは、継続的に設定および処理される小さなメッセージについてです。大きなメッセージは最終的に悪い冗談を言うので、それを避ける方が良いです。

enter image description here

それで、小さな情報を送る方が良いのですが、処理はどうですか?ジョブを送信するたびにオーバーヘッドが発生します。バッチ処理は、受信メッセージ率が高い場合に非常に役立ちます。

enter image description here

たとえば、単純なメッセージ処理ロジックがあり、メッセージが処理されるたびにスレッド固有のオーバーヘッドが発生したくないとしましょう。その非常に単純なCompositeRunnable can be introduced

class CompositeRunnable implements Runnable {

    protected Queue<Runnable> queue = new LinkedList<>();

    public void add(Runnable a) {
        queue.add(a);
    }

    @Override
    public void run() {
        for(Runnable r: queue) {
            r.run();
        }
    }
}

または、処理するメッセージを収集して、わずかに異なる方法で同じことを行います。

class CompositeMessageWorker<T> implements Runnable {

    protected Queue<T> queue = new LinkedList<>();

    public void add(T message) {
        queue.add(message);
    }

    @Override
    public void run() {
        for(T message: queue) {
            // process a message
        }
    }
}

このようにして、メッセージをより効率的に処理できます。

ヒント#3:メッセージ処理を最適化する

あなたが知っているという事実にもかかわらず、メッセージを並行して処理することができます(Tip #1)処理のオーバーヘッドを削減(Tip #2)すべてを迅速に行う必要があります。冗長な処理ステップ、重いループなどは、パフォーマンスに大きな影響を与える可能性があります。興味深い事例研究をご覧ください。

enter image description here

適切なXMLパーサーを選択することでメッセージキューのスループットを10倍向上

ヒント#4:接続とチャネル管理

  • 既存の接続で新しいチャネルを開始するには、1回のネットワークラウンドトリップが必要です。新しい接続の開始には数回かかります。
  • 各接続は、サーバー上のファイル記述子を使用します。チャンネルはそうではありません。
  • 1つのチャネルで大きなメッセージを公開すると、接続がブロックされます。それ以外は、多重化はかなり透過的です。
  • サーバーが過負荷になると、公開中の接続がブロックされる可能性があります-公開接続と消費接続を分離することをお勧めします
  • メッセージのバーストを処理する準備をする

ソース

すべてのヒントは完全に連携して機能します。追加の詳細が必要な場合は、お気軽にお知らせください。

完全な消費者の例( source

次の点に注意してください:

  • channel.basicQos(prefetch)-前に見たように、prefetchCountは非常に便利かもしれません:

    このコマンドを使用すると、コンシューマは受信準備ができている未確認メッセージの量を指定するプリフェッチウィンドウを選択できます。プリフェッチカウントをゼロ以外の値に設定すると、ブローカーはその制限に違反するメッセージをコンシューマに配信しません。ウィンドウを前方に移動するには、コンシューマーはメッセージ(またはメッセージのグループ)の受信を確認する必要があります。

  • ExecutorService threadExecutor-適切に構成されたexecutorサービスを指定できます。

例:

static class Worker extends DefaultConsumer {

    String name;
    Channel channel;
    String queue;
    int processed;
    ExecutorService executorService;

    public Worker(int prefetch, ExecutorService threadExecutor,
                  , Channel c, String q) throws Exception {
        super(c);
        channel = c;
        queue = q;
        channel.basicQos(prefetch);
        channel.basicConsume(queue, false, this);
        executorService = threadExecutor;
    }

    @Override
    public void handleDelivery(String consumerTag,
                               Envelope envelope,
                               AMQP.BasicProperties properties,
                               byte[] body) throws IOException {
        Runnable task = new VariableLengthTask(this,
                                               envelope.getDeliveryTag(),
                                               channel);
        executorService.submit(task);
    }
}

以下を確認することもできます。

126
Renat Gilmanov

複数のキューとの間でパブリッシュ/コンシュームするために1+チャネルを設定するにはどうすればよいですか?

スレッドとチャネルを使用して実装できます。必要なのは、ものを分類する方法、つまり、ログインからのすべてのキュー項目、security_eventsからのすべてのキュー要素などです。分類はroutingKeyを使用して達成できます。

例:アイテムをキューに追加するたびに、ルーティングキーを指定します。プロパティ要素として追加されます。これにより、特定のイベントから値を取得できますlogging

次のコードサンプルは、クライアント側でそれを行う方法を説明しています。

例:

ルーティングキーは、チャネルのタイプを識別し、タイプを取得するために使用されます。

たとえば、ログインタイプに関するすべてのチャネルを取得する必要がある場合、ルーティングキーをログインまたはその他のキーワードとして指定して、それを識別する必要があります。

            Connection connection = factory.newConnection();
            Channel channel = connection.createChannel();

            channel.exchangeDeclare(EXCHANGE_NAME, "direct");

            string routingKey="login";

            channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes());

あなたは見ることができます ここ 分類の詳細については..


スレッド部

公開部分が終了すると、スレッド部分を実行できます。

この部分では、カテゴリに基づいて公開データを取得できます。すなわち;あなたの場合、logging、security_events、customer_ordersなどのルーティングキー.

スレッドでデータを取得する方法については、例を参照してください。

例:

    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("localhost");
    Connection connection = factory.newConnection();
    Channel channel = connection.createChannel();
//**The threads part is as follows** 
 channel.exchangeDeclare(EXCHANGE_NAME, "direct");      
 String queueName = channel.queueDeclare().getQueue();
    // This part will biend the queue with the severity (login for eg:)
    for(String severity : argv){
              channel.queueBind(queueName, EXCHANGE_NAME, routingKey);
    }
    boolean autoAck = false;
    channel.basicConsume(queueName, autoAck, "myConsumerTag",
    new DefaultConsumer(channel) {
        @Override
        public void handleDelivery(String consumerTag,
                                Envelope envelope,
                                AMQP.BasicProperties properties,
                                byte[] body)
         throws IOException
     {
             String routingKey = envelope.getRoutingKey();
             String contentType = properties.contentType;
             long deliveryTag = envelope.getDeliveryTag();

             // (process the message components here ...)
             channel.basicAck(deliveryTag, false);
     }
 });

これで、タイプlogin(routing key)のキュー内のデータを処理するスレッドが作成されます。この方法により、複数のスレッドを作成できます。それぞれが異なる目的を果たします。

look here スレッド部分の詳細については..

20
Dileep