Kodokon kodokon.com

Observability: Logs, Healthcheck, sauberes Herunterfahren

Mache deinen Dienst beobachtbar und sauber stoppbar: JSON-Logs, Liveness-/Readiness-Probes und die Behandlung von SIGTERM.

9 Min. · 3 Fragen

Diese Lektion in Kodokon öffnen

In 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.

JAVASCRIPT
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 });
Ein minimaler strukturierter Logger, so wie er ist abfragbar.

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.

JAVASCRIPT
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);
Dynamische Readiness und sauberes Herunterfahren bei SIGTERM.

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.

JAVASCRIPT
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;
});
Letzter Ausweg: loggen, dann beenden.

Wissenscheck

Stelle sicher, dass du die wichtigsten Punkte dieser Lektion behalten hast.

  1. Was ist der Unterschied zwischen Liveness- und Readiness-Probes?
    • Keiner: Es sind zwei Namen für denselben Test
    • Liveness prüft, dass der Prozess läuft; Readiness, dass er Traffic annehmen kann, Abhängigkeiten inbegriffen
    • Readiness startet den Container neu, Liveness nimmt ihn aus dem Load Balancer
  2. Was macht server.close() genau?
    • Es kappt sofort alle offenen Verbindungen
    • Es beendet den Prozess nach festen 30 Sekunden
    • Es verweigert neue Verbindungen und wartet, bis die bestehenden fertig sind
  3. Was sollte ein uncaughtException-Handler tun?
    • Den Fehler loggen und dann den Prozess beenden
    • Den Fehler ignorieren: Node erholt sich von selbst
    • Die fehlgeschlagene Operation automatisch wiederholen