Ir al contenido

Automatización programada en NestJS

Optimización de sistemas backend: cómo usé una tarea CRON en un microservicio Nest.js

Cómo un microservicio programado en NestJS consolidó solicitudes repetidas de ubicación de flotas en una tarea del servidor diseñada para una carga de clientes mucho mayor.

9 min de lecturaPor

Contexto: un backend para localizar flotas

El backend servía una aplicación de gestión de camiones para empresas. Cada empresa gestionaba varios conductores y necesitaba seguir la ubicación de sus camiones, así que las aplicaciones de los conductores enviaban actualizaciones periódicas mientras las aplicaciones empresariales y administrativas consumían esos datos.

El backend inicial era sencillo porque había poca actividad. El problema de diseño apareció cuando la misma API debía soportar una carga prevista de unos 10.000 usuarios activos diarios y un historial de ubicaciones que crecía rápidamente.

El punto de acceso de ubicación se convirtió en el cuello de botella

El escalado horizontal y un balanceador de carga podían distribuir las consultas entrantes, pero no eliminaban el trabajo dentro del punto de acceso que actualizaba la ubicación. Cada conductor podía conservar una ventana de cinco días con 1.440 puntos: 12 por hora y 288 por día, separados por cinco minutos.

Mantener el orden del arreglo exigía aplicar shift seguido de push, con O(n) + O(1) de trabajo, o pop seguido de unshift, con O(1) + O(n). Además del mantenimiento del arreglo, cada actualización acumulaba cálculos de tiempo, comprobaciones de coordenadas, validación de datos y persistencia en la base de datos.

Los clientes de los conductores no eran un reloj fiable. Un dispositivo podía enviar cinco solicitudes durante un mismo intervalo de cinco minutos o no enviar ninguna al atravesar una zona sin cobertura. Tanto el volumen de solicitudes como el procesamiento del arreglo crecían con el número de conductores activos.

Con solo 1.000 clientes enviando una solicitud por minuto, el monolito recibiría 1.000 solicitudes por minuto y cerca de 1,4 millones al día.

Separar la verificación de la integración con la base de datos

El diseño separó la recepción y verificación de la integración con la base de datos principal. Un servicio de automatización recogía los datos de ubicación y se encargaba de enviar las actualizaciones pendientes de cada camión al monolito dentro de un límite controlado.

Una tarea cron se ejecutaba cada cinco minutos y enviaba una única solicitud por lotes con arreglos de ubicaciones. El cambio medible es la forma de las solicitudes, no una mejora demostrada de la base de datos: muchas escrituras de clientes pasaron a ser una solicitud por lotes por instancia del programador cada cinco minutos.

El mismo microservicio también atendía llamadas entrantes a la API y podía devolver información analítica, mientras el límite programado controlaba cuándo llegaban al monolito los datos acumulados.

Crear el microservicio NestJS

Instala Nest CLI, crea el proyecto y añade los paquetes oficiales de microservicios, programación y 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

El arranque que aparece abajo ejecuta un servidor HTTP normal en el puerto 8080 y conecta un microservicio TCP en el puerto 8081. Un servicio que no necesite HTTP puede omitir ese proceso de escucha.

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 combina los módulos de ubicación y programación para mantener separados el tratamiento de ubicaciones entrantes y la persistencia programada.

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 {}

Mantener la tarea cron en un módulo específico

El módulo de ubicación posee su controlador y su servicio. El controlador atiende las solicitudes GET entrantes y los eventos location_updated, mientras el servicio puede realizar operaciones de ubicación o crear tareas dinámicas en respuesta a eventos.

La programación permanece en un módulo específico para que Nest no registre tareas duplicadas innecesarias. LocationService también debe ser un único proveedor compartido. Volver a proporcionar LocationService dentro de SchedulerModule crearía otra instancia de Nest y podría hacer que el programador leyera un búfer en memoria distinto al del controlador.

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 {}

Programar y enviar la actualización por lotes

El programador usa CronExpression.EVERY_5_MINUTES. HttpService devuelve un Observable, por lo que firstValueFrom resuelve la solicitud PATCH. Los métodos siguientes son pseudocódigo: peek lee sin borrar y acknowledge elimina únicamente un lote confirmado por el monolito. El ejemplo de 2023 no implementaba una confirmación duradera.

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 proporciona objetos Observable y operadores para la respuesta asíncrona y la ruta de error.
  • @nestjs/schedule registra la tarea cron y proporciona expresiones predefinidas como EVERY_5_MINUTES, equivalente a */5 * * * *.
  • @nestjs/axios expone Axios mediante HttpService de Nest para enviar la solicitud PATCH al monolito.

Lo que no resuelve el ejemplo

Un decorador cron solo programa trabajo. No impide ejecuciones duplicadas, no conserva los datos almacenados en el búfer tras un reinicio ni garantiza que una sola réplica posea la programación.

  • Guarda las ubicaciones pendientes de forma duradera para que un reinicio no borre los datos del intervalo.
  • Añade una clave de idempotencia o de lote para que un reintento no escriba dos veces las mismas ubicaciones.
  • Da por confirmado un lote únicamente después de que el monolito lo confirme; consérvalo si vence el tiempo de espera o falla la solicitud.
  • Impide que las ejecuciones se solapen cuando una solicitud PATCH tarda más de cinco minutos.
  • Limita el tamaño del lote y define contrapresión cuando la recepción sea más rápida que la integración.
  • Ejecuta la programación mediante un único proceso elegido, un bloqueo distribuido o un programador externo. De lo contrario, cada réplica de la aplicación registrará la misma tarea cron.

Resultado y referencias

El programador convierte muchas escrituras de dispositivos en un lote cada cinco minutos. Crea un único lugar para verificar, reintentar y confirmar el trabajo, pero no tengo mediciones de producción sobre carga de la base de datos, disponibilidad o tasa de entrega.

  • El problema original combinaba el crecimiento del tráfico de clientes con un punto de actualización de ubicaciones cada vez más costoso.
  • Un microservicio de automatización independiente pasó a enviar actualizaciones de ubicación por lotes al monolito.
  • La tarea cron se ejecuta cada cinco minutos y consolida las ubicaciones pendientes en una sola solicitud.
  • La implementación usa microservicios NestJS, programación de tareas, el módulo HTTP y RxJS.