اجعل خدمتك قابلة للملاحظة وقابلة للإيقاف بنظافة: سجلّات JSON، ومسبار الحيوية/الجاهزية (liveness/readiness)، والتعامل مع SIGTERM.
افتح هذا الدرس في Kodokonفي بيئة الإنتاج، يُوجَّه السجلّ إلى آلة (Loki، Datadog، CloudWatch)، لا إلى إنسان: أصدِر كائن JSON واحدًا في كل سطر (سطر واحد = حدث واحد) مع مستوى، وطابع زمني بصيغة ISO، وحقول مُهيكلة - لا تُدرِج أبدًا نصوصًا حرّة يستحيل الاستعلام عنها. وثمة تفصيل قليل المعرفة: الكتابة إلى stdout متزامنة إلى ملف أو طرفية، لكنها غير متزامنة إلى أنبوب (pipe). لذا يمكن لمُسجِّل ثرثار أن يحجب حلقة الأحداث تبعًا للوجهة - وهذا هو كامل الغرض من مكتبات مثل 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);تسلسل الإيقاف السلس: يرسل المُنسِّق (orchestrator) الإشارة SIGTERM؛ فتقلب الجاهزية إلى 503؛ وترفض server.close() الاتصالات الجديدة وتنتظر انتهاء الطلبات الجارية؛ وتُغلِق closeIdleConnections() اتصالات keep-alive الخاملة التي، لولا ذلك، لأبقت close() مُعلَّقة إلى الأبد. ويعمل المؤقّت المُعلَّم بـ 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;
});