健康检查(Terminus)

Terminus 集成提供了就绪性/存活状态健康检查功能。在复杂的后端架构中,健康检查至关重要。简而言之,在 Web 开发领域,健康检查通常由一个特殊地址组成,例如 https://my-website.com/health/readiness 。您的服务或基础设施组件(如 Kubernetes)会持续检查该地址。根据对该地址 GET 请求返回的 HTTP 状态码,当收到"不健康"响应时,服务将采取相应措施。由于"健康"或"不健康"的定义因服务类型而异,Terminus 集成通过一组健康指标为您提供支持。

例如,如果您的 Web 服务器使用 MongoDB 存储数据,了解 MongoDB 是否仍在运行将是关键信息。在这种情况下,您可以使用 MongooseHealthIndicator。如果配置正确(稍后会详细介绍),您的健康检查地址将根据 MongoDB 是否运行返回健康或不健康的 HTTP 状态码。

快速开始

要开始使用 @nestjs/terminus,我们需要先安装所需的依赖项。

$ npm install --save @nestjs/terminus

设置健康检查

健康检查是对健康指标的汇总。健康指标会执行对服务的检查,判断其处于健康或不健康状态。当所有分配的健康指标都正常运行时,健康检查结果为通过。由于许多应用程序需要类似的健康指标,@nestjs/terminus 提供了一组预定义的指标,例如:

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

要开始我们的第一个健康检查,让我们创建 HealthModule 模块,并在其 imports 数组中导入 TerminusModule

info 提示 要使用 Nest CLI 创建该模块,只需执行 $ nest g module health 命令。

@@filename(health.module)
import { Module } from '@nestjs/common';
import { TerminusModule } from '@nestjs/terminus';

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

我们的健康检查可以通过 控制器 来执行,使用 Nest CLI 可以轻松设置。

$ nest g controller health

info 信息 强烈建议在应用程序中启用关闭钩子。如果启用,Terminus 集成会利用此生命周期事件。了解更多关于关闭钩子的信息 请点击这里

HTTP 健康检查

安装完 @nestjs/terminus、导入 TerminusModule 并创建新控制器后,我们就可以开始创建健康检查了。

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

$ npm i --save @nestjs/axios axios

现在我们可以设置 HealthController 了:

@@filename(health.controller)
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('nestjs-docs', 'https://docs.nestjs.com'),
    ]);
  }
}
```typescript
@@filename(health.module)
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 {}

我们的健康检查将向 https://docs.nestjs.com 地址发送一个 GET 请求。如果从该地址获得健康响应,我们在 http://localhost:3000/health 的路由将返回以下对象,状态码为 200。

{
  "status": "ok",
  "info": {
    "nestjs-docs": {
      "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)。

@@filename(health.controller)
// 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

@@filename(health.controller)
@Controller('health')
export class HealthController {
  constructor(
    private health: HealthCheckService,
    private db: TypeOrmHealthIndicator,
  ) {}

  @Get()
  @HealthCheck()
  check() {
    return this.health.check([
      () => this.db.pingCheck('database'),
    ]);
  }
}

如果您的数据库可访问,现在通过 GET 请求访问 http://localhost:3000/health 时,应该会看到以下 JSON 结果:

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

如果您的应用使用多个数据库 ,需要将每个连接注入到 HealthController 中。然后就可以直接将连接引用传递给 TypeOrmHealthIndicator

@@filename(health.controller)
@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%,健康检查将返回不健康状态。

@@filename(health.controller)
@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,健康状态将变为不健康。

@@filename(health.controller)
// Within the `HealthController`-class

@Get()
@HealthCheck()
check() {
  return this.health.check([
    () => this.disk.checkStorage('storage', {  path: '/', threshold: 250 * 1024 * 1024 * 1024, })
  ]);
}

内存健康指标

为确保你的进程不超过特定内存限制,可以使用 MemoryHealthIndicator。以下示例可用于检查进程的堆内存使用情况。

info 提示 堆是内存中动态分配内存(即通过 malloc 分配的内存)所在的区域。从堆中分配的内存将保持分配状态,直到发生以下情况之一:

  • 内存被释放
  • 程序终止
@@filename(health.controller)
@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 的内存,此示例将返回一个异常响应码。

info 提示 RSS(常驻内存集)用于显示分配给该进程且驻留在 RAM 中的内存量。它不包括被交换出去的内存,但包含来自共享库的内存(只要这些库的页面实际存在于内存中),同时包含所有栈和堆内存。

@@filename(health.controller)
// Within the `HealthController`-class

@Get()
@HealthCheck()
check() {
  return this.health.check([
    () => this.memory.checkRSS('memory_rss', 150 * 1024 * 1024),
  ]);
}

自定义健康指标

在某些情况下,@nestjs/terminus 提供的预定义健康指标无法满足您的所有健康检查需求。此时,您可以根据需要设置自定义健康指标。

让我们从创建一个代表自定义指标的服务开始。为了基本了解指标的结构,我们将创建一个示例 DogHealthIndicator。当每个 Dog 对象的类型为 'goodboy' 时,该服务应处于 'up' 状态。如果条件不满足,则应抛出错误。

@@filename(dog.health)
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();
  }
}

接下来我们需要将健康指标注册为提供者。

@@filename(health.module)
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 { }

info 提示 在实际应用中,DogHealthIndicator 应该在一个单独的模块中提供,例如 DogModule,然后由 HealthModule 导入。

最后一步是将现已可用的健康指标添加到所需的健康检查端点。为此,我们回到 HealthController 并将其添加到我们的 check 函数中。

@@filename(health.controller)
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() 方法,您可以更好地控制错误记录方式,甚至完全接管日志记录本身。

在本节中,我们将指导您如何创建自定义日志记录器 TerminusLogger。该日志记录器扩展了内置日志功能,因此您可以选择性地覆盖日志记录器的特定部分

提示 如需了解更多关于 NestJS 中自定义日志记录器的信息, 请点击此处阅读更多内容

@@filename(terminus-logger.service)
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
  }
}

创建自定义日志记录器后,您只需将其传入 TerminusModule.forRoot() 即可,如下所示。

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

若要完全抑制来自 Terminus 的所有日志消息(包括错误消息),请按如下方式配置 Terminus。

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

Terminus 允许您配置健康检查错误在日志中的显示方式。

错误日志样式 描述 示例
json(默认) 在出现错误时以 JSON 对象形式打印健康检查结果摘要
pretty 在格式化框内打印健康检查结果摘要,并高亮显示成功/错误结果

您可以通过如下代码片段所示的 errorLogStyle 配置选项来更改日志样式

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

优雅停机超时时间

如果您的应用程序需要延迟关闭过程,Terminus 可以为您处理。这一设置在配合 Kubernetes 等编排器使用时尤为有益。通过将延迟时间设置为略长于就绪检查间隔,您可以在关闭容器时实现零停机。

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

更多示例

更多工作示例可在此查看。