Node.js 可观测性实践:OpenTelemetry、监控与全链路追踪

Node.js 生产级可观测性完整方案:四大观测支柱、Pino 结构化日志、OpenTelemetry 自动/手动埋点、分布式链路追踪、Prometheus 指标采集、健康探针、Sentry 错误追踪、性能剖析与 Grafana 大盘告警实战。

可观测性(Observability) 不是简单的"加几个监控",而是通过 日志(Logs)、指标(Metrics)、追踪(Traces)和剖析(Profiling) 四大支柱,构建系统内在状态的完整理解能力。在 Node.js 异步事件驱动的世界里,没有链路 ID 的日志散落在各处,Promise 链中的异常可能吞没在线上,内存泄漏往往在流量高峰才显现……本文从工程实践出发,给出 Node.js 全栈可观测性的完整落地方案。


1. 可观测性四大支柱

可观测性的概念并非监控的简单升级。传统的监控聚焦于"系统是否正常运行",通过预设的阈值和告警来发现已知的问题模式。而可观测性更进一步,其目标是回答"系统为什么会这样表现",即使面对我们从未预料到的异常场景,也能通过数据推导出系统内部的运行状态。

对于 Node.js 来说,可观测性尤为重要。单线程事件循环的架构使得一个问题往往会产生连锁反应:某个数据库查询阻塞了事件循环,导致所有的 HTTP 请求堆积,内存持续增长直到进程崩溃。没有完善的可观测性数据,在这样的场景下排查根因无异于大海捞针。

支柱关注点典型问题工具示例
Logs离散事件记录“某个用户下单时发生了什么?”Pino、Winston
Metrics聚合数值趋势“接口 p99 延迟是否升高?”Prometheus、OTel
Traces请求全链路“订单请求经过了哪些服务?哪里最慢?”OpenTelemetry、Jaeger
Profiling运行时剖析“CPU 热点在哪个函数?内存泄漏在哪?”0x、Clinic.js、Node –prof

单一支柱无法回答复杂问题。日志定位"发生了什么"、指标回答"有多严重"、追踪揭示"在哪里"、剖析解释"为什么" —— 四者互补才能画出完整的故障全景图。实践中,一次典型的故障排查流程往往是:指标告警发现 p99 延迟异常升高,通过链路追踪定位到是订单服务的支付接口变慢,在对应 trace 中查看日志发现支付网关超时,最后用火焰图分析确认是 JSON 序列化操作成为热点——这就是四大支柱联动的价值。

用户请求 →  [Traces 链路]  → 服务 A → 服务 B → 数据库
              ↓
         每个 Span 附带 [Metrics] 延迟/错误率
              ↓
         异常 Span 关联 [Logs] 上下文
              ↓
         延迟异常触发 [Profiling] 采样

2. 结构化日志:Pino 与 Winston

生产环境日志必须具备两个特征:结构化(JSON 格式便于解析)和关联性(同一个请求的日志共享 trace_id)。

2.1 Pino 高性能日志

Pino 是 Node.js 生态中性能最优秀的日志库,日志开销比 Winston 低一个数量级。

// logger.js
const pino = require('pino');

const logger = pino({
    level: process.env.LOG_LEVEL || 'info',
    base: {
        service: 'order-service',
        env: process.env.NODE_ENV,
        pid: process.pid
    },
    // 开发环境美化输出
    transport: process.env.NODE_ENV === 'development'
        ? { target: 'pino-pretty', options: { colorize: true } }
        : undefined,
    // 关键字段自动序列化为标准格式
    formatters: {
        level(label) { return { level: label }; }
    }
});

module.exports = logger;
// app.js
const logger = require('./logger');

app.use((req, res, next) => {
    // 每个请求分配 trace_id
    req.id = req.headers['x-request-id'] || crypto.randomUUID();
    
    logger.info({
        req: {
            id: req.id,
            method: req.method,
            url: req.url,
            ip: req.ip
        }
    }, 'incoming request');
    
    // 响应时间记录
    const start = process.hrtime.bigint();
    res.on('finish', () => {
        const ms = Number(process.hrtime.bigint() - start) / 1e6;
        logger.info({
            req: { id: req.id, statusCode: res.statusCode },
            responseTime: ms
        }, 'request completed');
    });
    next();
});

