
为什么你的应用会以你从未预料到的方式失败
你对自己的应用进行了负载测试,谨慎地部署了它,它在正常条件下表现良好。然后凌晨2点流量激增,一个慢数据库查询开始阻塞连接池,内存使用量在12小时内逐渐攀升,你的应用崩溃了。等你发现时,成千上万的用户已经放弃了你的服务。
可观测性——随时了解你的应用在做什么——可以防止这种情况。可观测性的三大支柱是指标、日志和链路追踪。本指南将向你展示如何实现这三者。

可观测性的三大支柱
1. 指标:正在发生什么(现在和随时间变化)
指标是随时间采样的数值测量:CPU使用率、请求速率、错误率、响应时间百分位数。它们回答“是否出了问题?”
指标示例:
- http_requests_total{method="GET", status="200"} = 15203
- http_request_duration_seconds{p99} = 0.234
- nodejs_heap_used_bytes = 142606336
- database_connections_active = 12
2. 日志:发生了什么(事件)
日志是带有上下文的离散事件。它们回答“到底发生了什么,为什么?”
{
"timestamp": "2026-05-28T14:23:11.234Z",
"level": "error",
"message": "数据库连接超时",
"requestId": "req_abc123",
"userId": "user_456",
"query": "SELECT * FROM orders WHERE user_id = ?",
"duration_ms": 30002,
"error": "connect ETIMEDOUT 10.0.0.5:5432"
}
3. 链路追踪:请求如何传播(因果关系)
分布式链路追踪显示请求经过多个服务的路径。它们回答“为什么这个特定请求很慢?”
请求 abc123(总计:234ms)
├── API 网关(2ms)
├── 认证服务(8ms)
├── 用户服务(12ms)
│ └── PostgreSQL 查询(10ms)
└── 订单服务(212ms)← 慢!
├── Redis 缓存未命中(3ms)
└── PostgreSQL 查询(208ms)← 这是瓶颈

搭建 Prometheus + Grafana
Prometheus 从你的应用抓取指标。Grafana 将其可视化。
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports: ["3000:3000"]
prometheus:
image: prom/prometheus:latest
ports: ["9090:9090"]
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
grafana:
image: grafana/grafana:latest
ports: ["3001:3000"]
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana_data:/var/lib/grafana
volumes:
grafana_data:
# prometheus.yml
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'nodejs-app'
static_configs:
- targets: ['app:3000']
metrics_path: '/metrics'
从 Node.js 暴露指标
import express from 'express';
import { register, Counter, Histogram, Gauge } from 'prom-client';
import { collectDefaultMetrics } from 'prom-client';
const app = express();
// 收集默认的 Node.js 指标(CPU、内存、事件循环、GC)
collectDefaultMetrics({ prefix: 'nodejs_' });
// 自定义指标
const httpRequestsTotal = new Counter({
name: 'http_requests_total',
help: 'HTTP 请求总数',
labelNames: ['method', 'route', 'status_code'],
});
const httpRequestDuration = new Histogram({
name: 'http_request_duration_seconds',
help: 'HTTP 请求持续时间(秒)',
labelNames: ['method', 'route'],
buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5],
});
const activeConnections = new Gauge({
name: 'http_active_connections',
help: '活跃 HTTP 连接数',
});
// 中间件:追踪请求
app.use((req, res, next) => {
const end = httpRequestDuration.startTimer({
method: req.method,
route: req.path
});
activeConnections.inc();
res.on('finish', () => {
httpRequestsTotal.inc({
method: req.method,
route: req.route?.path ?? req.path,
status_code: res.statusCode,
});
end(); // 记录持续时间
activeConnections.dec();
});
next();
});
// 暴露指标端点(Prometheus 抓取此端点)
app.get('/metrics', async (req, res) => {
res.set('Content-Type', register.contentType);
res.send(await register.metrics());
});
有用的 Prometheus 查询(PromQL)
# 请求速率(每秒,5分钟窗口)
rate(http_requests_total[5m])
# 错误率百分比
rate(http_requests_total{status_code=~"5.."}[5m])
/ rate(http_requests_total[5m]) * 100
# 99% 延迟
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))
# 内存使用量(MB)
nodejs_heap_used_bytes / 1024 / 1024
# 事件循环延迟(p99)——指示 CPU 压力
histogram_quantile(0.99, rate(nodejs_eventloop_lag_seconds_bucket[5m]))
使用 Winston/Pino 进行结构化日志
// pino 更快,winston 更灵活
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL ?? 'info',
// 生产环境中输出 JSON 用于日志聚合
transport: process.env.NODE_ENV === 'development'
? { target: 'pino-pretty', options: { colorize: true } }
: undefined,
// 为每条日志添加基础元数据
base: {
service: 'api-service',
version: process.env.npm_package_version,
env: process.env.NODE_ENV,
},
// 脱敏敏感字段
redact: ['req.headers.authorization', 'req.body.password'],
});
// 使用子日志器添加上下文
app.use((req, res, next) => {
req.log = logger.child({
requestId: crypto.randomUUID(),
method: req.method,
url: req.url,
});
next();
});
// 在路由处理器中
app.get('/users/:id', async (req, res) => {
req.log.info('正在获取用户');
try {
const user = await db.users.findById(req.params.id);
req.log.info({ userId: user.id }, '用户已找到');
res.json(user);
} catch (error) {
req.log.error({ err: error }, '获取用户失败');
res.status(500).json({ error: '内部服务器错误' });
}
});

