ZeroMQ,史上最快的消息隊列 —– ZMQ的學習和研究

一、ZeroMQ 的背景介紹

  引用官方的說法: “ZMQ (以下 ZeroMQ 簡稱 ZMQ)是一個簡單好用的傳輸層,像框架一樣的一個 socket library,他使得 Socket 編程更加簡單、簡潔和性能更高。是一個消息處理隊列庫,可在多個線程、內核和主機盒之間彈性伸縮。ZMQ 的明確目標是“成爲標準網絡協議棧的一部分,之後進入 Linux 內核”。現在還未看到它們的成功。但是,它無疑是極具前景的、並且是人們更加需要的“傳統”BSD 套接字之上的一層封裝。ZMQ 讓編寫高性能網絡應用程序極爲簡單和有趣。”

  近幾年有關”Message Queue”的項目層出不窮,知名的就有十幾種,這主要是因爲後摩爾定律時代,分佈式處理逐漸成爲主流,業界需要一套標準來解決分佈式計算環境中節點之間的消息通信。幾年的競爭下來,Apache 基金會旗下的符合 AMQP/1.0標準的 RabbitMQ 已經得到了廣泛的認可,成爲領先的 MQ 項目。

  與 RabbitMQ 相比,ZMQ 並不像是一個傳統意義上的消息隊列服務器,事實上,它也根本不是一個服務器,它更像是一個底層的網絡通訊庫,在 Socket API 之上做了一層封裝,將網絡通訊、進程通訊和線程通訊抽象爲統一的 API 接口。

  二、ZMQ 是什麼?

  閱讀了 ZMQ 的 Guide 文檔後,我的理解是,這是個類似於 Socket 的一系列接口,他跟 Socket 的區別是:普通的 socket 是端到端的(1:1的關係),而 ZMQ 卻是可以N:M 的關係,人們對 BSD 套接字的瞭解較多的是點對點的連接,點對點連接需要顯式地建立連接、銷燬連接、選擇協議(TCP/UDP)和處理錯誤等,而 ZMQ 屏蔽了這些細節,讓你的網絡編程更爲簡單。ZMQ 用於 node 與 node 間的通信,node 可以是主機或者是進程。

  三、本文的目的

  在集羣對外提供服務的過程中,我們有很多的配置,需要根據需要隨時更新,那麼這個信息如果推動到各個節點?並且保證信息的一致性和可靠性?本文在介紹 ZMQ 基本理論的基礎上,試圖使用 ZMQ 實現一個配置分發中心。從一個節點,將信息無誤的分發到各個服務器節點上,並保證信息正確性和一致性。

  四、ZMQ 的三個基本模型

  ZMQ 提供了三個基本的通信模型,分別是“Request-Reply “,”Publisher-Subscriber“,”Parallel Pipeline”,我們從這三種模式一窺 ZMQ 的究竟

  ZMQ 的 hello world!

  由 Client 發起請求,並等待 Server 迴應請求。請求端發送一個簡單的 hello,服務端則迴應一個 world。請求端和服務端都可以是 1:N 的模型。通常把 1 認爲是 Server ,N 認爲是 Client 。ZMQ 可以很好的支持路由功能(實現路由功能的組件叫作 Device),把 1:N 擴展爲N:M (只需要加入若干路由節點)。如圖 1 所示:

  圖1:ZMQ 的 Request-Reply 通信

  服務端的 php 程序如下:

<?php
/*
* Hello World server
* Binds REP socket to tcp://*:5555
* Expects "Hello" from client, replies with "World"
* @author Ian Barber &lt;ian (dot) barber (at) gmail (dot) com&gt;
*/
$context = new ZMQContext (1);
// Socket to talk to clients
$responder = new ZMQSocket ($context, ZMQ::SOCKET_REP);
$responder-&gt;bind ("tcp://*:5555");
while(true) {
// Wait for next request from client
$request = $responder-&gt;recv ();
printf ("Received request: [%s]\n", $request);
 
// Do some 'work'
sleep (1);
 
// Send reply back to client
$responder-&gt;send ("World");
}

  Client 程序如下:

<?php
 