// 业务日志自动携带请求上下文
app.post('/orders', (req, res) => {
    const child = logger.child({ reqId: req.id, userId: req.user.id });
    child.info({ orderId: 'ORD-123', amount: 199.0 }, 'creating order');
    // ... 业务逻辑
    child.info({ orderId: 'ORD-123' }, 'order created');
});

2.2 日志级别规范

DEBUG  — 开发调试信息(默认不输出到生产)
INFO   — 正常业务事件(请求开始/完成、状态变更)
WARN   — 非致命异常(降级处理、重试逻辑触发)
ERROR  — 业务错误(订单创建失败、外部 API 超时)
FATAL  — 系统级故障(数据库连接断开、内存溢出)

黄金法则:日志中绝不记录密码、Token、身份证号等 PII(个人身份信息),字段名避免使用 passwordsecrettoken 等自动脱敏关键字。

2.3 Pino 与 Winston 的选择

维度PinoWinston
性能最快(JSON 直接生成,无字符串拼接)中等(支持多层 transport)
功能专注日志,轻量生态丰富,插件众多
生产推荐结构化日志首选需要多目标输出时
启动开销极小较大(默认加载多个 transport)

如果是新项目或追求极致性能,Pino 是更优选择。对于遗留系统迁移,Winston 的兼容性更友好。


3. OpenTelemetry 集成:自动与手动埋点

OpenTelemetry(OTel) 是 CNCF 孵化的可观测性标准,提供统一的 API/SDK 来生成 Traces、Metrics 和 Logs,解决各厂商 SDK 互不兼容的问题。

3.1 自动埋点(Auto-Instrumentation)

自动埋点库可以在无侵入的情况下为 HTTP 框架、数据库客户端、消息队列等自动创建 Span。

