Node-js 微服務如何實現註冊中心和配置中心
微服務架構的系統都會有配置中心和註冊中心。
爲什麼呢?
比如說配置中心:
系統中會有很多微服務,它們會有一些配置信息,比如環境變量、數據庫連接信息等。
這些配置信息散落在各個服務中,以配置文件的形式存在。
這樣你修改同樣的配置需要去各個服務下改下配置文件,然後重啓服務。
就很麻煩。
如果有一個服務專門用來集中管理配置信息呢?
這樣每個微服務都從這裏拿配置,可以統一的修改,並且配置更改後也會通知各個微服務。
這個集中管理配置信息的服務就叫配置中心。
再就是註冊中心:
微服務之間會相互依賴,共同完成業務邏輯的處理。
如果某個微服務掛掉了,那所有依賴它的服務就都不能工作了。
爲了避免這種情況,我們會通過集羣部署的方式,每種微服務部署若干個節點,並且還可能動態增加一些節點。
那麼問題來了:
微服務 A 依賴了微服務 B,寫代碼的時候 B 只有 3 個節點,但跑起來以後,某個節點掛掉了,並且還新增了幾個微服務 B 的節點。
這時候微服務 A 怎麼知道微服務 B 有哪些節點可用呢?
答案也是需要一個單獨的服務來管理,這個服務就是註冊中心:
微服務在啓動的時候,向註冊中心註冊,銷燬的時候向註冊中心註銷,並且定時發心跳包來彙報自己的狀態。
在查找其他微服務的時候,去註冊中心查一下這個服務的所有節點信息,然後再選一個來用,這個叫做服務發現。
這樣微服務就可以動態的增刪節點而不影響其他微服務了。
微服務架構的後端系統中,都會有這兩種服務。
下面是我網上找的幾張微服務系統的架構圖:
可以看到,配置中心和註冊中心是必備組件。
但是,雖然這是兩種服務,功能確實很類似,完全可以在一個服務裏實現。
可以做配置中心、註冊中心的中間件還是挺多的,比如 nacos、apollo、etcd 等。
今天我們來學下 etcd 實現註冊中心和配置中心。
它其實是一個 key-value 的存儲服務。
k8s 就是用它來做的註冊中心、配置中心:
我們通過 docker 把它跑起來。
如果你本地沒裝 docker,可以去 docker.com 下載個 docker desktop:
它可以可視化管理鏡像、容器等:
搜索 etcd,點擊 run:
輸入容器名,映射 2379 端口到容器內的 2379 端口,設置 ETCD_ROOT_PASSWORD 環境變量,也就是指定 root 的密碼。
然後就可以看到 etcd server 的 docker 鏡像成功跑起來了:
它帶了一個 etcdctl 的命令行工具,可以作爲客戶端和 etcd server 交互。
常用的命令有這麼幾個:
etcdctl put key value
etcdctl get key
etcdctl del key
etcdctl watch key
就是對 key value 的增刪改查和 watch 變動,還是比較容易理解的。
但是現在執行命令要加上 --user、--password 的參數纔可以:
etcdctl get --user=root --password=guang key
如果不想每次都指定用戶名密碼,可以設置環境變量:
export ETCDCTL_USER=root
export ETCDCTL_PASSWORD=guang
這裏的 password 就是啓動容器的時候指定的那個環境變量:
我們設置幾個 key:
etcdctl put /services/a xxxx
etcdctl put /services/b yyyy
之後可以 get 來查詢他們的值:
etcdctl get /services/a
etcdctl get /services/b
也可以通過 --prefix 查詢指定前綴的 key 的值:
etcdctl get --prefix /services
刪除也是可以單個刪和指定前綴批量刪:
etcdctl del /servcies/a
etcdctl del --prefix /services
這樣的 key-value 用來存儲 服務名 - 鏈接信息,那就是註冊中心,用來存儲配置信息,那就是配置中心。
我們在 node 裏面鏈接下 etcd 服務:
使用 etcd 官方提供的 npm 包 etcd3:
const { Etcd3 } = require('etcd3');
const client = new Etcd3({
hosts: 'http://localhost:2379',
auth: {
username: 'root',
password: 'guang'
}
});
(async () => {
const services = await client.get('/services/a').string();
console.log('service A:', services);
const allServices = await client.getAll().prefix('/services').keys();
console.log('all services:', allServices);
const watcher = await client.watch().key('/services/a').create();
watcher.on('put', (req) => {
console.log('put', req.value.toString())
})
watcher.on('delete', (req) => {
console.log('delete')
})
})();
get、getAll、watch 這些 api 和 ectdctl 命令行差不多,很容易搞懂。
我們再 put 幾個 key:
然後執行上面的 node 腳本:
確實取到了 etcd server 中的值。
然後在 etcdctl 裏 put 修改下 /services/a 的值:
在 node 腳本這裏收到了通知:
再 del 試下:
也收到了通知:
這樣,在 node 裏操作 etcd server 就跑通了。
然後我們封裝下配置中心和註冊中心的工具函數:
配置中心的實現比較簡單,就是直接 put、get、del 對應的 key:
// 保存配置
async function saveConfig(key, value) {
await client.put(key).value(value);
}
// 讀取配置
async function getConfig(key) {
return await client.get(key).string();
}
// 刪除配置
async function deleteConfig(key) {
await client.delete().key(key);
}
使用起來也很簡單;
(async function main() {
await saveConfig('config-key', 'config-value');
const configValue = await getConfig('config-key');
console.log('Config value:', configValue);
})();
你可以在這裏存各種數據庫連接信息、環境變量等各種配置。
然後是註冊中心:
服務註冊:
// 服務註冊
async function registerService(serviceName, instanceId, metadata) {
const key = `/services/${serviceName}/${instanceId}`;
const lease = client.lease(10);
await lease.put(key).value(JSON.stringify(metadata));
lease.on('lost', async () => {
console.log('租約過期,重新註冊...');
await registerService(serviceName, instanceId, metadata);
});
}
註冊的時候我們按照 /services / 服務名 / 實例 id 的格式來指定 key。
也就是一個微服務可以有多個實例。
設置了租約 10s,這個就是過期時間的意思,然後過期會自動刪除。
我們可以監聽 lost 事件,在過期後自動續租。
當不再續租的時候,就代表這個服務掛掉了。
然後是服務發現:
// 服務發現
async function discoverService(serviceName) {
const instances = await client.getAll().prefix(`/services/${serviceName}`).strings();
return Object.entries(instances).map(([key, value]) => JSON.parse(value));
}
服務發現就是查詢 /services / 服務名 下的所有實例,返回它的信息。
// 監聽服務變更
async function watchService(serviceName, callback) {
const watcher = await client.watch().prefix(`/services/${serviceName}`).create();
watcher .on('put', async event => {
console.log('新的服務節點添加:', event.key.toString());
callback(await discoverService(serviceName));
}).on('delete', async event => {
console.log('服務節點刪除:', event.key.toString());
callback(await discoverService(serviceName));
});
}
通過 watch 監聽 /services / 服務名下所有實例的變動,包括添加節點、刪除節點等,返回現在的可用節點。
我們來測試下:
(async function main() {
const serviceName = 'my_service';
await registerService(serviceName, 'instance_1', { host: 'localhost', port:3000 });
await registerService(serviceName, 'instance_2', { host: 'localhost', port:3002 });
const instances = await discoverService(serviceName);
console.log('所有服務節點:', instances);
watchService(serviceName, updatedInstances => {
console.log('服務節點有變動:', updatedInstances);
});
})();
跑起來確實能獲得服務的所有節點信息:
當在 etcdctl 裏 del 一個服務節點的時候,這裏也能收到通知:
這樣,我們就實現了服務註冊、服務發現功能。
有的同學可能問了:redis 不也是 key-value 存儲的麼?爲什麼不用 redis 做配置中心和註冊中心?
因爲 redis 沒法監聽不存在的 key 的變化,而 etcd 可以,而配置信息很多都是動態添加的。
當然,還有很多別的原因,畢竟 redis 只是爲了緩存設計的,不是專門的配置中心、註冊中心的中間件。
專業的事情還是交給專業的中間件來幹。
全部代碼如下:
const { Etcd3 } = require('etcd3');
const client = new Etcd3({
hosts: 'http://localhost:2379',
auth: {
username: 'root',
password: 'guang'
}
});
// 保存配置
async function saveConfig(key, value) {
await client.put(key).value(value);
}
// 讀取配置
async function getConfig(key) {
return await client.get(key).string();
}
// 刪除配置
async function deleteConfig(key) {
await client.delete().key(key);
}
// 服務註冊
async function registerService(serviceName, instanceId, metadata) {
const key = `/services/${serviceName}/${instanceId}`;
const lease = client.lease(10);
await lease.put(key).value(JSON.stringify(metadata));
lease.on('lost', async () => {
console.log('租約過期,重新註冊...');
await registerService(serviceName, instanceId, metadata);
});
}
// 服務發現
async function discoverService(serviceName) {
const instances = await client.getAll().prefix(`/services/${serviceName}`).strings();
return Object.entries(instances).map(([key, value]) => JSON.parse(value));
}
// 監聽服務變更
async function watchService(serviceName, callback) {
const watcher = await client.watch().prefix(`/services/${serviceName}`).create();
watcher .on('put', async event => {
console.log('新的服務節點添加:', event.key.toString());
callback(await discoverService(serviceName));
}).on('delete', async event => {
console.log('服務節點刪除:', event.key.toString());
callback(await discoverService(serviceName));
});
}
// (async function main() {
// await saveConfig('config-key', 'config-value');
// const configValue = await getConfig('config-key');
// console.log('Config value:', configValue);
// })();
(async function main() {
const serviceName = 'my_service';
await registerService(serviceName, 'instance_1', { host: 'localhost', port:3000 });
await registerService(serviceName, 'instance_2', { host: 'localhost', port:3002 });
const instances = await discoverService(serviceName);
console.log('所有服務節點:', instances);
watchService(serviceName, updatedInstances => {
console.log('服務節點有變動:', updatedInstances);
});
})();
總結
微服務架構的系統中少不了配置中心和註冊中心。
不同服務的配置需要統一管理,並且在更新後通知所有的服務,所以需要配置中心。
微服務的節點可能動態的增加或者刪除,依賴他的服務在調用之前需要知道有哪些實例可用,所以需要註冊中心。
服務啓動的時候註冊到註冊中心,並定時續租期,調用別的服務的時候,可以查一下有哪些服務實例可用,也就是服務註冊、服務發現功能。
註冊中心和配置中心可以用 etcd 來做,它就是一個專業做這件事的中間件,k8s 就是用的它來做的配置和服務註冊中心。
我們用 docker 跑了 etcd server,它內置了命令行工具 etcdctl 可以用來和 server 交互。
常用的命令有 put、get、del、watch 等。
在 node 裏可以通過 etcd3 這個包來操作 etcd server。
稍微封裝一下就可以實現配置管理和服務註冊、發現的功能。
在微服務架構的後端系統中,配置中心、註冊中心是必不可少的組件,不管是 java、go 還是 Node.js。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/4Nyj2i8kcgxejWBnaM2SVw