Skip to content

健康检查(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,我们需要安装所需依赖。

bash
$ npm install --save @nestjs/terminus

设置健康检查

一个健康检查代表多个健康指标的汇总。健康指标会检查某个服务当前是健康还是不健康。只有当所有分配给该健康检查的健康指标都正常时,这个健康检查才算通过。由于很多应用都需要类似的健康指标,@nestjs/terminus 提供了一组预定义指标,例如:

  • HttpHealthIndicator
  • TypeOrmHealthIndicator
  • MongooseHealthIndicator
  • SequelizeHealthIndicator
  • MikroOrmHealthIndicator
  • PrismaHealthIndicator
  • MicroserviceHealthIndicator
  • GRPCHealthIndicator
  • MemoryHealthIndicator
  • DiskHealthIndicator

要开始编写第一个健康检查,先创建 HealthModule,并在其 imports 数组中导入 TerminusModule

提示

如果你想使用 Nest CLI 来创建模块,只需执行 $ nest g module health 命令。

typescript
// health.module.ts
import { Module } from '@nestjs/common';
import { TerminusModule } from '@nestjs/terminus';

@Module({
  imports: [TerminusModule]
})
export class HealthModule {}

我们的健康检查可以通过一个 controller 来执行,而它同样可以借助 Nest CLI 很方便地创建。

bash
$ nest g controller health

提示

强烈建议你在应用中启用 shutdown hooks。若已启用,Terminus 集成会利用这一生命周期事件。关于 shutdown hooks 的更多内容请见这里

HTTP 健康检查

当我们安装了 @nestjs/terminus、导入了 TerminusModule 并创建了新 controller 后,就可以创建健康检查了。

HTTPHealthIndicator 需要 @nestjs/axios 包,因此请确保已安装:

bash
$ npm i --save @nestjs/axios axios

现在我们可以这样设置 HealthController

typescript
// 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'),
    ]);
  }
}
typescript
// 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 状态码。

json
{
  "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)。

typescript
// 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

typescript
// 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 结果:

json
{
  "status": "ok",
  "info": {
    "database": {
      "status": "up"
    }
  },
  "error": {},
  "details": {
    "database": {
      "status": "up"
    }
  }
}

如果你的应用使用了多个数据库,那么你需要把每个连接都注入到 HealthController 中。随后,只需将连接引用传给 TypeOrmHealthIndicator 即可。

typescript
// 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%,就会返回不健康的健康检查结果。

typescript
// 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,就会被判定为不健康。

typescript
// 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
  • 程序终止
typescript
// 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 中实际分配了多少内存。 它不包括已经被交换出的内存。 它会包括共享库占用的内存,只要这些库的页面当前实际驻留在内存中。 它也包括全部栈内存和堆内存。

typescript
// 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' 状态;否则就应该抛出错误。

typescript
// 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。

typescript
// 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,把这个已经可用的健康指标加入健康检查端点。

typescript
// 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,请参见这里

typescript
// 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()

typescript
// health.module.ts
@Module({
imports: [
  TerminusModule.forRoot({
    logger: TerminusLogger,
  }),
],
})
export class HealthModule {}

如果你想彻底屏蔽 Terminus 的任何日志输出,包括错误日志,可以像下面这样配置:

typescript
// health.module.ts
@Module({
imports: [
  TerminusModule.forRoot({
    logger: false,
  }),
],
})
export class HealthModule {}

Terminus 还允许你配置健康检查错误在日志中的展示样式。

错误日志样式描述示例
json(默认)发生错误时,将健康检查结果摘要以 JSON 对象形式输出
pretty发生错误时,将健康检查结果摘要以格式化盒状输出,并高亮成功/失败项

你可以通过 errorLogStyle 配置项来修改日志样式,如下所示:

typescript
// health.module.ts
@Module({
  imports: [
    TerminusModule.forRoot({
      errorLogStyle: 'pretty',
    }),
  ]
})
export class HealthModule {}

优雅关闭超时

如果你的应用在关闭时需要延迟一段时间,Terminus 也可以帮你处理。 当你与 Kubernetes 这类编排系统配合工作时,这个配置尤其有用。 通过把这个延迟设置得略长于 readiness 检查间隔,你可以在容器关闭时实现零停机。

typescript
// health.module.ts
@Module({
  imports: [
    TerminusModule.forRoot({
      gracefulShutdownTimeoutMs: 1000,
    }),
  ]
})
export class HealthModule {}

更多示例

更多可运行示例见这里

基于 NestJS 官方文档翻译