# 安装核心依赖
npm install @opentelemetry/sdk-node
npm install @opentelemetry/auto-instrumentations-node
npm install @opentelemetry/exporter-trace-otlp-http
npm install @opentelemetry/exporter-metrics-otlp-http
npm install @opentelemetry/resources
npm install @opentelemetry/semantic-conventions
// tracing.js — OpenTelemetry 初始化
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-http');
const { PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics');
const { Resource } = require('@opentelemetry/resources');
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');

const sdk = new NodeSDK({
    resource: new Resource({
        [SemanticResourceAttributes.SERVICE_NAME]: 'order-service',
        [SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0',
        [SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: 'production',
    }),
    traceExporter: new OTLPTraceExporter({
        url: 'http://otel-collector:4318/v1/traces',
    }),
    metricReader: new PeriodicExportingMetricReader({
        exporter: new OTLPMetricExporter({
            url: 'http://otel-collector:4318/v1/metrics',
        }),
        exportIntervalMillis: 60000,  // 每分钟导出一次指标
    }),
    instrumentations: [
        getNodeAutoInstrumentations({
            // 可按需排除不需要的自动埋点
            '@opentelemetry/instrumentation-fs': { enabled: false },
        }),
    ],
});

sdk.start();
console.log('OpenTelemetry SDK started');

// 优雅关闭时导出未发送的 spans
process.on('SIGTERM', async () => {
    await sdk.shutdown();
    console.log('OpenTelemetry SDK shut down');
    process.exit(0);
});
# 启动应用时加载 tracing.js
node -r ./tracing.js app.js

自动埋点会拦截以下库的调用并自动创建 Span:

模块自动追踪内容
http / https所有 HTTP 请求/响应的延迟和方法
express / fastify / koa路由处理时间、请求参数
@grpc/grpc-jsgRPC 调用元信息和状态码
mysql / pg / mongodb / redis数据库查询语句和执行时间
amqplib / ioredis消息队列操作、缓存读写

3.2 手动埋点(Manual Spans)

自动埋点覆盖通用场景,但业务关键路径需要手动创建 Span 来补充上下文。

const { trace, context, SpanStatusCode } = require('@opentelemetry/api');

const tracer = trace.getTracer('order-service', '1.0.0');

async function processPayment(orderId, amount) {
    // 创建一个新的 Span,嵌套在当前 context 下
    return tracer.startActiveSpan('process-payment', async (span) => {
        try {
            span.setAttributes({
                'order.id': orderId,
                'payment.amount': amount,
                'payment.currency': 'CNY',
            });

            // 内部再创建子 Span
            const result = await tracer.startActiveSpan('payment-gateway-call', async (childSpan) => {
                childSpan.setAttribute('gateway.provider', 'alipay');
                const res = await callAlipayAPI(orderId, amount);
                childSpan.setAttribute('gateway.status', res.status);
                childSpan.end();
                return res;
            });

            span.setStatus({ code: SpanStatusCode.OK });
            return result;
        } catch (err) {
            span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
            span.recordException(err);
            throw err;
        } finally {
            span.end();
        }
    });
}

3.3 Baggage 与跨服务上下文传递

const { propagation, baggage } = require('@opentelemetry/api');

// 上游服务设置业务上下文
const currentBaggage = baggage.setBaggage(
    context.active(),
    propagation.createBaggage({
        'user.tier': { value: 'premium' },
        'request.priority': { value: 'high' },
    })
);

// 通过 HTTP Header 传播到下游
const headers = {};
propagation.inject(currentBaggage, headers);
// headers 中自动包含 traceparent 和 baggage 字段
await fetch('http://payment-service/pay', { headers });

// 下游服务提取
const extracted = propagation.extract(context.active(), req.headers);
const userTier = baggage.getBaggage(extracted, 'user.tier')?.value;

4. 分布式追踪:跨微服务链路

微服务架构中,单个用户请求可能经过网关、认证服务、订单服务、库存服务、支付服务等多个节点。分布式追踪通过唯一的 trace_id 串联所有 Span,还原完整调用链。没有分布式追踪时,每个服务各自记录日志,想要在十几甚至几十个服务的日志中找到同属一个请求的条目几乎是不可能的任务。分布式追踪让每个服务在记录日志和指标时附加上 trace_id 和 span_id,使得我们可以在整个系统的视角下观察单个请求的完整生命周期。

4.1 追踪上下文传播

┌──────────┐    trace_id=abc123    ┌──────────┐
│  API网关  │ ────────────────────> │ 订单服务  │
│          │   span_id=span-1      │          │
└──────────┘                       │  span_id=span-2 │
                                   │           │
                                   │ span_id=span-3 │
                                   v            v
                              ┌──────────┐  ┌──────────┐
                              │ 库存服务  │  │ 支付服务  │
                              │ span-3   │  │ span-4   │
                              └──────────┘  └──────────┘

4.2 Express + HTTP 跨服务链路

// 服务 A:用户服务(上游)
const { trace } = require('@opentelemetry/api');

app.get('/users/:id/orders', async (req, res) => {
    const span = trace.getActiveSpan();
    span?.setAttribute('user.id', req.params.id);

    // traceparent header 会自动注入
    const orders = await fetch(`http://order-service/orders?userId=${req.params.id}`);
    res.json(await orders.json());
});

// 服务 B:订单服务(下游)
// 自动埋点已处理 traceparent 提取,无需额外代码
app.get('/orders', (req, res) => {
    const span = trace.getActiveSpan();
    // span 中已经包含了从上游传递的 trace_id
    span?.addEvent('database-query-start', { query: 'SELECT * FROM orders' });
    // ...
    res.json(orders);
});

4.3 消息队列异步链路

const amqp = require('amqplib');
const { context, propagation, trace } = require('@opentelemetry/api');

// 生产者:将追踪上下文注入消息头
async function publishOrderCreated(order) {
    const span = trace.getActiveSpan();
    const headers = {};
    propagation.inject(context.active(), headers);

    await channel.publish('orders', 'created', Buffer.from(JSON.stringify(order)), {
        headers,  // traceparent 和 baggage 随消息传递
    });
    span?.addEvent('order-published-to-queue', { orderId: order.id });
}

// 消费者:提取追踪上下文并续接链路
async function consumeOrders() {
    await channel.consume('orders.created', (msg) => {
        const parentContext = propagation.extract(context.active(), msg.properties.headers);
        
        // 在提取的上下文中创建子 Span
        context.with(parentContext, async () => {
            const tracer = trace.getTracer('consumer');
            await tracer.startActiveSpan('consume-order-created', async (span) => {
                const order = JSON.parse(msg.content.toString());
                span.setAttribute('order.id', order.id);
                await processOrder(order);
                span.end();
            });
        });

        channel.ack(msg);
    });
}

5. Prometheus 指标采集与应用指标

5.1 暴露 /metrics 端点

npm install prom-client
// metrics.js
const client = require('prom-client');

// 默认指标:GC、进程内存、CPU、事件循环延迟等
client.collectDefaultMetrics({
    prefix: 'nodejs_',
    gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5],
});

// 自定义计数器:记录 HTTP 请求总数
const httpRequestsTotal = new client.Counter({
    name: 'http_requests_total',
    help: 'Total number of HTTP requests',
    labelNames: ['method', 'route', 'status'],
});

// 自定义直方图:请求延迟分布
const httpRequestDuration = new client.Histogram({
    name: 'http_request_duration_seconds',
    help: 'HTTP request duration in seconds',
    labelNames: ['method', 'route', 'status'],
    buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5],
});

// 自定义 Gauge:当前活跃连接数
const activeConnections = new client.Gauge({
    name: 'http_active_connections',
    help: 'Number of active HTTP connections',
});

// Express 中间件
function metricsMiddleware(req, res, next) {
    const end = httpRequestDuration.startTimer();
    activeConnections.inc();

    res.on('finish', () => {
        const route = req.route?.path || req.path;
        const labels = { method: req.method, route, status: res.statusCode };
        httpRequestsTotal.inc(labels);
        end(labels);
        activeConnections.dec();
    });
    next();
}

// /metrics 端点供 Prometheus 抓取
app.get('/metrics', async (req, res) => {
    res.set('Content-Type', client.register.contentType);
    res.end(await client.register.metrics());
});

module.exports = { metricsMiddleware };

5.2 业务自定义指标

const { Counter, Gauge, Summary } = require('prom-client');

// 订单业务指标
const ordersCreated = new Counter({
    name: 'orders_created_total',
    help: 'Total orders created',
    labelNames: ['category', 'payment_method'],
});

const orderAmount = new Summary({
    name: 'order_amount_summary',
    help: 'Order amount statistics',
    labelNames: ['category'],
    percentiles: [0.5, 0.95, 0.99],
});

const activeUsers = new Gauge({
    name: 'active_users_current',
    help: 'Currently active users',
});

// 使用
ordersCreated.inc({ category: 'electronics', payment_method: 'alipay' });
orderAmount.observe({ category: 'electronics' }, 999.00);
activeUsers.set(1428);

5.3 四种指标类型速查

类型说明适用场景
Counter只增不减请求数、订单数、错误数
Gauge可增可减当前连接数、队列长度、CPU 使用率
Histogram分桶计数请求延迟(自带 bucket)、响应大小
Summary滑动时间窗口分位值p99 延迟(客户端计算)

6. 健康检查、就绪性与存活探针

容器编排平台(Kubernetes)通过探针决定流量是否路由到 Pod,以及是否应当重启容器。

// health.js
const express = require('express');
const router = express.Router();

let isReady = false;
let isHealthy = true;

// 启动时预热完成后设为 true
setTimeout(() => { isReady = true; }, 5000);

// 存活探针(Liveness)—— 最简单的检查
router.get('/healthz', (req, res) => {
    if (!isHealthy) {
        return res.status(503).json({ status: 'unhealthy' });
    }
    res.json({ status: 'ok', uptime: process.uptime() });
});

// 就绪探针(Readiness)—— 检查依赖是否就绪
router.get('/readyz', async (req, res) => {
    const checks = await Promise.all([
        checkDatabase(),      // 数据库连接
        checkRedis(),         // 缓存连接
        checkMessageQueue(),  // 消息队列
    ]);
    
    const allReady = isReady && checks.every(c => c.ok);
    if (!allReady) {
        return res.status(503).json({
            status: 'not ready',
            checks: checks.filter(c => !c.ok).map(c => c.name),
        });
    }
    res.json({ status: 'ready', checks: checks.map(c => c.name) });
});

// 启动探针(Startup)—— 慢启动服务
router.get('/startup', (req, res) => {
    if (isReady) return res.json({ status: 'started' });
    res.status(503).json({ status: 'starting' });
});

async function checkDatabase() {
    try {
        await db.raw('SELECT 1');
        return { name: 'database', ok: true };
    } catch (err) {
        return { name: 'database', ok: false, error: err.message };
    }
}

module.exports = router;
# Kubernetes 探针配置示例
livenessProbe:
  httpGet:
    path: /healthz
    port: 3000
  initialDelaySeconds: 10
  periodSeconds: 5
  failureThreshold: 3       # 连续 3 次失败才重启

readinessProbe:
  httpGet:
    path: /readyz
    port: 3000
  initialDelaySeconds: 5
  periodSeconds: 3

startupProbe:
  httpGet:
    path: /startup
    port: 3000
  failureThreshold: 30      # 允许最长 30 * 10 = 300s 启动时间
  periodSeconds: 10

7. Sentry 错误追踪

线上错误需要比日志更丰富的上下文:异常堆栈、用户信息、问题聚合、发布版本关联和影响面分析。单纯依赖日志捕获错误往往面临三个困境:一是相同错误的日志反复出现,淹没在大量重复信息中;二是日志缺乏用户上下文,难以判断影响面;三是不同环境(测试、预发布、生产)的错误混杂在一起,难以区分。Sentry 通过指纹算法将相同根因的异常聚合为单一 Issue,并自动关联最近的代码变更和发布版本,极大地缩短了从发现到修复的 MTTR(平均修复时间)。

npm install @sentry/node @sentry/profiling-node
// sentry.js
const Sentry = require('@sentry/node');
const { nodeProfilingIntegration } = require('@sentry/profiling-node');

Sentry.init({
    dsn: process.env.SENTRY_DSN,
    environment: process.env.NODE_ENV,
    release: process.env.APP_VERSION || '1.0.0',
    integrations: [
        Sentry.httpIntegration(),
        Sentry.expressIntegration(),
        nodeProfilingIntegration(),  // CPU Profiling(商业版功能)
    ],
    tracesSampleRate: 0.1,        // 10% 请求采样追踪
    profilesSampleRate: 0.05,     // 5% 请求采样 CPU Profile
    // 忽略常见噪音错误
    ignoreErrors: ['AbortError', 'ECONNRESET', 'ETIMEDOUT'],
    beforeSend(event) {
        // 脱敏:移除可能包含 PII 的字段
        if (event.request?.headers?.authorization) {
            event.request.headers.authorization = '[Filtered]';
        }
        return event;
    },
});

module.exports = Sentry;
// app.js
const Sentry = require('./sentry');
const express = require('express');
const app = express();

// Sentry 请求处理器必须在第一个
app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.tracingHandler());

