Node.js垂直水平擴充套件與訊息整合方案

banq發表於2014-04-24

這是一篇討論Node.js在無需修改任何程式碼從單核垂直擴充套件到多核,再水平擴充套件到多臺叢集和訊息整合的分散式系統,展示了Node.JS在無縫擴充套件性方面要強於Java。其主要架構是Node.js微服務 + 訊息Messaging + 叢集Clustering 。翻譯如下:

當使用微服務建立一個複雜的分散式系統時,關鍵問題是提供服務之間的通訊,微服務一般使用REST API,你一般會快取遠方的狀態,但系統是在不斷地變化,也許你狀態已經過期了,你可以透過輪詢重新整理你的複製,但這是不可擴充套件的,增加系統壓力,很多重複的請求通常反而得不到最新的狀態( ‘不,還沒有新的訊息。別再重複問了。”)

更好的方式是反轉資訊流,讓服務擁有資料當發生變化時主動告訴你。這是來自微服務為基礎的訊息系統,這需要某種訊息代理。

另一個重要的特點是可擴充套件性Scalable。你可能需要迅速提高你的處理能力,垂直或水平伸縮擴充套件微服務能力是關鍵。

水平和垂直擴充套件
通常認為從Java遷移到Node.js的開發人員其實並不真正理解水平擴充套件的含義,因為在JEE容器中執行緒池實現了魔術的處理,增加真實或虛擬的伺服器其實不需要軟體這邊做出什麼修改,舉例,如果你為你的JVM增加CPU核數,JEE容器會擴充套件利用它們,如果你增加更多記憶體,你就需要微調JVM引數才能充分利用擴大的記憶體,否則它像以往一樣工作,這時你可能需要重新啟動多個JVM, 在這點上你發現你的JEE應用實際並不是以叢集方式編寫的。

在Node.js這裡,增加更多CPU核數其實也不難做到,使用PM2如下命令:

pm2 start app.js -i max
無論如何,對於Node.js水平或垂直擴充套件是不管你編寫程式碼的方式,你只需要透過叢集利用在同一臺機器上所有CPU核心,不用像Java那樣需要在多個獨立的伺服器或虛擬機器實現負載均衡。

我真的喜歡Node.js這個特性–它迫使你從一開始就要考慮叢集,阻止你在請求之間持有資料,迫使你儲存狀態到一個共享的資料庫中,這個資料庫能夠被所有正在執行的例項訪問。這使得從垂直到水平的擴充套件性切換,根本不影響你。這裡沒什麼新的東西,只是基本的share-nothing的好處。

然而,使用PM2載入多個Node.js或使用Node的叢集模組與使用Nginx代理作為負載平衡器之間是有重要區別的:使用Nginx 作為代理伺服器時,我們有一個獨立的繫結到一個機器上的一個埠伺服器,負載平衡和URL代理同時已經完成。在Nginx中是這樣配置:

http {
    upstream myapp1 {
        server srv1.example.com;
        server srv2.example.com;
        server srv3.example.com;
    }
 
    server {
        listen 80;
 
        location / {
            proxy_pass http://myapp1;
        }
    }
}
<p class="indent">

如果你試圖在單臺機器上啟動多個Node伺服器時,除了第一個都將失敗,因為它們(srv1. srv2. srv3.)不能繫結到同一個80埠上,然而,如果你使用Node的叢集模組或使用PM2,會有一點魔法發生,主程式有一段程式碼會啟用Socket在多個工作程式之間共享,使用了一種共享策略(或者使用作業系統定義 或者Node 0.12的’round-robin’),這非常類似於Nginx在不同伺服器之間為你做的事情,Nginx也有負載平衡的選項(round-robin, 最少連線, IP-hash, weight-directed重導向等).


