工程实践 架构心得

分布式系统可观测性实战:Metrics + Logs + Traces 全链路指南

在开始讲技术之前,先厘清一个常见混淆:监控(Monitoring)和可观测性(Observability)不是一回事。

发布于 2026/03/19 4 分钟

当凌晨 3 点的告警响起,你有多少时间定位问题?


目录

  1. 可观测性不等于监控
  2. 可观测性三支柱:Metrics、Logs、Traces
  3. Prometheus + Grafana:Metrics 体系搭建
  4. RED 指标:微服务性能的金钥匙
  5. ELK Stack:结构化日志实战
  6. 分布式追踪:Jaeger 全链路追踪
  7. 银行生产环境案例:一次支付超时问题排查
  8. 告警策略:从狂轰滥炸到精准触达
  9. Grafana Dashboard 模板参考
  10. 可观测性文化建设

1. 可观测性不等于监控

在开始讲技术之前,先厘清一个常见混淆:监控(Monitoring)和可观测性(Observability)不是一回事

监控是被动的:定义指标 → 采集 → 告警 → 响应。它假设你知道问题会出在哪里。

可观测性是主动的:在系统正常和异常时都能回答问题。它不预设问题形态,而是让你能探索未知问题。

监控:"这个 CPU 使用率超过 80% 了吗?"
可观测性:"为什么这个交易延迟从 50ms 增加到 500ms?"
         "这两个指标之间的因果关系是什么?"
         "过去 10 分钟内,这个服务的错误率异常是否与某次部署相关?"

在银行环境中,可观测性尤其重要:

  • 监管要求:交易系统必须有完整的操作记录和可追溯性
  • SLA/SLO 承诺:与监管机构、合作伙伴的协议需要可证明的可用性数据
  • 快速恢复:银行交易中断的直接损失和声誉损失极高,MTTR(Mean Time To Recovery)越短越好

2. 可观测性三支柱:Metrics、Logs、Traces

这三者各有分工,组合起来才能构建完整的可观测性:

维度数据类型解决的问题工具代表
Metrics聚合数值”系统现在健康吗?“Prometheus + Grafana
Logs事件流”发生了什么?“ELK Stack / Loki
Traces请求链路”为什么慢?卡在哪里?“Jaeger / Zipkin

2.1 什么时候用什么

用户报告:支付接口超时

Metrics → 查看支付服务的 P99 延迟趋势,发现从 80ms 飙升到 1200ms

Traces → 查看一条具体慢请求的链路,发现卡在 Oracle 数据库查询(800ms)

Logs → 查看该查询的详细日志,发现缺少索引导致全表扫描

定位根因:上周的数据库迁移导致一个索引丢失

3. Prometheus + Grafana:Metrics 体系搭建

3.1 Prometheus 数据模型

Prometheus 使用时序数据库,每个数据点由四元组标识:

metric_name{label1="value1", label2="value2"} timestamp value
# Kubernetes 中的典型服务指标暴露
# 服务端:Spring Boot Actuator + Micrometer
# (Spring Boot Actuator 已自动暴露 /actuator/prometheus 端点)

# 示例指标:
# payment_service_request_duration_seconds{method="POST", endpoint="/api/payments", quantile="0.99"}
# 0.847

# hikaricp_connections_active{pool="PaymentService-HikariPool"}
# 15

# jvm_gc_pause_seconds_sum{gc="G1 Old Generation", cause="G1 Evacuation Pause"}
# 2.347

3.2 Spring Boot 服务暴露 Prometheus 指标

# pom.xml 添加依赖
# <dependency>
#     <groupId>org.springframework.boot</groupId>
#     <artifactId>spring-boot-starter-actuator</artifactId>
# </dependency>
# <dependency>
#     <groupId>io.micrometer</groupId>
#     <artifactId>micrometer-registry-prometheus</artifactId>
# </dependency>

# application.yml 配置
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  endpoint:
    health:
      show-details: always
  metrics:
    tags:
      application: ${spring.application.name}
      environment: ${DEPLOY_ENV:dev}
    distribution:
      percentiles-histogram:
        http.server.requests: true
      percentiles:
        http.server.requests: 0.5, 0.90, 0.95, 0.99