app.get('/orders/:id', async (req, res, next) => {
    try {
        // 手动设置额外上下文
        Sentry.setUser({ id: req.user?.id, email: req.user?.email });
        Sentry.setTag('order.id', req.params.id);
        Sentry.setContext('order', { id: req.params.id, source: 'api' });

        const order = await getOrder(req.params.id);
        if (!order) {
            // 业务异常也以 error 级别上报
            Sentry.captureException(new Error('Order not found'));
            return res.status(404).json({ error: 'Order not found' });
        }
        res.json(order);
    } catch (err) {
        next(err);
    }
});

// Sentry 错误处理器必须在最后
app.use(Sentry.Handlers.errorHandler());

// 兜底错误处理
app.use((err, req, res, next) => {
    res.status(500).json({ error: 'Internal server error' });
});

Sentry 的核心价值在于问题聚合 —— 将 10 万次相同的 TypeError: Cannot read property 'id' of undefined 聚合成单一 Issue,附带首次出现时间、最后出现时间、影响用户数、关联的 release 版本和 codeowners 等等。


8. 性能剖析:0x 与 Clinic.js

当指标显示延迟升高或 CPU 飙高时,需要深入运行时进行剖析。与日志和指标不同,Profiling 不是持续进行的观测手段,而是按需触发的深度诊断工具。Node.js 基于 V8 引擎,提供了多种 Profiling 方式,从基础的 CPU 采样到内存堆快照,从第三方工具到内置能力,覆盖了线上和线下两种场景。理想的做法是:线上环境保留 Profiling 接口但不自动采集,当告警触发时通过自动化脚本发起采集请求,生成火焰图和堆快照供开发团队分析。

