简单使用Nest+Nacos+Kafka实现微服务

简单实用Nest+Nacos+Kafka+Docker实现微服务架构

PS:并非实战,一个练习。

设计

一个网关,一个用户服务,一个邮件推送服务。

用户服务部署两份,邮件推送服务部署两份。

用户服务和邮件推送服务启动的时候,通过nacos拉取配置,如

新建三个Nest服务

bash 复制代码
nest new gateway
cd gateway
nest g app user-service
nest g app email-service

Nacos

配置中心

新增lib:remote-config
nest g lib remote-config

根目录新增.env

service.ts

ts 复制代码
import {
  Inject,
  Injectable,
  Logger,
  OnModuleDestroy,
  OnModuleInit,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { NacosConfigClient } from 'nacos';

@Injectable()
export class RemoteConfigService implements OnModuleInit, OnModuleDestroy {
  private readonly logger = new Logger(RemoteConfigService.name);
  private client: NacosConfigClient;

  @Inject()
  private configService: ConfigService;

  constructor() {}

  async onModuleInit() {
    try {
      this.client = new NacosConfigClient({
        serverAddr: this.configService.get<string>('NACOS_SERVER'),
        namespace: this.configService.get<string>('NACOS_NAMESPACE'),
        username: this.configService.get<string>('NACOS_SECRET_NAME'),
        password: this.configService.get<string>('NACOS_SECRET_PWD'),
      });

      this.logger.log('Nacos配置客户端初始化成功');
    } catch (error) {
      this.logger.error('Nacos配置客户端初始化失败', error);
    }
  }

  async onModuleDestroy() {
    await this.client.close();
  }
  async getConfig(
    dataId: string,
    group = this.configService.get('NACOS_GROUP_NAME'),
  ) {
    if (this.configService.get(dataId)) {
      return await this.configService.get(dataId);
    }
    this.watchConfig(dataId, group);
    const config = this.parseConfig(
      await this.client.getConfig(dataId, group),
      'json',
    );
    return config;
  }

  /**
   * 监听配置变化
   */
  watchConfig(
    dataId: string,
    group = this.configService.get('NACOS_GROUP_NAME'),
  ) {
    this.client.subscribe(
      {
        dataId,
        group,
      },
      (content, configType = 'json') => {
        const config = this.parseConfig(content, configType);
        this.configService.set(dataId, config);
      },
    );
  }

  /**
   * 解析配置内容
   */
  private parseConfig(content: string, type: string): any {
    try {
      if (type === 'json') {
        return JSON.parse(content);
      } else if (type === 'yaml' || type === 'yml') {
        // 简单的YAML解析,实际项目中可以使用js-yaml等库
        const config = {};
        content.split('\n').forEach((line) => {
          const parts = line.split(':').map((part) => part.trim());
          if (parts.length >= 2) {
            config[parts[0]] = parts.slice(1).join(':');
          }
        });
        return config;
      } else if (type === 'properties') {
        const config = {};
        content.split('\n').forEach((line) => {
          const parts = line.split('=').map((part) => part.trim());
          if (parts.length >= 2) {
            config[parts[0]] = parts.slice(1).join('=');
          }
        });
        return config;
      }
      return content;
    } catch (error) {
      this.logger.error('配置解析失败', error);
      return content;
    }
  }
}

module.ts

ts 复制代码
import { Module } from '@nestjs/common';
import { RemoteConfigService } from './remote-config.service';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: ['.env'],
    }),
  ],
  providers: [RemoteConfigService],
  exports: [RemoteConfigService],
})
export class RemoteConfigModule {}
尝试

在nacos web管理页面 新增配置

服务注册

nest g lib nacos

新增一个ip.service.ts

ts 复制代码
// src/utils/ip.service.ts
import { Injectable } from '@nestjs/common';
import * as os from 'os';

@Injectable()
export class IpService {
  /**
   * 获取本机非内部 IPv4 地址(优先返回第一个有效地址)
   * @returns 本机 IP 地址(如 192.168.1.100),无有效地址时返回 127.0.0.1
   */
  getLocalIp(): string {
    const interfaces = os.networkInterfaces(); // 获取所有网络接口
    let localIp = '127.0.0.1'; // 默认回环地址

    // 遍历所有网络接口
    for (const devName in interfaces) {
      const iface = interfaces[devName];
      if (!iface) continue;

      // 遍历接口下的所有地址
      for (const alias of iface) {
        // 筛选条件:IPv4、非内部地址(非 127.0.0.1 等回环地址)、已启动
        if (
          alias.family === 'IPv4' && // 只取 IPv4
          !alias.internal && // 排除内部地址(如 127.0.0.1)
          alias.address !== '127.0.0.1' && // 进一步排除回环地址
          !alias.address.startsWith('169.254.') // 排除 APIPA 地址(本地链路地址)
        ) {
          localIp = alias.address;
          return localIp; // 找到第一个有效地址后返回
        }
      }
    }

    return localIp; // 未找到有效地址时返回回环地址
  }
}

nacos.module.ts

动态模块,传入参数