3.3 Kubernetes 中的 Prometheus Operator

# 1. ServiceMonitor:自动发现并抓取指标
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: payment-service-monitor
  labels:
    release: prometheus  # 必须匹配 Prometheus Operator 的 prometheus.io/scrape: "true"
spec:
  selector:
    matchLabels:
      app: payment-service
  namespaceSelector:
    matchNames:
      - production
  endpoints:
    - port: actuator
      path: /actuator/prometheus
      interval: 15s  # 15 秒采样一次
      scrapeTimeout: 10s

---
# 2. PrometheusRule:定义告警规则
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: payment-service-alerts
spec:
  groups:
    - name: payment-service.rules
      rules:
        - alert: HighErrorRate
          expr: |
            sum(rate(http_server_requests_seconds_count{status=~"5..", job="payment-service"}[5m]))
            /
            sum(rate(http_server_requests_seconds_count{job="payment-service"}[5m])) > 0.01
          for: 2m
          labels:
            severity: critical
          annotations:
            summary: "Payment service error rate > 1%"
            description: "当前错误率: {{ $value | humanizePercentage }}"

4. RED 指标:微服务性能的金钥匙

RED 指标(Rate、Errors、Duration)是微服务性能监控的事实标准:

Rate(QPS):每秒钟处理的请求数
Errors(错误率):每秒失败请求的比例
Duration(延迟):请求的响应时间分布

4.1 在 Grafana 中实现 RED 指标

# Rate:QPS
# 所有 HTTP 请求的 QPS
sum(rate(http_server_requests_seconds_count{service="payment-service"}[1m]))

# 按接口分组的 QPS
sum by (uri) (rate(http_server_requests_seconds_count{service="payment-service"}[1m]))

# Errors:错误率(5xx 比例)
sum(rate(http_server_requests_seconds_count{status=~"5..", service="payment-service"}[1m]))
/
sum(rate(http_server_requests_seconds_count{service="payment-service"}[1m]))

# Duration:P99 延迟
histogram_quantile(0.99,
  sum(rate(http_server_requests_seconds_bucket{service="payment-service"}[5m]))
  by (le)
)

# Duration:P50 平均延迟
histogram_quantile(0.50,
  sum(rate(http_server_requests_seconds_bucket{service="payment-service"}[5m]))
  by (le)
)

4.2 服务健康状态矩阵

┌──────────────────┬─────────────┬─────────────┬──────────────────┐
│ 指标             │ 健康 (Green)│ 警告 (Amber)│ 危险 (Red)        │
├──────────────────┼─────────────┼─────────────┼──────────────────┤
│ QPS              │ > 100 req/s │ 50-100      │ < 50 req/s        │
│                  │ (正常范围)   │             │ (可能服务离线)    │
├──────────────────┼─────────────┼─────────────┼──────────────────┤
│ Error Rate       │ < 0.1%      │ 0.1% - 1%    │ > 1%              │
│                  │             │             │ > 5% 立即响应     │
├──────────────────┼─────────────┼─────────────┼──────────────────┤
│ P50 Latency     │ < 50ms      │ 50 - 200ms  │ > 200ms           │
├──────────────────┼─────────────┼─────────────┼──────────────────┤
│ P95 Latency     │ < 200ms     │ 200 - 500ms  │ > 500ms           │
├──────────────────┼─────────────┼─────────────┼──────────────────┤
│ P99 Latency     │ < 500ms     │ 500 - 1000ms │ > 1000ms          │
├──────────────────┼─────────────┼─────────────┼──────────────────┤
│ Connection Pool │ < 70%       │ 70% - 90%   │ > 90%             │
│ (HikariCP)      │             │             │ > 95% 立即响应    │
└──────────────────┴─────────────┴─────────────┴──────────────────┘

4.3 自定义业务指标

除了技术指标,银行系统还需要监控业务指标:

// 使用 Micrometer 注册自定义业务指标
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;

@Service
public class PaymentService {

    private final Timer paymentTimer;
    private final Counter paymentSuccessCounter;
    private final Counter paymentFailedCounter;
    private final Gauge activePaymentsGauge;