8.1 0x:火焰图生成

# 全局安装
npm install -g 0x

# 生成火焰图(采集样本后自动生成 HTML)
0x -- node app.js

# 压力测试同时采集
0x --collect-only -- node app.js
# 另一终端:npx autocannon -c 100 -d 30 http://localhost:3000
# 然后 Ctrl+C,0x 自动生成火焰图

火焰图阅读指南

  • 横轴 = 执行时长(越宽越耗时)
  • 纵轴 = 调用栈深度(从下往上是调用链)
  • 颜色 = 不同函数的随机区分色,不代表温度
  • 顶部的宽平层 = CPU 热点函数

8.2 Clinic.js:多维度诊断

Clinic.js 是 NearForm(Node.js 核心贡献公司)开发的诊断套件,包含三个工具:

npm install -g clinic

# Doctor — 综合评价:CPU、内存、事件循环、句柄泄漏
clinic doctor -- node app.js
# 输出:HTML 报告,标记 **warning/error** 项

# Bubbleprof — 异步流可视化(哪个异步操作耗时最长)
clinic bubbleprof -- node app.js
# 输出:气泡图展示异步调用关系

# Flame — 优化版火焰图(比 0x 更精准)
clinic flame -- node app.js
# 输出:可交互式火焰图,支持反向视图

