Zum Inhalt springen

Zeitgesteuerte Automatisierung mit NestJS

Backendsysteme optimieren: Wie ich einen Cronjob in einem Nest.js-Microservice eingesetzt habe

Wie ein zeitgesteuerter NestJS-Microservice wiederholte Anfragen nach Flottenstandorten in einem serverseitigen Auftrag bündelte, der für eine wesentlich größere Clientlast ausgelegt war.

9 Min. LesezeitVon

Hintergrund: ein Backend für Flottenstandorte

Das Backend bediente eine Lkw-Verwaltungsanwendung für Unternehmen. Jedes Unternehmen verwaltete mehrere Fahrer und musste die Standorte seiner Lkw verfolgen. Daher sendeten die Fahrer-Clients in regelmäßigen Abständen Standortaktualisierungen, während Unternehmens- und Administrator-Clients diese Daten nutzten.

Das ursprüngliche Backend war einfach aufgebaut, weil nur wenig Nutzeraktivität vorlag. Das Entwurfsproblem zeigte sich, als dieselbe API eine geplante Last von etwa 10.000 täglich aktiven Nutzern und einen schnell wachsenden Standortverlauf unterstützen sollte.

Der Standortendpunkt wurde zum Engpass

Horizontale Skalierung und ein Load Balancer konnten eingehende Abfragen verteilen, entfernten aber nicht die Arbeit innerhalb des Endpunkts für Standortaktualisierungen. Jeder Fahrer konnte ein Fünf-Tage-Fenster mit 1.440 Punkten behalten: 12 Punkte pro Stunde, 288 pro Tag, mit einem Abstand von fünf Minuten.

Um das Array geordnet zu halten, war entweder shift gefolgt von push mit O(n) + O(1) Aufwand oder pop gefolgt von unshift mit O(1) + O(n) Aufwand nötig. Zusätzlich zu dieser Arraypflege kamen bei jeder Aktualisierung Zeitberechnungen, Koordinatenprüfungen, Datenvalidierung und Datenbankpersistenz hinzu.

Fahrer-Clients waren keine verlässliche Uhr. Ein Gerät konnte innerhalb eines Fünf-Minuten-Intervalls fünf Anfragen senden oder in einem Funkloch gar nichts übertragen. Sowohl das Anfragevolumen als auch die Array-Verarbeitung wuchsen mit der Anzahl aktiver Fahrer.

Schon bei 1.000 Clients mit einer Anfrage pro Minute hätte der Monolith 1.000 Anfragen pro Minute und rund 1,4 Millionen Anfragen pro Tag erhalten.

Prüfung von der Datenbankintegration trennen

Der Entwurf trennte Annahme und Prüfung von der Integration in die Hauptdatenbank. Ein Automatisierungsdienst sammelte Standortdaten und war dafür zuständig, die ausstehenden Aktualisierungen jedes Lkw an einer kontrollierten Grenze in den Monolithen zu übertragen.

Alle fünf Minuten lief ein Cronjob und sendete eine Sammelanfrage mit Standortarrays. Die messbare Entwurfsänderung betrifft die Anfrageform, nicht eine nachgewiesene Datenbankgeschwindigkeit: Viele Client-Schreibvorgänge wurden zu einer Stapelanfrage pro Scheduler-Instanz alle fünf Minuten.

Derselbe Microservice nahm außerdem eingehende API-Aufrufe entgegen und konnte Analysedaten zurückgeben, während der zeitgesteuerte Übergabepunkt bestimmte, wann gesammelte Daten den Monolithen erreichten.

NestJS-Microservice erstellen

Installieren Sie die Nest CLI, erstellen Sie das Projekt, und fügen Sie die offiziellen Pakete für Microservices, Zeitplanung und HTTP hinzu.

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

Der nachstehende Bootstrap startet einen normalen HTTP-Server auf Port 8080 und verbindet einen TCP-Microservice auf Port 8081. Ein Dienst, der kein HTTP benötigt, kann den HTTP-Listener weglassen.

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 setzt die Standort- und Scheduler-Module zusammen, damit eingehende Standortverarbeitung und zeitgesteuerte Persistenz getrennt bleiben.

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

