跳到主要内容

NestJS 定时自动化

优化后端系统:如何在 Nest.js 微服务中使用 CRON 任务

定时运行的 NestJS 微服务如何将重复的车队位置请求整合为服务端任务,并为更大的客户负载而设计。

阅读时间:9分钟作者

背景:车队位置后端

该后端为企业使用的卡车管理应用提供服务。每家企业管理多名驾驶员,并需要跟踪卡车位置,因此驾驶员客户端会定期发送位置更新,企业和管理员客户端则使用这些数据。

初期用户活动很少,后端设计也很直接。当同一 API 需要支撑约 10,000 名日活跃用户的预估负载和快速增长的位置历史时,设计问题开始显现。

位置端点成为瓶颈

水平扩展和负载均衡器可以分配传入查询,但无法消除位置更新端点内部的工作。每名驾驶员可以保留一个包含 1,440 个点的五天窗口:每小时 12 个点,每天 288 个点,各点间隔五分钟。

保持数组有序意味着要么先 shift 再 push,工作量为 O(n) + O(1);要么先 pop 再 unshift,工作量为 O(1) + O(n)。除了维护数组,每次更新还会累积时间计算、坐标检查、数据验证和数据库持久化工作。

驾驶员客户端并不是可靠的时钟。一台设备可能在一个五分钟间隔内发送五次请求,也可能在网络盲区完全不发送请求。请求量和数组处理工作都会随活跃驾驶员数量增长。

即使只有 1,000 个客户端每分钟发送一次请求,单体应用每分钟也会收到 1,000 次请求,每天约 140 万次。

将验证与数据库集成拆分

该设计将数据接收与验证从主数据库集成中分离。自动化服务负责收集位置数据,并在受控边界把每辆卡车的待处理更新推送到单体应用。

cron 任务每五分钟运行一次,发送一个包含多组位置数据的批量请求。可衡量的设计变化是请求形态,而不是已经证实的数据库提速:大量客户端写入变成每个调度器实例每五分钟发送一个批量请求。

同一个微服务还会监听传入的 API 调用并返回分析信息,而定时边界控制累积数据何时进入单体应用。

创建 NestJS 微服务

安装 Nest CLI,创建项目,并添加官方的微服务、任务调度和 HTTP 包。

npm i -g @nestjs/cli
nest new project-name
# Or create the project in the current directory:
nest new .
npm i --save @nestjs/microservices
npm install --save @nestjs/schedule
npm install --save-dev @types/cron
npm i --save @nestjs/axios axios

下面的引导代码在端口 8080 上运行普通 HTTP 服务器,并连接端口 8081 上的 TCP 微服务。不需要 HTTP 的服务可以省略 HTTP 侦听器。

import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { Transport } from '@nestjs/microservices'

async function bootstrap() {
  const app = await NestFactory.create(AppModule)

  app.connectMicroservice({
    transport: Transport.TCP,
    options: {
      port: 8081
    }
  })

  await app.startAllMicroservices()
  await app.listen(8080)
}

bootstrap()

AppModule 组合位置模块和调度器模块,使传入的位置处理逻辑与定时持久化保持分离。

import { Module } from '@nestjs/common'
import { LocationModule } from './location/location.module'
import { SchedulerModule } from './scheduler/scheduler.module'

@Module({
  imports: [SchedulerModule, LocationModule],
  providers: []
})
export class AppModule {}

将 cron 任务放在专用模块中

位置模块负责自己的控制器和服务。控制器处理传入的 GET 请求和 location_updated 事件,服务则可以执行位置操作,或根据事件创建动态任务。

调度逻辑留在专用模块中,避免 Nest 注册不必要的重复任务。位置服务也必须是共享的单一提供程序。如果在 SchedulerModule 中再次提供 LocationService,Nest 会创建另一个实例,导致调度器读取的内存缓冲区与控制器不同。

import { Module } from '@nestjs/common'
import { ScheduleModule } from '@nestjs/schedule'
import { HttpModule } from '@nestjs/axios'
import { LocationModule } from '../location/location.module'
import { SchedulerService } from './scheduler.service'

@Module({
  imports: [ScheduleModule.forRoot(), HttpModule, LocationModule],
  providers: [SchedulerService]
})
export class SchedulerModule {}

定时发送批量更新

调度器使用 CronExpression.EVERY_5_MINUTES。HttpService 返回 Observable,因此使用 firstValueFrom 等待 PATCH 请求完成。后续方法是伪代码:peek 读取但不删除数据,acknowledge 只移除单体应用已确认的批次。2023 年示例并未实现持久确认机制。

import { HttpService } from '@nestjs/axios'
import { Injectable } from '@nestjs/common'
import { Cron, CronExpression } from '@nestjs/schedule'
import { firstValueFrom } from 'rxjs'
import { externalLinks } from 'src/links'
import { LocationService } from 'src/location/location.service'

@Injectable()
export class SchedulerService {
  constructor(
    private readonly locationService: LocationService,
    private readonly httpService: HttpService
  ) {}

  @Cron(CronExpression.EVERY_5_MINUTES)
  async locationBulkUpdateJob() {
    const batch = this.locationService.peekPendingLocations()
    if (batch.length === 0) return

    await firstValueFrom(
      this.httpService.patch(externalLinks.backendProdLink, { locations: batch })
    )

    this.locationService.acknowledgeLocations(batch)
  }
}
  • RxJS 为异步响应和错误路径提供 Observable 与操作符。
  • @nestjs/schedule 注册 cron 任务,并提供 EVERY_5_MINUTES 等预定义表达式;该表达式等同于 */5 * * * *。
  • @nestjs/axios 通过 Nest 的 HttpService 暴露 Axios,用于向单体应用发送 PATCH 请求。

示例没有解决的问题

cron 装饰器只负责安排工作。它无法阻止重复运行、在重启后保留缓冲数据,也无法保证只有一个副本拥有调度权。

  • 持久化待处理位置,避免重启清除当前间隔的数据。
  • 附加幂等键或批次键,避免重试重复写入同一批位置。
  • 仅在单体应用确认后才确认批次;超时或失败时保留该批次。
  • 当一次 PATCH 超过五分钟时,防止运行过程重叠。
  • 限制批次大小,并定义数据接收速度高于集成速度时的背压策略。
  • 通过一个选举出的工作进程、分布式锁或外部调度器执行定时任务,否则每个应用副本都会注册同一个 cron 任务。

结果与参考资料

调度器将大量设备写入改为每五分钟发送一个批次。它提供了一个统一位置,用于验证、重试和确认工作,但我没有数据库负载、运行时间或交付率的生产测量数据。

  • 最初的问题同时包含不断增长的客户端流量和成本日益提高的位置更新端点。
  • 单独的自动化微服务负责向单体应用发送批量位置更新。
  • cron 任务每五分钟运行一次,把待处理位置合并成一个请求。
  • 该实现使用 NestJS 微服务、任务调度、HTTP 模块和 RxJS。