摘要: 使用RabbitMQ的訊息佇列,可以有效提高系統的峰值處理能力。
RabbitMQ簡介
RabbitMQ是訊息代理(Message Broker),它支援多種非同步訊息處理方式,最常見的有:
- Work Queue:將訊息快取到一個佇列,預設情況下,多個worker按照Round Robin的方式處理佇列中的訊息。每個訊息只會分配給單個worker。
- Publish/Subscribe:每個訂閱訊息的消費者都會收到訊息,因此每個訊息通常會分配給多個worker,每個worker對訊息進行不同的處理。
RabbitMQ還支援Routing、Topics、以及Remote procedure calls (RPC)等方式。
對於不同的訊息處理方式,有一點是相同的,RabbitMQ是介於訊息的生產者和消費者的中間節點,負責快取和分發訊息。RabbitMQ接收來自生產者的訊息,快取到記憶體中,按照不同的方式分發給消費者。RabbitMQ還可以將訊息寫入磁碟,保證持久化,這樣即使RabbitMQ意外崩潰了,訊息資料不至於完全丟失。
為什麼使用RabbitMQ?
最簡單的一點在於,它支援Work Queue等不同的訊息處理方式,可以用於不同的業務場景。對於我們Fundebug來說,目前只用過RabbitMQ的Work Queue,即訊息佇列。
使用訊息佇列,可以將不算緊急、但是非常消耗資源的計算任務,以訊息的方式插入到RabbitMQ的佇列中,然後使用多個處理模組處理這些訊息。
這樣做最大的好處在於:提高了系統峰值處理能力。因為,來不及處理的訊息快取在RabbitMQ中,避免了同時進行大量計算導致系統因超負荷執行而崩潰。而那些來不及處理的訊息,會在峰值過去之後慢慢處理掉。
另一個好處在於解耦。訊息的生產者只需要將訊息傳送給RabbitMQ,這些訊息什麼時候處理完,不會影響生產者的響應效能。
廣告:歡迎免費試用Fundebug,為您監控線上程式碼的BUG,提高使用者體驗~
安裝並執行RabbitMQ
使用Docker執行RabbitMQ非常簡單,只需要執行一條簡單的命令:
sudo docker run -d --name rabbitmq -h rabbitmq -p 5672:5672 -v /var/lib/rabbitmq:/var/lib/rabbitmq registry.docker-cn.com/library/rabbitmq:3.7
複製程式碼
對於不熟悉Docker的朋友,我解釋一下docker的命令選項:
- -d : 後臺執行容器
- --name rabbitmq : 將容器的名字設為rabbitmq
- -h rabbitmq : 將容器的主機名設為rabbitmq,希望RabbitMQ訊息資料持久化儲存到本地磁碟是需要設定主機名,因為RabbitMQ儲存資料的目錄為主機名
- -p 5672:5672 : 將容器的5672埠對映為本地主機的5672埠,這樣可以通過本地的5672埠訪問rabbitmq
- -v /var/lib/rabbitmq:/var/lib/rabbitmq:將容器的/var/lib/rabbitmq目錄對映為本地主機的/var/lib/rabbitmq目錄,這樣可以將RabbitMQ訊息資料持久化儲存到本地磁碟,即使RabbitMQ容器被刪除,資料依然還在。
Docker為官方映象提供了加速服務,因此命令中Rabbit的Docker映象名為registry.docker-cn.com/library/rabbitmq:3.7。
如果你不會Docker,建議你學習一下。如果你不想學,Ubuntu 14.04下安裝RabbitMQ的命令是這樣的:
sudo echo "deb http://www.rabbitmq.com/debian testing main" | sudo tee -a /etc/apt/sources.list
wget -O- https://www.rabbitmq.com/rabbitmq-release-signing-key.asc | sudo apt-key add -
sudo apt-get update
sudo apt-get install rabbitmq-server
複製程式碼
啟動RabbitMQ:
sudo service rabbitmq-server start
複製程式碼
訊息佇列程式碼示例
下面,我們使用Node.js實現一個簡單訊息佇列。
訊息的生產者:sender.js
const amqp = require("amqplib");
const queue = "demo";
async function sendMessage(message)
{
const connection = await amqp.connect("amqp://localhost");
const channel = await connection.createChannel();
await channel.assertQueue(queue);
await channel.sendToQueue(queue, new Buffer(message),
{
// RabbitMQ關閉時,訊息會被儲存到磁碟
persistent: true
});
}
setInterval(function()
{
sendMessage("Hello, Fundebug!");
}, 1000)
複製程式碼
- 在sender中,不斷地往訊息佇列中傳送"Hello, Fundebug!"。
訊息的消費者:receiver.js
const amqp = require("amqplib");
const queue = "demo";
async function receiveMessage()
{
const connection = await amqp.connect("amqp://localhost");
const channel = await connection.createChannel();
await channel.assertQueue(queue);
await channel.consume(queue, function(message)
{
console.log(message.content.toString());
channel.ack(message);
});
}
receiveMessage();
複製程式碼
- 在receiver中,從訊息佇列中讀出message並列印。
我們用到了amqplib模組,用於與RabbitMQ進行通訊,對於具體介面的細節,可以檢視文件。
在呼叫sendToQueue時,將persistent屬性設為true,這樣RabbitMQ關閉時,訊息會被儲存到磁碟。測試這一點很簡單:
- 關閉receiver
- 啟動sender,傳送訊息給RabbitMQ
- 重啟RabbitMQ(sudo docker restart rabbitmq)
- 啟動receiver,會發現它可以接收sender在RabbitMQ重啟之前傳送的訊息
由於RabbitMQ容器將儲存資料的目錄(/var/lib/rabbitmq)以資料卷的形式儲存在本地主機,因此即使將RabbitMQ容器刪除(sudo docker rm -f rabbitmq)後重新執行,效果也是一樣的。
另外,這段程式碼採用了Node.js最新的非同步程式碼編寫方式:Async/Await,因此非常簡潔,感興趣的同學可以瞭解一下。
這個Demo的執行方式非常簡單:
- 執行RabbitMQ容器
sudo ./start_rabbitmq.sh
複製程式碼
- 傳送訊息
node ./sender.js
複製程式碼
- 接收訊息
node ./receiver.js
複製程式碼
在receiver端,可以看到不停地列印"Hello, Fundebug!"。
程式碼倉庫地址為:Fundebug/rabbitmq-demo
自動重連程式碼示例
在生產環境中,RabbitMQ難免會出現重啟的情況,比如更換磁碟或者伺服器、負載過高導致崩潰。因為RabbitMQ可以將訊息寫入磁碟,所以資料是"安全"的。但是,程式碼中必須實現自動重連機制,否則RabbitMQ停止時會導致Node.js應用崩潰。這裡提供一個自動重連的程式碼示例,給大家參考:
訊息生產者:sender_reconnect.js
const amqp = require("amqplib");
const queue = "demo";
var connection;
// 連線RabbitMQ
async function connectRabbitMQ()
{
try
{
connection = await amqp.connect("amqp://localhost");
console.info("connect to RabbitMQ success");
const channel = await connection.createChannel();
await channel.assertQueue(queue);
await channel.sendToQueue(queue, new Buffer("Hello, Fundebug!"),
{
// RabbitMQ重啟時,訊息會被儲存到磁碟
persistent: true
});
connection.on("error", function(err)
{
console.log(err);
setTimeout(connectRabbitMQ, 10000);
});
connection.on("close", function()
{
console.error("connection to RabbitQM closed!");
setTimeout(connectRabbitMQ, 10000);
});
}
catch (err)
{
console.error(err);
setTimeout(connectRabbitMQ, 10000);
}
}
connectRabbitMQ();
複製程式碼
訊息消費者:receiver_reconnect.js
const amqp = require("amqplib");
const queue = "demo";
var connection;
// 連線RabbitMQ
async function connectRabbitMQ()
{
try
{
connection = await amqp.connect("amqp://localhost");
console.info("connect to RabbitMQ success");
const channel = await connection.createChannel();
await channel.assertQueue(queue);
await channel.consume(queue, async function(message)
{
console.log(message.content.toString());
channel.ack(message);
});
connection.on("error", function(err)
{
console.log(err);
setTimeout(connectRabbitMQ, 10000);
});
connection.on("close", function()
{
console.error("connection to RabbitQM closed!");
setTimeout(connectRabbitMQ, 10000);
});
}
catch (err)
{
console.error(err);
setTimeout(connectRabbitMQ, 10000);
}
}
connectRabbitMQ();
複製程式碼
這樣的話,即使RabbitMQ重啟,sender和receiver也可以自動重新連線RabbitMQ。如果你希望監控RabbitMQ是否出錯,不妨使用我們Fundebug的Node.js錯誤監控服務,在連線觸發"error"或者"close"事件時,第一時間傳送報警,這樣開發者可以及時定位和處理BUG。
參考
- AMQP library (RabbitMQ) - async/await
- RbbitMQ文件:Work Queue(JavaScript)
- Won't persist data
- How to build reconnect logic for amqplib
關於Fundebug
Fundebug專注於JavaScript、微信小程式、微信小遊戲、支付寶小程式、React Native、Node.js和Java線上應用實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了10億+錯誤事件,付費客戶有Google、360、金山軟體、百姓網等眾多品牌企業。歡迎大家免費試用!
版權宣告
轉載時請註明作者Fundebug以及本文地址:
blog.fundebug.com/2018/04/20/…