Cronjob in einem eigenen Modul halten

Das Standortmodul besitzt seinen Controller und seinen Dienst. Der Controller verarbeitet eingehende GET-Anfragen und location_updated-Ereignisse, während der Dienst Standortvorgänge ausführen oder als Reaktion auf Ereignisse dynamische Aufträge erstellen kann.

Die Zeitplanung bleibt in einem eigenen Modul, damit Nest keine unnötigen doppelten Aufträge registriert. Der Standortdienst muss ebenfalls ein gemeinsam genutzter Provider sein. Würde LocationService in SchedulerModule erneut bereitgestellt, entstünde eine weitere Nest-Instanz, und der Scheduler könnte einen anderen speicherinternen Puffer lesen als der Controller.

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

Sammelaktualisierung planen und senden

Der Scheduler nutzt CronExpression.EVERY_5_MINUTES. HttpService gibt ein Observable zurück, daher löst firstValueFrom die PATCH-Anfrage auf. Die folgenden Methoden sind Pseudocode: peek liest, ohne zu löschen, und acknowledge entfernt nur einen vom Monolithen bestätigten Stapel. Das Beispiel von 2023 implementierte keine dauerhafte Bestätigung.

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 stellt Observables und Operatoren für den asynchronen Antwort- und Fehlerweg bereit.
  • @nestjs/schedule registriert den Cronjob und stellt vordefinierte Ausdrücke wie EVERY_5_MINUTES bereit, was */5 * * * * entspricht.
  • @nestjs/axios stellt Axios über HttpService von Nest für die PATCH-Anfrage an den Monolithen bereit.

Was das Beispiel nicht löst

Ein Cron-Decorator plant lediglich Arbeit. Er verhindert weder doppelte Ausführungen noch bewahrt er gepufferte Daten nach einem Neustart oder garantiert, dass genau eine Replik für den Zeitplan zuständig ist.

  • Speichern Sie ausstehende Standorte dauerhaft, damit ein Neustart die Daten des Intervalls nicht löschen kann.
  • Fügen Sie einen Idempotenz- oder Stapelschlüssel hinzu, damit eine Wiederholung nicht dieselben Standorte zweimal schreibt.
  • Bestätigen Sie einen Stapel erst, nachdem der Monolith ihn bestätigt hat; behalten Sie ihn bei einer Zeitüberschreitung oder einem Fehler.
  • Verhindern Sie überlappende Läufe, wenn eine PATCH-Anfrage länger als fünf Minuten dauert.
  • Begrenzen Sie die Stapelgröße, und definieren Sie eine Rückstaukontrolle, wenn die Annahme schneller als die Integration erfolgt.
  • Führen Sie den Zeitplan über einen per Wahl bestimmten Worker, eine verteilte Sperre oder einen externen Scheduler aus. Andernfalls registriert jede Anwendungsreplik denselben Cron-Auftrag.

Ergebnis und Verweise

Der Scheduler wandelt viele Geräteschreibvorgänge alle fünf Minuten in einen Stapel um. Dadurch gibt es einen Ort, an dem Arbeit geprüft, wiederholt und bestätigt werden kann; mir liegen jedoch keine Produktivmessungen für Datenbanklast, Verfügbarkeit oder Zustellrate vor.

  • Das ursprüngliche Problem verband wachsenden Clientverkehr mit einem zunehmend aufwendigen Endpunkt für Standortaktualisierungen.
  • Ein getrennter Automatisierungs-Microservice übernahm die Übertragung gebündelter Standortaktualisierungen an den Monolithen.
  • Der Cronjob läuft alle fünf Minuten und bündelt ausstehende Standorte in einer Anfrage.
  • Die Implementierung nutzt NestJS-Microservices, Aufgabenplanung, das HTTP-Modul und RxJS.