ts 复制代码
import { DynamicModule, Module } from '@nestjs/common';
import { NacosOptions, NacosService } from './nacos.service';
import { IpService } from './ip.service';

@Module({})
export class NacosModule {
  static forRoot(options: NacosOptions): DynamicModule {
    return {
      module: NacosModule,
      providers: [
        IpService,
        {
          provide: 'CONFIG_OPTIONS',
          useValue: options,
        },
        NacosService,
      ],
      exports: [NacosService],
    };
  }
}

nacos.service.ts

ts 复制代码
import {
  Inject,
  Injectable,
  Logger,
  OnModuleDestroy,
  OnModuleInit,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { NacosNamingClient } from 'nacos';
import { IpService } from './ip.service';

interface Instance {
  instanceId: string;
  healthy: boolean;
  enabled: boolean;
  serviceName?: string;
  weight?: number;
  ephemeral?: boolean;
  clusterName?: string;
}

export interface NacosOptions {
  noRegister?: boolean;
  serviceName: string; //Service name
  instance?: Partial<Instance>; //Instance
  groupName?: string;
  subList?: { groupName?: string; dataId: string }[];
}

@Injectable()
export class NacosService implements OnModuleInit, OnModuleDestroy {
  @Inject()
  private configService: ConfigService;

  @Inject(IpService)
  private ipService: IpService;

  @Inject('CONFIG_OPTIONS')
  private options: NacosOptions;

  private readonly logger = new Logger(NacosService.name);
  private client: NacosNamingClient;
  async onModuleInit() {
    this.client = new NacosNamingClient({
      logger: {
        ...console,
        log: () => {},
        debug: () => {},
      },
      serverList: this.configService.get<string>('NACOS_SERVER'), // replace to real nacos serverList
      namespace: this.configService.get<string>('NACOS_NAMESPACE'),
      username: this.configService.get<string>('NACOS_SECRET_NAME'),
      password: this.configService.get<string>('NACOS_SECRET_PWD'),
    });
    await this.client.ready();
    if (!this.options.noRegister) {
      await this.destroy();
      await this.register();
    }
    this.logger.log('Nacos客户端准备就绪');
  }

  async getSub(serviceName: string) {
    const service = {
      dataId: serviceName,
      groupName: this.configService.get('NACOS_GROUP_NAME'),
    };
    const res = await this.client.getAllInstances(
      service.dataId,
      service.groupName,
    );
    this.configService.set(`nacos_${serviceName}`, res);
    return res.filter((item) => item.healthy);
  }

  async register() {
    await this.client.registerInstance(
      this.options.serviceName,
      // @ts-ignore
      {
        ...this.options.instance,
        ip: this.ipService.getLocalIp(),
        port: this.configService.get<number>('PORT'),
      },
      this.options.groupName,
    );
  }
  async destroy() {
    await this.client.deregisterInstance(
      this.options.serviceName,
      // @ts-ignore
      {
        ...this.options.instance,
        ip: this.ipService.getLocalIp(),
        port: this.configService.get<number>('PORT'),
      },
      this.options.groupName,
    );
  }

  async onModuleDestroy() {}
}
尝试

在 user-service服务

启动服务

这个时候就可以在其他服务获取到user-service的请求地址,可以使用http请求来处理。

改造user-service (TCP)

将user-service改造成微服务,使用TCP

ts 复制代码
import { NestFactory } from '@nestjs/core';
import { UserServiceModule } from './user-service.module';
import { Transport, MicroserviceOptions } from '@nestjs/microservices';

async function bootstrap() {
  const port = Number(process.env.PORT) || 3001;
  const app = await NestFactory.createMicroservice<MicroserviceOptions>(
    UserServiceModule,
    {
      transport: Transport.TCP,
      options: {
        port,
      },
    },
  );
  await app.listen();
}
bootstrap();

修改user-service.controller.ts

ts 复制代码
import { Controller, Get } from '@nestjs/common';
import { UserServiceService } from './user-service.service';
import { MessagePattern } from '@nestjs/microservices';

@Controller()
export class UserServiceController {
  constructor(private readonly userServiceService: UserServiceService) {}

  @MessagePattern('user')
  handleFindById() {
    return this.userServiceService.register();
  }
}

启动服务

这里因为是本地,需要修改下获取IP的逻辑

改造gateway

引入nacos

app.service.ts中 注册微服务

ts 复制代码
import { NacosService } from '@app/nacos';
import { Inject, Injectable, OnApplicationBootstrap } from '@nestjs/common';
import {
  ClientProxyFactory,
  Transport,
  ClientProxy,
} from '@nestjs/microservices';

@Injectable()
export class AppService implements OnApplicationBootstrap {
  private client: ClientProxy;
  @Inject(NacosService)
  private readonly nacosService: NacosService;

  async onApplicationBootstrap() {
    const res = await this.nacosService.getSub('user-service');
    const instance = res[0];

    this.client = ClientProxyFactory.create({
      transport: Transport.TCP,
      options: {
        host: instance.ip,
        port: instance.port,
      },
    });
    await this.client.connect();
  }

  getHello(): string {
    return 'Hello World!';
  }
  async getInstance() {
    const res = await this.client.send('user', '');
    return !res;
  }
}

启动服务

改造email-service(Kafka)

service.ts

这里打印接收的数据

ts 复制代码
import { Controller, Get, Logger } from '@nestjs/common';
import { EmailServiceService } from './email-service.service';
import { MessagePattern, Payload } from '@nestjs/microservices';

@Controller()
export class EmailServiceController {
  constructor(private readonly emailServiceService: EmailServiceService) {}

  @MessagePattern('email')
  getHello(@Payload() data) {
    Logger.log('email-service', data);
  }
}

对于 main.ts

需要先创建一个临时configApp,用来获取kafka-config

ts 复制代码
import { NestFactory } from '@nestjs/core';
import { EmailServiceModule } from './email-service.module';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { RemoteConfigModule, RemoteConfigService } from '@app/remote-config';

async function bootstrap() {
  const configApp = await NestFactory.create(RemoteConfigModule);
  await configApp.init();
  const configService = configApp.get<RemoteConfigService>(RemoteConfigService);
  const res = await configService.getConfig('kafka-config');

  const app = await NestFactory.createMicroservice<MicroserviceOptions>(
    EmailServiceModule,
    {
      transport: Transport.KAFKA,
      options: {
        client: {
          clientId: 'email-service',
          brokers: res.brokers,
        },
        consumer: {
          groupId: 'email-service',
        },
      },
    },
  );
  await app.listen();
  await configApp.close();
}
bootstrap();

在gateway使用
app.servite.ts

ts 复制代码
import { NacosService } from '@app/nacos';
import { RemoteConfigService } from '@app/remote-config';
import {
  Inject,
  Injectable,
  Logger,
  OnApplicationBootstrap,
  OnModuleDestroy,
} from '@nestjs/common';
import {
  ClientProxyFactory,
  Transport,
  ClientProxy,
  ClientKafka,
} from '@nestjs/microservices';

@Injectable()
export class AppService implements OnApplicationBootstrap, OnModuleDestroy {
  private client: ClientProxy;
  private kafkaClient: ClientKafka;
  @Inject(NacosService)
  private readonly nacosService: NacosService;
  @Inject(RemoteConfigService)
  private readonly configService: RemoteConfigService;

  async onApplicationBootstrap() {
    const res = await this.nacosService.getSub('user-service');
    const instance = res[0];

    this.client = ClientProxyFactory.create({
      transport: Transport.TCP,
      options: {
        host: instance.ip,
        port: instance.port,
      },
    });
    await this.client.connect();
    Logger.log(`user-service 连接成功`);
    const config = await this.configService.getConfig('kafka-config');
    this.kafkaClient = new ClientKafka({
      client: {
        brokers: config.brokers,
      },
      producer: {
        allowAutoTopicCreation: true,
      },
    });

    await this.kafkaClient.connect();
    Logger.log(`email-service 连接成功`);
  }
  async onModuleDestroy() {
    await this.kafkaClient.close(); // 自动断开连接(避免资源泄漏)
  }

  getHello(): string {
    return 'Hello World!';
  }
  async getInstance() {
    const res = await this.client.send('user', '');
    return !res;
  }
  sendEmail() {
    this.kafkaClient.emit('email', {
      email: '<EMAIL>',
      content: '测试发送邮件',
    });
  }
}

测试一下

如果需要返回邮件的发送状态。如返回true

如果直接这样修改。

会报错。

send的话,接收reply,需要先订阅下。

修改app.service.ts

在kafkaClient连接之前。

本地测试email-service开启多个实例:

当关掉一个后,接下来的请求会发送到另外一个上面

同样的也可以给email-service加上Nacos

因为本地启动多个相同服务的原因,临时给nacos加了个随机数,

可以判断是否有健康的实例来做进一步处理。

其他

这是一个练习。

其他的,比如,服务下线后,nacos是能检测到健康状态的,那么就要通过配置或者服务变化(nacos有订阅),来通知使用者如gateway。

部署

如通过docker打包,注入环境变量port等等,应用会通过nacos注册,网关使用的时候去拿。

这样就实现了服务的注册和发现?

相关推荐
devlei14 分钟前
从源码泄露看AI Agent未来:深度对比Claude Code原生实现与OpenClaw开源方案
android·前端·后端
努力的小郑2 小时前
Canal 不难,难的是用好:从接入到治理
后端·mysql·性能优化
Victor3563 小时前
MongoDB(87)如何使用GridFS?
后端
Victor3563 小时前
MongoDB(88)如何进行数据迁移?
后端
小红的布丁3 小时前
单线程 Redis 的高性能之道
redis·后端
GetcharZp3 小时前
Go 语言只能写后端?这款 2D 游戏引擎刷新你的认知!
后端
宁瑶琴4 小时前
COBOL语言的云计算
开发语言·后端·golang
普通网友5 小时前
阿里云国际版服务器,真的是学生党的性价比之选吗?
后端·python·阿里云·flask·云计算
IT_陈寒6 小时前
Vue的这个响应式问题,坑了我整整两小时
前端·人工智能·后端
Soofjan6 小时前
Go 内存回收-GC 源码1-触发与阶段
后端