NestJS 健康监测最佳实践

主要内容

  1. 为什么需要健康检测
  2. 健康指标的定性和定量结论
  3. API Ping的区别
  4. 健康检测的对象
  5. terminus关键对象和类
  6. 水平扩展(检测项)
  7. terminus优缺点
  8. 搭配Docker实现健康检测
  9. 搭配日志做问题搜集

@nestjs提供了强大高效的Web应用程序后端服务,并随着面向服务、微服务等(架构)技术兴起,越来越多的企业和组织引入此类技术。面向服务(或微服务)的开发虽然很大程度上在业务层面实现了解耦,缩小了项目规模以及版本发布的颗粒度。由于发布的应用(服务)越来越多,而有些服务由于使用频率较低,可能很长时间都不会被调用,所以上线之后如何判断是否正常的工作变得关键起来。

健康检测目标

目的

健康检测的目的是确保服务的各项资源可用(定性分析),以及量化指标在合理的区间之内(定量分析)。举个简单的例子:是否能够连接数据库,只有“能”或者“不能”,两个结论。但数据库响应时间可能是100ms,也可能是2000ms。说明可能存在一定问题。

API ping

有些项目会用简单的方式来表示服务程序的情况,比如用HTTP的状态码200表示正常,其他则表示有异常。稍微复杂一点的,用不同的短点表示不同服务的情况,例如检测mongo数据库用/api/health/mongo,检测redis用/api/health/redis

这样做有两个缺点:检测程序事先配置被检测服务的检测项目;定量的分析结论传递困难。

检测项目

根据应用程序所需的上游服务资源不同,有不同的健康监测目标和项目。常见的有:

  • 硬件容量:例如有磁盘、内存、CPU等;
  • 数据库:连接情况、响应时间(是否超时);
  • 其他API服务:通过HTTP或GRPC等协议检测服务是否在线;

@nestjs/terminus

快速上手

@nestjs是一套较为成熟的后端nodejs/typescript框架。它有一套官方开发的健康检测的类库(需要另外安装),能很好(符合@nestjs的设计哲学)地以模块方式集成到应用程序中。

npm install –save @nestjs/terminux

terminus添加@nestjs的项目中,并在app.modules.ts导入模块。

// src/app.module.ts


import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TerminusModule } from '@nestjs/terminus';


