为什么需要微服务的动态注册与动态发现?
后端架构在业务复杂时,好的解决方案就是用微服务做解耦。每个服务只开发相对应业务功能,如果服务流量大,还可以多部署几台服务器。
那服务多了以后,就涉及到了都有啥服务呀?服务下的哪个实例是好的呀?等等问题。
动态注册比较好理解,当我一台服务起来以后,自动向服务中心发送一条通知:XX 服务上线了一个实例!
那为啥要动态发现?我理解的有两方面:
-
服务有新节点上线、老节点崩溃等行为,网关需要根据节点状态将流量转发到正常的节点上去。
-
如果每增加一种服务,网关就开发对应的转发规则,那不就开发两遍了吗?
别管加了什么功能,你约定一个前缀,我直接就发给你,多好!
什么是 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/
显示如下:
Nest.js 项目创建
通过命令创建 3 个项目,项目代码可以看 example_nest_nacos 仓库。
# 创建 gateway 项目
nest new gateway
# 创建 ms_aaa 项目
nest new ms_aaa
# 创建 ms_bbb 项目
nest new ms_bbb
微服务改造
在微服务 ms_aaa
和 ms_bbb
同样执行以下操作。
- 安装依赖包
pnpm install @nestjs/microservices nacos-config nacos-naming
- 添加
src/nacos.ts
文件,代码如下:
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);
},
);
};
- 将
src/main.ts
改为如下代码:
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();
- 在
src/app.controller.ts
中添加方法以供测试
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 里看到两个服务了。
同时,它们还能监听配置 ms_aaa
和 ms_bbb
的变化。
网关服务改造
- 安装依赖包
pnpm install @nestjs/microservices nacos-config nacos-naming
- 获取服务列表
没有某个方法可以直接获取所有服务和对应实例列表,所以只能先 查询服务列表,然后再根据查询结果 serviceName
调用 getAllInstances
方法、selectInstances
方法或 subscribe
方法。
- 将
src/app.controller.ts
改为如下代码:
我只是简单写了下 Get 请求做测试,动态连接微服务也直接写在了函数中,这些都需要自行优化。
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 仓库,拜~。