Geplande NestJS-automatisering
Backendsystemen optimaliseren: hoe ik een CRON-job in een Nest.js-microservice gebruikte
Hoe een geplande NestJS-microservice herhaalde aanvragen voor wagenparklocaties samenvoegde tot een servertaak die voor een veel grotere clientbelasting was ontworpen.
9 min. leestijdDoor Abdullah Raheel
Achtergrond: een backend voor wagenparklocaties
De backend bediende een toepassing waarmee bedrijven vrachtwagens beheerden. Elk bedrijf beheerde meerdere chauffeurs en moest de locaties van de vrachtwagens volgen. Daarom stuurden chauffeurclients regelmatig locatie-updates, terwijl bedrijfs- en beheerclients die gegevens gebruikten.
De oorspronkelijke backend was eenvoudig omdat er weinig gebruikersactiviteit was. Het ontwerpprobleem ontstond toen dezelfde API een verwachte belasting van ongeveer 10.000 dagelijks actieve gebruikers en een snel groeiende locatiegeschiedenis moest ondersteunen.
Het locatie-eindpunt werd de flessenhals
Horizontaal schalen en een load balancer konden binnenkomende query's verdelen, maar verwijderden het werk binnen het eindpunt voor locatie-updates niet. Elke chauffeur kon een venster van vijf dagen met 1.440 punten bewaren: 12 punten per uur en 288 per dag, met vijf minuten tussen de punten.
Om de array geordend te houden, was shift gevolgd door push nodig, met O(n) + O(1) werk, of pop gevolgd door unshift, met O(1) + O(n) werk. Boven op dat arrayonderhoud kwamen bij elke update tijdberekeningen, coördinaatcontroles, gegevensvalidatie en databaseopslag.
Chauffeurclients waren geen betrouwbare klok. Een apparaat kon binnen één periode van vijf minuten vijf aanvragen sturen of in een gebied zonder netwerk niets sturen. Zowel het aanvraagvolume als het arrayverwerkingswerk groeide met het aantal actieve chauffeurs.
Met slechts 1.000 clients die één aanvraag per minuut deden, zou de monoliet 1.000 aanvragen per minuut en ongeveer 1,4 miljoen aanvragen per dag ontvangen.
Scheid verificatie van database-integratie
Het ontwerp scheidde invoer en verificatie van integratie met de hoofddatabase. Een automatiseringsservice verzamelde locatiegegevens en werd verantwoordelijk voor het op een gecontroleerd moment doorsturen van de openstaande updates van elke vrachtwagen naar de monoliet.
Elke vijf minuten draaide een cronjob die één bulkaanvraag met arrays van locaties verstuurde. De meetbare ontwerpwijziging is de vorm van de aanvragen, niet bewezen databasesnelheid: veel schrijfacties van clients werden één batchaanvraag per scheduler-instance per vijf minuten.
Dezelfde microservice luisterde ook naar binnenkomende API-aanroepen en kon analytische informatie teruggeven, terwijl de geplande grens bepaalde wanneer verzamelde gegevens de monoliet bereikten.
De NestJS-microservice maken
Installeer de Nest CLI, maak het project en voeg de officiële pakketten voor microservices, planning en HTTP toe.
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 axiosDe onderstaande bootstrap draait een normale HTTP-server op poort 8080 en verbindt een TCP-microservice op poort 8081. Een service die geen HTTP nodig heeft, kan de HTTP-listener weglaten.
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 combineert de locatie- en schedulermodules, zodat gedrag voor binnenkomende locaties en geplande opslag gescheiden blijven.
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 {}Houd de cronjob in een aparte module
De locatiemodule beheert de eigen controller en service. De controller verwerkt binnenkomende GET-aanvragen en location_updated-gebeurtenissen, terwijl de service locatiebewerkingen kan uitvoeren of dynamische jobs kan maken als reactie op gebeurtenissen.
Planning blijft in een aparte module, zodat Nest geen onnodige dubbele jobs registreert. De locatieservice moet ook één gedeelde provider zijn. Als LocationService opnieuw binnen SchedulerModule wordt aangeboden, ontstaat een tweede Nest-instance en kan de scheduler een andere buffer in het geheugen lezen dan de 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 {}De bulkupdate plannen en versturen
De scheduler gebruikt CronExpression.EVERY_5_MINUTES. HttpService retourneert een Observable, waarna firstValueFrom de PATCH-aanvraag afhandelt. De volgende methoden zijn pseudocode: peek leest zonder te verwijderen en acknowledge verwijdert alleen een batch die door de monoliet is bevestigd. Het voorbeeld uit 2023 implementeerde geen duurzame bevestiging.
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 levert Observables en operators voor het asynchrone antwoord- en foutpad.
- @nestjs/schedule registreert de cronjob en levert vooraf gedefinieerde expressies zoals EVERY_5_MINUTES, gelijk aan */5 * * * *.
- @nestjs/axios stelt Axios beschikbaar via Nests HttpService voor de PATCH-aanvraag naar de monoliet.
Wat het voorbeeld niet oplost
Een crondecorator plant alleen werk. Deze voorkomt geen dubbele uitvoeringen, bewaart geen gebufferde gegevens na een herstart en garandeert niet dat één replica de planning beheert.
- Sla openstaande locaties duurzaam op, zodat de gegevens van de periode niet door een herstart kunnen worden gewist.
- Voeg een idempotentie- of batchsleutel toe, zodat een nieuwe poging dezelfde locaties niet tweemaal kan schrijven.
- Bevestig een batch pas nadat de monoliet deze heeft bevestigd; bewaar de batch bij een time-out of fout.
- Voorkom overlappende uitvoeringen wanneer één PATCH langer dan vijf minuten duurt.
- Begrens de batchgrootte en definieer tegendruk wanneer de invoer sneller verloopt dan de integratie.
- Voer de planning uit via één verkozen worker, een gedistribueerde vergrendeling of een externe scheduler. Anders registreert elke toepassingsreplica dezelfde cronjob.
Resultaat en bronnen
De scheduler zet veel schrijfacties van apparaten om in één batch per vijf minuten. Er ontstaat één plaats om werk te verifiëren, opnieuw te proberen en te bevestigen, maar ik heb geen productiemetingen voor databasebelasting, beschikbaarheid of leveringspercentage.
- Het oorspronkelijke probleem combineerde groeiend clientverkeer met een steeds duurder eindpunt voor locatie-updates.
- Een afzonderlijke automatiseringsmicroservice werd verantwoordelijk voor het versturen van bulklocatie-updates naar de monoliet.
- De cronjob draait elke vijf minuten en voegt openstaande locaties samen tot één aanvraag.
- De implementatie gebruikt NestJS-microservices, taakplanning, de HTTP-module en RxJS.

