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

1,520 阅读5分钟

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

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

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

动态注册比较好理解,当我一台服务起来以后,自动向服务中心发送一条通知: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 仓库。

# 创建 gateway 项目
nest new gateway

# 创建 ms_aaa 项目
nest new ms_aaa

# 创建 ms_bbb 项目
nest new ms_bbb

微服务改造

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

  1. 安装依赖包
pnpm install @nestjs/microservices nacos-config nacos-naming
  1. 添加 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);
    },
  );
};
  1. 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();
  1. 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 里看到两个服务了。

图 1

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

图 2

网关服务改造

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

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

  1. 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 仓库,拜~。