因爲做者纔剛開始學習NodeJs,水平實在有限,本文更像是一篇學習筆記,適合同剛開始學習NodeJs的朋友閱讀。javascript
若是你的團隊正在探索微服務的搭建,那麼大家可能就在尋找一種機制,這個機制讓每一個服務能動態的建立地址,同時調用方要能感知到這些服務地址的動態變化。服務註冊與服務發現就是這其中一種機制,大概的流程爲:前端
其中:java
ZooKeeper的身份是管理者,它是一個分佈式數據一致性的解決方案,分佈式任務能夠基於它實現數據的發佈與訂閱、負載均衡、命名服務、分佈式協調與通知、集羣管理、領導選舉、分佈式鎖、分佈式隊列等。本文並不會對全部的方面都展開講解,由於做者也還沒涉及到。咱們的目標是利用ZooKeeper來實現一個服務的註冊中心,若是你感興趣,能夠本身去研究看看,後面我研究了會再來分享的。node
zk內部有一個樹狀的內存模型,相似於文件系統,有若干目錄,每一個目錄又能夠有若干文件夾、文件,以下圖:api
zk有4種節點(會話指客戶端鏈接zk的長鏈接):promise
只有持久節點纔能有子節點。緩存
如今通常是集羣部署應用,因此咱們來看下集羣部署下的服務地址情況。例如,當你擁有應用A,應用A部署在2臺機器上,機器IP分別爲:127.0.0.1和127.0.0.2,應用服務端口6666,應用A就有這麼兩個服務地址:127.0.0.1:666六、127.0.0.2:6666數據結構
咱們指定一個節點來做爲全部服務地址的根節點(相似命名空間),因此該節點應該爲一個持久節點。咱們有n個應用,每一個應用下有n臺機器,因此應用節點也擁有子節點,也應該是持久節點。每臺機器在啓動應用服務的時候要向zk註冊一個地址,在服務下線的時候要刪除zk中的地址,因此使用臨時節點特色正好符合這個行爲,同時可使用順序節點自動幫咱們管理節點名稱。app
由於咱們都是使用node操做,因此使用zk的node客戶端node-zookeeper-client。負載均衡
原本我是用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存起來。
這樣其實服務註冊就完成了。
上面已經在服務啓動的時候都註冊到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變化;好比在選擇服務的時候能夠作負載均衡。