8.3 Node.js 内置 Profiler

# V8 CPU Profile(无需额外安装)
node --prof app.js
# 压力测试后停止
node --prof-process isolate-0x*-v8.log > processed.txt
# 查看 processed.txt 中各函数的 Self 时间和调用次数

# V8 Heap Snapshot(分析内存泄漏)
node --inspect app.js
# 连接 Chrome DevTools → Memory → Take Heap Snapshot
# 对比两次 Snapshot 的 Delta,寻找泄漏对象

9. Grafana 仪表盘构建

可观测性数据的最终消费界面是仪表盘。建议遵循 USE 法(Utilization、Saturation、Errors)和 RED 法(Rate、Errors、Duration)设计面板。

9.1 Node.js 核心仪表盘(PromQL 查询)

# 左侧:RED 指标(服务健康)
# 请求速率
sum(rate(http_requests_total[5m])) by (route)
# 错误率
sum(rate(http_requests_total{status=~"5.."}[5m])) by (route)
# 延迟分位值
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))

# 中间:资源使用(USE 指标)
# CPU 使用率
irate(process_cpu_user_seconds_total[5m])
# 内存堆使用量
nodejs_heap_size_used_bytes / 1024 / 1024
# 事件循环延迟
nodejs_eventloop_lag_seconds
# 活跃句柄数
nodejs_active_handles_total

# 右侧:业务指标
# 当前活跃连接
http_active_connections
# 自定义业务计数
rate(orders_created_total[5m])
# GC 频率
rate(nodejs_gc_runs_total[5m])

9.2 追踪可视化(Jaeger / Grafana Tempo)

将 OpenTelemetry Collector 配置为写入 Tempo(或 Jaeger),在 Grafana 中通过 Trace to LogsTrace to Metrics 关联跳转:

链路追踪面板(Tempo)
  ├── 搜索:trace_id = abc123
  ├── 拓扑图展示服务调用关系
  ├── 点击某个 Span → 关联 Logs(Loki)
  ├── 点击某个时间戳 → 关联 Metrics(Prometheus)
  └── 火焰图展示 Span 层次结构

10. 告警规则与值班集成

仪表盘用于日常观察,告警规则用于异常通知。原则是:告警必须可行动(Actionable),避免"狼来了"效应。 告警 fatigue(告警疲劳)是生产环境中常见的问题:当告警频繁触发却无人响应,或者大部分告警最终都被证明是误报时,工程师会渐渐对告警麻木,真正严重的故障反而被忽视。设计告警时应该遵循几个原则:每个告警都应该有明确的处理手册和数据支撑;告警阈值不是静态不变的,应该随着业务增长和系统容量变化而动态调整; Critical 级别的告警数量应该尽可能少,理想情况下每周不超过几次。另外,告警规则本身也应该被监控——如果一条告警连续一周都没有触发过,需要检查告警表达式是否仍然有效。