/*
 
 *  Hello World client
 
 *  Connects REQ socket to tcp://localhost:5555
 
 *  Sends "Hello" to server, expects "World" back
 
 * @author Ian Barber &lt;ian (dot) barber (at) gmail (dot) com&gt;
 
 */
$context = new ZMQContext ();
 
//  Socket to talk to server
 
echo "Connecting to hello world server...\n";
 
$requester = new ZMQSocket ($context, ZMQ::SOCKET_REQ);
 
$requester-&gt;connect ("tcp://localhost:5555");
 
for($request_nbr = 0; $request_nbr != 10; $request_nbr++) {
 
    printf ("Sending request %d...\n", $request_nbr);
 
    $requester-&gt;send ("Hello");
 
    $reply = $requester-&gt;recv ();
 
    printf ("Received reply %d: [%s]\n", $request_nbr, $reply);
 
}

  從以上的過程,我們可以瞭解到使用 ZMQ 寫基本的程序的方法,需要注意的是:

  a) 服務端和客戶端無論誰先啓動,效果是相同的,這點不同於 Socket。

  b) 在服務端收到信息以前,程序是阻塞的,會一直等待客戶端連接上來。

  c) 服務端收到信息以後,會 send 一個“World”給客戶端。值得注意的是一定是 client 連接上來以後,send 消息給 Server,然後 Server 再 rev 然後響應 client,這種一問一答式的。如果 Server 先 send,client 先 rev 是會報錯的。

  d) ZMQ 通信通信單元是消息,他除了知道 Bytes 的大小,他並不關心的消息格式。因此,你可以使用任何你覺得好用的數據格式。Xml、Protocol Buffers、Thrift、json 等等。

  e) 雖然可以使用 ZMQ 實現 HTTP 協議,但是,這絕不是他所擅長的。

  ZMQ 的 Publish-subscribe 模式

  我們可以想象一下天氣預報的訂閱模式,由一個節點提供信息源,由其他的節點,接受信息源的信息,如圖 2 所示:

  圖2:ZMQ 的 Publish-subscribe

  示例代碼如下 :

  Publisher:

<?php
/*
* Weather update server
* Binds PUB socket to tcp://*:5556
* Publishes random weather updates
* @author Ian Barber &lt;ian (dot) barber (at) gmail (dot) com&gt;
*/
 
// Prepare our context and publisher
$context = new ZMQContext ();
$publisher = $context-&gt;getSocket (ZMQ::SOCKET_PUB);
$publisher-&gt;bind ("tcp://*:5556");
 
while (true) {
// Get values that will fool the boss
$zipcode = mt_rand(0, 100000);
$temperature = mt_rand(-80, 135);
$relhumidity = mt_rand(10, 60);
 
// Send message to all subscribers
$update = sprintf ("%05d %d %d", $zipcode, $temperature, $relhumidity);
$publisher-&gt;send ($update);
}</pre>
Subscriber
<pre>&lt;?php
/*
* Weather update client
* Connects SUB socket to tcp://localhost:5556
* Collects weather updates and finds avg temp in zipcode
* @author Ian Barber &lt;ian (dot) barber (at) gmail (dot) com&gt;
*/
 
$context = new ZMQContext ();
 
// Socket to talk to server
echo "Collecting updates from weather server…", PHP_EOL;
$subscriber = new ZMQSocket ($context, ZMQ::SOCKET_SUB);
$subscriber-&gt;connect ("tcp://localhost:5556");
 
// Subscribe to zipcode, default is NYC, 10001
$filter = $_SERVER['argc'] &gt; 1 ? $_SERVER['argv'][1] : "10001";
$subscriber-&gt;setSockOpt (ZMQ::SOCKOPT_SUBSCRIBE, $filter);
 