    public PaymentService(MeterRegistry registry) {
        // 计时器:测量支付处理时间
        this.paymentTimer = Timer.builder("payment.process.duration")
            .description("支付处理耗时")
            .tag("service", "payment-service")
            .publishPercentiles(0.5, 0.95, 0.99)
            .register(registry);

        // 成功计数器
        this.paymentSuccessCounter = Counter.builder("payment.success.count")
            .description("支付成功次数")
            .tag("service", "payment-service")
            .register(registry);

        // 失败计数器(按失败原因分类)
        this.paymentFailedCounter = Counter.builder("payment.failed.count")
            .description("支付失败次数")
            .tag("service", "payment-service")
            .register(registry);

        // Gauge:实时监控正在处理的支付数量
        this.activePaymentsGauge = Gauge.builder("payment.active.count", this::getActivePaymentCount)
            .description("正在处理的支付数量")
            .register(registry);
    }

    public PaymentResult processPayment(PaymentRequest request) {
        return paymentTimer.record(() -> {
            try {
                PaymentResult result = doProcess(request);
                paymentSuccessCounter.increment();
                return result;
            } catch (InsufficientBalanceException e) {
                paymentFailedCounter.increment();  // 只计数,不打日志(高频)
                throw e;
            } catch (Exception e) {
                logger.error("支付处理失败", exception("paymentId", request.getPaymentId(), "error", e.getMessage()));
                paymentFailedCounter.increment();
                throw e;
            }
        });
    }
}

5. ELK Stack:结构化日志实战

5.1 为什么结构化日志是必须的

非结构化日志:

2026-03-19 10:23:45 [ERROR] PaymentService - Payment failed for order 12345, amount 5000 HKD, error: insufficient balance

结构化日志(JSON):

{
  "timestamp": "2026-03-19T10:23:45.123Z",
  "level": "ERROR",
  "service": "payment-service",
  "traceId": "a3f7c21d8e9b4",
  "spanId": "2b5d8c3f",
  "paymentId": "PAY-12345",
  "orderId": "ORD-67890",
  "amount": 5000.00,
  "currency": "HKD",
  "errorType": "InsufficientBalanceException",
  "errorMessage": "Account balance insufficient",
  "accountId": "ACC-00123",
  "environment": "production"
}

结构化日志的优势:

  • 可搜索:Elasticsearch 可以精确查询任何字段
  • 可聚合:统计某类错误的出现频率,不需要 grep + awk
  • 可关联:通过 traceId 与 Jaeger 的链路追踪打通
  • 可展示:在 Kibana 中构建可视化 Dashboard

5.2 Spring Boot 中的结构化日志配置

<!-- pom.xml: 使用 Logstash Encoder 输出 JSON 格式 -->
<!-- <dependency>
    <groupId>net.logstash.logback</groupId>
    <artifactId>logstash-logback-encoder</artifactId>
    <version>7.4</version>
</dependency> -->
<!-- logback-spring.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>

    <springProperty scope="context" name="APP_NAME" source="spring.application.name"/>

    <!-- 控制台输出:使用 Spring Boot 默认格式(开发环境可读) -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- JSON 输出:生产环境(给 ELK 消费) -->
    <appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/${APP_NAME}.json.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>logs/${APP_NAME}.%d{yyyy-MM-dd}.json.log.gz</fileNamePattern>
            <maxHistory>30</maxHistory>
            <totalSizeCap>5GB</totalSizeCap>
        </rollingPolicy>
        <encoder class="net.logstash.logback.encoder.LogstashEncoder">
            <includeMdcKeyName>traceId</includeMdcKeyName>
            <includeMdcKeyName>spanId</includeMdcKeyName>
            <includeMdcKeyName>userId</includeMdcKeyName>
            <includeMdcKeyName>accountId</includeMdcKeyName>
            <customFields>{"service":"${APP_NAME}","environment":"${ENVIRONMENT:-dev}"}</customFields>
        </encoder>
    </appender>

    <!-- 生产环境使用 JSON -->
    <springProfile name="prod">
        <root level="INFO">
            <appender-ref ref="JSON_FILE"/>
        </root>
    </springProfile>

    <!-- 开发环境使用控制台 -->
    <springProfile name="dev,default">
        <root level="INFO">
            <appender-ref ref="CONSOLE"/>
        </root>
    </springProfile>