OpenTelemetry:分布式链路追踪
// 插桩设置(必须在其他导入之前运行!)
// tracing.js
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { Resource } from '@opentelemetry/resources';
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
const sdk = new NodeSDK({
resource: new Resource({
[ATTR_SERVICE_NAME]: 'my-api-service',
}),
traceExporter: new OTLPTraceExporter({
url: 'http://jaeger:4318/v1/traces', // 或 Tempo、Zipkin 等
}),
// 自动插桩:http、express、pg、redis 等
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
// 优雅关闭
process.on('SIGTERM', () => sdk.shutdown());
// 向链路追踪添加自定义 Span
import { trace, context, SpanStatusCode } from '@opentelemetry/api';
const tracer = trace.getTracer('my-service');
async function processOrder(orderId) {
// 创建一个自定义 Span
return tracer.startActiveSpan('processOrder', async (span) => {
span.setAttribute('order.id', orderId);
span.setAttribute('order.processor', 'v2');
try {
const order = await db.orders.findById(orderId);
span.setAttribute('order.amount', order.total);
// 嵌套 Span 会自动关联
await chargePayment(order);
await updateInventory(order);
await sendConfirmationEmail(order);
span.setStatus({ code: SpanStatusCode.OK });
return order;
} catch (error) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message
});
span.recordException(error);
throw error;
} finally {
span.end(); // 始终结束 Span!
}
});
}
使用 Sentry 进行错误监控
import * as Sentry from '@sentry/node';
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
// 性能监控(捕获链路追踪)
tracesSampleRate: 0.1, // 采样 10% 的事务
// 面包屑:错误前的自动上下文
integrations: [
Sentry.httpIntegration(),
Sentry.expressIntegration(),
Sentry.postgresIntegration(),
],
// 不发送某些错误
ignoreErrors: [
'ResizeObserver loop limit exceeded',
/^Network Error/,
],
beforeSend(event) {
// 擦除敏感数据
if (event.request?.data?.password) {
event.request.data.password = '[REDACTED]';
}
return event;
},
});
// 向错误添加用户上下文
app.use((req, res, next) => {
if (req.user) {
Sentry.setUser({ id: req.user.id, email: req.user.email });
}
next();
});
// 错误处理器必须放在路由之后
app.use(Sentry.expressErrorHandler());
需要监控的关键指标
四个黄金信号(Google SRE)
1. 延迟 - 请求耗时(p50、p95、p99)
2. 流量 - 每秒请求数
3. 错误 - 失败百分比
4. 饱和度 - 接近容量的程度(CPU、内存、队列深度)
Node.js 特定指标
// 事件循环延迟——最重要的 Node.js 指标
// 高延迟 = CPU 密集型代码阻塞异步操作
const Histogram = require('prom-client').Histogram;
const lag = new Histogram({ name: 'eventloop_lag', help: '事件循环延迟' });
let prev = Date.now();
setInterval(() => {
const now = Date.now();
lag.observe((now - prev - 1000) / 1000); // 预期 1000ms,测量偏差
prev = now;
}, 1000);
// 堆使用量
// 持续增长 → 内存泄漏
// GC 暂停时间 → 垃圾回收压力
// 外部内存 → 原生插件、Buffer
告警最佳实践
# Prometheus 告警规则
groups:
- name: app-alerts
rules:
# 高错误率
- alert: HighErrorRate
expr: |
rate(http_requests_total{status_code=~"5.."}[5m])
/ rate(http_requests_total[5m]) > 0.05
for: 2m
labels:
severity: critical
annotations:
summary: "错误率超过 5%"
# 高延迟
- alert: HighLatency
expr: |
histogram_quantile(0.99,
rate(http_request_duration_seconds_bucket[5m])
) > 1.0
for: 5m
labels:
severity: warning
annotations:
summary: "p99 延迟超过 1 秒"
# 内存泄漏指示
- alert: HighMemoryUsage
expr: nodejs_heap_used_bytes / nodejs_heap_size_total_bytes > 0.9
for: 10m
labels:
severity: critical
→ 使用 Benchmark Builder 测量和比较不同代码实现的性能。