部署与运维

可观测性实践:监控、告警与故障定位

系统出现慢请求时,CPU 图、应用日志与链路追踪可能分别正常。正文从 Prometheus 监控栈与告警体系入手,再进入日志与链路追踪、APM 集成,最后用 SLO 与错误预算统一目标和告警,并沿同一请求完成跨层故障定位。

系统出现慢请求时,CPU 图、应用日志和链路追踪可能分别正常。要让这些材料共同解释一次失败,需要先定义用户可感知的目标,再统一请求标识、时间和服务版本,沿调用路径查找异常。可观测性建立在指标、日志与链路追踪三大支柱上:指标说明发生了什么,日志说明为什么发生,链路追踪说明在哪里发生。本文按监控与告警、日志与链路追踪、SLO 与跨层故障定位三部分展开。

一、监控与告警

监控体系三大支柱

支柱 含义 代表工具
指标(Metrics) 可聚合的数值型时间序列数据 Prometheus、InfluxDB、VictoriaMetrics
日志(Logs) 离散的事件记录 Loki、ELK Stack、ClickHouse
链路追踪(Traces) 请求在分布式系统中的完整路径 Jaeger、Zipkin、Tempo

三类信号互补:指标告诉你“发生了什么”(CPU 飙到 95%),日志告诉你“为什么发生”(OOM Killer 杀掉了进程),链路追踪告诉你“在哪里发生的”(哪个微服务调用超时)。本章覆盖指标监控(Prometheus 生态)与告警;日志采集独立维护在日志收集与分析,链路与 APM 见第二章。

Prometheus:时序数据引擎

Prometheus 采用 Pull 模型:主动从目标拉取指标,目标地址由静态配置或服务发现提供;服务发现与拉取是两项独立机制。

plaintext11 行
                  ┌──────────────┐
                  │  Alertmanager │
                  └──────┬───────┘
                         │ 告警推送
┌──────────────┐   ┌─────┴──────┐   ┌──────────────┐
│ node_exporter │──▶│ Prometheus │◀──│ 应用 Exporter │
└──────────────┘   └─────┬──────┘   └──────────────┘
                         │
                  ┌──────┴───────┐
                  │   Grafana    │
                  └──────────────┘

二进制安装(生产环境常用)

bash8 行
PROM_VERSION="2.52.0"
wget https://github.com/prometheus/prometheus/releases/download/v${PROM_VERSION}/prometheus-${PROM_VERSION}.linux-amd64.tar.gz
tar xzf prometheus-${PROM_VERSION}.linux-amd64.tar.gz
sudo mv prometheus-${PROM_VERSION}.linux-amd64/{prometheus,promtool} /usr/local/bin/
sudo mkdir -p /etc/prometheus /var/lib/prometheus

sudo useradd --no-create-home --shell /bin/false prometheus
sudo chown -R prometheus:prometheus /var/lib/prometheus

配置文件 /etc/prometheus/prometheus.yml:

yaml33 行
global:
  scrape_interval: 15s          # 全局采集间隔
  evaluation_interval: 15s      # 规则评估间隔
  scrape_timeout: 10s           # 采集超时