</configuration>

5.3 在日志中关联 Trace ID

// 使用 MDC(Mapped Diagnostic Context)自动注入 traceId
// Spring Cloud Sleuth/Boot 3 的 Micrometer Tracing 已自动处理
// 但在自定义日志点中,手动获取更可靠

import org.slf4j.MDC;

@Service
public class PaymentService {

    private static final Logger LOGGER = LoggerFactory.getLogger(PaymentService.class);

    public PaymentResult processPayment(PaymentRequest request) {
        try {
            // Sleuth 自动注入 traceId 到 MDC,无需手动设置
            // MDC.get("traceId") 可以获取当前请求的 traceId

            LOGGER.info("开始处理支付",
                entry("paymentId", request.getPaymentId()),
                entry("amount", request.getAmount()),
                entry("currency", request.getCurrency())
            );

            // 业务逻辑...

            LOGGER.info("支付处理成功",
                entry("paymentId", result.getPaymentId()),
                entry("transactionId", result.getTransactionId()),
                entry("processingTimeMs", duration.toMillis())
            );

        } catch (InsufficientBalanceException e) {
            // 错误日志:包含完整的上下文信息
            LOGGER.error("支付失败:余额不足",
                exception("paymentId", request.getPaymentId(),
                    "accountId", request.getAccountId(),
                    "availableBalance", account.getBalance(),
                    "requiredAmount", request.getAmount(),
                    "shortfall", shortfall
                )
            );
            throw e;
        }
    }
}

// 提示:entry() 是 Slf4j-Extensions(项目中自定义的日志构建器)
// 用于实现结构化日志的 key-value 对
// 如果没有自定义实现,可以用 MDC 直接设置:
// MDC.put("paymentId", request.getPaymentId());

6. 分布式追踪:Jaeger 全链路追踪

6.1 为什么微服务需要链路追踪

在单体应用中,一个请求在一个进程内完成,排查问题相对简单。在微服务架构中:

用户点击「支付」→
  前端 React (10ms) →
    网关 Kong (5ms) →
      支付服务 (20ms) →
        账户服务 RPC (15ms) →
          Oracle 数据库 (80ms) ← 这里慢了!
        风险控制服务 RPC (30ms) →
          Redis 查询 (5ms) →
        通知服务异步 (2ms)
      支付服务完成 (10ms)
    网关响应 (5ms)
  前端显示 (5ms)

总耗时:约 180ms
如果只是看 Prometheus Metrics,只知道「支付服务处理时间 20ms」
但实际上账户服务已经 80ms 了,迟早成为瓶颈

6.2 Spring Boot 集成 Jaeger

# pom.xml
# <dependency>
#     <groupId>io.opentelemetry</groupId>
#     <artifactId>opentelemetry-exporter-otlp</artifactId>
# </dependency>
# <dependency>
#     <groupId>io.micrometer</groupId>
#     <artifactId>micrometer-tracing-bridge-otel</artifactId>
# </dependency>

# application.yml
spring:
  application:
    name: payment-service
  tracing:
    sampling:
      probability: 1.0  # 生产环境建议 0.1-0.3(全量采样开销大)
    otlp:
      endpoint: http://jaeger-collector:4317

6.3 在代码中使用链路追踪

// 自动埋点:Spring Boot + Spring Cloud Sleuth 会自动追踪:
// - HTTP 请求(入站和出站)
// - 数据库操作(JDBC)
// - Redis 操作
// - Kafka/RabbitMQ 消息发送和消费

// 手动埋点:在关键业务节点添加自定义 Span
import io.micrometer.tracing.Tracer;
import io.micrometer.tracing.Span;

@Service
public class PaymentService {

    private final Tracer tracer;

    public PaymentService(Tracer tracer) {
        this.tracer = tracer;
    }