// Process 100 updates
$total_temp = 0;
for ($update_nbr = 0; $update_nbr &lt; 100; $update_nbr++) {
$string = $subscriber-&gt;recv ();
sscanf ($string, "%d %d %d", $zipcode, $temperature, $relhumidity);
$total_temp += $temperature;
}
printf ("Average temperature for zipcode '%s' was %dF\n",
$filter, (int) ($total_temp / $update_nbr));

  這段代碼講的是,服務器端生成隨機數 zipcode、temperature、relhumidity 分別代表城市代碼、溫度值和溼度值。然後不斷的廣播信息,而客戶端通過設置過濾參數,接受特定城市代碼的信息,收集完了以後,做一個平均值。

  a) 與 Hello World 不同的是,Socket 的類型變成 SOCKET_PUB 和 SOCKET_SUB 類型。

  b) 客戶端需要$subscriber->setSockOpt (ZMQ::SOCKOPT_SUBSCRIBE, $filter);設置一個過濾值,相當於設定一個訂閱頻道,否則什麼信息也收不到。

  c) 服務器端一直不斷的廣播中,如果中途有 Subscriber 端退出,並不影響他繼續的廣播,當 Subscriber 再連接上來的時候,收到的就是後來發送的新的信息了。這對比較晚加入的,或者是中途離開的訂閱者,必然會丟失掉一部分信息,這是這個模式的一個問題,所謂的 Slow joiner。稍後,會解決這個問題。

  d) 但是,如果 Publisher 中途離開,所有的 Subscriber 會 hold 住,等待 Publisher 再上線的時候,會繼續接受信息。

  ZMQ 的 PipeLine 模型

  想象一下這樣的場景,如果需要統計各個機器的日誌,我們需要將統計任務分發到各個節點機器上,最後收集統計結果,做一個彙總。PipeLine 比較適合於這種場景,他的結構圖,如圖 3 所示。

  圖3:ZMQ 的 PipeLine 模型

  Parallel task ventilator in PHP

<?php
/*
* Task ventilator
* Binds PUSH socket to tcp://localhost:5557
* Sends batch of tasks to workers via that socket
* @author Ian Barber &lt;ian (dot) barber (at) gmail (dot) com&gt;
*/
 
$context = new ZMQContext ();
 
// Socket to send messages on
$sender = new ZMQSocket ($context, ZMQ::SOCKET_PUSH);
$sender-&gt;bind ("tcp://*:5557");
 
echo "Press Enter when the workers are ready: ";
$fp = fopen('php://stdin', 'r');
$line = fgets($fp, 512);
fclose($fp);
echo "Sending tasks to workers…", PHP_EOL;
 
// The first message is "0" and signals start of batch
$sender-&gt;send (0);
 
// Send 100 tasks
$total_msec = 0; // Total expected cost in msecs
for ($task_nbr = 0; $task_nbr &lt; 100; $task_nbr++) {
// Random workload from 1 to 100msecs
$workload = mt_rand(1, 100);
$total_msec += $workload;
$sender-&gt;send ($workload);
 
}
printf ("Total expected cost: %d msec\n", $total_msec);
sleep (1); // Give 0MQ time to deliver

  Parallel task worker in PHP

<?php
/*
* Task worker
* Connects PULL socket to tcp://localhost:5557
* Collects workloads from ventilator via that socket
* Connects PUSH socket to tcp://localhost:5558
* Sends results to sink via that socket
* @author Ian Barber &lt;ian (dot) barber (at) gmail (dot) com&gt;
*/
 
$context = new ZMQContext ();
 
// Socket to receive messages on
$receiver = new ZMQSocket ($context, ZMQ::SOCKET_PULL);
$receiver-&gt;connect ("tcp://localhost:5557");
 
// Socket to send messages to
$sender = new ZMQSocket ($context, ZMQ::SOCKET_PUSH);
$sender-&gt;connect ("tcp://localhost:5558");
 
// Process tasks forever
while (true) {
$string = $receiver-&gt;recv ();
 
// Simple progress indicator for the viewer
echo $string, PHP_EOL;
 
// Do the work
usleep($string * 1000);
 
// Send results to sink
$sender-&gt;send ("");
}

  Parallel task sink in PHP

<?php
/*
* Task sink
* Binds PULL socket to tcp://localhost:5558
* Collects results from workers via that socket
* @author Ian Barber &lt;ian (dot) barber (at) gmail (dot) com&gt;
*/
 
// Prepare our context and socket
$context = new ZMQContext ();
$receiver = new ZMQSocket ($context, ZMQ::SOCKET_PULL);
$receiver-&gt;bind ("tcp://*:5558");
 
// Wait for start of batch
$string = $receiver-&gt;recv ();
 
// Start our clock now
$tstart = microtime(true);
 
