前言
由於作者才剛開始學習NodeJs,水平實在有限,本文更像是一篇學習筆記,適合同剛開始學習NodeJs的朋友閱讀。
服務治理
如果你的團隊正在探索微服務的搭建,那麼你們可能就在尋找一種機制,這個機制讓每個服務能動態的建立地址,同時呼叫方要能感知到這些服務地址的動態變化。服務註冊與服務發現就是這其中一種機制,大概的流程為:
其中:
- 註冊中心:zookeeper
- 服務提供者:NodeJs應用服務
- 服務消費者:NodeJs API Gateway
ZooKeeper服務註冊中心
ZooKeeper的身份是管理者,它是一個分散式資料一致性的解決方案,分散式任務可以基於它實現資料的釋出與訂閱、負載均衡、命名服務、分散式協調與通知、叢集管理、領導選舉、分散式鎖、分散式佇列等。本文並不會對所有的方面都展開講解,因為作者也還沒涉及到。我們的目標是利用ZooKeeper來實現一個服務的註冊中心,如果你感興趣,可以自己去研究看看,後面我研究了會再來分享的。
1、利用樹狀模型構建服務地址儲存資料結構
zk內部有一個樹狀的記憶體模型,類似於檔案系統,有若干目錄,每個目錄又可以有若干資料夾、檔案,如下圖:
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還可以轉換協議,整合資料、認證、限速等邏輯。
例如前端有個使用者獲取的請求,應該這樣寫:
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變化;比如在選擇服務的時候可以做負載均衡。