健康检查(Terminus)
Terminus 集成为你提供 readiness/liveness 健康检查。对于复杂的后端架构来说,健康检查至关重要。 简而言之,在 Web 开发领域里,健康检查通常表现为一个特殊地址,例如 https://my-website.com/health/readiness。 某个服务或基础设施组件(例如 Kubernetes)会持续检查这个地址。根据对此地址发起 GET 请求所返回的 HTTP 状态码,该服务会在收到“不健康”响应时采取行动。 由于“健康”与“不健康”的定义会因你提供的服务类型不同而有所差异,因此 Terminus 集成为你提供了一组 health indicators(健康指标)。
例如,如果你的 Web 服务器使用 MongoDB 存储数据,那么 MongoDB 是否仍然正常运行就是一条非常关键的信息。 这时你就可以使用 MongooseHealthIndicator。只要配置正确,稍后会讲,你的健康检查地址就会依据 MongoDB 是否可用而返回健康或不健康的 HTTP 状态码。
开始使用
要开始使用 @nestjs/terminus,我们需要安装所需依赖。
$ npm install --save @nestjs/terminus设置健康检查
一个健康检查代表多个健康指标的汇总。健康指标会检查某个服务当前是健康还是不健康。只有当所有分配给该健康检查的健康指标都正常时,这个健康检查才算通过。由于很多应用都需要类似的健康指标,@nestjs/terminus 提供了一组预定义指标,例如:
HttpHealthIndicatorTypeOrmHealthIndicatorMongooseHealthIndicatorSequelizeHealthIndicatorMikroOrmHealthIndicatorPrismaHealthIndicatorMicroserviceHealthIndicatorGRPCHealthIndicatorMemoryHealthIndicatorDiskHealthIndicator
要开始编写第一个健康检查,先创建 HealthModule,并在其 imports 数组中导入 TerminusModule。
提示
如果你想使用 Nest CLI 来创建模块,只需执行 $ nest g module health 命令。
// health.module.ts
import { Module } from '@nestjs/common';
import { TerminusModule } from '@nestjs/terminus';
@Module({
imports: [TerminusModule]
})
export class HealthModule {}我们的健康检查可以通过一个 controller 来执行,而它同样可以借助 Nest CLI 很方便地创建。
$ nest g controller health提示
强烈建议你在应用中启用 shutdown hooks。若已启用,Terminus 集成会利用这一生命周期事件。关于 shutdown hooks 的更多内容请见这里。
HTTP 健康检查
当我们安装了 @nestjs/terminus、导入了 TerminusModule 并创建了新 controller 后,就可以创建健康检查了。
HTTPHealthIndicator 需要 @nestjs/axios 包,因此请确保已安装:
$ npm i --save @nestjs/axios axios现在我们可以这样设置 HealthController:
// health.controller.ts
import { Controller, Get } from '@nestjs/common';
import { HealthCheckService, HttpHealthIndicator, HealthCheck } from '@nestjs/terminus';
@Controller('health')
export class HealthController {
constructor(
private health: HealthCheckService,
private http: HttpHealthIndicator,
) {}
@Get()
@HealthCheck()
check() {
return this.health.check([
() => this.http.pingCheck('app-root', 'http://localhost:3000'),
]);
}
}// health.module.ts
import { Module } from '@nestjs/common';
import { TerminusModule } from '@nestjs/terminus';
import { HttpModule } from '@nestjs/axios';
import { HealthController } from './health.controller';
@Module({
imports: [TerminusModule, HttpModule],
controllers: [HealthController],
})
export class HealthModule {}现在,这个健康检查会向 http://localhost:3000 发送一个 GET 请求。 如果该地址返回健康响应,那么访问 http://localhost:3000/health 时会返回如下对象,并带有 200 状态码。
{
"status": "ok",
"info": {
"app-root": {
"status": "up"
}
},
"error": {},
"details": {
"nestjs-docs": {
"status": "up"
}
}
}这个响应对象的接口可以通过 @nestjs/terminus 包中的 HealthCheckResult 接口获取。
| 说明 | 类型 | |
|---|---|---|
status | 若任一健康指标失败,状态为 'error'。若 NestJS 应用正在关闭但仍接受 HTTP 请求,状态为 'shutting_down'。 | 'error' | 'ok' | 'shutting_down' |
info | 包含所有状态为 'up' 的健康指标信息,也就是“健康”的指标。 | object |
error | 包含所有状态为 'down' 的健康指标信息,也就是“不健康”的指标。 | object |
details | 包含所有健康指标的完整信息。 | object |
检查特定 HTTP 响应码
在某些场景下,你可能希望检查更具体的条件并验证响应内容。比如假设 https://my-external-service.com 返回状态码 204。这时可以使用 HttpHealthIndicator.responseCheck 专门检查该响应码,并将其他所有状态码都视为不健康。
如果返回的状态码不是 204,那么下面的示例就会判定为不健康。第三个参数要求你提供一个函数(同步或异步均可),它返回一个布尔值,表示该响应是否应被视为健康(true)或不健康(false)。
// health.controller.ts
// Within the `HealthController`-class
@Get()
@HealthCheck()
check() {
return this.health.check([
() =>
this.http.responseCheck(
'my-external-service',
'https://my-external-service.com',
(res) => res.status === 204,
),
]);
}TypeOrm 健康指标
Terminus 支持把数据库检查加入健康检查中。要开始使用这个健康指标,请先查看数据库章节,并确保应用中的数据库连接已经建立。
提示
在底层,TypeOrmHealthIndicator 实际上只是执行了一条 SELECT 1 SQL 命令,这是一种常见的数据库存活检查方式。如果你使用的是 Oracle 数据库,它则会执行 SELECT 1 FROM DUAL。
// health.controller.ts
@Controller('health')
export class HealthController {
constructor(
private health: HealthCheckService,
private db: TypeOrmHealthIndicator,
) {}
@Get()
@HealthCheck()
check() {
return this.health.check([
() => this.db.pingCheck('database'),
]);
}
}如果你的数据库可达,那么向 http://localhost:3000/health 发起 GET 请求后,应该会看到如下 JSON 结果:
{
"status": "ok",
"info": {
"database": {
"status": "up"
}
},
"error": {},
"details": {
"database": {
"status": "up"
}
}
}如果你的应用使用了多个数据库,那么你需要把每个连接都注入到 HealthController 中。随后,只需将连接引用传给 TypeOrmHealthIndicator 即可。
// health.controller.ts
@Controller('health')
export class HealthController {
constructor(
private health: HealthCheckService,
private db: TypeOrmHealthIndicator,
@InjectConnection('albumsConnection')
private albumsConnection: Connection,
@InjectConnection()
private defaultConnection: Connection,
) {}
@Get()
@HealthCheck()
check() {
return this.health.check([
() => this.db.pingCheck('albums-database', { connection: this.albumsConnection }),
() => this.db.pingCheck('database', { connection: this.defaultConnection }),
]);
}
}磁盘健康指标
使用 DiskHealthIndicator 可以检查当前存储空间的使用情况。首先,将 DiskHealthIndicator 注入到 HealthController 中。下面的示例会检查路径 / 的磁盘使用情况(在 Windows 上可以使用 C:\\)。 如果该路径占用超过总存储空间的 50%,就会返回不健康的健康检查结果。
// health.controller.ts
@Controller('health')
export class HealthController {
constructor(
private readonly health: HealthCheckService,
private readonly disk: DiskHealthIndicator,
) {}
@Get()
@HealthCheck()
check() {
return this.health.check([
() => this.disk.checkStorage('storage', { path: '/', thresholdPercent: 0.5 }),
]);
}
}通过 DiskHealthIndicator.checkStorage,你也可以按固定空间大小进行检查。 下面的示例中,如果路径 /my-app/ 超过 250GB,就会被判定为不健康。
// health.controller.ts
// Within the `HealthController`-class
@Get()
@HealthCheck()
check() {
return this.health.check([
() => this.disk.checkStorage('storage', { path: '/', threshold: 250 * 1024 * 1024 * 1024, })
]);
}内存健康指标
为了确保你的进程不会超出某个内存上限,可以使用 MemoryHealthIndicator。 下面的示例用于检查进程的堆内存。
提示
Heap(堆)是动态分配内存所在的区域(即通过 malloc 分配的内存)。从堆中分配的内存会一直保留,直到发生以下情况之一:
- 该内存被 free
- 程序终止
// health.controller.ts
@Controller('health')
export class HealthController {
constructor(
private health: HealthCheckService,
private memory: MemoryHealthIndicator,
) {}
@Get()
@HealthCheck()
check() {
return this.health.check([
() => this.memory.checkHeap('memory_heap', 150 * 1024 * 1024),
]);
}
}你也可以通过 MemoryHealthIndicator.checkRSS 检查进程的 RSS 内存。下面这个例子中,如果进程占用内存超过 150MB,就会返回不健康状态码。
提示
RSS(Resident Set Size,常驻集大小)用于表示进程当前在 RAM 中实际分配了多少内存。 它不包括已经被交换出的内存。 它会包括共享库占用的内存,只要这些库的页面当前实际驻留在内存中。 它也包括全部栈内存和堆内存。
// health.controller.ts
// Within the `HealthController`-class
@Get()
@HealthCheck()
check() {
return this.health.check([
() => this.memory.checkRSS('memory_rss', 150 * 1024 * 1024),
]);
}自定义健康指标
在某些场景下,@nestjs/terminus 提供的预定义健康指标无法覆盖你的全部需求。这时你就可以根据自己的需要创建一个自定义健康指标。
我们先创建一个 service,作为自定义指标。为了便于理解健康指标的结构,我们用一个 DogHealthIndicator 举例。如果每个 Dog 对象的 type 都是 'goodboy',这个 service 就应当返回 'up' 状态;否则就应该抛出错误。
// dog.health.ts
import { Injectable } from '@nestjs/common';
import { HealthIndicatorService } from '@nestjs/terminus';
export interface Dog {
name: string;
type: string;
}
@Injectable()
export class DogHealthIndicator {
constructor(
private readonly healthIndicatorService: HealthIndicatorService
) {}
private dogs: Dog[] = [
{ name: 'Fido', type: 'goodboy' },
{ name: 'Rex', type: 'badboy' },
];
async isHealthy(key: string){
const indicator = this.healthIndicatorService.check(key);
const badboys = this.dogs.filter(dog => dog.type === 'badboy');
const isHealthy = badboys.length === 0;
if (!isHealthy) {
return indicator.down({ badboys: badboys.length });
}
return indicator.up();
}
}下一步,我们需要把这个健康指标注册为 provider。
// health.module.ts
import { Module } from '@nestjs/common';
import { TerminusModule } from '@nestjs/terminus';
import { DogHealthIndicator } from './dog.health';
@Module({
controllers: [HealthController],
imports: [TerminusModule],
providers: [DogHealthIndicator]
})
export class HealthModule { }提示
在真实项目中,DogHealthIndicator 应该放在单独模块中提供,例如 DogModule,然后由 HealthModule 导入。
最后一步,就是回到 HealthController,把这个已经可用的健康指标加入健康检查端点。
// health.controller.ts
import { HealthCheckService, HealthCheck } from '@nestjs/terminus';
import { Injectable, Dependencies, Get } from '@nestjs/common';
import { DogHealthIndicator } from './dog.health';
@Injectable()
export class HealthController {
constructor(
private health: HealthCheckService,
private dogHealthIndicator: DogHealthIndicator
) {}
@Get()
@HealthCheck()
healthCheck() {
return this.health.check([
() => this.dogHealthIndicator.isHealthy('dog'),
])
}
}日志
Terminus 默认只会记录错误信息,例如健康检查失败时。通过 TerminusModule.forRoot() 方法,你可以更细粒度地控制错误日志如何记录,甚至可以完全接管日志行为。
本节中,我们将演示如何创建一个自定义 logger:TerminusLogger。它继承自内建 logger,因此你可以按需覆盖其中任何部分。
提示
如果你想进一步了解 NestJS 中的自定义 logger,请参见这里。
// terminus-logger.service.ts
import { Injectable, Scope, ConsoleLogger } from '@nestjs/common';
@Injectable({ scope: Scope.TRANSIENT })
export class TerminusLogger extends ConsoleLogger {
error(message: any, stack?: string, context?: string): void;
error(message: any, ...optionalParams: any[]): void;
error(
message: unknown,
stack?: unknown,
context?: unknown,
...rest: unknown[]
): void {
// Overwrite here how error messages should be logged
}
}创建好自定义 logger 后,只需像下面这样把它传给 TerminusModule.forRoot():
// health.module.ts
@Module({
imports: [
TerminusModule.forRoot({
logger: TerminusLogger,
}),
],
})
export class HealthModule {}如果你想彻底屏蔽 Terminus 的任何日志输出,包括错误日志,可以像下面这样配置:
// health.module.ts
@Module({
imports: [
TerminusModule.forRoot({
logger: false,
}),
],
})
export class HealthModule {}Terminus 还允许你配置健康检查错误在日志中的展示样式。
| 错误日志样式 | 描述 | 示例 |
|---|---|---|
json(默认) | 发生错误时,将健康检查结果摘要以 JSON 对象形式输出 | ![]() |
pretty | 发生错误时,将健康检查结果摘要以格式化盒状输出,并高亮成功/失败项 | ![]() |
你可以通过 errorLogStyle 配置项来修改日志样式,如下所示:
// health.module.ts
@Module({
imports: [
TerminusModule.forRoot({
errorLogStyle: 'pretty',
}),
]
})
export class HealthModule {}优雅关闭超时
如果你的应用在关闭时需要延迟一段时间,Terminus 也可以帮你处理。 当你与 Kubernetes 这类编排系统配合工作时,这个配置尤其有用。 通过把这个延迟设置得略长于 readiness 检查间隔,你可以在容器关闭时实现零停机。
// health.module.ts
@Module({
imports: [
TerminusModule.forRoot({
gracefulShutdownTimeoutMs: 1000,
}),
]
})
export class HealthModule {}更多示例
更多可运行示例见这里。