10.1 Prometheus 告警规则

# alerts.yml
groups:
  - name: nodejs-service
    interval: 1m
    rules:
      # 高错误率告警(持续 2 分钟触发)
      - alert: HighErrorRate
        expr: |
          sum(rate(http_requests_total{status=~"5.."}[5m]))
          /
          sum(rate(http_requests_total[5m])) > 0.05
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "服务错误率超过 5%"
          description: "{{ $labels.service }} 错误率 {{ $value | humanizePercentage }}"

      # 高延迟告警(p99 超过 1s 持续 3 分钟)
      - alert: HighLatency
        expr: |
          histogram_quantile(0.99,
            rate(http_request_duration_seconds_bucket[5m])
          ) > 1
        for: 3m
        labels:
          severity: warning
        annotations:
          summary: "p99 延迟超过 1s"

      # 内存使用过高
      - alert: HighMemoryUsage
        expr: |
          nodejs_heap_size_used_bytes
          /
          nodejs_heap_size_total_bytes > 0.85
        for: 5m
        labels:
          severity: warning

      # 事件循环阻塞
      - alert: EventLoopBlocked
        expr: nodejs_eventloop_lag_seconds > 0.5
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "事件循环延迟超过 500ms"

      # 客户端连接异常(下游服务雪崩预警)
      - alert: UpstreamErrors
        expr: |
          sum(rate(http_client_errors_total[5m]))
          /
          sum(rate(http_client_requests_total[5m])) > 0.1
        for: 2m
        labels:
          severity: warning

10.2 Alertmanager 路由与值班集成

# alertmanager.yml
route:
  group_by: ['alertname', 'severity']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  receiver: 'oncall'
  routes:
    - match:
        severity: critical
      receiver: 'pagerduty'
      group_wait: 0s  # 立即发送
    - match:
        severity: warning
      receiver: 'slack'
      continue: true

receivers:
  - name: 'oncall'
    slack_configs:
      - api_url: '${SLACK_WEBHOOK_URL}'
        channel: '#alerts'
        title: '⚠️ {{ .GroupLabels.alertname }}'
        text: '{{ range .Alerts }}{{ .Annotations.summary }}{{ end }}'

  - name: 'pagerduty'
    pagerduty_configs:
      - service_key: '${PAGERDUTY_KEY}'
        severity: critical
        description: '{{ .GroupLabels.alertname }}: {{ .CommonAnnotations.summary }}'

inhibit_rules:
  # 若 node 宕机,抑制该 node 上所有服务的报警
  - source_match:
      severity: 'critical'
    target_match:
      severity: 'warning'
    equal: ['instance']

10.3 告警分级实践

级别通知方式响应时间示例
P0(Critical)电话 + PagerDuty + Slack5 分钟全站不可用、数据严重不一致、p99 > 5s
P1(Warning)Slack + 邮件30 分钟单pod重启、磁盘使用率 > 85%、错误率 > 1%
P2(Info)邮件工作日处理版本发布通知、配置变更、证书即将过期

可观测性实施路线图

阶段一:基础(Week 1-2)

  • 统一 Pino 结构化日志,所有服务输出 JSON
  • 部署日志收集(Loki / ELK / Datadog)
  • 接入 Sentry,所有未捕获异常自动上报

阶段二:指标与追踪(Week 3-4)

  • 接入 OpenTelemetry 自动埋点
  • 部署 Prometheus + Grafana,配置核心大盘
  • 设置 /healthz、/readyz 探针

阶段三:深度(Week 5-6)

  • 业务关键路径手动 Span 埋点
  • 配置 Prometheus 告警规则 + Alertmanager
  • 建立值班轮询与事后复盘(Postmortem)流程

阶段四:成熟(长期)

  • 全链路追踪覆盖 100% 核心交易
  • 自动化 Profiling(慢请求自动触发火焰图)
  • SLO/SLI 定义,错误预算驱动发布节奏

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「nodejs」更多文章

  1. Node.js ORM 深度对比:Prisma、TypeORM、Sequelize 与 Drizzle
  2. Node.js 设计模式与最佳实践:从 SOLID 到六边形架构
  3. Node.js 高级测试策略:从单元测试到混沌工程的完整实践