系列文章1: juejin.cn/post/752267…
使用consul进行服务注册发现
consul初始化
在根目录创建 consul 相关的文件夹,在 /consul/config
文件夹中增加配置文件 consul.hcl
datacenter = "dc1"
data_dir = "/consul/data"
log_level = "INFO"
server = true
bootstrap_expect = 1
ui = true
# Enable the Consul UI
connect {
enabled = true
}
acl {
enabled = true
default_policy = "deny"
down_policy = "extend-cache"
tokens {
master = "123456" # 注意这里,这里是登录consul配置的密token
}
}
然后在 consul 目录下增加 docker-compose.yml
文件
services:
consul:
image: hashicorp/consul
container_name: consul
ports:
- 8500:8500
- 8600:8600/udp
volumes:
- ./data:/consul/data
- ./config:/consul/config
command:
[
'agent',
'-server',
'-ui',
'-client=0.0.0.0',
'-bootstrap-expect=1',
'-data-dir=/consul/data',
'-config-dir=/consul/config',
]
environment:
- CONSUL_BIND_INTERFACE=eth0
在 consul 文件夹下,执行 docker 命令启动 consul 服务
docker-compose up -d
启动完成之后,服务器访问 127.0.0.1:8500
出现如下界面则启动成功
选择登录,输入上面配置的 token,这里配置的是 123456
因为 consul
这个 npm 包已经不在更新了,而且我没有找到更好的 npm 包,而 consul 官方提供了对应的 API,所以我们自己将 consul 封装成 lib 模块。
consul 模块封装
nest g lib consul
libs/
├── consul/
│ ├── interfaces
│ │ ├── consul-module.interface.ts
│ │ ├── consul-service.interface.ts
│ ├── consul.module.ts
│ ├── consul.module.ts
│ ├── consul.service.ts
│ ├── consul.constants.ts
│ └── index.ts
定义 interface
consul-service.interface.ts
export interface ServiceRegisterOptions {
ID: string; // 服务唯一标识
Name: string; // 服务名称
Address: string; // host/ip 服务地址
Port: number; // 服务端口
TAgs?: string[];
/**
* 健康检查配置,可自行扩展更多字段
* 这里只演示 HTTP Check
*/
Check?: {
path: string; // '/health'
interval: string; // '10s'
};
}
export interface NodeService {
Node: {
ID: string;
Node: string;
Address: string;
Datacenter: string;
TaggedAddresses: string | null;
Meta: Record<string, any>;
};
Service: {
ID: string;
Service: string;
Tags: string[];
Address: string;
TaggedAddresses: Record<string, any>;
Port: number;
};
}
export interface ResolveAddress {
address: string;
port: number;
}
consul-module.interface.ts
import { ServiceRegisterOptions } from './consul-service.interface';
export interface ConsulModuleOptions {
/** Consul 服务器地址(含协议和端口)例如 'http://127.0.0.1:8500' */
consulHost: string;
/** consul登录token */
token?: string;
/** 当前服务注册信息 */
service?: ServiceRegisterOptions;
}
consul.constants.ts
export const CONSUL_OPTIONS = 'CONSUL_OPTIONS';
consul.module.ts
在 consul.module.ts
文件中,实现同步注册方法和异步注册方法
import { DynamicModule, Global, Module, Provider } from '@nestjs/common';
import { CONSUL_OPTIONS } from './consul.constants';
import { ConsulService } from './consul.service';
import { ConsulModuleOptions } from './interfaces/consul-module.interface';
@Global()
@Module({})
export class ConsulModule {
/** 同步注册 */
static forRoot(options: ConsulModuleOptions): DynamicModule {
return {
module: ConsulModule,
providers: [
{ provide: CONSUL_OPTIONS, useValue: options },
ConsulService,
],
exports: [ConsulService],
};
}
/** 异步注册(从 ConfigService、ENV 等拿配置) */
static forRootAsync(
optionsFactory: () => Promise<ConsulModuleOptions> | ConsulModuleOptions,
): DynamicModule {
const asyncOptionsProvider: Provider = {
provide: CONSUL_OPTIONS,
useFactory: optionsFactory,
};
return {
module: ConsulModule,
providers: [asyncOptionsProvider, ConsulService],
exports: [ConsulService],
};
}
}
consul.service.ts
因为调用 consul 的 api,因此,需要安装 got
npm install got
然后修改 tsconfig.ts
的配置项 "module": "NodeNext"
,否则 got 会报错。
在 consul.service.ts
中,增加服务注册和发现的方法.
import {
Inject,
Injectable,
Logger,
OnModuleDestroy,
OnModuleInit,
} from '@nestjs/common';
import type { Got } from 'got';
import got from 'got';
import { CONSUL_OPTIONS } from './consul.constants';
import { ConsulModuleOptions } from './interfaces/consul-module.interface';
import {
NodeService,
ResolveAddress,
ServiceRegisterOptions,
} from './interfaces/consul-service.interface';
@Injectable()
export class ConsulService implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger('ConsulService');
private readonly request: Got;
private readonly defServiceRegisterOptions: ServiceRegisterOptions;
constructor(@Inject(CONSUL_OPTIONS) opts: ConsulModuleOptions) {
this.request = got.extend({
prefixUrl: `${opts.consulHost}/v1`,
responseType: 'json',
resolveBodyOnly: true,
headers: {
'X-Consul-Token': opts.token,
},
});
// ✅ 只有传了 service 配置才保存
if (opts.service) {
this.defServiceRegisterOptions = opts.service;
}
}
/** 🟢 模块启动时自动注册 */
async onModuleInit() {
// 因为在gateway中,不需要注册服务,只需要初始化consul就行
if (this.defServiceRegisterOptions) {
await this.registerService();
}
}
/** 🔴 应用关闭时自动注销 */
async onModuleDestroy() {
if (this.defServiceRegisterOptions) {
await this.deregisterService();
}
}
/**
* 注册服务
*/
async registerService(opts?: ServiceRegisterOptions) {
const registerPayload = { ...this.defServiceRegisterOptions, ...opts };
const serviceName = registerPayload.Name;
try {
await this.request.put('agent/service/register', {
json: registerPayload,
});
this.logger.log(`[Consul] Service [${serviceName}] registered successfully`);
} catch (error) {
this.logger.error(`[Consul] Error registering service ${serviceName}: ${error.message}`,);
}
return true;
}
/**
* 服务注销:注销注册到 Consul 的服务
* @param serviceId 服务唯一标识
*/
async deregisterService(serviceId?: string) {
serviceId = serviceId ? serviceId : this.defServiceRegisterOptions.ID;
try {
await this.request.put(`agent/service/deregister/${serviceId}`);
this.logger.log( `[Consul] Service [${serviceId}] deregistered successfully`,);
} catch (error) {
this.logger.error(`[Consul] Error deregistering service ${serviceId}: ${error.message}`,);
}
}
/**
* 服务发现:通过 Consul 查找健康服务
* @param serviceName 服务名称
*/
async resolveAddress(serviceName: string): Promise<ResolveAddress> {
const res = await this.request
.get(`health/service/${serviceName}?passing=true`)
.json<NodeService[]>();
if (!res.length) {
throw new Error(`No healthy instance found for ${serviceName}`);
}
const node = res[0].Service;
this.logger.log(`[Consul] Service [${serviceName}] get resolveAddress successfully. [${node.Address}:${node.Port}]`);
return { address: node.Address, port: node.Port };
}
}
服务注册
现在我们的 consul 模块就已经封装好了,只需要对应的微服务中进行调用就可以了。
分别修改 user-service
和 order-service
的 app.module.ts
文件,让服务在启动的时候就注册到 consul 中。
import { ConsulModule } from '@libs/consul';
import { Module } from '@nestjs/common';
import { UserModule } from './user/user.module';
@Module({
imports: [
ConsulModule.forRoot({
consulHost: 'http://127.0.0.1:8500',
token: '123456',
service: {
ID: 'user-service-id',
Name: 'user-service',
Address: '127.0.0.1',
Port: 3001,
},
}),
UserModule,
],
controllers: [],
providers: [],
})
export class AppModule {}
order-service 只需要替换对应的 service 信息就行。
服务发现
在 gateway 中,我们需要发现我们需要调用的 service,因此,我们需要在 gateway 的 src 目录下,创建 micro-clients.module.ts
import { ConsulModule, ConsulService } from '@libs/consul';
import { ResolveAddress } from '@libs/consul/interfaces/consul-service.interface';
import { DynamicModule, Module } from '@nestjs/common';
import { ClientsModule, GrpcOptions, Transport } from '@nestjs/microservices';
import { ClientsProviderAsyncOptions } from '@nestjs/microservices/module/interfaces/clients-module.interface';
import { join } from 'path';
import { USER_PACKAGE_NAME } from 'proto/generated/user.interface';
import { ORDER_PACKAGE_NAME } from '../../../proto/generated/order.interface';
/**
* 获取grpc服务连接
* @param packageName 服务包名
* @param serviceName 服务名称
* @param protoFile 服务所调用的proto文件名称
*/
function createGrpcClient(packageName: string, serviceName: string, protoFile: string): ClientsProviderAsyncOptions {
return {
name: packageName,
inject: [ConsulService],
useFactory: async (consul: ConsulService): Promise<GrpcOptions> => {
// 通过 consul 的服务发现方法获取对应服务的address和port
const svc: ResolveAddress = await consul.resolveAddress(serviceName);
if (!svc) throw new Error(`${serviceName} 不可用`);
return {
transport: Transport.GRPC,
options: {
url: `${svc.address}:${svc.port}`,
package: packageName,
protoPath: join(process.cwd(), 'proto', protoFile),
},
};
},
};
}
@Module({})
export class MicroClientsModule {
static register(): DynamicModule {
return {
module: MicroClientsModule,
global: true, // 标记为全局包
imports: [
ConsulModule,
// 通过 ClientsModule 对需要调用的服务进行注册
ClientsModule.registerAsync([
createGrpcClient(USER_PACKAGE_NAME, 'user-service', 'user.proto'),
createGrpcClient(ORDER_PACKAGE_NAME, 'order-service', 'order.proto'),
]),
],
exports: [ClientsModule],
};
}
}
然后,我们在 app.module.ts
中,进行服务注册,让服务启动的时候,就对我们所有依赖的服务进行发现。
import { ConsulModule } from '@libs/consul';
import { Module } from '@nestjs/common';
import { MicroClientsModule } from './micro-clients.module';
import { UserModule } from './user/user.module';
@Module({
imports: [
// 初始化consul
ConsulModule.forRoot({
consulHost: 'http://127.0.0.1:8500',
token: '123456',
}),
// 从consul中获取注册的微服务
MicroClientsModule.register(),
UserModule,
],
controllers: [],
providers: [],
})
export class AppModule {}
然后,我们需要删掉之前在 user.module.ts
中ClientsModule.register
相关的内容。变成:
import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';
@Module({
imports: [],
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}
然后先启动 user-service 和 order-service,查看 consul 的服务,这里发现已经注册成功。
最后启动 gateway,调用接口就 OK 了。