NodeJs服務註冊與服務發現實現

frontdog發表於2019-01-09

前言

由於作者才剛開始學習NodeJs,水平實在有限,本文更像是一篇學習筆記,適合同剛開始學習NodeJs的朋友閱讀。

服務治理

如果你的團隊正在探索微服務的搭建,那麼你們可能就在尋找一種機制,這個機制讓每個服務能動態的建立地址,同時呼叫方要能感知到這些服務地址的動態變化。服務註冊與服務發現就是這其中一種機制,大概的流程為:

螢幕快照 2019-01-07 下午10.09.12.png

其中:

  • 註冊中心:zookeeper
  • 服務提供者:NodeJs應用服務
  • 服務消費者:NodeJs API Gateway

ZooKeeper服務註冊中心

ZooKeeper的身份是管理者,它是一個分散式資料一致性的解決方案,分散式任務可以基於它實現資料的釋出與訂閱、負載均衡、命名服務、分散式協調與通知、叢集管理、領導選舉、分散式鎖、分散式佇列等。本文並不會對所有的方面都展開講解,因為作者也還沒涉及到。我們的目標是利用ZooKeeper來實現一個服務的註冊中心,如果你感興趣,可以自己去研究看看,後面我研究了會再來分享的。

1、利用樹狀模型構建服務地址儲存資料結構

zk內部有一個樹狀的記憶體模型,類似於檔案系統,有若干目錄,每個目錄又可以有若干資料夾、檔案,如下圖:

螢幕快照 2019-01-07 下午10.37.56.png

zk有4種節點(會話指客戶端連線zk的長連線):

  • 持久節點:當會話結束時,節點不會被刪除
  • 持久順序節點:當會話結束時,節點不會刪除,節點名自帶增數字尾
  • 臨時節點:當會話結束時,節點會被刪除
  • 臨時順序節點:當會話結束時,節點會被刪除,節點名自帶增數字尾

只有持久節點才能有子節點。

現在一般是叢集部署應用,所以我們來看下叢集部署下的服務地址狀況。例如,當你擁有應用A,應用A部署在2臺機器上,機器IP分別為:127.0.0.1和127.0.0.2,應用服務埠6666,應用A就有這麼兩個服務地址:127.0.0.1:6666、127.0.0.2:6666

我們指定一個節點來作為所有服務地址的根節點(類似名稱空間),所以該節點應該為一個持久節點。我們有n個應用,每個應用下有n臺機器,所以應用節點也擁有子節點,也應該是持久節點。每臺機器在啟動應用服務的時候要向zk註冊一個地址,在服務下線的時候要刪除zk中的地址,所以使用臨時節點特點正好符合這個行為,同時可以使用順序節點自動幫我們管理節點名稱。

因為我們都是使用node操作,所以使用zk的node客戶端node-zookeeper-client。

2、app啟動前連線zk

本來我是用eggjs外掛寫的,這裡將框架的東西剔除,其他提取出來,這樣就不和框架掛鉤了。

const { createClient, ACL, CreateMode } = require('node-zookeeper-client');

const zkClient = createClient('127.0.0.1:2181');
const promisify = require('util').promisify;

zkClient.connect();

zkClient.once('connected', () => {
   registerService();
});

// 讓zkClient支援promise
const proto = Object.getPrototypeOf(zkClient);
Object.keys(proto).forEach(fnName => {
  const fn = proto[fnName];
  if (proto.hasOwnProperty(fnName) && typeof fn === 'function') {
    zkClient[`${fnName}Async`] = promisify(fn).bind(zkClient);
  }
});

// host和port應該和部署系統結合分配
// serviceName要求唯一
const { serviceName, host, port } = config;

async function registerService() {
    try {
      // 建立根節點,持久節點
      const rootNode = await zkClient.existsAsync('/services');
      if (rootNode == null) {
        await zkClient.createAsync('/services', null, ACL.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
      }
      // 建立服務節點,持久節點
      const servicePath = `/services/${serviceName}`;
      const serviceNode = await zkClient.existsAsync(servicePath);
      if (serviceNode == null) {
        await zkClient.createAsync(servicePath, null, ACL.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
      }
      // 建立地址節點,臨時順序節點,這樣name就不需要我們去維護了,遞增
      const addressPath = `${servicePath}/address-`;
      const serviceAddress = `${host}:${port}`;
      const addressNode = await zkClient.createAsync(addressPath, Buffer.from(serviceAddress), ACL.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
    } catch (error) {
      throw new Error(error);
    }
}
複製程式碼

上面的程式碼其實很簡單,就是連線zk後先判斷根節點是不是建立,如果沒有就建立(第一個應用),然後判斷應用節點是否建立,沒有就建立(叢集第一臺機器),最後就是建立機器節點,這裡使用臨時順序節點,省去了我們維護唯一name的麻煩,讓其遞增,注意,host和port作為儲存內容,這個需要app部署的時候部署系統提供(如果是使用自動部署系統的話),然後地址轉為Buffer存起來。

這樣其實服務註冊就完成了。

NodeJs API Gateway 閘道器服務

上面已經在服務啟動的時候都註冊到zk中了,當前端呼叫介面訪問服務的時候,我們需要知道服務的地址,這就是服務發現過程。

API Gateway如它的字面意思來看,是API的入口,用來路由請求。其實,不單單是路由請求,API Gateway還可以轉換協議,整合資料、認證、限速等邏輯。

螢幕快照 2019-01-08 上午12.26.26.png

例如前端有個使用者獲取的請求,應該這樣寫:

fetch('/api/user/get', {
    method: 'POST',
    body: { id: 1 },
    headers: {
        // header的方式指定service
        'servive-name': 'user'
    }
})
複製程式碼

API Gateway本質也是是個服務,使用Eggjs編寫,我們的服務發現封裝成一箇中介軟體,所以這裡只展示中介軟體的內容,其他的自己看egg的文件。

const proxy = require('koa-proxies');

module.exports = (options, app) => {
  return async (ctx, next) => {
    const serviceName = ctx.request.headers['servive-name'];
    if (!serviceName) {
      ctx.throw(404, 'no service found.');
    }
    const servicePath = `/services/${serviceName}`;
    const addressNodes = await app.zookeeper.getChildrenAsync(servicePath);
    const size = addressNodes.length;
    if (size === 0) {
      ctx.throw(404, 'no service found.');
    }
    let addressPath = `${servicePath}/`;
    if (size === 1) {
      addressPath += addressNodes[0];
    } else {
      // 這裡你可以做負載均衡
      addressPath += addressNodes[parseInt(Math.random() * size)];
    }
    const serviceAddress = await app.zookeeper.getDataAsync(addressPath);
    if (!serviceAddress) {
      ctx.throw(404, 'no service found.');
    }
    await proxy('/', {
      target: `http://${serviceAddress}/`,
    })(ctx, next);
  };
};
複製程式碼

上面的中介軟體中根據headers中的service-name去獲取到該應用下所有的服務地址,然後根據某個策略選擇一個服務,使用代理轉發到對應的服務。

總結

以上很簡陋地實現了,雖然可以使用,還有很多細節要處理,比如API Gateway中對於已經拿到的服務地址可以快取起來,然後訂閱zk變化;比如在選擇服務的時候可以做負載均衡。

相關文章