Mache deinen Dienst beobachtbar und sauber stoppbar: JSON-Logs, Liveness-/Readiness-Probes und die Behandlung von SIGTERM.
Diese Lektion in Kodokon öffnenIn der Produktion richtet sich ein Log an eine Maschine (Loki, Datadog, CloudWatch), nicht an einen Menschen: Gib ein JSON-Objekt pro Zeile aus (eine Zeile = ein Ereignis) mit einem Level, einem ISO-Zeitstempel und strukturierten Feldern - niemals frei formulierte Interpolationen, die sich unmöglich abfragen lassen. Ein wenig bekanntes Detail: Das Schreiben nach stdout ist synchron, wenn das Ziel eine Datei oder ein Terminal ist, aber asynchron bei einer Pipe. Ein geschwätziger Logger kann die Event-Loop also je nach Ziel blockieren - genau darum geht es bei Bibliotheken wie pino, die schnell serialisieren und das Schreiben auslagern können.
function log(level, message, fields = {}) {
const entry = {
level,
message,
time: new Date().toISOString(),
pid: process.pid,
...fields,
};
process.stdout.write(JSON.stringify(entry) + "\n");
}
log("info", "server started", { port: 3000 });
log("error", "db unreachable", { retryInMs: 5000 });Unterscheide zwei Probes. Liveness beantwortet die Frage "muss der Prozess neu gestartet werden?": Sie muss trivial bleiben, ohne externe Abhängigkeit. Readiness beantwortet "kann ich Traffic empfangen?": Sie darf die Datenbank prüfen und vor allem während des Herunterfahrens auf 503 wechseln, damit der Load Balancer die Instanz herausnimmt, bevor sie ausfällt. Beide zu verwechseln führt dazu, dass gesunde Prozesse neu gestartet werden, sobald eine Abhängigkeit hustet.
import { createServer } from "node:http";
let ready = true;
const server = createServer((req, res) => {
if (req.url === "/healthz") {
res.writeHead(ready ? 200 : 503);
res.end(ready ? "ok" : "draining");
return;
}
res.end("hello");
});
server.listen(3000);
function shutdown() {
ready = false;
server.closeIdleConnections();
server.close(() => process.exit(0));
setTimeout(() => process.exit(1), 10_000).unref();
}
process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);Der Ablauf eines sauberen Herunterfahrens: Der Orchestrator sendet SIGTERM; du stellst die Readiness auf 503; server.close() verweigert neue Verbindungen und wartet, bis die laufenden Anfragen fertig sind; closeIdleConnections() schließt die untätigen Keep-alive-Verbindungen, die close() sonst unbegrenzt festhalten würden. Der mit unref() markierte Timer dient als Sicherheitsnetz: Er beendet den Prozess nach 10 Sekunden, ohne ihn von sich aus daran zu hindern, schon früher zu enden.
process.on("uncaughtException", (err) => {
const entry = {
level: "fatal",
message: err.message,
stack: err.stack,
time: new Date().toISOString(),
};
process.stderr.write(JSON.stringify(entry) + "\n");
process.exit(1);
});
process.on("unhandledRejection", (reason) => {
throw reason;
});