Nest.js + Nacos 实现微服务的动态注册与动态发现

为什么需要微服务的动态注册与动态发现?

后端架构在业务复杂时,好的解决方案就是用微服务做解耦。每个服务只开发相对应业务功能,如果服务流量大,还可以多部署几台服务器。

那服务多了以后,就涉及到了都有啥服务呀?服务下的哪个实例是好的呀?等等问题。

动态注册比较好理解,当我一台服务起来以后,自动向服务中心发送一条通知:XX 服务上线了一个实例!

那为啥要动态发现?我理解的有两方面:

  1. 服务有新节点上线、老节点崩溃等行为,网关需要根据节点状态将流量转发到正常的节点上去。

  2. 如果每增加一种服务,网关就开发对应的转发规则,那不就开发两遍了吗?

别管加了什么功能,你约定一个前缀,我直接就发给你,多好!

什么是 Nacos?

Nacos 是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。

阿里巴巴开发,因为官方提供 Java SDK,所以国内 Java 的微服务大部分都是用的 Nacos。

其实只要是微服务架构都可以使用,像:go、nodejs、python 等等。

说到底还是 API 的调用,哪怕你不是微服务架构,也可以通过调用接口来用用配置中心或开发个服务状态监控页啥的。

如何实现微服务的动态注册与动态发现?

这里我们简单做个区分:

  • nacos
    • 端口:8848
    • 提供配置中心、服务中心功能
  • gateway
    • 端口:3000
    • Nest 网关服务,提供 HTTP 接口访问并转发到对应的微服务
  • ms_aaa
    • 端口:4000
    • Nest 微服务,我们约定路径以 /aaa 开头的都会被转发到这个微服务
  • ms_bbb
    • 端口:5000
    • Nest 微服务,我们约定路径以 /bbb 开头的都会被转发到这个微服务

Nacos 安装

Nacos 支持二进制安装Docker 安装Kubernetes 安装,根据自己喜好来,安装还是比较简单的。

因为有 NAS,所以我这里直接在 Container Manager 里启动 Docker 镜像。

本地测试环境变量 MODE 必须改为 standalone 表示单机模式。而默认的 cluster 表示集群,需要配置一些额外的变量,是正式环境下,多容器的建议方式。

本地测试不必填写数据库相关环境变量,因为内置了 Derby,够咱们测试使用了。

安装完启动后访问 http://ip:8848/nacos/ 显示如下:

图 0

Nest.js 项目创建

通过命令创建 3 个项目,项目代码可以看 example_nest_nacos 仓库。

1
2
3
4
5
6
7
8
# 创建 gateway 项目
nest new gateway

# 创建 ms_aaa 项目
nest new ms_aaa

# 创建 ms_bbb 项目
nest new ms_bbb

微服务改造

在微服务 ms_aaams_bbb 同样执行以下操作。

  1. 安装依赖包
1
pnpm install @nestjs/microservices nacos-config nacos-naming
  1. 添加 src/nacos.ts 文件,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import { NacosConfigClient } from 'nacos-config';
import { NacosNamingClient } from 'nacos-naming';

/**
* Constants
*/
// Nacos 服务地址
const serverAddr = '192.168.28.28:8848';

export const startNacos = async (props) => {
// 参数
const {
group = 'DEFAULT_GROUP',
namespace = 'public',
ip,
port,
serverName,
} = props;

/**
* 服务中心
*/
// 创建客户端
const naming = new NacosNamingClient({
logger: console,
namespace,
serverList: serverAddr,
});

// 初始化
await naming.ready();

// 注册服务
await naming.registerInstance(
serverName,
{
enabled: true,
healthy: true,
instanceId: `${ip}:${port}`,
ip, // 微服务地址
port,
},
group,
);

// 订阅通知
naming.subscribe(serverName, (value) => {
console.log('naming subscribe :>> ', value);
});

/**
* 配置中心
*/
// 创建客户端
const config = new NacosConfigClient({
namespace,
serverAddr,
});

// 订阅通知
config.subscribe(
{
dataId: serverName,
group,
},
(value) => {
console.log('config subscribe :>> ', value);
},
);
};
  1. src/main.ts 改为如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { NestFactory } from '@nestjs/core';
