Graciøs nedlukning og dræning af igangværende forespørgsler
Håndtér SIGTERM for at dræne forbindelser, færdiggøre arbejdet og lukke ressourcer korrekt under deploys.
Graciøs nedlukning og dræning af igangværende forespørgsler er en gratis Bootcamp i backendudvikling med Node.js-lektion på CoddyKit. Dette er lektion 3 af 4. Du kan læse hele lektionen gratis nedenfor — og derefter øve dig praktisk i browseren med en indbygget kodeeditor og en AI-vejleder, der er tilgængelig døgnet rundt. Den er en del af læringsforløbet i Bootcamp i backendudvikling med Node.js, og dine fremskridt synkroniseres på tværs af nettet og CoddyKit-appen. Bootcamp i backendudvikling med Node.js-kurset indeholder 4 lektioner i alt.
Hvorfor kontrolleret nedlukning er vigtig
Under en udrulning sender din orkestrator (Kubernetes, ECS, systemd) en signal til din proces og giver den et kort tidsrum, før den tvangsafslutter den. Hvis du afslutter med det samme, mister du aktive forespørgsler, afbryder halvt skrevne databasetransaktioner og returnerer ødelagte svar til brugerne.
En kontrolleret nedlukning betyder:
- Stop med at acceptere nye forbindelser.
- Lad aktive forespørgsler afslutte (dræn).
- Luk ressourcer korrekt: DB-puljer, meddelelsesmæglere og timere.
- Afslut med koden
0, før drabstimeren udløber.
Denne lektion opbygger en nedlukningssti på produktionsniveau trin for trin.
Signalerne: SIGTERM vs SIGINT vs SIGKILL
Procesadministratorer kommunikerer hensigten om nedlukning via POSIX-signaler. Du skal håndtere de rigtige:
SIGTERM– det høflige "stop venligst", som Kubernetes, Docker og systemd sender under en udrulning. Det er dette signal, du skal håndtere.SIGINT– sendes afCtrl+Ci en terminal. Håndtér også dette for at få samme adfærd under lokal udvikling.SIGKILL(signal 9) – kan ikke opfanges, blokeres eller ignoreres. Operativsystemet afslutter processen øjeblikkeligt. Det er dette signal, der udløses, hvis du overskrider nådevinduet.
Node lader dig registrere lyttere for signaler, der kan opfanges, på process.
process.on('SIGTERM', () => {
console.log('Received SIGTERM, starting graceful shutdown');
shutdown();
});
process.on('SIGINT', () => {
console.log('Received SIGINT, starting graceful shutdown');
shutdown();
});
function shutdown() {
// close servers, drain requests, release resources
}server.close() dræner, men afslutter ikke
Den centrale primitive er server.close([callback]) på en http.Server. Dens adfærd er præcis det, der kræves for at dræne:
- Den stopper serveren fra at acceptere nye forbindelser med det samme.
- Den holder eksisterende forbindelser i live, indtil deres aktive forespørgsler er færdige.
- Callback-funktionen udløses først, når alle forbindelser er lukket.
Fælden er, at inaktive forbindelser med HTTP keep-alive forbliver åbne, så close()-callbacken kan hænge. Det løser vi om lidt. Først ser vi den problemfri sti med en komplet, selvstændig server.
const http = require('http');
const server = http.createServer((req, res) => {
// simulate a slow in-flight request
setTimeout(() => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('done\n');
}, 500);
});
server.listen(3000, () => console.log('listening on 3000'));
process.on('SIGTERM', () => {
console.log('SIGTERM: closing server, draining requests');
server.close(() => {
console.log('all connections drained, exiting');
process.exit(0);
});
});Den hårde frist: Afvikl aldrig for evigt
Din orchestrator sender SIGKILL, når terminationGracePeriodSeconds udløber (standardværdien er 30 sekunder i Kubernetes). En klient, der opfører sig forkert og holder en forbindelse åben, kan ellers få server.close() til at hænge ud over dette tidsrum, så en kontrolleret nedlukning bliver til en hård afslutning.
Reglen er: kombinér altid afvikling med en hård timeout, der er kortere end orchestratorens frist. Hvis afviklingen bliver færdig først, afslutter du med 0. Hvis timeouten udløber først, afslutter du med en værdi, der ikke er nul, så fejlen bliver synlig.
Brug timer.unref(), så timeouten ikke selv holder hændelsesløkken i gang.
function gracefulShutdown(server, { timeoutMs = 25000 } = {}) {
let shuttingDown = false;
return () => {
if (shuttingDown) return;
shuttingDown = true;
console.log('draining...');
const forceExit = setTimeout(() => {
console.error('drain timed out, forcing exit');
process.exit(1);
}, timeoutMs);
forceExit.unref();
server.close((err) => {
clearTimeout(forceExit);
if (err) { console.error(err); process.exit(1); }
console.log('drained cleanly');
process.exit(0);
});
};
}Idempotent nedlukning: Beskyt mod dobbelte signaler
Signaler kan ankomme mere end én gang: en operatør trykker på Ctrl+C to gange, eller både SIGTERM og SIGINT udløses. Hvis nedlukningen kører to gange, kalder du server.close() på en server, der allerede er ved at lukke ned, og du kan komme til at lukke ressourcepuljer to gange, så der opstår fejl midt under nedlukningen.
Gør handleren idempotent med en boolesk vagt (vist i den foregående scene som shuttingDown). Det første signal starter afviklingen, og efterfølgende signaler ignoreres. Her er en lille, fuldstændig selvstændig demonstration af vagtmønstret, som du kan køre direkte.
let shuttingDown = false;
function shutdown(reason) {
if (shuttingDown) {
console.log('already shutting down, ignoring ' + reason);
return;
}
shuttingDown = true;
console.log('shutdown started by ' + reason);
}
// simulate two rapid signals
shutdown('SIGTERM');
shutdown('SIGINT');
shutdown('SIGTERM');
console.log('handler ran exactly once');Stop loadbalanceren fra at sende ny trafik
Der opstår et kapløb under udrulninger: orchestratoren sender SIGTERM omtrent samtidig med, at den fjerner din pod fra loadbalancerens endpointliste. I et kort øjeblik kan LB stadig dirigere nye forespørgsler til en pod, der allerede er ved at blive afviklet.
Den almindelige løsning er en readiness-probe, der skifter til "not ready", så snart nedlukningen begynder. LB'en holder op med at sende ny trafik, før server.close() afviser forbindelser, så du undgår falske 502-fejl.
Skift et flag, lad proben fejle, og begynd derefter afviklingen (eventuelt efter en kort forsinkelse, så LB'en når at registrere ændringen).
let isShuttingDown = false;
// readiness endpoint the orchestrator polls
app.get('/readyz', (req, res) => {
if (isShuttingDown) {
return res.status(503).send('shutting down');
}
res.status(200).send('ok');
});
process.on('SIGTERM', () => {
isShuttingDown = true; // probe now fails -> LB stops new traffic
setTimeout(beginDrain, 5000); // give the LB a few probe cycles
});Styring af HTTP-forbindelser med keep-alive
HTTP keep-alive holder TCP-forbindelser åbne mellem forespørgsler. Under nedlukning har en inaktiv keep-alive-forbindelse ingen igangværende forespørgsel, men den forhindrer stadig callbacken fra server.close() i at blive kaldt, fordi socketen stadig er åben.
To robuste muligheder:
- Send
Connection: closei svar, når nedlukningen begynder, så klienterne holder op med at genbruge socketen. - Indstil
server.keepAliveTimeoutogheadersTimeoutfornuftigt, og hold styr på sockets, så du kan ødelægge inaktive sockets under afviklingen.
Ved at holde styr på sockets får du præcis kontrol: Afslut aktive forespørgsler, men luk inaktive sockets med tvang.
const http = require('http');
const connections = new Set();
const server = http.createServer((req, res) => {
if (isShuttingDown) res.setHeader('Connection', 'close');
res.end('ok\n');
});
server.on('connection', (socket) => {
connections.add(socket);
socket.on('close', () => connections.delete(socket));
});
function destroyIdleConnections() {
for (const socket of connections) {
if (socket._isIdle) socket.destroy();
}
}Markering af inaktive og aktive sockets
Hvis du kun vil ødelægge inaktive sockets sikkert, skal du markere en socket som aktiv, når en forespørgsel starter, og som inaktiv, når dens svar er afsluttet. Under afviklingen ødelægger du dem, der stadig er inaktive; aktive sockets lader du være, så de kan blive færdige.
Det er præcis den strategi, som gennemprøvede biblioteker som stoppable og http-terminator implementerer for dig. Hvis du forstår mekanismen, kan du fejlsøge den, når den ikke fungerer korrekt.
const connections = new Set();
server.on('connection', (socket) => {
socket._isIdle = true;
connections.add(socket);
socket.on('close', () => connections.delete(socket));
});
server.on('request', (req, res) => {
req.socket._isIdle = false; // active: a request is in flight
res.on('finish', () => {
req.socket._isIdle = true; // response sent: back to idle
if (isShuttingDown) req.socket.destroy(); // no reuse during drain
});
});
function closeIdle() {
for (const s of connections) if (s._isIdle) s.destroy();
}Lukning af downstream-ressourcer i den rigtige rækkefølge
Når HTTP-serveren er færdig med afviklingen, skal du frigive alt andet. Rækkefølgen er vigtig: Luk HTTP-serveren først, så der ikke kommer nyt arbejde ind, afvikl derefter køer og forbrugere, og luk databasepuljen til sidst (igangværende handlere kan stadig have brug for den, mens de afslutter).
En typisk nedlukningssekvens:
server.close()— stop med at acceptere forespørgsler, og afvikl HTTP.- Stop meddelelsesforbrugere (Kafka/RabbitMQ/BullMQ), så der ikke starter nye job.
- Vent på igangværende job, og luk derefter brokerforbindelser.
await pool.end()— afvikl og luk DB-forbindelsespuljen.- Skyl logge og målinger, og kør derefter
process.exit(0).
async function shutdown(server, pool, broker) {
await new Promise((resolve, reject) =>
server.close((err) => (err ? reject(err) : resolve()))
);
console.log('http drained');
await broker.stopConsuming(); // no new jobs
await broker.drainInFlight(); // finish started jobs
await broker.close();
console.log('broker closed');
await pool.end(); // close DB pool last
console.log('db pool closed');
}En promisificeret orchestrator til tidsbegrænset nedlukning
Saml det hele i én genbrugelig funktion. Den kører hele den asynkrone nedlukning op mod en hård frist ved hjælp af Promise.race. Den, der bliver færdig først, bestemmer afslutningskoden. Mønstret er uafhængigt af frameworks og fungerer med Express, Fastify eller en almindelig http.Server.
De vigtigste egenskaber er: idempotent via vagten, begrænset af timeoutMs og tydelig omkring succes (afslutning med 0) kontra tvungen afslutning (afslutning med 1).
function installGracefulShutdown(teardown, { timeoutMs = 25000 } = {}) {
let started = false;
const run = async (signal) => {
if (started) return;
started = true;
console.log('shutdown via ' + signal);
const deadline = new Promise((_, reject) => {
const t = setTimeout(() => reject(new Error('timeout')), timeoutMs);
t.unref();
});
try {
await Promise.race([teardown(), deadline]);
console.log('clean shutdown');
process.exit(0);
} catch (err) {
console.error('forced shutdown:', err.message);
process.exit(1);
}
};
process.on('SIGTERM', () => run('SIGTERM'));
process.on('SIGINT', () => run('SIGINT'));
}Glem ikke nedbrud: uncaughtException
En kontrolleret nedlukning håndterer bevidst terminering. Fatale fejl er anderledes. En uncaughtException eller unhandledRejection efterlader processen i en ubestemt tilstand; den sikre reaktion er at skrive til loggen, forsøge at tømme ressourcerne så godt som muligt og afslutte med en værdi, der ikke er nul, så orchestratoren genstarter dig.
Forsøg ikke at fortsætte med at levere trafik efter en uncaught exception. Den officielle vejledning til Node er at behandle det som et nedbrud. Her er en komplet illustration, der kan køres direkte, af en lytter, som udløses præcis én gang før afslutningen.
let crashing = false;
process.on('uncaughtException', (err) => {
if (crashing) return;
crashing = true;
console.error('uncaught:', err.message);
// best-effort: flush logs/metrics here, then exit non-zero
console.log('exiting with code 1');
process.exit(1);
});
// simulate a fatal bug somewhere deep in the app
setImmediate(() => {
throw new Error('boom: null pointer in handler');
});
console.log('server running, awaiting the crash');Hurtigt tjek: Kapløbet om afvikling
Du udruller en Node-tjeneste til Kubernetes med terminationGracePeriodSeconds: 30. Ved SIGTERM kalder du straks server.close(() => process.exit(0)) og intet andet. I produktion ser du stadig lejlighedsvise 502-fejl hos klienter under udrulninger. Hvad er den mest sandsynlige årsag, og hvad er den korrekte løsning?
Opsummering: Tjekliste til kontrolleret nedlukning
Du har nu en komplet, produktionsklar vej til nedlukning. Det vigtigste:
- Håndtér SIGTERM og SIGINT, og gør handleren idempotent med en vagt.
- Få readiness-proben til at fejle først, så loadbalanceren holder op med at sende ny trafik, før du begynder afviklingen.
- server.close() stopper nye forbindelser og afvikler igangværende forespørgsler; håndtér keep-alive ved at sende
Connection: closeog ødelægge inaktive sockets. - Sæt altid en hård timeout, der er kortere end orchestratorens frist, og brug
unref(); afslut med en værdi, der ikke er nul, hvis afviklingen hænger. - Luk ressourcer i den rigtige rækkefølge: HTTP-server, derefter forbrugere og broker, så DB-puljen og til sidst skylning af telemetri.
- Behandl uncaughtException/unhandledRejection som nedbrud: oprydning efter bedste evne og derefter afslutning med en værdi, der ikke er nul, så processen genstartes.
Når du gør dette rigtigt, holder udrulninger uden nedetid op med at miste brugerforespørgsler.
Lær JavaScript med en AI-underviser — gratis
Skriv og kør rigtig kode i din browser, få øjeblikkelig hjælp fra en AI-underviser døgnet rundt, og fortsæt, hvor du slap, på web eller i appen.
- Kurser
- 22
- Lektioner
- 92
Ofte stillede spørgsmål
Er lektionen “Graciøs nedlukning og dræning af igangværende forespørgsler” gratis?
Ja — hele teksten til “Graciøs nedlukning og dræning af igangværende forespørgsler” kan læses gratis her på nettet. Hvis du vil øve dig interaktivt med en indbygget kodeeditor og en AI-vejleder døgnet rundt og få adgang til resten af Bootcamp i backendudvikling med Node.js-kurset, skal du opgradere til CoddyKit PRO. Bootcamp i backendudvikling med Node.js-kurset indeholder 4 lektioner i alt.
Hvad lærer jeg i “Graciøs nedlukning og dræning af igangværende forespørgsler”?
Håndtér SIGTERM for at dræne forbindelser, færdiggøre arbejdet og lukke ressourcer korrekt under deploys. Du øver dig i Bootcamp i backendudvikling med Node.js med praktisk kode, som du kører direkte i browseren, og en AI-vejleder døgnet rundt besvarer dine spørgsmål, mens du arbejder dig gennem lektionen.
Skal jeg have erfaring for at begynde på Bootcamp i backendudvikling med Node.js?
Der kræves ingen tidligere erfaring. Bootcamp i backendudvikling med Node.js på CoddyKit er tilrettelagt for både begyndere og øvede, så du kan starte her eller fra begyndelsen og lære i dit eget tempo. Dette er lektion 3 af 4.
Hvor lang tid tager lektionen “Graciøs nedlukning og dræning af igangværende forespørgsler”?
De fleste CoddyKit-lektioner tager cirka 5–10 minutter. Hver lektion er kort og interaktiv, så du gør løbende fremskridt og kan fortsætte, hvor du slap – på både web og app.
Kan jeg skrive og køre kode i denne Bootcamp i backendudvikling med Node.js-lektion?
Ja. Alle Bootcamp i backendudvikling med Node.js-lektioner har en indbygget kodeeditor, så du kan skrive og køre rigtig kode direkte i din browser og få øjeblikkelig feedback fra AI – uden lokal opsætning.
Alle lektioner i dette kursus
- Timeouts, genforsøg og eksponentiel backoff med jitter
- Circuit breaker-mønsteret og bulkhead-isolering
- Graciøs nedlukning og dræning af igangværende forespørgsler
- Health checks, readiness probes og load shedding