@Module({
  imports: [TerminusModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

如果你的检测项目在其他的模块注意把terminus模块的导入放在最近的位置上。最后,在对应的service中添加检测逻辑:

import { Injectable } from '@nestjs/common';
import {
  DiskHealthIndicator,
  HealthCheckService,
  MemoryHealthIndicator,
} from '@nestjs/terminus';

@Injectable()
export class AppService {
  constructor(
    private health: HealthCheckService,
    private disk: DiskHealthIndicator,
    private memory: MemoryHealthIndicator,
  ) {}

  getHello(): string {
    return 'Hello World!';
  }

  async healthCheck() {
    return this.health.check([
      () =>
        this.disk.checkStorage('disk health', {
          thresholdPercent: 0.8,
          path: '/',
        }),
      async () => this.memory.checkHeap('memory_heap', 200 * 1024 * 1024),
      async () => this.memory.checkRSS('memory_rss', 3000 * 1024 * 1024),
    ]);
  }
}

controller中把服务放到路由中,用curl检测后会得到:

curl -XGET "http://localhost:3000/health" | jq
{
  "status": "ok",
  "info": {
    "disk health": {
      "status": "up"
    },
    "memory_heap": {
      "status": "up"
    },
    "memory_rss": {
      "status": "up"
    }
  },
  "error": {},
  "details": {
    "disk health": {
      "status": "up"
    },
    "memory_heap": {
      "status": "up"
    },
    "memory_rss": {
      "status": "up"
    }
  }
}

表示检测磁盘和内存项目都正常(目前只有定性的检测)。

terminus内置了10个检测器,具体用法可以参考项目源码目录中sample目录。

深入理解terminus

以模块为切口,深入了解terminus。在imports时,支持动态和静态两种方式引入。若采用动态方式,参数TerminusModuleOptions由两个字段组成:

  • errorLogStyle:pretty或者json,默认json
  • logger:boolean类型或者符合@nestjs/common的LoggerSerivce接口的实例(可以参考《NestJS日志最佳实践》)。默认为@nestjs/commonConsoleLogger类,但是被扩展为DefaultTerminusLogger。如果这个参数输入为false,则会屏蔽类库的log,error和warn输出。

通过这两个参数,terminus会根据开发者的设置,调整错误日志输出和普通日志输出provider。除此以外,terminus的模块对外导出(exports)有:

  • HealthCheckExecutor,健康监测执行器
  • HealthCheckService,健康监测核心,只有一个check方法
  • 内置健康监测工具。

HealthCheckService

其构造函数由@nestjs管理实例的依赖关系,注入:

  • HealthCheckExecutor 执行健康监测任务的执行器,在类的check函数中执行健康检测的任务,任务是数组载体,元素是健康监测函数。
  • ErrorLogger,用于提取错误信息,格式化为pretty或json格式
  • LoggerService,当出现错误时,输出ErrorLogger处理后的信息

任意一个健康任务出现问题时,都会触发@nestjs/common中的ServiceUnavailableException(503)异常。

@HealthCheck

此装饰器用于生成swagger文档,如果项目没有采用swagger,也就无所谓了。

HealthIndicatorResult

健康检测项目的健康情况,包含多个结论,每个结论有一个表示是否健康的状态属性以及关联表示附加信息的属性,通常在出现不健康时用message相关信息。

HealthCheckExecutor

健康检测逻辑的执行器,Promise.allSettled方式执行所有监测项,并重新组装检测结果(HealthIndicatorResult)。最终如果结果中有任意错误,则返回对象的status为’error’,全部通过的值为’ok’。

自定义检测器

作为@nestjs的全家桶成员之一,terminus内置相关的成员的检测逻辑,基本可以做到“开箱即用”,对于其他服务需要自己动手,例如下面的检测日志程序的写入日志文件的工作正常:

// libs/console-logger-plus.health-indicator.ts
import { Injectable, Scope } from "@nestjs/common";
import { HealthCheckError, HealthIndicator, HealthIndicatorResult } from "@nestjs/terminus";
import { checkPackages } from "@nestjs/terminus/dist/utils";
import * as fs from "fs";


@Injectable({ scope: Scope.TRANSIENT })
export class ConsoleLoggerPlusHealthIndicator extends HealthIndicator {
  constructor() {
    super();
    checkPackages(["rotating-file-stream"], this.constructor.name);
  }


  public checkLog(key: string, logFilePath: string): HealthIndicatorResult {
    let isHealth = false;

    try {
      isHealth = fs.existsSync(logFilePath);
    } catch (error) {
      if (error instanceof Error) {
        return this.getStatus(key, isHealth, { message: '#ConsoleLoggerPlus' + error.message });
      } else {
        return this.getStatus(key, isHealth, { message: '#ConsoleLoggerPlus'+ error });
      }
    }
    if (!isHealth) {
      const message = `#ConsoleLoggerPlus 日志文件不存在:${logFilePath}`;
      throw new HealthCheckError(message, this.getStatus(key, isHealth, { message }));
    }
    return this.getStatus(key, isHealth);

  }
}

检测逻辑比较简单,首先是确认类所依赖的rotating-file-stream类库是否已经被安装,这里用了terminus库内的工具(所以上面的代码是直接从编译后的目录中提取的)。检测逻辑在checkLog方法中,key表示被检测项的明明,logFilePath则是被检测的日志目录(也可以是文件)。

使用的代码如下:

// src/app.controller.ts
import { Controller, Get, Logger } from '@nestjs/common';
import { AppService } from './app.service';
import {
  ConsoleLoggerPlusHealthIndicator,
  ILoggerConfig,
  LOGGER_CONFIG_REGISTER_KEY,
} from 'nest-logger-utility';
import { ConfigService } from '@nestjs/config';

@Controller()
export class AppController {


  @Get('health')
  async healthCheck() {
    const logConfig = this.config.get<ILoggerConfig>(
      LOGGER_CONFIG_REGISTER_KEY,
    );
    return this.health.check([
      () =>
        this.consoleLoggerPlusHealthIndicator.checkLog(
          'consoleLoggerPlus',
          logConfig.dir,
        ),
    ]);
  }
}

小结

terminus提供的框架还是比较清晰的,方便开发进行水平扩展。但还只能做定性的分析,如需要定量分析,要么通过增加检测项,例如:中等健康(true|false),优秀(true|false),的方式做分割。

要想完美实现这个需求,只能垂直扩展(extend)terminus的关键类。

搭配Docker

健康监测需要通过API调用的方式检测和反馈,在宿主机中可以通过cron的方式,每间隔一段时间就自行请求一次。如果应用程序通过Docker来部署,就可以利用Docker镜像功能来实现健康监测,例如:

# Dockerfile
FROM node:lts-alpine3.16
# .....
HEALTHCHECK --interval=15s --timeout=2s --start-period=30s --retries=2 CMD curl --fail -s localhost:${PORT} || exit 1

在编写应用镜像时,最后放上HEALTHCHECK,那么在容器运行时,就会自动标记上容器的健康状况。

$ docker ps


091962bce5b2        writing_system:latest     "docker-entrypoint.s…"   4 days ago          Up 4 days (healthy)     0.0.0.0:3300->3000/tcp   app


$ docker inspect --format "{{json .State.Health}}" app | jq
{
  "Status": "healthy",
  "FailingStreak": 0,
  "Log": [
    {
      "Start": "2023-07-02T11:24:09.695749191+08:00",
      "End": "2023-07-02T11:24:09.812807464+08:00",
      "ExitCode": 0,
      "Output":"..."
    }
  ]
}

搭配日志

搭配《NestJS日志最佳实践》,可以通过设置关注标签,将不健康的内容输出到日志中,同时配置日志工具予以关注。

相关参考

A guide to API health check

Documentation | NestJS – A progressive Node.js framework

Dockerfile reference | Docker Documentation

NestJS 日志最佳实践

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYQJhgeJ' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片