import { Transport } from '@nestjs/microservices';
import { AppModule } from './app.module';
import { startNacos } from './nacos';

async function bootstrap() {
const app = await NestFactory.createMicroservice(AppModule, {
transport: Transport.TCP,
options: {
port: 4000, // 如果是 ms_bbb 则改为 5000
},
});

startNacos({
ip: 'localhost',
port: 4000, // 如果是 ms_bbb 则改为 5000
serverName: 'ms_aaa', // 如果是 ms_bbb 则改为 ms_bbb
});

await app.listen();
}
bootstrap();
  1. src/app.controller.ts 中添加方法以供测试
1
2
3
4
5
6
7
8
9
10
11
import { MessagePattern } from '@nestjs/microservices';

@Controller()
export class AppController {
// ...

@MessagePattern('hello')
hello(): string {
return 'hello from ms_aaa'; // 如果是 ms_bbb 则改为 ms_bbb
}
}

执行 pnpm run start:dev 之后,应该能在 Nacos 里看到两个服务了。

图 1

同时,它们还能监听配置 ms_aaams_bbb 的变化。

图 2

网关服务改造

  1. 安装依赖包
1
pnpm install @nestjs/microservices nacos-config nacos-naming
  1. 获取服务列表

没有某个方法可以直接获取所有服务和对应实例列表,所以只能先 查询服务列表,然后再根据查询结果 serviceName 调用 getAllInstances 方法、selectInstances 方法或 subscribe 方法。

  1. src/app.controller.ts 改为如下代码:

我只是简单写了下 Get 请求做测试,动态连接微服务也直接写在了函数中,这些都需要自行优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import { Controller, Get, Req } from '@nestjs/common';
import { ClientProxyFactory, Transport } from '@nestjs/microservices';
import { NacosNamingClient } from 'nacos-naming';
import { AppService } from './app.service';

@Controller()
export class AppController {
// 客户端
naming;

// 服务列表
serviceList = {
count: 0,
data: [],
map: {},
};

constructor(private readonly appService: AppService) {
// 创建客户端
this.naming = new NacosNamingClient({
logger: console,
namespace: 'public',
serverList: '192.168.28.28:58848',
});

// 查询服务列表
this.naming._serverProxy
.getServiceList(1, 20, 'DEFAULT_GROUP')
.then((service) => {
this.serviceList.count = service.count;
this.serviceList.data = service.data;

service.data.forEach((serviceName) => {
// 获取 serviceName 服务下 所有 实例列表
this.naming.getAllInstances(serviceName).then((instance) => {
console.log('getAllInstances :>> ', instance);
});

// 获取 serviceName 服务下 可用 实例列表
this.naming.selectInstances(serviceName).then((instance) => {
console.log('selectInstances :>> ', instance);

this.serviceList.map[serviceName] = instance;
});

// 监听 serviceName 服务下实例变化
this.naming.subscribe(serviceName, (hosts) => {
console.log('subscribe :>> ', hosts);

// 获取 serviceName 服务下 可用 实例列表
this.naming.selectInstances(serviceName).then((instance) => {
this.serviceList.map[serviceName] = instance;
});
});
});
});
}

@Get('*')
get(@Req() req): any {
const serviceName = req.url.startsWith('/aaa') ? 'ms_aaa' : 'ms_bbb';

// 我这里做演示,每次都动态创建服务,应该做缓存
const microservice = ClientProxyFactory.create({
transport: Transport.TCP,
options: {
host: this.serviceList.map[serviceName][0].ip,
port: this.serviceList.map[serviceName][0].port,
},
});

return microservice.send('hello', '123123');
}
}

结尾

至此,访问 /aaa/hello 会返回 hello from ms_aaa,而 /bbb/hello 会返回 hello from ms_bbb

源码查看 example_nest_nacos 仓库,拜~。