Kodokon kodokon.com

可观测性:日志、健康检查、优雅关闭

让你的服务可观测且能干净地停止:JSON 日志、liveness/readiness 探针,以及 SIGTERM 处理。

9 分钟 · 3 题

在 Kodokon 中打开本课

在生产环境中,一条日志是给机器(Loki、Datadog、CloudWatch)看的,而不是给人看的:请每行输出一个 JSON 对象(一行 = 一个事件),带上一个级别、一个 ISO 时间戳和结构化的字段 - 绝不要用无法查询的自由格式插值。一个鲜为人知的细节:向 stdout 写入时,写到文件或终端是同步的,但写到管道是异步的。所以一个话痨式的日志器可能会根据目标而阻塞事件循环 - 这正是 pino 这类库的意义所在,它们序列化很快,并且能把写入操作卸载出去。

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 });
一个最简的结构化日志器,可原样查询。

要区分两种探针。Liveness(存活)回答的是"进程应该被重启吗?":它必须保持极其简单,不依赖任何外部依赖。Readiness(就绪)回答的是"我能接收流量吗?":它可以检查数据库,而且尤其重要的是在关闭期间切换到 503,好让负载均衡器在实例停机之前把它移除。把两者混为一谈会导致健康的进程在某个依赖打个嗝的那一刻就被重启。

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);
动态的 readiness 和在 SIGTERM 上的优雅关闭。

一次优雅关闭的流程:编排器发送 SIGTERM;你把 readiness 翻转为 503;server.close() 拒绝新连接并等待处理中的请求完成;closeIdleConnections() 关闭那些空闲的 keep-alive 连接,否则它们会无限期地拖住 close()。那个被 unref() 标记的定时器充当安全网:它会在 10 秒后杀掉进程,同时自身又不会阻止进程更早地退出。

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;
});
最后手段:先记录日志,再退出。

知识检测

确认你已牢记本课的重点内容。

  1. liveness 探针和 readiness 探针之间有什么区别?
    • 没有区别:它们是同一个测试的两个名字
    • liveness 检查进程是否在运行;readiness 检查它是否能接受流量,包括各个依赖
    • readiness 重启容器,liveness 把它从负载均衡器中移除
  2. server.close() 究竟做了什么?
    • 它立即切断所有已打开的连接
    • 它在固定的 30 秒延迟后杀掉进程
    • 它拒绝新连接并等待现有连接完成
  3. 一个 uncaughtException 处理器应该做什么?
    • 记录错误,然后终止进程
    • 忽略错误:Node 会自行恢复
    • 自动重试失败的操作