訊息
下面我們將訊息和叢集兩個概念放在一起。
為了讓事情變得更具體生動,讓我們看一個真實世界的例子。我們已經編寫的活動的流微服務。它的工作是收集活動流規範2草案中的活動,並將它們儲存在cloudant資料庫,這樣它們以後可以作為一種活動流檢索。這項微服務做一件事情,並且做得很好–它聚集來自系統任何地方的活動–然後發射一個活動到一個專用的MQTT主題。

我們使用mqtt作為MQTT客戶端,RabbitMQ作為我們的多語種訊息代理,Node.js作為我們的活動微服務,這已經是我們不止第一次這麼架構了。

當前面談的叢集加入其中時,MQTT是一個pub/sub協議.為了讓每個訂閱者從佇列中讀取訊息, RabbitMQ為叢集中每個Node例項開設一個單獨佇列例項。

[img index=1]

但這並不是我們需要的,每個例項都將收到一個“新活動”訊息,並試圖將它寫入資料庫,資料庫這裡需要避免競爭。即使資料庫可以阻止其他Node保證只有一個Node節點成功寫入記錄,這也是一種浪費,因為所有的Node節點都在執行同一個任務。

這裡的問題是,用於叢集模組“白魔法”來處理HTTP / HTTPS伺服器請求但並沒有延伸到MQTT模組。

我們解決這個問題原始想法是,如果我們遷移訊息客戶端到主例項,它會對進來的訊息響應,然後傳遞他們到後面的從工作例項,以round- robin方式,這似乎是合理的,但需要調整一個ICK引數,因為需要我們自己實施自己的負載平衡,它會阻止我們使用PM2(因為我們必須對從工作例項進行控制),如果我們使用多個虛擬機器和Nginx的負載平衡,我們將回到原點(Nginx並不支援單機同一個埠的負載平衡)。

幸運的是,我們發現RabbitMQ已經可以處理這部分,如果我們放棄持久,並且確認我們是MQTT抽象下執行AMQP。RabbitMQ的pub / sub拓撲方式是:釋出者提交到 “主題”交流,然後被繫結到一個使用路由作為Key的佇列上(事實上,在AMQP路由key和MQTT主題topic有直接對映)。

原來使用MQTT客戶端使得每個群集例項接收自己的佇列。現在透過遷移到AMQP客戶端和將所有例項繫結到同一個佇列,我們讓RabbitMQ對客戶端使用round-robin實現基本負載平衡。

[img index=2]

下面是使用Eclipse PAHO Java客戶端釋出MQTT訊息給主題Topic程式碼,使用Node.js或•Ruby等其他客戶端幾乎一樣:

public static final String TOPIC = "activities";
MqttClient client;
 
try {
    client = new MqttClient("tcp://"+host+":1883",
                                      "mqtt-client1");
    client.connect();
    MqttMessage message = new MqttMessage(messageText.getBytes());
    client.publish(TOPIC, message);
    System.out.println (" [x] Sent to MQTT topic '"
                              +TOPIC+"': "+ message + "'");
    client.disconnect();
} catch (MqttException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
}
<p class="indent">

上面是發出訊息到‘activities’主題,下面是接受訊息,然後將佇列使用匹配的路由key(還是‘activities’)和預設的AMQP主題交換(topic exchange (“amq.topic”))繫結,佇列的名稱不重要,只要所有Node的伺服器都使用同一個即可,實際上名稱在它們之間會相互複製。

var amqp = require('amqp');
 
var connection = amqp.createConnection({ host: 'localhost' });
 
// Wait for connection to become established.
connection.on('ready', function () {
  // Use the default 'amq.topic' exchange
  connection.queue('worker-queue', { durable: true}, function (q) {
      // Route key identical to the MQTT topic
      q.bind('activities');
 
      // Receive messages
      q.subscribe(function (message, headers, deliveryInfo, messageObject) {
        // Print messages to stdout
        console.log('Node AMQP('+process.pid+'): received topic &quot;'+
                deliveryInfo.routingKey+
                '", message: "'+
                message.data.toString ()+'"');
      });
  });
});
<p class="indent">

[該貼被banq於2014-04-24 12:27修改過]

相關文章