// Process 100 confirmations
$total_msec = 0; // Total calculated cost in msecs
for ($task_nbr = 0; $task_nbr &lt; 100; $task_nbr++) {
$string = $receiver-&gt;recv ();
if($task_nbr % 10 == 0) {
echo ":";
} else {
echo ".";
}
}
 
$tend = microtime(true);
 
$total_msec = ($tend - $tstart) * 1000;
echo PHP_EOL;
printf ("Total elapsed time: %d msec", $total_msec);
echo PHP_EOL;

  從程序中,我們可以看到,task ventilator 使用的是 SOCKET_PUSH,將任務分發到 Worker 節點上。而 Worker 節點上,使用 SOCKET_PULL 從上游接受任務,並使用 SOCKET_PUSH 將結果彙集到 Slink。值得注意的是,任務的分發的時候也同樣有一個負載均衡的路由功能,worker 可以隨時自由加入,task ventilator 可以均衡將任務分發出去。

  五、其他擴展模式

  通常,一個節點,即可以作爲 Server,同時也能作爲 Client,通過 PipeLine 模型中的 Worker,他向上連接着任務分發,向下連接着結果蒐集的 Sink 機器。因此,我們可以藉助這種特性,豐富的擴展原有的三種模式。例如,一個代理 Publisher,作爲一個內網的 Subscriber 接受信息,同時將信息,轉發到外網,其結構圖如圖 4 所示。

  圖4:ZMQ 的擴展模式

  六、多個服務器

  ZMQ 和 Socket 的區別在於,前者支持N:M的連接,而後者則只是1:1的連接,那麼一個 Client 連接多個 Server 的情況是怎樣的呢,我們通過圖 5 來說明。

  圖5:ZMQ 的N:1的連接情況

  我們假設 Client 有 R1,R2,R3,R4四個任務,我們只需要一個 ZMQ 的 Socket,就可以連接四個服務,他能夠自動均衡的分配任務。如圖 5 所示,R1,R4自動分配到了節點A,R2到了B,R3到了C。如果我們是N:M的情況呢?這個擴展起來,也不難,如圖 6 所示。

  圖6:N:M的連接

  我們通過一箇中間結點(Broker)來進行負載均衡的功能。我們通過代碼瞭解,其中的 Client 和我們的 Hello World 的 Client 端是一樣的,而 Server 端的不同是,他不需要監聽端口,而是需要連接 Broker 的端口,接受需要處理的信息。所以,我們重點閱讀 Broker 的代碼:

<?php
/*
* Simple request-reply broker
* @author Ian Barber &lt;ian (dot) barber (at) gmail (dot) com&gt;
*/
 
// Prepare our context and sockets
$context = new ZMQContext ();
$frontend = new ZMQSocket ($context, ZMQ::SOCKET_ROUTER);
$backend = new ZMQSocket ($context, ZMQ::SOCKET_DEALER);
$frontend-&gt;bind ("tcp://*:5559");
$backend-&gt;bind ("tcp://*:5560");
 
// Initialize poll set
$poll = new ZMQPoll ();
$poll-&gt;add ($frontend, ZMQ::POLL_IN);
$poll-&gt;add ($backend, ZMQ::POLL_IN);
$readable = $writeable = array();
 
// Switch messages between sockets
while(true) {
$events = $poll-&gt;poll ($readable, $writeable);
 
foreach($readable as $socket) {
if($socket === $frontend) {
// Process all parts of the message
while(true) {
$message = $socket-&gt;recv ();
// Multipart detection
$more = $socket-&gt;getSockOpt (ZMQ::SOCKOPT_RCVMORE);
$backend-&gt;send ($message, $more ? ZMQ::MODE_SNDMORE : null);
if(!$more) {
break; // Last message part
}
}
}
else if($socket === $backend) {
$message = $socket-&gt;recv ();
// Multipart detection
$more = $socket-&gt;getSockOpt (ZMQ::SOCKOPT_RCVMORE);
$frontend-&gt;send ($message, $more ? ZMQ::MODE_SNDMORE : null);
if(!$more) {
break; // Last message part
}
}
}
}

  Broker 監聽了兩個端口,接受從多個 Client 端發送過來的數據,並將數據,轉發給 Server。在 Broker 中,我們監聽了兩個端口,使用了兩個 Socket,那麼對於多個 Socket 的情況,我們是不需要通過輪詢的方式去處理數據的,在之前,我們可以使用 libevent 實現,異步的信息處理和傳輸。而現在,我們只需要使用 ZMQ 的$poll->poll 以實現多個 Socket 的異步處理。

  七、進程間的通信

  ZMQ 不僅能通過 TCP 完成節點間的通信,也可以通過 Socket 文件完成進程間的通信。如圖 7 所示,我們 fork 三個 PHP 進程,將進程 1 的數據,通過 Socket 文件發送到進程3。

