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