通過 .NET 學習 RabbitMQ 建立可靠的訊息佇列系統:任務分配 通過 .NET 學習 RabbitMQ 建立可靠的訊息佇列系統:任務分配

Published on Thursday, May 25, 2023

RabbitMQ Multiple Workers

到目前為止我們專案同時只會運行一個 producer 和 一個 consumer,不過當 producer 產生過多的任務或者 consumer 執行每一個任務都需要許多時間, 這時候我們可以選則執行多個 consumer 來分擔執行任務的壓力,例如說上一篇文章中的例子我們產生了五個任務。

dotnet run "First message."
dotnet run "Second message.."
dotnet run "Third message..."
dotnet run "Fourth message...."
dotnet run "Fifth message....."

我們知道 一個 consumer 要完成這五個任務最少需要 15 秒的時間,那麼理論上如果開啟多個 consumer 任務的執行速度應該會快上不少, 不過同時也會牽涉到列一個問題那就是如何公平分配任務給每個 consumer,不然假如說開啟了五個 consumer 但結果所有任務只會分派給第一個 consumer 負責處理, 那總運行時間還是 15 秒不會有任何得加快,今天將針對這個問題做討論。

首先我們先開啟兩個 shell 試試看運行兩個 consumer 等待之後的任務分配

# shell 1
cd src/Receive
dotnet run
# shell 2
cd src/Receive
dotnet run

接著我們運行 producer 專案並發送五個任務到 Queue

cd src/Send
dotnet run "First message."
dotnet run "Second message.."
dotnet run "Third message..."
dotnet run "Fourth message...."
dotnet run "Fifth message....."

從輸出可以得知 shell 1 被分派到任務一,任務三,任務五

# shell 1
[*] Waiting for messages.
Press [enter] to exit.
[x] Received First message.
[x] Done
[x] Received Third message...
[x] Done
[x] Received Fifth message.....
[x] Done

從輸出可以得知 shell 2 被分派到任務二,任務四

# shell 2
[*] Waiting for messages.
Press [enter] to exit.
[x] Received Second message..
[x] Done
[x] Received Fourth message....
[x] Done

由此可知 RabbitMQ 是根據 round-robin 方式來分配任務,基本上就是根據 consumer 的數量來平均分配任務給每一個 consumer,所以根據這種分派方法 假如果們有三個 consumer,那們 consumer1 會被分派到任務一跟任務四, consumer2 會被分派到任務二跟任務五, consumer3 只會分派到 任務三。


Fair Dispatch

round-robin 方法看似很公平但是事實上公平的只有任務的數量,那麼假如說偶數任務需要執行的時間需要特別長呢?這時候就變得不太公平了。

例如說我們發送以下五個任務,可以看到任務二跟四的運行時間將會為 10 秒

dotnet run "First message."
dotnet run "Second message...................."
dotnet run "Third message..."
dotnet run "Fourth message...................."
dotnet run "Fifth message....."

這時 shell 1 的執行時間還是為 9 秒但是 shell 2 需要的時間為 40 秒,明顯看得出來雖然任務執行的數量差不多但是 shell 2 的負擔將會比 shell 1 還要大很多, 還有一個有趣的情況那就是 shell 1 執行完任務一三五後 shell 2 還在執行任務二,那麼根據目前情況 shell 1 現在為閒置中理論上應該分派任務四給 shell 1 幫忙分擔壓力才是比較好的作法,但在預設情況下 RabbitMQ 不會知道也不會管這些事情,它還是會根據任務數量平均配給每一個 consumer

要解決這個問題我們需要稍微修改一下 Receive 專案,呼叫 BasicQos 並設定每次只分配一筆任務

channel.QueueDeclare(queue: "task_queue",
                     durable: true,
                     exclusive: false,
                     autoDelete: false,
                     arguments: null);

channel.BasicQos(prefetchSize: 0, prefetchCount: 1, global: false);

修改完成後 RabbitMQ 就不會預先分派所有任務了,會變成是需要呼叫昨天提到的 ack 確保任務真的完成後才分派下一筆任務給 consumer

所以根據調整完的結果 shell 1shell 2 需要確實完成任務才能跟 RabbitMQ 要求下一個任務,這樣就能更公平的分派任務給每一個 consumer

# shell 1
[*] Waiting for messages.
Press [enter] to exit.
[x] Received First message.
[x] Done
[x] Received Third message...
[x] Done
[x] Received Fourth message....................
[x] Done
# shell 2
[*] Waiting for messages.
Press [enter] to exit.
[x] Received Second message....................
[x] Done
[x] Received Fifth message.....
[x] Done

Summary

今天學習到了只要使用 RabbitMQ 就能很快速的水平擴展我們的服務,例如在活動期間我們可以事先開啟更多個服務就能分擔整個系統的壓力, 還有如何讓 RabbitMQ 更公平的分派任務給每一個服務,這樣就可以盡量分擔每一台機器的負擔,並不會有特定一台機器比較忙碌的狀態。

今天的進度 Github