圖7:進程間的通信

<?php
 
function step1() {
 
        $context = new ZMQContext ();
 
        // Signal downstream to step 2
 
        $sender = new ZMQSocket ($context, ZMQ::SOCKET_PAIR);
 
        $sender-&gt;connect ("ipc://step2.ipc");
 
        $sender-&gt;send ("hello ,i am step1");
 
}
 
function step2() {
 
        $pid = pcntl_fork ();
 
        if($pid == 0) {
 
                step1();
 
                exit();
 
        }
 
        $context = new ZMQContext ();
 
        //  Bind to ipc: endpoint, then start upstream thread
 
        $receiver = new ZMQSocket ($context, ZMQ::SOCKET_PAIR);
 
        $receiver-&gt;bind ("ipc://step2.ipc");
 
        // Wait for signal  
 
        sleep(10);
 
        $strings = $receiver-&gt;recv ();
 
        echo "step2 receiver is $strings". PHP_EOL;
 
        sleep(10);
 
        // Signal downstream to step 3
 
        $sender = new ZMQSocket ($context, ZMQ::SOCKET_PAIR);
 
        $sender-&gt;connect ("ipc://step3.ipc");
 
        $sender-&gt;send ($strings);
 
}
 
// Start upstream thread then bind to icp: endpoint
 
$pid = pcntl_fork ();
 
if($pid == 0) {
 
        step2();
 
        exit();
 
}
 
$context = new ZMQContext ();
 
$receiver = new ZMQSocket ($context, ZMQ::SOCKET_PAIR);
 
$receiver-&gt;bind ("ipc://step3.ipc");
 
// Wait for signal
 
$sr = $receiver-&gt;recv ();
 
echo "the result is {$sr}".PHP_EOL;

  在運行中,我們可以看到多了兩個文件,如圖 8 所示。

  圖8:運行過程中生成的文件

  八、利用 ZeroMQ 實現一個配置推送中心

  當我們將 WEB 代碼部署到集羣上的時候,如果需要實時的將最新的配置信息,主動的推送到各個機器節點。在此過程中,我們一定要保證,各個節點收到的信息的一致性和正確性,如果使用 HTTP,由於他的無狀態性,我們無法保證信息的一致性,當然,你可以使用 HTTP 來實現,只是更復雜,爲什麼不用 ZMQ?他能讓你更簡單的實現這些功能。

  我們使用 ZMQ 的信息訂閱模式。在那個模式中,我們注意到,對於後來的加入節點,始終會丟失在他加入之前,已經發送的信息(Slow joiner)。我們可以開啓另外一個 ZMQ 的通信通道,用於報告當前節點的情況(節點的身份、準備狀態等),其結構如圖 9 所示。

  圖9:擴展 ZMQ 的訂閱者模式

  我們通過$context->getSocket (ZMQ::SOCKET_REQ);設置一個新的 Request-Reply 連接,來用於 Subscriber 向 Publisher 報告自己的身份信息,而 Publisher 則等待所有的 Subscriber 都連接上的時候,再選擇 Publish 自己的信息。

  Subscriber 端的程序如下:

<?php
 
$hostname = $_SERVER['argc'] &gt; 1 ? $_SERVER['argv'][1] : "s1";
 
$context = new ZMQContext (2);
 
$sub = new ZMQSocket ($context,ZMQ::SOCKET_SUB);
 
$sub-&gt;connect ("tcp://localhost:5561");
 
//$subscriber-&gt;setSockOpt (ZMQ::SOCKOPT_IDENTITY, $hostname);
 
