サービスを可観測にし、きれいに停止できるようにしましょう。JSONログ、liveness/readinessプローブ、そしてSIGTERMの処理です。
このレッスンを Kodokon で開く本番環境では、ログは人間ではなく機械(Loki、Datadog、CloudWatch)のためのものです。1行につき1つのJSONオブジェクト(1行 = 1イベント)を、レベル、ISOタイムスタンプ、構造化されたフィールドとともに出力しましょう。問い合わせが不可能な自由形式の文字列補間は決して使わないでください。あまり知られていない細かい点があります。stdoutへの書き込みは、ファイルやターミナルに対しては同期的ですが、パイプに対しては非同期です。したがっておしゃべりなロガーは、出力先しだいでイベントループをブロックしかねません。これこそがpinoのようなライブラリの存在意義であり、高速にシリアライズし、書き込みを別に肩代わりさせられるのです。
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 });二つのプローブを区別しましょう。Livenessは「プロセスを再起動すべきか?」に答えます。これは外部への依存を持たず、ごく単純なままでなければなりません。Readinessは「トラフィックを受け取れるか?」に答えます。データベースを確認してもよく、そして何よりもシャットダウン中に503へ切り替わることで、ロードバランサーがインスタンスの停止前にそれを外せるようにします。この二つを混同すると、依存先が一瞬つまずいた途端に、健全なプロセスが再起動されてしまいます。
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);グレースフルシャットダウンの流れはこうです。オーケストレーターがSIGTERMを送る。あなたはreadinessを503へ切り替える。server.close()は新しい接続を拒否し、処理中のリクエストが終わるのを待つ。closeIdleConnections()は、そうしなければclose()を永遠に引き止めてしまうアイドルのkeep-alive接続を閉じる。unref()を付けたタイマーは安全網の役割を果たします。10秒後にプロセスを終了させますが、それ自体がプロセスがもっと早く終了するのを妨げることはありません。
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;
});