    public PaymentResult processPayment(PaymentRequest request) {
        // 创建子 Span:在现有链路中添加一个命名节点
        Span span = tracer.nextSpan().name("validate-payment-request");
        try (Tracer.SpanInScope ignored = tracer.withSpan(span)) {
            span.tag("paymentId", request.getPaymentId());
            span.tag("amount", String.valueOf(request.getAmount()));

            // 业务验证
            validatePaymentRequest(request);

            span.event("validation-passed");

            // 继续主流程...
            return doProcess(request);

        } catch (Exception e) {
            span.error(e);  // 将异常记录到链路中
            throw e;
        } finally {
            span.end();  // 结束 Span
        }
    }
}

6.4 Jaeger UI 使用指南

Jaeger Search 页面操作流程:

1. 选择服务:payment-service
2. 选择 Operation:all(查看所有请求)
3. 选择时间范围:Last 30 minutes
4. 输入搜索条件:
   - traceId: a3f7c21d8e9b4  (已知 traceId 时直接搜索)
   - tag: error=true          (搜索错误请求)
   - service.version: 1.2.3   (搜索特定版本的请求)

5. 查看 Trace 详情:
   - Waterfall 图:每个 Span 的耗时和依赖关系
   - Span 信息:
     · service: payment-service
     · operation: POST /api/payments
     · duration: 847ms
     · tags: http.status_code=200, error=false
     · logs: [{timestamp, "message": "支付处理成功"}]

6. 找到慢 Span:
   - 在 Waterfall 中找最长(最宽)的 Span
   - 点击展开,查看详细耗时
   - 如果是 DB 操作,查看 SQL 语句和执行计划

7. 银行生产环境案例:一次支付超时问题排查

7.1 告警触发

📱 告警 (Critical) - 03:47
支付服务 P99 延迟超过 2 秒
当前值: 3.2s | 阈值: 2s | 持续: 5 分钟
服务: payment-service | 环境: production

7.2 排查过程(完整路径)

Step 1:确认影响范围(Metrics)

# 查看错误率和延迟趋势
sum(rate(http_server_requests_seconds_count{status=~"5..", service="payment-service"}[5m])) > 0

# 发现:确实有 504 (Gateway Timeout) 错误出现
# 错误率约 2.3%,不是全部失败

# 查看延迟分布
histogram_quantile(0.99, sum(rate(http_server_requests_seconds_bucket{
  service="payment-service", uri="/api/payments"}[5m])) by (le))

# 发现:P99 从正常的 800ms 飙升到 3200ms
# 但 P50 仍然正常(150ms)
# 说明不是整体变慢,而是部分请求卡住了

Step 2:追踪具体慢请求(Traces → Jaeger)

在 Jaeger 中搜索:
- 服务:payment-service
- 操作:POST /api/payments
- 过滤:duration > 2s
- 时间:过去 30 分钟

找到一条代表性慢请求:traceId = a3f7c21d8e9b

Waterfall 视图:
payment-service [========== 3200ms ==========]
  ├─ validateRequest [50ms]
  ├─ checkAccountBalance [2800ms] ← 这里卡住了!
  │   └─ account-service RPC [2780ms]
  │       └─ Oracle Query [2750ms]
  ├─ checkRisk [300ms]
  └─ sendToPaymentGateway [50ms]

根因定位:account-service 对 Oracle 的查询耗时 2750ms

Step 3:深挖数据库(Logs → Kibana)

在 Kibana 中搜索:
- traceId: a3f7c21d8e9b
- index: account-service-*

找到慢 SQL 日志:
{
  "sql": "SELECT * FROM accounts WHERE account_number = ?",
  "params": ["123456789"],
  "executionTime": 2743ms,
  "executionPlan": "TABLE ACCESS FULL" ← 全表扫描!
  "table": "ACCOUNTS",
  "estimatedRows": 2500000,
  "actualRows": 1
}

问题确认:缺少 account_number 上的索引

Step 4:根因定位

回到 Git:
git log --since="2026-03-15" --oneline -- database/

找到可疑提交:
e4f8a21 feat: add new transaction type column to ACCOUNTS table

查看该提交的 migration 脚本:
ALTER TABLE accounts ADD COLUMN transaction_type VARCHAR(20);

问题确认:添加新列的 migration 脚本包含了
"DROP INDEX idx_account_number"(或者新版本 ORM 框架重建了表)