$sub-&gt;setSockOpt (ZMQ::SOCKOPT_SUBSCRIBE,"");
 
$client = $context-&gt;getSocket (ZMQ::SOCKET_REQ);
 
$client-&gt;connect ("tcp://localhost:5562");
 
while(1) {
 
//$client-&gt;connect ("tcp://localhost:5562");
 
$client-&gt;send ($hostname);
 
$version = $client-&gt;recv ();
 
echo $version."\r\n";
 
if (!empty($version)) {
 
$recive = $sub-&gt;recv ();
 
$vars = json_decode ($recive);
 
var_dump($vars);
 
}
 
}

  Publisher 端的程序如下:

<?php
 
$CONFIG["TAOKE_BTS"]["ENABLE"] = true;
 
$CONFIG["QP_BTS"]["ENABLE"] = true;
 
$CONFIG["QP_BTS"]["TK_TEST"] = 13;
 
$string = json_encode ($CONFIG);
 
$clients = array("s2","s1","s3");
 
$context = new ZMQContext (10);
 
//Socket talk to clients
 
$publisher = new ZMQSocket ($context,ZMQ::SOCKET_PUB);
 
$publisher-&gt;bind ("tcp://*:5561");
 
//Socket to publish message
 
$server = new ZMQSocket ($context,ZMQ::SOCKET_REP);
 
$server-&gt;bind ("tcp://*:5562");
 
while(count($clients)!=0) {
 
      $client_name = $server-&gt;recv ();
 
        echo "{$client_name} is connect!\r\n";
 
if (in_array($client_name, $clients)) { //coming one client
 
        $key = array_search($client_name, $clients);
 
        unset($clients[$key]);
 
        echo "$client_name has come in!\r\n";
 
        $server-&gt;send ("Version is 2.0");
 
} else {
 
        $server-&gt;send ("You are a stranger!");
 
}
 
}
 
$publisher-&gt;send ($string);
 
?&gt;

  每個節點通過 5562 端口,使用 Rep 模式和 Publisher 連接,通過這個連接告之 Publisher 自己的機器名,而 Publisher 端通過白名單的方式,維護一個機器列表,當機器列表中所有的機器連接上來以後,通過 5561 端口,將最新的配置信息發送出去。

  後續的處理,Subscriber 可以選擇將配置信息寫入到 APC 緩存,程序將始終從緩存中讀取部分配置信息,Subscriber 並將更新後的狀態信息,實時的通過 5562 報告給 Publisher。

  雖然,在本示例中不會出現,但是,如果需要發佈的信息量過大,在接受信息的過程中,Subscriber 端突然中斷網絡(或者是程序崩潰),那麼當他在連接上來的時候,有部分信息就會丟失?ZMQ 考慮到這個問題,通過$subscriber->setSockOpt (ZMQ::SOCKOPT_IDENTITY, $hostname);設置一個 id,當這個 id 的 Subscriber 重新連接上來的時候,他可以從上次中斷的地方,繼續接受信息,當然,節點的中斷,不會影響其他的節點繼續的接受信息。

  那麼 ZMQ 是怎麼實現斷線重連後,繼續發送信息呢 ?他會將斷開的 Subscriber 應該接受到的信息發到內存中,等待他重新上線後,將緩存的信息,繼續發送給他。當然,內存必然是有限的,過多就會出現內存溢出。ZMQ 通過

  SetSockOpt (ZMQ::SOCKOPT_SWAP, 250000)設置 Swap 空間的大小,來防止 out of memory and crash。最終,我們的程序運行結果,如圖 10 所示。

  圖 10:配置中心的運行結果

  當然,這只是一個大體的思路,如果應用到實際的成產環境中,還需要考慮更多的問題,包含穩定性,容錯等等。然而,ZMQ 由於高併發,以及穩定性和易用性,前景不錯,他的目標是進入 Linux 內核,我們期待那一天的到來。

  參考資料 :

  http://www.infoq.com/cn/news/2010/09/introduction-zero-mq Infoq 對 zeromq 的簡介

  http://zguide.zeromq.org/page:all ZeroMQ 的 guide 文檔

 

轉載地址:http://news.cnblogs.com/n/154000/

發佈了94 篇原創文章 · 獲贊 13 · 訪問量 68萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章