rule_files:
  - /etc/prometheus/rules/*.yml

alerting:
  alertmanagers:
    - static_configs:
        - targets: ['localhost:9093']

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

  - job_name: 'node'
    static_configs:
      - targets:
          - '192.168.1.10:9100'
          - '192.168.1.11:9100'
          - '192.168.1.12:9100'
        labels:
          env: production
          region: cn-east

  - job_name: 'file-sd'
    file_sd_configs:
      - files:
          - /etc/prometheus/targets/*.json
        refresh_interval: 30s

创建 Systemd 服务并启动:

bash27 行
cat <<'EOF' | sudo tee /etc/systemd/system/prometheus.service
[Unit]
Description=Prometheus Monitoring
After=network-online.target
Wants=network-online.target

[Service]
User=prometheus
Group=prometheus
Type=simple
ExecStart=/usr/local/bin/prometheus \
  --config.file=/etc/prometheus/prometheus.yml \
  --storage.tsdb.path=/var/lib/prometheus \
  --storage.tsdb.retention.time=30d \
  --storage.tsdb.retention.size=50GB \
  --web.enable-lifecycle \
  --web.enable-admin-api
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now prometheus

验证:curl -s http://localhost:9090/-/healthy,并确认 systemctl status prometheus 处于 active 状态。

Docker Compose 快速搭建

yaml19 行
version: '3.8'
services:
  prometheus:
    image: prom/prometheus:v2.52.0
    container_name: prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - ./rules:/etc/prometheus/rules
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.retention.time=30d'
      - '--web.enable-lifecycle'
    restart: unless-stopped

volumes:
  prometheus_data:

PromQL 实战

基础查询:

plaintext11 行
# CPU 使用率(所有核心)
100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)

# 内存使用率
(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100

# 磁盘使用率
(1 - node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) * 100

# 网络流入速率(MB/s)
rate(node_network_receive_bytes_total{device="eth0"}[5m]) / 1024 / 1024

进阶技巧:

plaintext14 行
# 增长率:过去 1 小时磁盘写入总量
increase(node_disk_written_bytes_total[1h])

# 预测:基于过去 6 小时趋势,预测 24 小时后磁盘使用量
predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 24*3600)

# 排序:CPU 使用率 Top 5 的实例
topk(5, 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100))

# 比较:当前值 vs 昨日同期
node_load1 offset 1d

# 聚合:按环境汇总 HTTP 请求总量
sum by(env) (rate(http_requests_total[5m]))

常用函数:

函数 用途 示例
rate() 计算每秒增长率 rate(http_requests_total[5m])
increase() 计算区间增量 increase(counter[1h])
irate() 瞬时增长率(更敏感) irate(http_requests_total[5m])
avg_over_time() 区间平均值 avg_over_time(http_requests_total[5m])
histogram_quantile() 百分位计算 histogram_quantile(0.99, ...)
predict_linear() 线性预测 predict_linear(disk_free[6h], 86400)
label_replace() 标签重写 label_replace(up, "host", "$1", "instance", "(.*):.*")

node_exporter:主机指标采集

bash28 行
NODE_VERSION="1.8.0"
wget https://github.com/prometheus/node_exporter/releases/download/v${NODE_VERSION}/node_exporter-${NODE_VERSION}.linux-amd64.tar.gz
tar xzf node_exporter-${NODE_VERSION}.linux-amd64.tar.gz
sudo mv node_exporter-${NODE_VERSION}.linux-amd64/node_exporter /usr/local/bin/

sudo useradd --no-create-home --shell /bin/false node_exporter
cat <<'EOF' | sudo tee /etc/systemd/system/node_exporter.service
[Unit]
Description=Node Exporter
After=network.target

[Service]
User=node_exporter
Group=node_exporter
Type=simple
ExecStart=/usr/local/bin/node_exporter \
  --collector.systemd \
  --collector.processes \
  --collector.tcpstat \
  --web.listen-address=:9100
Restart=always

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now node_exporter

验证:curl -s http://localhost:9100/metrics | head 应返回指标文本。

值得关注的采集器:

采集器 采集内容 启用方式
systemd systemd 单元状态 --collector.systemd
processes 进程状态统计 --collector.processes
tcpstat TCP 连接状态 --collector.tcpstat
textfile 自定义指标文件 --collector.textfile.directory=/var/lib/node_exporter/textfile

textfile 采集器生成自定义指标:

bash8 行
cat <<'EOF' > /usr/local/bin/custom_metrics.sh
#!/bin/bash
# 待更新的包数量
UPDATES=$(apt list --upgradable 2>/dev/null | grep -c upgradable)
echo "node_pending_security_updates ${UPDATES}" > /var/lib/node_exporter/textfile/security_updates.prom
EOF

echo "0 * * * * root /usr/local/bin/custom_metrics.sh" | sudo tee /etc/cron.d/custom_metrics

常用主机指标:

plaintext25 行
# CPU 相关
node_cpu_seconds_total              # CPU 时间(按模式)
node_load1 / node_load5 / node_load15  # 系统负载

# 内存相关
node_memory_MemTotal_bytes          # 总内存
node_memory_MemAvailable_bytes      # 可用内存
node_memory_Buffers_bytes           # Buffer 缓存
node_memory_Cached_bytes            # Page Cache

# 磁盘相关
node_filesystem_size_bytes          # 文件系统总大小
node_filesystem_avail_bytes         # 可用空间
node_disk_io_time_seconds_total     # 磁盘 IO 时间
node_disk_read_bytes_total          # 读取字节数

# 网络相关
node_network_receive_bytes_total    # 接收字节数
node_network_transmit_bytes_total   # 发送字节数
node_network_receive_errs_total     # 接收错误数

# 系统相关
node_time_seconds                   # 当前时间戳
node_boot_time_seconds              # 启动时间戳
node_filefd_allocated               # 已分配文件描述符

Grafana:数据可视化

bash6 行
# Debian/Ubuntu
sudo apt-get install -y apt-transport-https software-properties-common
wget -q -O - https://apt.grafana.com/gpg.key | gpg --dearmor | sudo tee /etc/apt/keyrings/grafana.gpg > /dev/null
echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main" | sudo tee /etc/apt/sources.list.d/grafana.list
sudo apt-get update && sudo apt-get install grafana
sudo systemctl enable --now grafana-server

默认访问 http://localhost:3000,初始账号 admin / admin。通过 API 添加 Prometheus 数据源:

bash3 行
curl -s -X POST http://admin:admin@localhost:3000/api/datasources \
  -H "Content-Type: application/json" \
  -d '{"name": "Prometheus", "type": "prometheus", "url": "http://localhost:9090", "access": "proxy", "isDefault": true}'

社区面板可以直接导入,按需挑选:

Dashboard ID 名称 用途
1860 Node Exporter Full 主机监控全览
9628 Prometheus 2.0 Overview Prometheus 自身监控
12006 Kafka Exporter Kafka 监控
763 Redis Dashboard Redis 监控
12740 Kubernetes Pods K8s Pod 监控

一个最小面板 JSON(CPU 使用率)可作为自定义面板起点:

json28 行
{
  "panels": [
    {
      "title": "CPU 使用率",
      "type": "timeseries",
      "targets": [
        {
          "expr": "100 - (avg by(instance) (rate(node_cpu_seconds_total{mode=\"idle\"}[5m])) * 100)",
          "legendFormat": "{{instance}}"
        }
      ],
      "fieldConfig": {
        "defaults": {
          "unit": "percent",
          "min": 0,
          "max": 100,
          "thresholds": {
            "steps": [
              { "value": null, "color": "green" },
              { "value": 70, "color": "yellow" },
              { "value": 90, "color": "red" }
            ]
          }
        }
      }
    }
  ]
}

常见问题排查:

bash8 行
# 采集目标的健康状态与最近错误
curl -s http://localhost:9090/api/v1/targets | jq '.data.activeTargets[] | select(.health=="down") | {instance, lastError}'

# 规则文件语法
promtool check rules /etc/prometheus/rules/*.yml

# 数据源联通性
curl -s http://localhost:9090/api/v1/query?query=up

监控最佳实践

分层监控策略:

plaintext9 行
┌─────────────────────────────────────────────┐
│  L1: 基础设施层  (CPU/内存/磁盘/网络)        │  ← node_exporter
├─────────────────────────────────────────────┤
│  L2: 平台服务层  (DB/缓存/消息队列/K8s)      │  ← 各组件 Exporter
├─────────────────────────────────────────────┤
│  L3: 应用服务层  (QPS/延迟/错误率/业务指标)   │  ← 应用埋点
├─────────────────────────────────────────────┤
│  L4: 业务指标层  (订单量/转化率/活跃用户)     │  ← 自定义 Metrics
└─────────────────────────────────────────────┘

容量规划与存储估算:

plaintext6 行
# 磁盘容量预测(线性回归)
predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[7d], 30*24*3600)

# Prometheus 存储大小 ~ 每个样本 2 字节
prometheus_tsdb_head_series          # 当前活跃时间序列数
rate(prometheus_tsdb_head_chunks_created_total[1h])  # 每小时新增 chunk 数

生产环境推荐部署两套 Prometheus 做冗余,Alertmanager 用集群去重:

yaml6 行
# alertmanager.yml 中配置集群
cluster:
  listen-address: "0.0.0.0:9094"
  peers:
    - "alertmanager-1:9094"
    - "alertmanager-2:9094"

长期存储可选 Thanos(全局查询与对象存储)、VictoriaMetrics(兼容 Prometheus 协议的高性能时序库)或 Cortex(可水平扩展的 Prometheus 后端)。

监控系统自身也要被监控:

plaintext5 行
prometheus_config_last_reload_successful                    # 配置是否加载成功
prometheus_rule_evaluation_duration_seconds                 # 规则评估耗时
up                                                          # 目标是否可达
alertmanager_notifications_total                            # 通知发送总量
alertmanager_notifications_failed_total                     # 通知失败量

告警体系与分级

一个完整的告警体系由五层组成:

plaintext4 行
数据采集 → 指标计算 → 规则评估 → 告警路由 → 响应处置
Prometheus   PromQL     AlertManager   通知渠道    Runbook/SOP
Telegraf     recording  评估引擎       飞书/钉钉   自动化修复
Exporters    rules      Silences       邮件/SMS    Postmortem

设计目标:每条告警关联明确的响应动作;不同严重程度走不同通道和响应时效;压制噪声;从触发到恢复的记录可追溯。

P0-P4 五级分级:

级别 定义 响应时效 通知方式 示例
P0 致命 核心业务全面不可用 5分钟内响应 电话+短信+IM群 数据库主库宕机、支付链路中断
P1 严重 核心业务部分降级 15分钟内响应 短信+IM群+邮件 API响应延迟大于5s、错误率大于10%
P2 一般 非核心功能异常 30分钟内响应 IM群+邮件 磁盘使用率大于85%、非核心服务重启
P3 提醒 潜在风险预警 4小时内响应 邮件+工单 SSL证书30天内过期、版本落后
P4 观察 趋势关注,无需立即处理 下一工作日 工单 资源使用率缓慢增长

升级机制:

plaintext7 行
┌─────────────┐     15min未响应     ┌─────────────┐     30min未响应     ┌─────────────┐
│  初级值班     │ ──────────────→  │  高级值班     │ ──────────────→  │  值班经理     │
│  (一线)      │                   │  (二线)      │                   │  (三线)      │
└─────────────┘                   └─────────────┘                   └─────────────┘
                                    ↑                                 ↑
                                    │  P0直接通知                      │  P0超30min直接
                                    └─────────────────────────────────┘
yaml17 行
route:
  receiver: 'oncall-primary'
  group_wait: 30s
  routes:
    - match:
        severity: critical
      receiver: 'oncall-primary'
      group_wait: 10s
      routes:
        - receiver: 'oncall-secondary'
          matchers:
            - ack = ""
          repeat_interval: 15m
        - receiver: 'oncall-manager'
          matchers:
            - ack = ""
          repeat_interval: 30m

Alertmanager 基础部署

bash5 行
AM_VERSION="0.27.0"
wget https://github.com/prometheus/alertmanager/releases/download/v${AM_VERSION}/alertmanager-${AM_VERSION}.linux-amd64.tar.gz
tar xzf alertmanager-${AM_VERSION}.linux-amd64.tar.gz
sudo mv alertmanager-${AM_VERSION}.linux-amd64/{alertmanager,amtool} /usr/local/bin/
sudo mkdir -p /etc/alertmanager /var/lib/alertmanager

基础配置 /etc/alertmanager/alertmanager.yml:

yaml45 行
global:
  resolve_timeout: 5m
  smtp_smarthost: 'smtp.example.com:587'
  smtp_from: 'alert@example.com'
  smtp_auth_username: 'alert@example.com'
  smtp_auth_password: 'your-password'
  smtp_require_tls: true

route:
  receiver: 'default-receiver'
  group_by: ['alertname', 'instance']
  group_wait: 30s          # 首次告警等待时间(聚合)
  group_interval: 5m       # 同组告警间隔
  repeat_interval: 4h      # 重复告警间隔
  routes:
    - match:
        severity: critical
      receiver: 'critical-receiver'
      group_wait: 10s
      repeat_interval: 1h
    - match:
        severity: warning
      receiver: 'warning-receiver'
      repeat_interval: 4h

inhibit_rules:
  - source_match:
      severity: 'critical'
    target_match:
      severity: 'warning'
    equal: ['alertname', 'instance']

receivers:
  - name: 'default-receiver'
    email_configs:
      - to: 'ops-team@example.com'
  - name: 'critical-receiver'
    email_configs:
      - to: 'ops-emergency@example.com'
    webhook_configs:
      - url: 'http://localhost:8060/dingtalk/ops/send'
        send_resolved: true
  - name: 'warning-receiver'
    email_configs:
      - to: 'ops-team@example.com'

告警规则文件与验证

创建 /etc/prometheus/rules/node_alerts.yml:

yaml73 行
groups:
  - name: node_alerts
    rules:
      - alert: HighCpuUsage
        expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 85
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "CPU 使用率过高 (实例 {{ $labels.instance }})"
          description: 'CPU 使用率已超过 85%,当前值 {{ $value | printf "%.1f" }}%,持续 5 分钟。'

      - alert: HighMemoryUsage
        expr: (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 > 90
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "内存使用率过高 (实例 {{ $labels.instance }})"
          description: '内存使用率已超过 90%,当前值 {{ $value | printf "%.1f" }}%。'

      - alert: DiskSpaceLow
        expr: (1 - node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"} / node_filesystem_size_bytes) * 100 > 85
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "磁盘空间不足 ({{ $labels.instance }}:{{ $labels.mountpoint }})"
          description: '磁盘 {{ $labels.mountpoint }} 使用率 {{ $value | printf "%.1f" }}%。'

      - alert: DiskWillFillIn24h
        expr: predict_linear(node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"}[6h], 24*3600) < 0
        for: 30m
        labels:
          severity: critical
        annotations:
          summary: "磁盘预计 24 小时内写满 ({{ $labels.instance }}:{{ $labels.mountpoint }})"

      - alert: InstanceDown
        expr: up == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "实例宕机 ({{ $labels.instance }})"
          description: "{{ $labels.job }} 的 {{ $labels.instance }} 已宕机超过 1 分钟。"

      - alert: SystemReboot
        expr: changes(node_boot_time_seconds[15m]) > 0
        labels:
          severity: warning
        annotations:
          summary: "系统发生重启 ({{ $labels.instance }})"

  - name: application_alerts
    rules:
      - alert: HighHttp5xxRate
        expr: sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m])) > 0.05
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "HTTP 5xx 错误率超过 5%"
          description: '当前 5xx 错误率 {{ $value | printf "%.2f" }}%。'

      - alert: HighRequestLatency
        expr: histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le)) > 2
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "P99 请求延迟超过 2 秒"
          description: '当前 P99 延迟 {{ $value | printf "%.2f" }} 秒。'

验证与热加载:

bash3 行
promtool check rules /etc/prometheus/rules/*.yml
curl -X POST http://localhost:9090/-/reload
curl -s http://localhost:9090/api/v1/alerts | jq .

在 Prometheus Web UI 的 Status → Rules 查看规则状态、Alerting → Alerts 查看当前告警;用 amtool config routes test alertname=HighCpuUsage severity=warning 测试路由命中。

告警规则设计

阈值告警适合指标有明确安全边界的场景:

yaml32 行
groups:
  - name: host_alerts
    rules:
      - alert: HighCpuUsage
        expr: |
          100 - (avg by(instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 85
        for: 5m
        labels:
          severity: warning
          team: infra
        annotations:
          summary: "CPU 使用率过高: {{ $labels.instance }}"
          description: 'CPU 使用率 {{ $value | printf "%.1f" }}%,已持续5分钟'
          runbook: "https://wiki.internal/runbook/high-cpu"

      - alert: DiskSpaceRunningLow
        expr: |
          (node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"} / node_filesystem_size_bytes) * 100 < 15
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "磁盘空间不足: {{ $labels.instance }}:{{ $labels.mountpoint }}"
          description: '剩余空间 {{ $value | printf "%.1f" }}%'

      - alert: OOMKillDetected
        expr: increase(node_vmstat_oom_kill[5m]) > 0
        labels:
          severity: critical
        annotations:
          summary: "OOM Kill 发生: {{ $labels.instance }}"
          description: "5分钟内发生 {{ $value }} 次 OOM Kill"

趋势告警针对指标未达阈值但恶化速度异常的场面:

yaml20 行
      - alert: MemoryLeakSuspected
        expr: |
          deriv(node_memory_MemAvailable_bytes[30m]) < -104857600  # 30分钟下降>100MB
          and
          node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes < 0.3
        for: 1h
        labels:
          severity: warning
        annotations:
          summary: "疑似内存泄漏: {{ $labels.instance }}"
          description: "可用内存30分钟内下降超过100MB,当前剩余 {{ $value }}"

      - alert: DiskWriteRateSpike
        expr: |
          rate(node_disk_written_bytes_total[5m]) > 3 * rate(node_disk_written_bytes_total[1h] offset 1d)
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "磁盘写入速率异常飙升: {{ $labels.device }}"

同比环比适合有明显周期性的业务指标:

yaml18 行
      - alert: TrafficDropHourOverHour
        expr: |
          sum(rate(http_requests_total[5m])) < 0.5 * sum(rate(http_requests_total[5m] offset 1h))
        for: 10m
        labels:
          severity: critical
        annotations:
          summary: "流量环比骤降50%"
          description: "当前QPS {{ $value }},1小时前为对照值的2倍以上"

      - alert: TrafficDropWeekOverWeek
        expr: |
          sum(rate(http_requests_total[5m])) < 0.3 * sum(rate(http_requests_total[5m] offset 7d))
        for: 15m
        labels:
          severity: warning
        annotations:
          summary: "流量同比大幅下降"

基于统计学的异常检测用动态阈值替代静态阈值:

yaml35 行
groups:
  - name: anomaly_detection
    rules:
      - alert: ResponseTimeAnomaly
        expr: |
          histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))
          >
          (
            avg_over_time(
              histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))[24h:5m]
            )
            +
            3 * stddev_over_time(
              histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))[24h:5m]
            )
          )
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "P99延迟异常偏离: {{ $labels.service }}"
          description: "当前P99 {{ $value }}s,超出历史3σ范围"

      - alert: ErrorRateAnomaly
        expr: |
          sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m]))
          >
          3 * quantile_over_time(0.99,
            (sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m])))[7d:5m]
          )
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "错误率异常飙升,超过历史P99的3倍"

告警降噪

去重:相同 labelset 的告警在一个 group 中只发送一次,直到 resolve,关键是控制 group_by 粒度。

聚合:把相关的多条告警打包成一条通知。

抑制:高优先级告警触发时抑制低优先级的相关告警。

yaml26 行
route:
  group_by: ['alertname', 'cluster', 'service']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  routes:
    - match:
        team: platform
      group_by: ['cluster', 'service']
      group_wait: 60s
      group_interval: 10m
      receiver: 'platform-oncall'

inhibit_rules:
  - source_matchers:
      - severity = "critical"
      - alertname = "ClusterDown"
    target_matchers:
      - severity =~ "warning|info"
    equal: ['cluster']

  - source_matchers:
      - alertname = "ServiceDown"
    target_matchers:
      - alertname =~ "High.*Latency|HighErrorRate"
    equal: ['service']

静默用于维护窗口和已知问题期间的临时屏蔽:

bash10 行
# 2 小时维护窗口
amtool silence add \
  --author="ops-team" \
  --comment="数据库维护窗口 2026-06-10 02:00-04:00" \
  --duration=2h \
  alertname=~"Database.*" \
  cluster="prod-cn-east"

amtool silence query
amtool silence expire <silence-id>

通过 API 创建静默:

bash9 行
curl -X POST http://alertmanager:9093/api/v2/silences \
  -H "Content-Type: application/json" \
  -d '{
    "matchers": [{"name": "service", "value": "payment", "isRegex": false}],
    "startsAt": "2026-06-10T02:00:00Z",
    "endsAt": "2026-06-10T04:00:00Z",
    "createdBy": "ops-bot",
    "comment": "支付服务计划维护"
  }'

标签规范化避免同类告警分裂:

yaml7 行
alerting:
  alert_relabel_configs:
    - source_labels: [instance]
      regex: '(.+):\d+'
      target_label: instance
    - action: labeldrop
      regex: 'request_id|trace_id'

规则本身保持克制:设置明确的 for 时长避免瞬时抖动误报,抑制与聚合规则定期 review,删除反复触发却无行动的规则。过宽的抑制可能隐藏独立事件,因此用两个同时发生的故障检查规则。

路由树与通知渠道

路由树决定“什么样的告警发给谁”:

yaml74 行
global:
  resolve_timeout: 5m
  smtp_from: 'alertmanager@company.com'
  smtp_smarthost: 'smtp.company.com:587'
  smtp_auth_username: 'alertmanager@company.com'
  smtp_auth_password: '***'
  slack_api_url: 'https://hooks.slack.com/services/xxx'

route:
  receiver: 'default-webhook'
  group_by: ['alertname', 'cluster', 'namespace']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h

  routes:
    - matchers:
        - severity = "critical"
      receiver: 'p0-pager'
      group_wait: 10s
      group_interval: 1m
      repeat_interval: 5m
      routes:
        - matchers:
            - team = "dba"
          receiver: 'dba-oncall'
        - matchers:
            - team = "network"
          receiver: 'network-oncall'

    - matchers:
        - severity = "warning"
      receiver: 'p1-webhook'
      group_wait: 1m
      repeat_interval: 1h

    - matchers:
        - severity =~ "info|none"
      receiver: 'low-priority-email'
      repeat_interval: 12h

receivers:
  - name: 'default-webhook'
    webhook_configs:
      - url: 'http://alert-gateway:8080/webhook'
        send_resolved: true

  - name: 'p0-pager'
    webhook_configs:
      - url: 'http://alert-gateway:8080/webhook?level=p0'
    slack_configs:
      - channel: '#alert-critical'
        title: ' P0 告警'
        text: '{{ range .Alerts }}{{ .Annotations.summary }}\n{{ end }}'
    email_configs:
      - to: 'oncall@company.com'
        headers:
          Subject: '[P0] {{ .GroupLabels.alertname }}'

  - name: 'p1-webhook'
    webhook_configs:
      - url: 'http://alert-gateway:8080/webhook?level=p1'

  - name: 'low-priority-email'
    email_configs:
      - to: 'ops@company.com'

  - name: 'dba-oncall'
    webhook_configs:
      - url: 'http://alert-gateway:8080/webhook?team=dba'

  - name: 'network-oncall'
    webhook_configs:
      - url: 'http://alert-gateway:8080/webhook?team=network'

按维度分组实现精细化路由:

yaml16 行
route:
  routes:
    - matchers:
        - category = "infrastructure"
      group_by: ['cluster', 'env']
      receiver: 'infra-oncall'

    - matchers:
        - category = "application"
      group_by: ['service', 'namespace']
      receiver: 'app-oncall'

    - matchers:
        - category = "business"
      group_by: ['business_line']
      receiver: 'biz-oncall'

通知模板 /etc/alertmanager/templates/custom.tmpl:

plaintext21 行
{{ define "custom.title" }}
[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}]
{{ .GroupLabels.SortedPairs.Values | join " " }}
{{ end }}

{{ define "custom.text" }}
{{ range .Alerts }}
====
**告警名称**: {{ .Labels.alertname }}
**严重程度**: {{ .Labels.severity }}
**实例**: {{ .Labels.instance }}
**集群**: {{ .Labels.cluster }}
**状态**: {{ .Status }}
**开始时间**: {{ .StartsAt.Format "2006-01-02 15:04:05" }}
{{ if .EndsAt }}**恢复时间**: {{ .EndsAt.Format "2006-01-02 15:04:05" }}{{ end }}
**描述**: {{ .Annotations.description }}
{{ if .Annotations.runbook }}**Runbook**: {{ .Annotations.runbook }}{{ end }}
{{ end }}
====
**聚合标签**: {{ .GroupLabels.SortedPairs.Values | join " / " }}
{{ end }}

引用模板与邮件接收器:

yaml9 行
templates:
  - '/etc/alertmanager/templates/*.tmpl'

receivers:
  - name: 'mail-critical'
    email_configs:
      - to: 'oncall@example.com'
        headers:
          Subject: '[{{ .GroupLabels.alertname }}] {{ .Status | toUpper }}'

邮件发送可以用 amtool 触发测试告警:

bash3 行
amtool alert add alertname=TestAlert severity=warning instance=localhost:9090 \
  --annotation=summary="测试告警" \
  --annotation=description="这是一条测试告警"

飞书自定义机器人(脚本读取 Alertmanager Webhook 后转为卡片消息):

bash53 行
cat > feishu-webhook.py << 'EOF'
import json, requests, sys

def send_feishu_alert(webhook_url, alert_data):
    for alert in alert_data.get('alerts', []):
        status = alert.get('status', 'firing')
        severity = alert.get('labels', {}).get('severity', 'unknown')
        card = {
            "msg_type": "interactive",
            "card": {
                "header": {
                    "title": {
                        "tag": "plain_text",
                        "content": f"[{severity.upper()}] {alert['labels'].get('alertname', 'Unknown')}"
                    },
                    "template": "red" if severity == "critical" else "orange" if severity == "warning" else "blue"
                },
                "elements": [
                    {
                        "tag": "div",
                        "fields": [
                            {"is_short": True, "text": {"tag": "lark_md", "content": f"**实例**\n{alert['labels'].get('instance', 'N/A')}"}},
                            {"is_short": True, "text": {"tag": "lark_md", "content": f"**集群**\n{alert['labels'].get('cluster', 'N/A')}"}},
                            {"is_short": True, "text": {"tag": "lark_md", "content": f"**状态**\n{status}"}},
                            {"is_short": True, "text": {"tag": "lark_md", "content": f"**时间**\n{alert.get('startsAt', '')[:19]}"}}
                        ]
                    },
                    {
                        "tag": "div",
                        "text": {"tag": "lark_md", "content": f"**描述**: {alert.get('annotations', {}).get('description', 'N/A')}"}
                    },
                    {
                        "tag": "action",
                        "actions": [
                            {
                                "tag": "button",
                                "text": {"tag": "plain_text", "content": " Runbook"},
                                "url": alert.get('annotations', {}).get('runbook', '#'),
                                "type": "primary"
                            }
                        ]
                    }
                ]
            }
        }
        requests.post(webhook_url, json=card, timeout=5)
        print(f"Sent alert: {alert['labels'].get('alertname')}")

if __name__ == '__main__':
    webhook_url = sys.argv[1]
    data = json.load(sys.stdin)
    send_feishu_alert(webhook_url, data)
EOF

Alertmanager 中接飞书:

yaml5 行
receivers:
  - name: 'feishu-alerts'
    webhook_configs:
      - url: 'http://localhost:9095/alertmanager/feishu'
        send_resolved: true

钉钉通过 prometheus-webhook-dingtalk 中间件转发:

bash4 行
docker run -d --name dingtalk-hook \
  -p 8060:8060 \
  timonwong/prometheus-webhook-dingtalk \
  --ding.profile="ops=https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN"
yaml7 行
targets:
  ops:
    url: https://oapi.dingtalk.com/robot/send?access_token=xxxx
    secret: SECxxxxxxxx
    message:
      title: '{{ template "ding.link.title" . }}'
      text: '{{ template "ding.link.content" . }}'

Alertmanager 中接钉钉:

yaml4 行
receivers:
  - name: 'dingtalk-ops'
    webhook_configs:
      - url: 'http://dingtalk-hook:8060/dingtalk/ops/send'

企业微信:

bash27 行
cat > wecom-webhook.py << 'EOF'
import json, requests, sys

WEBHOOK_BASE = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={key}"

def send_wecom(key, alert_data):
    webhook_url = WEBHOOK_BASE.format(key=key)
    for alert in alert_data.get('alerts', []):
        status = alert['status']
        labels = alert.get('labels', {})
        annotations = alert.get('annotations', {})
        content = f"""## {' 告警触发' if status == 'firing' else ' 告警恢复'}
> **告警名称**: {labels.get('alertname', 'N/A')}
> **严重程度**: {labels.get('severity', 'N/A')}
> **实例**: {labels.get('instance', 'N/A')}
> **集群**: {labels.get('cluster', 'N/A')}
> **时间**: {alert.get('startsAt', '')[:19]}
> **描述**: {annotations.get('description', 'N/A')}
---
[ 查看Runbook]({annotations.get('runbook', '#')})"""
        payload = {"msgtype": "markdown", "markdown": {"content": content}}
        resp = requests.post(webhook_url, json=payload, timeout=5)
        print(f"WeCom response: {resp.json()}")

if __name__ == '__main__':
    send_wecom(sys.argv[1], json.load(sys.stdin))
EOF

Slack:

yaml16 行
receivers:
  - name: 'slack-ops'
    slack_configs:
      - api_url: 'https://hooks.slack.com/services/T00/B00/xxx'
        channel: '#ops-alerts'
        title: '{{ template "slack.title" . }}'
        text: '{{ template "slack.text" . }}'
        color: '{{ if eq .Status "firing" }}danger{{ else }}good{{ end }}'
        send_resolved: true
        actions:
          - type: button
            text: 'Silence'
            url: '{{ template "slack.silenceURL" . }}'
          - type: button
            text: 'Runbook'
            url: '{{ (index .Alerts 0).Annotations.runbook }}'

通用 Webhook 可对接任意自定义系统:

yaml11 行
receivers:
  - name: 'universal-webhook'
    webhook_configs:
      - url: 'http://alert-gateway:8080/api/v1/alerts'
        send_resolved: true
        http_config:
          authorization:
            type: Bearer
            credentials: 'your-api-token'
          follow_redirects: true
        max_alerts: 50

接收端示例(Go):

bash44 行
cat > webhook-gateway.go << 'GOEOF'
package main

import (
    "encoding/json"
    "log"
    "net/http"
    "time"
)

type AlertGroup struct {
    Status       string            `json:"status"`
    Alerts       []Alert           `json:"alerts"`
    GroupLabels  map[string]string `json:"groupLabels"`
    CommonLabels map[string]string `json:"commonLabels"`
}

type Alert struct {
    Status       string            `json:"status"`
    Labels       map[string]string `json:"labels"`
    Annotations  map[string]string `json:"annotations"`
    StartsAt     time.Time         `json:"startsAt"`
    EndsAt       time.Time         `json:"endsAt"`
}

func alertHandler(w http.ResponseWriter, r *http.Request) {
    var group AlertGroup
    if err := json.NewDecoder(r.Body).Decode(&group); err != nil {
        http.Error(w, err.Error(), 400)
        return
    }
    for _, alert := range group.Alerts {
        log.Printf("[%s] %s - %s", alert.Status,
            alert.Labels["alertname"],
            alert.Annotations["summary"])
    }
    w.WriteHeader(200)
}

func main() {
    http.HandleFunc("/api/v1/alerts", alertHandler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}
GOEOF

通知渠道验收:机器人创建成功只代表配置存在。用受控测试告警检查触发、分组、发送、接收与恢复通知;内部进程和资源指标解释失败原因,外部探测反映访问路径是否可用,也要验证监控系统自身断采时能被发现。不要依靠缺失数据自动解释为零错误。

On-Call 与告警治理

轻量轮值排班可用脚本按天或按周轮转,复杂场景建议 PagerDuty / OpsGenie 或自建排班系统。

bash27 行
#!/bin/bash
# oncall-scheduler.sh - 每日值班通知脚本
# 放入 crontab: 0 9 * * * /opt/scripts/oncall-scheduler.sh

DATE=$(date +%Y-%m-%d)

PRIMARY_ONCALL=("张三" "李四" "王五" "赵六")
SECONDARY_ONCALL=("高级A" "高级B" "高级C")

# 按周轮转一线,按天轮转二线
PRIMARY_IDX=$(( ($(date +%s) / 86400 / 7) % ${#PRIMARY_ONCALL[@]} ))
SECONDARY_IDX=$(( ($(date +%s) / 86400) % ${#SECONDARY_ONCALL[@]} ))

PRIMARY=${PRIMARY_ONCALL[$PRIMARY_IDX]}
SECONDARY=${SECONDARY_ONCALL[$SECONDARY_IDX]}

curl -s -X POST "$FEISHU_WEBHOOK" \
  -H "Content-Type: application/json" \
  -d "{
    \"msg_type\": \"interactive\",
    \"card\": {
      \"header\": {\"title\": {\"tag\": \"plain_text\", \"content\": \" 今日值班通知 - $DATE\"}, \"template\": \"green\"},
      \"elements\": [{\"tag\": \"div\", \"text\": {\"tag\": \"lark_md\", \"content\": \"**一线值班**: $PRIMARY\n**二线值班**: $SECONDARY\"}}]
    }
  }"

echo "[$(date)] On-call notification sent: Primary=$PRIMARY, Secondary=$SECONDARY"

值班原则:每班次至少两人(主和备),交接时同步未关闭告警和进行中的事件,记录所有告警处理过程,轮值周期不超过一周;避免同一个人连续值班超过七天、P0 只通知一个人、没有备份值班人员或在值班期间做高风险变更。

告警质量审计:

bash26 行
#!/bin/bash
# alert-audit.sh - 告警质量审计脚本
PROMETHEUS_URL="http://prometheus:9090"

echo "=== 告警质量月度审计 ==="
echo ""

echo "--- 告警规则统计 ---"
total_rules=$(curl -s "$PROMETHEUS_URL/api/v1/rules" | jq '.data.groups[].rules | length' | awk '{sum+=$1} END{print sum}')
echo "总规则数: $total_rules"

echo ""
echo "--- 高频告警 Top 20(过去30天)---"
curl -s "$PROMETHEUS_URL/api/v1/query" \
  --data-urlencode 'query=topk(20, changes(ALERTS_FOR_STATE[30d]))' \
  | jq -r '.data.result[] | "\(.metric.alertname) | \(.value[1]) 次 | severity=\(.metric.severity // "N/A")"'

echo ""
echo "--- 冷却告警(30天未触发,考虑清理)---"
curl -s "$PROMETHEUS_URL/api/v1/rules" \
  | jq -r '.data.groups[].rules[] | select(.type=="alerting") | .name' \
  | sort > /tmp/all_alerts.txt

echo ""
echo "--- 告警响应率(近30天)---"
echo "请从告警管理平台导出数据计算"

告警生命周期:创建 → 审核(同行评审、Runbook 必备、分级标注)→ 上线 → 运营(季度审计,30 天未触发考虑清理,误报率超 50% 优化规则)→ 退役归档。

季度审计清单:每条告警有明确的 severity 标签与 annotations;路由树覆盖所有 team 标签、默认 receiver 已配置;通知模板与值班名单更新;Runbook 经过验证;上月复盘后的改进措施已落地。

推荐按文件组织规则:

plaintext10 行
/etc/prometheus/rules/
├── 00-global.rules.yml         # 全局基础告警
├── 10-node.rules.yml           # 节点级告警
├── 20-container.rules.yml      # 容器/K8s 告警
├── 30-middleware.rules.yml     # 中间件告警(DB/Redis/MQ)
├── 40-application.rules.yml    # 应用级告警
├── 50-business.rules.yml       # 业务指标告警
├── 60-slo.rules.yml            # SLA/SLO 相关告警
├── 90-recording.rules.yml      # Recording rules
└── 99-test.rules.yml           # 测试规则(不带入生产)

联动部署:

yaml41 行
# docker-compose.alerting.yml
services:
  prometheus:
    image: prom/prometheus:v2.52.0
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
      - ./prometheus/rules:/etc/prometheus/rules
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.retention.time=30d'
      - '--web.enable-lifecycle'
    restart: unless-stopped

  alertmanager:
    image: prom/alertmanager:v0.27.0
    ports:
      - "9093:9093"
    volumes:
      - ./alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml
      - ./alertmanager/templates:/etc/alertmanager/templates
      - alertmanager_data:/alertmanager
    command:
      - '--config.file=/etc/alertmanager/alertmanager.yml'
      - '--storage.path=/alertmanager'
      - '--cluster.listen-address=0.0.0.0:9094'
    restart: unless-stopped

  dingtalk-adapter:
    image: timonwong/prometheus-webhook-dingtalk:latest
    ports:
      - "8060:8060"
    volumes:
      - ./dingtalk/config.yml:/etc/prometheus-webhook-dingtalk/config.yml
    restart: unless-stopped

volumes:
  prometheus_data:
  alertmanager_data:

告警响应 Runbook:CPU 持续高占用

先记录告警实例、时间窗与业务影响;CPU 阈值只是调查入口。以下命令在已确认的目标主机执行,PID 需要从进程列表中选择。

bash4 行
uptime
top -bn1 | head -20
mpstat -P ALL 1 5
ps aux --sort=-%cpu | head -10

进一步检查目标进程的线程与调用栈。采样工具、权限和开销需要事先确认;发现高 CPU 时保留证据,避免直接重启丢失现场。

bash2 行
: "${PID:?设置已确认的目标进程 PID}"
top -H -p "$PID"

对照请求量、慢查询、GC 和进程来源判断可能原因。临时限流或限制 CPU 前核对资源单位、目标服务和恢复动作,并观察是否把 CPU 压力转成排队超时。处置后验证请求延迟、错误率及任务完成情况。升级给其他负责人时附已收集证据、已排除方向和当前影响;具体响应时限由值班策略确定。

二、日志、链路追踪与 APM

日志采集与存储统一维护在日志收集与分析的 Alloy 章节,Promtail 已于 2026-03-02 结束支持。指标中保留有限标签,单次请求通过 trace ID 或请求 ID 关联日志;先核对时间同步和实例标识,再解释趋势。本章集中介绍链路追踪与应用性能管理。

链路追踪基础

分布式追踪的核心是:在请求经过的每个服务中埋点,记录调用关系和耗时,把这些片段拼接成一条完整的调用链。

工作流程:

  1. 请求到达入口服务时,生成全局唯一的 Trace ID
  2. 每个服务处理请求时创建一个 Span(跨度),记录操作名、开始/结束时间、状态
  3. Span 之间通过 Parent-Span ID 形成树状结构
  4. 所有 Span 上报到后端存储,拼接成完整的 Trace

Trace 是一个完整请求的全链路记录,由多个 Span 组成;Span 是一个逻辑操作单元,包含 traceId、spanId、parentSpanId、operationName、startTime / endTime、status(OK / ERROR / UNSET)、attributes、events 和 links。

一个典型 Trace 结构:

plaintext7 行
Trace ID: abc123
├── Span A: API Gateway (0-200ms)
│   ├── Span B: User Service (10-150ms)
│   │   └── Span C: MySQL Query (20-80ms)
│   └── Span D: Order Service (30-180ms)
│       ├── Span E: Redis Cache (40-50ms)
│       └── Span F: RabbitMQ Publish (60-70ms)

OpenTracing 与 OpenTelemetry 对比:

特性 OpenTracing OpenTelemetry
定义 追踪 API 规范 统一的可观测性框架
范围 仅追踪 追踪 + 指标 + 日志
状态 已归档,合并入 OTel CNCF 活跃项目,事实标准
厂商锁定 无(仅 API) 无(API + SDK + Collector)

OpenTelemetry(OTel)由 OpenTracing 和 OpenCensus 合并而来,被所有主流云厂商和 APM 工具支持。

OpenTelemetry 安装

架构分三层:应用层(Trace/Metrics/Logs 的 API、SDK 与 Exporter)、Collector(Receiver → Processor → Exporter)、后端存储(Jaeger / Tempo / Prometheus / Loki)。Collector 有 Core 与 Contrib 两种发行版,推荐 Contrib。

bash7 行
docker run -d --name otel-collector \
  -p 4317:4317 \
  -p 4318:4318 \
  -p 8888:8888 \
  -p 8889:8889 \
  -v $(pwd)/otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml \
  otel/opentelemetry-collector-contrib:latest

配置文件 otel-collector-config.yaml:

yaml61 行
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318
  jaeger:
    protocols:
      grpc:
        endpoint: 0.0.0.0:14250
      thrift_http:
        endpoint: 0.0.0.0:14268
  zipkin:
    endpoint: 0.0.0.0:9411

processors:
  batch:
    timeout: 5s
    send_batch_size: 1024
  memory_limiter:
    check_interval: 1s
    limit_mib: 512
    spike_limit_mib: 128
  resource:
    attributes:
      - key: environment
        value: production
        action: upsert
        key: team
        value: platform
        action: upsert

exporters:
  otlp/jaeger:
    endpoint: jaeger:4317
    tls:
      insecure: true
  prometheus:
    endpoint: 0.0.0.0:8889
    namespace: otel
  debug:
    verbosity: basic

extensions:
  health_check:
    endpoint: 0.0.0.0:13133
  zpages:
    endpoint: 0.0.0.0:55679

service:
  extensions: [health_check, zpages]
  pipelines:
    traces:
      receivers: [otlp, jaeger, zipkin]
      processors: [memory_limiter, resource, batch]
      exporters: [otlp/jaeger, debug]
    metrics:
      receivers: [otlp]
      processors: [memory_limiter, resource, batch]
      exporters: [prometheus]

验证:curl http://localhost:13133 返回 OK,且应用上报后能在 Jaeger 查到对应按服务、时间过滤的 Trace。

Jaeger 部署与使用

本地验证用 All-in-One 镜像,存储换成 Elasticsearch:

yaml32 行
version: "3.9"
services:
  jaeger:
    image: jaegertracing/all-in-one:1.58
    environment:
      COLLECTOR_OTLP_ENABLED: "true"
      SPAN_STORAGE_TYPE: "elasticsearch"
      ES_SERVER_URLS: "http://elasticsearch:9200"
      ES_INDEX_PREFIX: "jaeger"
      ES_TAGS_AS_FIELDS_ALL: "true"
    ports:
      - "16686:16686"   # UI
      - "4317:4317"     # OTLP gRPC
      - "4318:4318"     # OTLP HTTP
      - "14250:14250"   # gRPC
      - "14268:14268"   # HTTP Thrift
    depends_on:
      - elasticsearch

  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.14.0
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
      - "ES_JAVA_OPTS=-Xms1g -Xmx1g"
    volumes:
      - es-data:/usr/share/elasticsearch/data
    ports:
      - "9200:9200"

volumes:
  es-data:

生产环境拆成独立组件:

bash17 行
# 1. Elasticsearch 集群(推荐 3 节点以上)
# 2. 分别部署 collect/query/ingester
jaeger-collector \
  --es.server-urls=http://es1:9200,es2:9200,es3:9200 \
  --es.index-jaeger-spans-days=7 \
  --es.index-jaeger-services-days=7 \
  --es.index-jaeger-dependencies-days=7 \
  --es.max-span-age=168h0m0s \
  --collector.otlp.enabled=true

jaeger-query \
  --es.server-urls=http://es1:9200,es2:9200,es3:9200

jaeger-ingester \
  --kafka.consumer.brokers=kafka:9092 \
  --kafka.consumer.topic=jaeger-spans \
  --es.server-urls=http://es1:9200,es2:9200,es3:9200

组件职责:

组件 作用
Agent 部署在每台主机上,接收应用的 Span,转发给 Collector
Collector 接收、验证、处理、存储 Span 到后端存储
Ingester 从 Kafka 消费 Span,写入存储(可选)
Query 提供查询 API 和 UI
Storage 后端存储:Elasticsearch / Cassandra / Badger

采样策略配置文件 sampling-strategies.json:

json25 行
{
  "default_strategy": {
    "type": "probabilistic",
    "param": 0.1
  },
  "service_strategies": [
    {
      "service": "payment-service",
      "type": "rateLimiting",
      "param": 100
    },
    {
      "service": "gateway",
      "type": "probabilistic",
      "param": 0.5,
      "operation_strategies": [
        {
          "operation": "/health",
          "type": "probabilistic",
          "param": 0.01
        }
      ]
    }
  ]
}

采样类型:probabilistic 按比例随机采样(0.1 表示 10%);rateLimiting 限制每秒采样数;remote 由后端远程控制采样率。

UI 核心能力:按服务名、操作名、时间范围、标签、Duration 筛选;火焰图/甘特图查看调用链;选择两个 Trace 做 Diff 定位性能退化;依赖图自动绘制服务间调用拓扑;Monitor 基于 Trace 数据做 RED 指标监控。

Jaeger 与 Zipkin:新项目优先 Jaeger(原生 OTLP、Go 实现、支持远程动态采样),已有 Zipkin 基础设施的可继续使用。

SkyWalking 部署与接入

Java 体系零侵入接入时常用 SkyWalking。OAP 加 UI 加 Elasticsearch 的 Compose 部署:

yaml38 行
version: "3.9"
services:
  oap:
    image: apache/skywalking-oap-server:9.7.0
    environment:
      SW_STORAGE: elasticsearch
      SW_STORAGE_ES_CLUSTER_NODES: elasticsearch:9200
      SW_HEALTH_CHECKER: default
      SW_TELEMETRY: prometheus
    ports:
      - "11800:11800"   # gRPC
      - "12800:12800"   # REST
      - "1234:1234"     # Agent gRPC
    depends_on:
      - elasticsearch

  ui:
    image: apache/skywalking-ui:9.7.0
    environment:
      SW_OAP_ADDRESS: http://oap:12800
    ports:
      - "8080:8080"
    depends_on:
      - oap

  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.14.0
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
      - "ES_JAVA_OPTS=-Xms1g -Xmx1g"
    volumes:
      - sw-es-data:/usr/share/elasticsearch/data
    ports:
      - "9200:9200"

volumes:
  sw-es-data:

Java Agent 配置(命令行方式):

bash10 行
wget https://dlcdn.apache.org/skywalking/java-agent/9.1.0/apache-skywalking-java-agent-9.1.0.tgz
tar -xzf apache-skywalking-java-agent-9.1.0.tgz

java -javaagent:/path/to/skywalking-agent/skywalking-agent.jar \
  -Dskywalking.agent.service_name=my-service \
  -Dskywalking.collector.backend_service=oap-host:11800 \
  -Dskywalking.agent.sample_n_per_3_secs=10 \
  -Dskywalking.agent.ignore_suffix=".jpg,.css,.js,.png" \
  -Dskywalking.agent.trace.ignore_path="/health,/metrics,/actuator/**" \
  -jar my-app.jar

配置文件方式 agent/config/agent.config(推荐):

plaintext6 行
agent.service_name=${SW_AGENT_NAME:my-service}
collector.backend_service=${SW_AGENT_COLLECTOR_BACKEND_SERVICES:oap-host:11800}
agent.sample_n_per_3_secs=${SW_AGENT_SAMPLE_N_PER_3_SECS:10}
agent.ignore_suffix=${SW_IGNORE_SUFFIX:.jpg,.css,.js,.png,.ico}
agent.snapshot.depth=${SW_AGENT_SNAPSHOT_DEPTH:5}
agent.cross_thread_propagation=${SW_AGENT_CROSS_THREAD:true}

OAP 内置告警规则 config/alarm-settings.yml:

yaml41 行
rules:
  service_resp_time_rule:
    metrics-name: service_resp_time
    op: ">"
    threshold: 2000
    period: 10
    count: 3
    silence-period: 5
    message: "服务 {name} 响应时间超过 2 秒(最近 10 分钟内触发 3 次)"

  service_sla_rule:
    metrics-name: service_sla
    op: "<"
    threshold: 8000
    period: 10
    count: 2
    silence-period: 5
    message: "服务 {name} 可用性低于 80%"

  endpoint_resp_time_rule:
    metrics-name: endpoint_avg
    op: ">"
    threshold: 3000
    period: 10
    count: 3
    silence-period: 5
    message: "端点 {name} 平均响应时间超过 3 秒"

  database_access_resp_time_rule:
    metrics-name: database_access_resp_time
    op: ">"
    threshold: 1000
    period: 10
    count: 3
    silence-period: 5
    message: "数据库 {name} 响应时间超过 1 秒"

webhooks:
  - url: http://alertmanager:9095/api/v1/alerts
    receivers:
      - platform-team@company.com

自定义埋点:

java32 行
import org.apache.skywalking.apm.toolkit.trace.Tag;
import org.apache.skywalking.apm.toolkit.trace.Trace;
import org.apache.skywalking.apm.toolkit.trace.ActiveSpan;

public class OrderService {

    @Trace(operationName = "createOrder")
    @Tag(key = "orderId", value = "arg[0]")
    public Order createOrder(String orderId, OrderRequest request) {
        ActiveSpan.tag("userId", request.getUserId());
        ActiveSpan.tag("amount", String.valueOf(request.getAmount()));

        ActiveSpan.info("开始创建订单");
        try {
            Order order = processOrder(orderId, request);
            ActiveSpan.tag("orderStatus", order.getStatus().name());
            return order;
        } catch (Exception e) {
            ActiveSpan.error(e);
            ActiveSpan.tag("error", e.getMessage());
            throw e;
        }
    }

    public void asyncProcess(Order order) {
        Runnable task = RunnableWrapper.of(() -> {
            ActiveSpan.tag("async", "true");
            doAsyncWork(order);
        });
        executor.submit(task);
    }
}

应用性能指标:RED、USE 与四个黄金信号

RED 方法用于面向请求的服务监控:

指标 含义 计算方式
Rate 请求速率 每秒请求数(QPS/RPS)
Errors 错误率 错误请求数 / 总请求数 × 100%
Duration 延迟 请求耗时(通常关注 P50/P95/P99)
plaintext10 行
# 请求速率
rate(http_server_requests_seconds_count{service="order-service"}[5m])

# 错误率
sum(rate(http_server_requests_seconds_count{status=~"5..", service="order-service"}[5m]))
/
sum(rate(http_server_requests_seconds_count{service="order-service"}[5m]))

# P99 延迟
histogram_quantile(0.99, rate(http_server_requests_seconds_bucket{service="order-service"}[5m]))

USE 方法用于基础设施资源监控:

指标 含义 示例
Utilization 利用率 CPU 使用率、内存使用率
Saturation 饱和度 运行队列长度、磁盘 I/O 队列
Errors 错误数 磁盘错误、网络丢包
bash4 行
mpstat -P ALL 1     # CPU 利用率
vmstat 1            # 内存饱和度(swap 使用)
iostat -xz 1        # 磁盘利用率
netstat -i          # 网络错误

四个黄金信号(Google SRE):延迟(Latency)对应 RED 的 Duration,流量(Traffic)对应 RED 的 Rate,错误(Errors)对应 RED 的 Errors,饱和度(Saturation)对应 USE 的 Saturation。服务层面用 RED,基础设施层面用 USE,整体架构层面关注四个黄金信号。

Java 应用集成

Spring Boot + OpenTelemetry

方式一:Java Agent,零代码修改。

yaml14 行
# docker-compose.yml
services:
  app:
    image: my-spring-boot-app:latest
    environment:
      JAVA_TOOL_OPTIONS: "-javaagent:/app/otel-agent.jar"
      OTEL_SERVICE_NAME: order-service
      OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4317
      OTEL_TRACES_SAMPLER: parentbased_traceidratio
      OTEL_TRACES_SAMPLER_ARG: "0.1"
      OTEL_LOGS_EXPORTER: none
      OTEL_METRICS_EXPORTER: prometheus
    volumes:
      - ./opentelemetry-javaagent.jar:/app/otel-agent.jar

方式二:Spring Boot Starter,细粒度控制。

xml19 行
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.opentelemetry</groupId>
            <artifactId>opentelemetry-bom</artifactId>
            <version>1.40.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>io.opentelemetry.instrumentation</groupId>
        <artifactId>opentelemetry-spring-boot-starter</artifactId>
        <version>2.6.0-alpha</version>
    </dependency>
</dependencies>
yaml17 行
# application.yml
otel:
  service:
    name: order-service
  exporter:
    otlp:
      endpoint: http://otel-collector:4317
  traces:
    sampler: parentbased_traceidratio
    sampler-arg: 0.1
  instrumentations:
    spring-webmvc:
      enabled: true
    jdbc:
      enabled: true
    logback-appender:
      enabled: true

SkyWalking Agent + Spring Boot

bash4 行
java -javaagent:/opt/skywalking/agent/skywalking-agent.jar \
  -Dskywalking.agent.service_name=order-service \
  -Dskywalking.collector.backend_service=skywalking-oap:11800 \
  -jar order-service.jar

Docker/K8s 用环境变量配置,并用 Init Container 注入 Agent:

yaml30 行
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  template:
    spec:
      initContainers:
        - name: skywalking-agent
          image: apache/skywalking-java-agent:9.1.0
          command: ['sh', '-c', 'cp -r /agent /skywalking-agent']
          volumeMounts:
            - name: sw-agent
              mountPath: /skywalking-agent
      containers:
        - name: app
          image: order-service:latest
          env:
            - name: JAVA_TOOL_OPTIONS
              value: "-javaagent:/skywalking-agent/skywalking-agent.jar"
            - name: SW_AGENT_NAME
              value: "order-service"
            - name: SW_AGENT_COLLECTOR_BACKEND_SERVICES
              value: "skywalking-oap:11800"
          volumeMounts:
            - name: sw-agent
              mountPath: /skywalking-agent
      volumes:
        - name: sw-agent
          emptyDir: {}

Node.js 应用集成

bash6 行
npm install @opentelemetry/sdk-node \
  @opentelemetry/auto-instrumentations-node \
  @opentelemetry/exporter-trace-otlp-grpc \
  @opentelemetry/exporter-metrics-otlp-grpc \
  @opentelemetry/resources \
  @opentelemetry/semantic-conventions

创建 tracing.ts,必须在应用代码之前加载:

typescript43 行
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc';
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
import { Resource } from '@opentelemetry/resources';
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';

const sdk = new NodeSDK({
  resource: new Resource({
    [ATTR_SERVICE_NAME]: process.env.OTEL_SERVICE_NAME || 'node-service',
    [ATTR_SERVICE_VERSION]: '1.0.0',
    'deployment.environment': process.env.NODE_ENV || 'development',
  }),
  traceExporter: new OTLPTraceExporter({
    url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4317',
  }),
  metricReader: new PeriodicExportingMetricReader({
    exporter: new OTLPMetricExporter({
      url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4317',
    }),
    exportIntervalMillis: 15000,
  }),
  instrumentations: [
    getNodeAutoInstrumentations({
      '@opentelemetry/instrumentation-http': {
        ignoreIncomingPaths: ['/health', '/metrics', '/favicon.ico'],
      },
      '@opentelemetry/instrumentation-express': {
        enabled: true,
      },
      '@opentelemetry/instrumentation-redis': {
        enabled: true,
      },
      '@opentelemetry/instrumentation-mysql2': {
        enabled: true,
      },
    }),
  ],
});

sdk.start();
process.on('SIGTERM', () => sdk.shutdown());

启动方式:

bash6 行
# 使用 --require 预加载
node --require ./tracing.js app.js

# 或者在入口文件最顶部
import './tracing';
import express from 'express';

手动创建 Span:

typescript24 行
import { trace, SpanStatusCode } from '@opentelemetry/api';

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

async function createOrder(orderData: OrderRequest): Promise<Order> {
  return tracer.startActiveSpan('createOrder', async (span) => {
    try {
      span.setAttribute('order.userId', orderData.userId);
      span.setAttribute('order.amount', orderData.amount);
      span.addEvent('validate_order', { 'order.items': orderData.items.length });

      const order = await processOrder(orderData);
      span.setAttribute('order.id', order.id);
      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();
    }
  });
}

装饰器方式需要配合提供 @Trace 装饰器的第三方库,按所选库的实际导出导入:

typescript7 行
class OrderService {
  @Trace('processPayment')
  async processPayment(orderId: string, amount: number): Promise<PaymentResult> {
    const result = await this.paymentGateway.charge(orderId, amount);
    return result;
  }
}

Node.js 自动埋点支持:http / https(状态码、URL、耗时),express / koa / fastify / hapi(路由、中间件耗时),pg / mysql2 / mongodb / redis(SQL/命令、耗时),grpc-js(方法名、状态码),kafkajs / amqplib(Topic、消息数),undici(HTTP 客户端)。

追踪数据分析

延迟分析关注:最长 Span(耗时最大的操作)、串行调用(可并行的调用被串行执行)、重复调用(同服务被多次调用,N+1 问题)、Span 之间的等待时间(可能来自排队或 GC)。

bash6 行
# jaeger-query API 查询慢请求
curl -s "http://jaeger:16686/api/traces?service=order-service&minDuration=2s&limit=20" \
  | jq '.data[] | {traceID, spans: [.spans[] | {operationName, duration}]}'

# 查询包含错误的 Trace
curl -s "http://jaeger:16686/api/traces?service=order-service&tags=%7B%22error%22%3A%22true%22%7D&limit=20"

P99 延迟按服务维度分析:

plaintext14 行
histogram_quantile(0.99,
  sum by (le, service) (
    rate(traces_spanmetrics_latency_bucket[5m])
  )
)

# 延迟 Top 10 操作
topk(10,
  histogram_quantile(0.99,
    sum by (le, operation) (
      rate(traces_spanmetrics_latency_bucket[5m])
    )
  )
)

错误分析检查清单:查看 Span 的 status 确认 ERROR;检查 events 中的异常堆栈;关注 attributes 中的 HTTP 状态码(4xx/5xx);对比正常与异常 Trace 的差异。

依赖拓扑:

bash5 行
# Jaeger 依赖图 API
curl -s "http://jaeger:16686/api/dependencies?endTs=$(date +%s)000&lookback=86400000"

# SkyWalking 拓扑图 API
curl -s "http://skywalking:12800/api/topology/service?duration.start=2024-01-01&duration.end=2024-01-02&duration.step=MINUTE"

分析要点:扇出比(一个服务依赖多少下游)、调用频率(哪些依赖调用量最大)、错误传播(错误从哪个节点开始扩散)。

性能瓶颈识别流程:

plaintext11 行
1. 找到慢 Trace → 查看火焰图
2. 识别最慢的 Span → 确定是 CPU/IO/网络/锁
3. 查看该 Span 的 attributes:
   - db.statement → SQL 慢查询
   - http.url → 外部 API 慢
   - net.peer.name → 网络延迟
4. 关联基础设施指标:
   - CPU 高 → 计算密集
   - IO 高 → 数据库/磁盘瓶颈
   - 网络延迟 → 跨区/跨机房调用
5. 对比历史数据 → 确认是新问题还是老问题

Trace 指标与告警集成

OpenTelemetry Collector 的 spanmetrics processor 可以把 Trace 数据转成 Prometheus 指标:

yaml25 行
processors:
  spanmetrics:
    metrics_exporter: prometheus
    latency_histogram_buckets: [5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 2.5s, 5s, 10s]
    dimensions:
      - name: http.method
      - name: http.status_code
      - name: db.system

exporters:
  prometheus:
    endpoint: 0.0.0.0:8889
    namespace: otel
    const_labels:
      environment: production

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [spanmetrics, batch]
      exporters: [jaeger]
    metrics:
      receivers: [spanmetrics]
      exporters: [prometheus]

生成的关键指标:otel_traces_spanmetrics_latency_bucket(请求总耗时直方图)、otel_traces_spanmetrics_calls_total(请求总数)、otel_traces_spanmetrics_calls_total{status_code="STATUS_CODE_ERROR"}(错误数)。

一个按操作名分组的 Grafana 面板可以使用:

plaintext9 行
# Request Rate (QPS)
sum(rate(otel_traces_spanmetrics_calls_total{service_name="$service"}[5m])) by (operation)

# Error Rate
sum(rate(otel_traces_spanmetrics_calls_total{service_name="$service", status_code="STATUS_CODE_ERROR"}[5m]))
/ sum(rate(otel_traces_spanmetrics_calls_total{service_name="$service"}[5m]))

# P99 Latency
histogram_quantile(0.99, sum(rate(otel_traces_spanmetrics_latency_bucket{service_name="$service"}[5m])) by (le, operation))

告警规则:

yaml37 行
groups:
  - name: tracing-alerts
    rules:
      - alert: HighP99Latency
        expr: |
          histogram_quantile(0.99,
            sum(rate(otel_traces_spanmetrics_latency_bucket[5m])) by (le, service_name)
          ) > 3
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "{{ $labels.service_name }} P99 延迟超过 3 秒"
          description: "当前 P99 延迟: {{ $value }}s"

      - alert: HighErrorRate
        expr: |
          sum(rate(otel_traces_spanmetrics_calls_total{status_code="STATUS_CODE_ERROR"}[5m])) by (service_name)
          /
          sum(rate(otel_traces_spanmetrics_calls_total[5m])) by (service_name)
          > 0.05
        for: 3m
        labels:
          severity: critical
        annotations:
          summary: "{{ $labels.service_name }} 错误率超过 5%"
          description: "当前错误率: {{ $value | humanizePercentage }}"

      - alert: TrafficSpike
        expr: |
          sum(rate(otel_traces_spanmetrics_calls_total[5m])) by (service_name)
          > 3 * sum(rate(otel_traces_spanmetrics_calls_total[1h] offset 1d)) by (service_name)
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "{{ $labels.service_name }} 请求量异常增长"

采样、上下文传播与性能开销

全量采集适用于开发/测试环境(100%);概率采样适合流量稳定的生产环境(1%-10%);速率限制适合高流量服务(100-1000 条/秒);尾部采样保留完整错误链(错误 100%,正常 1%);基于规则按服务/操作差异化。

Collector 端尾部采样:

yaml23 行
processors:
  tail_sampling:
    decision_wait: 10s
    num_traces: 100000
    expected_new_traces_per_sec: 1000
    policies:
      - name: errors
        type: status_code
        status_code:
          status_codes: [ERROR]
      - name: slow-requests
        type: latency
        latency:
          threshold_ms: 3000
      - name: payment-service
        type: string_attribute
        string_attribute:
          key: service.name
          values: [payment-service]
      - name: probabilistic
        type: probabilistic
        probabilistic:
          sampling_percentage: 5

上下文传播使用 W3C Trace Context 标准:

plaintext9 行
# HTTP Header
traceparent: 00-<trace-id>-<span-id>-<trace-flags>
tracestate: vendor1=value1,vendor2=value2

# gRPC Metadata
grpc-trace-bin: <二进制格式>

# Kafka: Header "traceparent"
# RabbitMQ: Message properties

传播要求:所有服务使用相同的传播格式;异步操作(消息队列、定时任务)需要手动传播上下文;跨语言调用统一使用 OTLP 协议。

Java Agent 性能参数:

plaintext7 行
otel.javaagent.exclude-classes=com.example.NoisyClass
otel.instrumentation.http.client.emit-spans=false
otel.instrumentation.common.db-statement-sanitizer.enabled=true

otel.bsp.schedule.delay=5000
otel.bsp.max.queue.size=2048
otel.bsp.max.export.batch.size=512

参考开销:Java Agent 增加约 1-3% CPU、10-30MB 内存;Node.js SDK 增加约 2-5% CPU;Collector 单实例可处理 10,000+ spans/s。

生产部署要点:在应用和存储之间用 Kafka 做缓冲;ES 用 ILM 自动清理过期数据;追踪系统与业务系统分开部署;用独立的 Prometheus 监控 Collector 和存储健康;Span 中不记录密码、Token、身份证号等敏感信息;Collector 启用 TLS。容量参考:每 1000 QPS 约产生 5,000-20,000 spans/s(取决于服务深度),每个 Span 约 1-2 KB,存储 7 天约需 100-500 GB。

ES 索引生命周期管理:

bash22 行
curl -X PUT "http://elasticsearch:9200/_ilm/policy/jaeger-retention" -H 'Content-Type: application/json' -d '{
  "policy": {
    "phases": {
      "hot": {
        "actions": {
          "rollover": {"max_size": "50gb", "max_age": "1d"}
        }
      },
      "warm": {
        "min_age": "3d",
        "actions": {
          "shrink": { "number_of_shards": 1 },
          "forcemerge": { "max_num_segments": 1 }
        }
      },
      "delete": {
        "min_age": "7d",
        "actions": { "delete": {} }
      }
    }
  }
}'

快速选型:

场景 推荐方案
新项目,全栈可观测性 OpenTelemetry + Jaeger + Prometheus + Grafana
已有 Java 微服务,快速接入 SkyWalking(零侵入)
混合语言(Java + Go + Node.js) OpenTelemetry + Jaeger
需要商业支持 Datadog / New Relic / 阿里云 ARMS
低成本起步 OpenTelemetry + Grafana Tempo

Trace 数据受采样、上下文传播与采集背压影响。找不到 span 时,先检查采样规则和传播头,再判断调用是否发生。异步消息需要任务或消息标识辅助关联,不能假设所有工作都保持同一同步调用栈。

先在测试环境用同一个已知请求验证 Trace 入口、上下文传播、导出和查询,再把链路扩展到全站。正文保留的 Jaeger 1.x、旧 SDK 与安装片段属于版本示例。

三、SLO 与跨层故障定位

先定义成功请求

SLI 是观测指标,SLO 是目标。对查询接口,可以定义“观察窗内返回有效结果且延迟不超过阈值的请求占比”。记录哪些请求进入分母、客户端取消如何处理、维护窗口是否排除,并确保定义与真实用户体验一致。

text3 行
合格比例 = 满足全部约束的有效请求数 / 纳入统计的请求数
错误预算 = (1 - SLO 目标比例) × 纳入统计的请求数
预算消耗速率 = 当前坏请求比例 / (1 - SLO 目标比例)

零请求窗口没有可计算的比例;SLO 为 100% 时最后一个公式分母为零,需改用绝对错误或其他处理规则。按请求计算的错误预算与按时间计算的不可用预算应分开维护。SRE Workbook 的 SLO 告警章节介绍了消耗速率与多窗口设计。

错误预算与 SLO 告警

SLO 指标作为 recording rule 预先计算,再围绕消耗速率告警。SLA 的计算可以这样落到 Prometheus:

yaml24 行
# Prometheus recording rules - SLO 计算
groups:
  - name: sla_rules
    interval: 1m
    rules:
      # 服务可用性(按月滚动)
      - record: service:sla:availability_30d
        expr: |
          1 - (
            sum_over_time(http_requests_total{status=~"5.."}[30d])
            /
            sum_over_time(http_requests_total[30d])
          )

      # 错误预算消耗速率
      - record: service:error_budget_burn_rate
        expr: |
          (
            1 - (
              sum(rate(http_requests_total{status!~"5.."}[1h]))
              /
              sum(rate(http_requests_total[1h]))
            )
          ) / (1 - 0.999)  # 假设 SLO 目标 99.9%

错误预算告警需要固定 SLO 分母与低流量处理,避免把无请求窗口或采集缺失解释为服务健康。

三类信号提供不同证据

指标便于观察分布和趋势;日志保留事件与错误细节;Trace 描述一次执行的调用关系。Trace 通常受采样影响,日志也可能因限流或采集故障缺失,缺少记录时应说明证据范围。

关联至少需要时间窗、服务名、部署版本和实例标识。一次请求跨进程时传播 trace context,并在日志中附 trace ID。对于异步队列或批处理任务,还需保留消息或任务 ID,明确它与请求 Trace 的关系。

用同一请求逐层排查

假设入口 P95 延迟上升,按以下顺序缩小范围:

  1. 确认请求量、错误率和延迟统计口径,排除监控断采或路由标签变化。
  2. 在相同时间窗比较入口、应用、数据库与队列指标,判断时间主要花在排队、执行还是网络等待。
  3. 选择慢请求 Trace,检查关键路径、重试和缺失 span。并行子调用的耗时不能简单相加。
  4. 关联对应实例日志、连接池、线程和资源压力,形成可以验证的假设。
  5. 对一个变量进行受控调整,复跑同类请求,检查用户指标与资源指标是否一致改善。

数据库连接等待增长时,增加应用副本可能继续增加争用。需要检查连接池总量、事务时长与数据库容量,再决定限流、优化查询或扩容。

直方图与采样会改变结论

多个实例的 P95 无法直接取平均得到集群 P95。使用兼容桶边界的直方图聚合计数,再计算分位数;同时标注统计窗口和样本量。平均值适合描述总量关系,但可能掩盖长尾。

高基数标签会增加时序数量。用户 ID、请求 ID 和完整 URL 通常保留在日志或 Trace 中,指标使用路由模板、状态类别等有限维度。采样比例变化时,前后 Trace 数量不可直接作为流量增长证据。Prometheus 直方图说明提供了聚合边界。

告警围绕可行动的影响

错误预算快速消耗可触发值班通知,短暂资源尖峰可先记录或工单化。每条告警注明影响、责任人、排查入口与恢复标准;抑制和静默需有范围与过期时间。突发故障可能在发生后才被检测,告警质量不能以提前预测所有故障衡量。

用受控故障验证完整路径:产生用户可见失败,确认指标采集、规则评估、通知送达、值班响应和恢复通知。仅看到规则配置存在,无法证明通知链可用。将检测延迟、通知送达、响应时间和恢复时间分开记录。

演练与事故复盘

演练先限定目标实例、时间、最大影响和停止条件。注入进程退出、网络延迟或依赖失败后,保存时间线与原始记录。真实事故材料与演练结果分别标注,不能把示意日志写成事故证据。

复盘记录用户影响、检测时间、处置时间、支持根因的证据、仍未解释的现象和后续验证。一次修复后的状态恢复需要再经历相同负载或触发条件,才能判断问题是否被解决。复盘模板字段由真实记录填写;没有账单或业务核算依据时,损失金额保持待核算。

字段 填写要求
标题与等级 描述实际影响及分级依据
时间线 发生、发现、响应、缓解、恢复分别记录
用户影响 请求、任务、时间窗和分母
直接损失 数据来源、计算口径与未知项
根因证据 原始日志、指标、变更和反例
已采取动作 动作、执行者、结果与副作用
改进措施 负责人、期限与完成验收

正文覆盖监控、告警、链路与 APM 部署,提供方法与验证设计。

← 返回文章