DBA 紧急处理:
CREATE INDEX idx_account_number ON accounts(account_number);

P99 延迟立即恢复到 800ms

7.3 事后改进

# 1. 添加慢 SQL 告警(自动检测)
- alert: SlowDatabaseQuery
  expr: |
    rate(postgresql_statements_mean_exec_time_seconds{service="account-service"}[5m]) > 1
  for: 1m
  annotations:
    summary: "数据库查询 P99 超过 1 秒"

# 2. 代码层面:禁止在生产环境执行全表扫描的 SQL
# MyBatis XML 或 JPA @Query 中添加:
# /*+ INDEX(idx_account_number) */  -- Oracle Hint

# 3. Database Migration 规范:
# 所有表结构变更必须包含索引变更
# CI/CD 中加入:migration 前后对比索引列表,不一致则 Block

8. 告警策略:从狂轰滥炸到精准触达

8.1 告警疲劳的代价

“告警疲劳”是 SRE 最大的敌人之一。当告警太多、太不准时,团队会选择:

  • 将告警静音
  • 不再认真对待告警
  • 真正的 Critical 告警淹没在噪声中

好的告警标准(Google SRE 红皮书):

  • 每个告警都应该有明确的行动项
  • 告警应该是可操作的(Actionable)
  • PagerDuty 级别的告警(半夜叫醒人):最多每天 1-2 次

8.2 告警分级策略

# 告警分级
P1 - Critical(立即响应):
  - 服务完全不可用(健康检查失败,自动重启)
  - 支付成功率 < 90% 且持续 > 2 分钟
  - 数据库连接池耗尽
  → 触发 PagerDuty,10 分钟内响应

P2 - High(30 分钟内响应):
  - P99 延迟 > 2 秒且持续 > 5 分钟
  - 错误率 > 1% 且持续 > 5 分钟
  - 依赖服务响应超时(> 1 秒)
  → Slack #incidents 频道 + 值班工程师

P3 - Medium(工作时间处理):
  - P95 延迟 > 500ms
  - 磁盘使用率 > 80%
  - 连接池使用率 > 80%
  → Jira Ticket 创建

P4 - Low(计划内处理):
  - 证书即将过期(30 天内)
  - 非核心服务告警
  → Sprint Backlog

8.3 抑制告警(告警聚合)

# Prometheus Alertmanager 抑制规则
# 当"支付网关不可用"时,抑制所有"依赖支付网关的服务"的下游告警
groups:
  - name: silencing
    interval: 30s
    rules:
      - alert: PaymentGatewayDown
        expr: payment_gateway_health == 0
        labels:
          severity: critical
          team: payments
        annotations:
          summary: "支付网关不可用"

      - alert: PaymentServiceErrorSpike
        expr: payment_error_rate > 0.01
        labels:
          severity: high
          team: payments
        annotations:
          summary: "支付服务错误率上升"

# Alertmanager config: 抑制规则
route:
  group_by: ['alertname', 'service']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  receiver: 'slack'
  routes:
    - match:
        alertname: PaymentGatewayDown
      receiver: 'pagerduty-critical'
      # 当 PaymentGatewayDown 触发时,抑制 PaymentServiceErrorSpike
      # 因为服务错误很可能是网关宕机导致的,不需要单独处理
      continue: true

receivers:
  - name: 'slack'
    slack_configs:
      - channel: '#incidents'
        send_resolved: true
        title: |
          {{ if eq .Status "resolved" }}✅{{ else }}🚨{{ end }}
          {{ .GroupLabels.alertname }}
        text: |
          {{ range .Alerts }}
          **{{ .Labels.service }}** - {{ .Annotations.summary }}
          持续时间: {{ .StartsAt | since }}
          {{ end }}

  - name: 'pagerduty-critical'
    pagerduty_configs:
      - service_key: ${PAGERDUTY_KEY}
        severity: critical
        event_action: trigger

9. Grafana Dashboard 模板参考

9.1 微服务健康 Dashboard 变量

# Dashboard 顶部变量(方便切换服务/环境)
# $service: label_values(http_server_requests_seconds_count, service)
# $environment: label_values(http_server_requests_seconds_count, environment)

