Planlagt automatisering med NestJS
Optimalisering av tjenersystemer: slik brukte jeg en CRON-jobb i en Nest.js-mikrotjeneste
Slik samlet en planlagt NestJS-mikrotjeneste gjentatte forespørsler om flåteposisjoner i én jobb på tjenersiden, dimensjonert for langt flere klienter.
9 min lesetidAv Abdullah Raheel
Bakgrunn: en tjenerside for flåteposisjoner
Tjenersiden betjente en applikasjon for lastebiladministrasjon i bedrifter. Hver bedrift administrerte flere sjåfører og måtte spore lastebilenes posisjoner. Sjåførklientene sendte derfor posisjonsoppdateringer med jevne mellomrom, mens bedrifts- og administratorklientene brukte disse dataene.
Den første tjenersiden var enkel fordi brukeraktiviteten var lav. Utfordringen i utformingen oppsto da det samme API-et måtte dimensjoneres for rundt 10 000 daglig aktive brukere og en raskt voksende posisjonshistorikk.
Lokasjonsendepunktet ble flaskehalsen
Horisontal skalering og en lastbalanserer kunne fordele innkommende forespørsler, men fjernet ikke arbeidet inne i endepunktet for posisjonsoppdateringer. Hver sjåfør kunne beholde et vindu på fem dager med 1 440 punkter: 12 punkter per time og 288 per dag, med fem minutter mellom punktene.
For å holde tabellen sortert måtte systemet enten kjøre shift og deretter push, med O(n) + O(1) arbeid, eller pop og deretter unshift, med O(1) + O(n) arbeid. I tillegg til vedlikeholdet av tabellen omfattet hver oppdatering tidsberegninger, kontroll av koordinater, datavalidering og lagring i databasen.
Sjåførklientene var ikke en pålitelig klokke. En enhet kunne sende fem forespørsler i løpet av ett femminuttersintervall, eller ingen forespørsler i et område uten nettdekning. Både antall forespørsler og arbeidet med å behandle tabellene vokste med antall aktive sjåfører.
Med bare 1 000 klienter som sendte én forespørsel per minutt, ville monolitten motta 1 000 forespørsler i minuttet og rundt 1,4 millioner i døgnet.
Skill verifisering fra integrasjonen mot databasen
Utformingen skilte mottak og verifisering fra integrasjonen mot hoveddatabasen. En automatiseringstjeneste samlet inn posisjonsdata og fikk ansvaret for å sende hver lastebils ventende oppdateringer til monolitten på et kontrollert tidspunkt.
En cron-jobb kjørte hvert femte minutt og sendte én samlet forespørsel med lister over posisjoner. Den målbare endringen gjaldt formen på forespørslene, ikke dokumentert databasehastighet: mange skrivinger fra klienter ble til én batchforespørsel per planleggerinstans hvert femte minutt.
Den samme mikrotjenesten lyttet også etter innkommende API-kall og kunne returnere analysedata, mens den tidsstyrte grensen styrte når de oppsamlede dataene nådde monolitten.
Opprett NestJS-mikrotjenesten
Installer Nest CLI, opprett prosjektet og legg til de offisielle pakkene for mikrotjenester, planlegging og 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 axiosOppstartskoden nedenfor kjører en vanlig HTTP-tjener på port 8080 og kobler til en TCP-mikrotjeneste på port 8081. En tjeneste som ikke trenger HTTP, kan utelate HTTP-lytteren.
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 setter sammen posisjons- og planleggermodulene, slik at behandling av innkommende posisjoner og planlagt lagring forblir adskilt.
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 {}Hold cron-jobben i en egen modul
Posisjonsmodulen eier kontrolleren og tjenesten sin. Kontrolleren håndterer innkommende GET-forespørsler og location_updated-hendelser, mens tjenesten kan utføre posisjonsoperasjoner eller opprette dynamiske jobber som svar på hendelser.
Planleggingen ligger i en egen modul, slik at Nest ikke registrerer unødvendige duplikatjobber. Posisjonstjenesten må også være én delt provider. Hvis LocationService tilbys på nytt i SchedulerModule, opprettes en ny Nest-instans. Planleggeren kan da lese en annen minnebuffer enn kontrolleren.
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 {}Planlegg og send masseoppdateringen
Planleggeren bruker CronExpression.EVERY_5_MINUTES. HttpService returnerer en Observable, så firstValueFrom fullfører PATCH-forespørselen. De neste metodene er pseudokode: peek leser uten å slette, og acknowledge fjerner bare en batch som monolitten har bekreftet. Eksemplet fra 2023 implementerte ikke varig bekreftelse.
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 leverer Observables og operatorer for den asynkrone responsen og feilforløpet.
- @nestjs/schedule registrerer cron-jobben og leverer forhåndsdefinerte uttrykk som EVERY_5_MINUTES, tilsvarende */5 * * * *.
- @nestjs/axios eksponerer Axios gjennom Nests HttpService for PATCH-forespørselen til monolitten.
Dette løser ikke eksemplet
En cron-dekoratør planlegger bare arbeid. Den forhindrer ikke dupliserte kjøringer, bevarer ikke bufrede data etter omstart og garanterer ikke at én replika eier tidsplanen.
- Lagre ventende posisjoner varig, slik at en omstart ikke kan slette dataene fra intervallet.
- Legg ved en idempotens- eller batchnøkkel, slik at et nytt forsøk ikke kan skrive de samme posisjonene to ganger.
- Bekreft en batch først etter at monolitten har bekreftet den. Behold den ved tidsavbrudd eller feil.
- Forhindre overlappende kjøringer når én PATCH varer lenger enn fem minutter.
- Avgrens batchstørrelsen og definer mottrykk når mottaket går raskere enn integrasjonen.
- Kjør tidsplanen gjennom én valgt arbeider, en distribuert lås eller en ekstern planlegger. Ellers registrerer hver applikasjonsreplika den samme cron-jobben.
Resultat og referanser
Planleggeren samler mange skrivinger fra enheter i én batch hvert femte minutt. Det gir ett sted for verifisering, nye forsøk og bekreftelse, men jeg har ingen produksjonsmålinger for databasebelastning, oppetid eller leveringsgrad.
- Det opprinnelige problemet kombinerte økende klienttrafikk med et stadig dyrere endepunkt for posisjonsoppdateringer.
- En egen automatiseringsmikrotjeneste fikk ansvaret for å sende samlede posisjonsoppdateringer til monolitten.
- Cron-jobben kjører hvert femte minutt og samler ventende posisjoner i én forespørsel.
- Implementasjonen bruker NestJS-mikrotjenester, oppgaveplanlegging, HTTP-modulen og RxJS.

