How Node.js implements microservice registration center and configuration center

A system with a microservice architecture will have a configuration center and a registration center.

why?

For example, the configuration center:

There will be many microservices in the system, and they will have some configuration information, such as environment variables, database connection information, and so on.

These configuration information are scattered in various services and exist in the form of configuration files.

In this way, if you modify the same configuration, you need to change the configuration file under each service, and then restart the service.

It's very troublesome.

What if there is a service dedicated to centrally managing configuration information?

In this way, each microservice gets the configuration from here, which can be modified uniformly, and each microservice will be notified after the configuration changes.

This service for centralized management of configuration information is called the configuration center.

Then there is the registration center:

Microservices will depend on each other to complete business logic processing together.

If a microservice goes down, all services that depend on it won't work.

In order to avoid this situation, we will deploy several nodes for each microservice through cluster deployment, and some nodes may also be dynamically added.

So here comes the question:

Microservice A depends on Microservice B. When the code was written, B only had 3 nodes, but after running, a node died, and several new nodes of Microservice B were added.

At this time, how does microservice A know which nodes are available for microservice B?

The answer also requires a separate service to manage, this service is the registration center:

When the microservice starts, it registers with the registration center, when it is destroyed, it logs out with the registration center, and sends heartbeat packets regularly to report its status.

When looking for other microservices, go to the registration center to check all the node information of this service, and then choose one to use. This is called service discovery.

In this way, microservices can dynamically add or delete nodes without affecting other microservices.

These two services will exist in the backend system of the microservice architecture.

The following are the architecture diagrams of several microservice systems I found online:

As you can see, the configuration center and registration center are essential components.

However, although these are two services, the functions are indeed similar and can be implemented in one service.

There are quite a lot of middleware that can be used as configuration centers and registration centers, such as nacos, apollo, etcd, etc.

Today we will learn how etcd implements the registration center and configuration center.

It is actually a key-value storage service.

k8s is used as a registration center and configuration center:

We run it through 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。

Guess you like

Origin juejin.im/post/7234060695254990909