# 变量查询使用 label_values()
# 这会自动从 Prometheus 中发现所有活跃的服务和标签值

9.2 核心 Panel 配置

// 面板1:服务 QPS(Rate)
{
  "title": "Request Rate (QPS)",
  "type": "stat",
  "gridPos": { "x": 0, "y": 0, "w": 6, "h": 4 },
  "targets": [
    {
      "expr": "sum(rate(http_server_requests_seconds_count{service=\"$service\"}[1m]))",
      "legendFormat": "QPS"
    }
  ],
  "fieldConfig": {
    "defaults": {
      "unit": "reqps",
      "thresholds": {
        "mode": "absolute",
        "steps": [
          { "value": 0, "color": "green" },
          { "value": 50, "color": "amber" },
          { "value": 100, "color": "red" }
        ]
      }
    }
  }
}

// 面板2:P99 延迟趋势(Time Series)
{
  "title": "Latency (P50 / P95 / P99)",
  "type": "timeseries",
  "gridPos": { "x": 6, "y": 0, "w": 18, "h": 8 },
  "targets": [
    {
      "expr": "histogram_quantile(0.50, sum(rate(http_server_requests_seconds_bucket{service=\"$service\"}[5m])) by (le)) * 1000",
      "legendFormat": "P50"
    },
    {
      "expr": "histogram_quantile(0.95, sum(rate(http_server_requests_seconds_bucket{service=\"$service\"}[5m])) by (le)) * 1000",
      "legendFormat": "P95"
    },
    {
      "expr": "histogram_quantile(0.99, sum(rate(http_server_requests_seconds_bucket{service=\"$service\"}[5m])) by (le)) * 1000",
      "legendFormat": "P99"
    }
  ],
  "fieldConfig": {
    "defaults": {
      "unit": "ms",
      "custom": {
        "lineWidth": 2,
        "fillOpacity": 10
      }
    }
  }
}

10. 可观测性文化建设

技术工具只是基础,真正的可观测性需要文化支撑。

10.1 Toil 最小化

如果每次告警都需要人工介入,就是技术负债。可观测性做得好的团队:

  • 自动处理而非人工处理:自动扩容、自动熔断、自动切换
  • 根因告警而非症状告警:告”数据库连接池耗尽”而非告”服务响应慢”
  • 预防性而非响应性:在问题发生前发现趋势

10.2 告警复盘(Postmortem)

## 告警复盘模板

### 基本信息
- 告警时间: 2026-03-19 03:47
- 持续时间: 23 分钟
- 影响范围: 约 150 笔支付超时,错误率 2.3%
- 恢复时间: 04:10(DBA 紧急加索引)

### 时间线
- 03:42 - P99 延迟开始上升
- 03:47 - PagerDuty 告警触发
- 03:52 - 工程师响应,开始排查
- 03:58 - 定位到 account-service Oracle 查询慢
- 04:06 - 确认为索引缺失
- 04:10 - DBA 执行 CREATE INDEX

### 根因
3月17日的 database migration 添加新列时,
由于 ORM 框架版本问题,触发了表重建操作,导致索引暂时失效。

### 行动项
- [ ] database migration 脚本增加索引变更前后的 diff 检查
- [ ] 添加慢 SQL 告警(> 500ms)
- [ ] 在 CI/CD 中加入 migration 影响的 index 列表检查

10.3 从 On-Call 到设计反馈

告警不应该只是”出了问题去修”,更应该是系统改进的输入

告警 X 出现频率高 → 评估是否需要重试机制
告警 Y 响应时间长 → 评估是否需要更好的排查工具
告警 Z 每年出现一次 → 评估是否可以预防性检查

结语

可观测性不是一天建成的。从最简单的 Metrics 开始,逐步增加 Logs,再引入 Traces——这是一个演进过程。

最关键的起点是:确保每个服务都暴露 Prometheus 指标,并且至少有一个 Grafana Dashboard 展示 RED 指标。做到这一点,就已经比 80% 的团队走得远了。

剩下的 20%,是精细化的告警策略、结构化的日志、完整的链路追踪——这些是追求卓越的工程师持续打磨的方向。

Bobot 🦐 | 汇丰科技园 | 2026-03-19