CC 咖啡猫的工作空间 Coding Space

系统可观测性实践

可观测性(Observability)是系统稳定性的基石。没有可观测性,线上出问题就是"抓瞎"。三大支柱:日志(Logging)监控(Metrics)追踪(Tracing)


一、为什么需要可观测性

1.1 单体 vs 微服务的区别

单体应用:
  一个应用 → 一个日志文件 → 出了问题打开看 → 搞定

微服务:
  10+ 服务 → 请求跨多个服务 → 一个请求的日志散落在 5 台机器上
  → 出问题了不知道从哪看 → 崩溃

1.2 三大支柱各解决什么问题

支柱 解决的问题 问题举例
日志(Logging) 发生了什么? "订单创建时报了什么错?"
监控(Metrics) 系统现在正常吗? "CPU 是不是 90% 了?"
追踪(Tracing) 一个请求经过了哪些服务? "这个慢请求卡在哪个环节?"

1.3 三者关系

一次请求的生命周期:

Tracing:  TraceID=abc123 ─────────────────────────→
            │               │               │
            ▼               ▼               ▼
          SpanA           SpanB           SpanC
        (Gateway)      (订单服务)       (库存服务)
            │               │               │
            ▼               ▼               ▼
Logging:  [INFO] 请求到达   [INFO] 创建订单   [INFO] 扣减库存
          [ERROR] 参数错误  [WARN] 库存不足   [DEBUG] 调用Redis

Metrics:  QPS=1000        QPS=200          QPS=150
          P99=50ms        P99=200ms        P99=30ms

二、监控方法论

2.1 监控的三个层次

层次 说明 示例
基础设施监控 服务器、网络、磁盘 CPU、内存、磁盘 I/O
应用监控 应用性能、错误率 QPS、P99、错误率
业务监控 业务指标、转化率 订单成功率、DAU

2.2 RED + USE 方法

RED 方法(面向服务)

RED 是监控微服务系统的通用方法论:

指标 含义 具体监控 告警示例
Rate 请求速率 每秒处理多少请求(QPS) QPS 突增 50%以上
Errors 错误率 多少%的请求失败 错误率 > 1% 告警
Duration 响应时间 请求平均耗时多少毫秒 P99延迟 > 500ms 告警

USE 方法(面向资源)

USE (Utilization, Saturation, Errors) 方法用于监控系统资源:

维度 含义 监控指标示例
Utilization 资源利用率 CPU使用率、内存使用率、磁盘使用率
Saturation 资源饱和度 CPU run queue长度、磁盘I/O队列深度
Errors 错误计数 磁盘读写错误、网络包错误

2.3 延迟分位数说明

  • p50(中位数) - 50%的请求在这个时间内完成
  • p95 - 95%的请求在这个时间内完成
  • p99 - 99%的请求在这个时间内完成(关键指标,代表最坏情况)
  • p999 - 99.9%的请求在这个时间内完成(大促场景关注)

三、日志体系(ELK)

3.1 整体架构

应用服务器
    │
    ├── /var/log/app/app.log   ← Spring Boot 输出的 JSON 日志
    │
    ▼
Filebeat(采集)
    │  监听日志文件变化,每分钟增量读取
    │  多行合并(Java 异常堆栈合并为一条日志)
    │
    ▼
Kafka(缓冲)
    │  削峰填谷:日志量突增时 Kafka 缓冲
    │  解耦:采集端和消费端独立扩缩容
    │
    ▼
Logstash(清洗,可选)
    │  解析 JSON、字段转换、GeoIP 富化
    │  注意:Logstash 资源消耗大,简单清洗可跳过
    │
    ▼
Elasticsearch(存储 + 索引)
    │  按天建索引(如 app-logs-2024.01.01)
    │  倒排索引 → 全文搜索 → 毫秒级返回
    │
    ▼
Kibana(可视化)
    │  搜索日志、看板、告警
    │  Discover 模式:TraceID=abc123 查一条请求全链路日志

3.2 Spring Boot 日志配置

依赖(Spring Boot 默认使用 Logback)

<!-- spring-boot-starter-web 已内置 Logback,无需额外添加 -->
<!-- 如果要用 Log4j2,需要排除 Logback -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

logback-spring.xml(最常用配置)

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 1. 定义日志格式(JSON 格式,方便 ELK 解析) -->
    <!--    包含 TraceID:通过 %mdc{traceId} 或 Sleuth 自动注入 -->
    <springProperty name="APP_NAME" source="spring.application.name"/>

    <!-- JSON 格式模板(推荐,方便 ES 索引) -->
    <appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>/var/log/app/${APP_NAME}.log</file>

        <!-- 按时间和大小滚动 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>/var/log/app/${APP_NAME}-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
            <!-- 单个文件最大 500MB -->
            <maxFileSize>500MB</maxFileSize>
            <!-- 保留 7 天 -->
            <maxHistory>7</maxHistory>
            <!-- 日志总大小上限 10GB -->
            <totalSizeCap>10GB</totalSizeCap>
        </rollingPolicy>

        <encoder class="net.logstash.logback.encoder.LogstashEncoder">
            <!-- 添加自定义字段 -->
            <customFields>{"app_name":"${APP_NAME}"}</customFields>
            <!-- 异常堆栈格式化 -->
            <throwableConverter class="net.logstash.logback.stacktrace.ShortenedThrowableConverter">
                <!-- 每行最大长度 -->
                <maxLength>2048</maxLength>
                <!-- 最多打印 30 层堆栈 -->
                <maxDepthPerThrowable>30</maxDepthPerThrowable>
            </throwableConverter>
        </encoder>
    </appender>

    <!-- 2. 控制台输出(开发环境) -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 3. 调整日志级别 -->
    <logger name="com.example" level="DEBUG"/>
    <logger name="org.springframework" level="INFO"/>
    <logger name="com.alibaba.nacos" level="WARN"/>
    <logger name="org.apache.kafka" level="WARN"/>

    <!-- 4. 根日志级别 -->
    <root level="INFO">
        <appender-ref ref="JSON_FILE"/>
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>

JSON 日志格式示例

生产环境输出的每条日志是这样的:

{
  "@timestamp": "2024-01-15T10:30:00.123+08:00",
  "level": "INFO",
  "thread": "http-nio-8080-exec-1",
  "logger": "com.example.controller.OrderController",
  "message": "创建订单成功, orderId=12345",
  "app_name": "order-service",
  "traceId": "abc123def456",
  "spanId": "abc123def456",
  "X-B3-TraceId": "abc123def456",
  "stack_trace": null
}

关键:每条日志都有 traceId,在 Kibana 搜 traceId=abc123 能看到这个请求在所有服务中的完整日志。

3.3 Filebeat 配置

# filebeat.yml
filebeat.inputs:
  # 采集 Spring Boot 日志
  - type: log
    enabled: true
    paths:
      - /var/log/app/*.log           # 采集所有应用的日志
    fields:
      log_type: app                  # 自定义字段,方便 ES 过滤
    fields_under_root: true

    # 多行合并:Java 异常堆栈合并为一条日志
    # 下一条如果不是以时间戳开头,就是上一行的续行
    multiline:
      pattern: '^\d{4}-\d{2}-\d{2}'  # 以日期开头的行是新日志
      negate: true                    # 不匹配的做合并
      match: after                   # 合并到前一行后面

  # 采集 Nginx 访问日志
  - type: log
    enabled: true
    paths:
      - /var/log/nginx/access.log
    fields:
      log_type: nginx-access

# 输出到 Kafka(推荐:缓冲 + 解耦)
output.kafka:
  hosts: ["kafka1:9092", "kafka2:9092"]
  topic: "app-logs"                 # 所有应用的日志进同一个 topic
  partition:
    hash:                            # 按 app_name 哈希分区
      reachable_only: true
  required_acks: 1                   # acks=1 保证不丢消息
  compression: gzip                  # 压缩传输

# 如果不用 Kafka,也可以直接写 ES
# output.elasticsearch:
#   hosts: ["es1:9200", "es2:9200"]
#   index: "app-logs-%{+yyyy.MM.dd}"

3.4 Elasticsearch 配置

// 索引模板:自动为每天的日志索引应用配置
PUT _index_template/app-logs-template
{
  "index_patterns": ["app-logs-*"],
  "template": {
    "settings": {
      "number_of_shards": 3,          // 3 个分片
      "number_of_replicas": 1,        // 1 个副本
      "refresh_interval": "30s",      // 搜索场景降低刷新频率
      "index.lifecycle.name": "app-logs-policy"  // 生命周期策略
    },
    "mappings": {
      "properties": {
        "@timestamp": { "type": "date" },
        "level":       { "type": "keyword" },  // keyword = 精确匹配
        "message":     { "type": "text" },      // text = 全文检索(分词)
        "app_name":    { "type": "keyword" },
        "traceId":     { "type": "keyword" },
        "logger":      { "type": "keyword" },
        "thread":      { "type": "keyword" },
        "stack_trace": { "type": "text" }
      }
    }
  }
}

// 生命周期策略:自动删除旧日志
PUT _ilm/policy/app-logs-policy
{
  "policy": {
    "phases": {
      "hot": {
        "actions": { "rollover": { "max_size": "50GB", "max_age": "1d" } }
      },
      "warm": {
        "min_age": "3d",
        "actions": { "shrink": { "number_of_shards": 1 } }  // 3 天后缩分片
      },
      "delete": {
        "min_age": "7d",
        "actions": { "delete": {} }  // 7 天后删除
      }
    }
  }
}

3.5 Kibana 日志查询

# Discover 模式搜索语句

# 1. 根据 TraceID 查全链路日志
traceId:"abc123def456"

# 2. 查订单服务的 ERROR 日志
app_name:"order-service" AND level:"ERROR"

# 3. 查最近 15 分钟包含"OutOfMemory"的日志
message:"OutOfMemory" AND @timestamp:>now-15m

# 4. 排除特定关键词
NOT message:"health check" AND app_name:"order-service"

# 5. 通配符搜索
message:*timeout*

# 6. 聚合统计:按应用统计错误数
# Visualize → Bar Chart → group by app_name → count where level=ERROR

四、监控体系(Prometheus + Grafana)

4.1 整体架构

                        ┌─────────────┐
                        │  Alertmanager│  ← 告警路由(钉钉/邮件/企微)
                        └──────┬──────┘
                               │ 触发告警规则
                        ┌──────▼──────┐
                        │  Prometheus │  ← 时序数据库 + 告警引擎
                        └──────┬──────┘
                               │ Pull 模式:每 15s 拉一次指标
                   ┌───────────┼───────────┐
                   ▼           ▼           ▼
            ┌──────────┐┌──────────┐┌──────────┐
            │ 服务 A   ││ 服务 B   ││  MySQL   │
            │ /actuator││ /actuator││ exporter │  ← Exporter 暴露指标
            │/prometheus││/prometheus││ 9104     │
            └──────────┘└──────────┘└──────────┘

4.2 Spring Boot 暴露指标

<!-- Actuator + Micrometer Prometheus -->
<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,prometheus,metrics  # 暴露 Prometheus 端点
      base-path: /actuator
  endpoint:
    health:
      show-details: always            # 显示详细健康信息
  metrics:
    export:
      prometheus:
        enabled: true
    tags:
      application: ${spring.application.name}  # 所有指标加上应用名标签

4.3 关键指标解读

# 访问 /actuator/prometheus 可以看到所有指标

# ===== JVM 指标 =====

# JVM 内存使用(单位:字节)
jvm_memory_used_bytes{area="heap"}
jvm_memory_used_bytes{area="nonheap"}
jvm_memory_max_bytes{area="heap"}

# JVM 线程数
jvm_threads_live_threads

# GC 次数和时间
jvm_gc_pause_seconds_count{action="end of minor GC"}
jvm_gc_pause_seconds_sum{action="end of minor GC"}

# ===== HTTP 指标 =====

# 请求数(按 URI、状态、方法分组)
http_server_requests_seconds_count{uri="/orders", status="200"}
http_server_requests_seconds_count{uri="/orders", status="500"}

# 响应时间(P99/P95/P50)
http_server_requests_seconds{uri="/orders", quantile="0.99"}
http_server_requests_seconds{uri="/orders", quantile="0.95"}
http_server_requests_seconds{uri="/orders", quantile="0.5"}

# 最大响应时间
http_server_requests_seconds_max{uri="/orders"}

# ===== 数据库指标 =====

# 数据库连接池
hikaricp_connections_active     # 活跃连接
hikaricp_connections_idle       # 空闲连接
hikaricp_connections_pending    # 等待队列
hikaricp_connections_timeout_total  # 超时总数

# ===== 系统指标 =====

# CPU 使用率(需要用公式计算)
system_cpu_usage

# 系统内存
system_memory_used_bytes

4.4 Prometheus 配置

# prometheus.yml
global:
  scrape_interval: 15s        # 每 15 秒拉取一次指标
  evaluation_interval: 15s    # 每 15 秒评估一次告警规则
  external_labels:
    cluster: 'production'     # 集群标签

# 告警规则文件
rule_files:
  - "alerts/*.yml"

# 抓取目标
scrape_configs:
  # Spring Boot 应用
  - job_name: 'spring-boot-apps'
    metrics_path: '/actuator/prometheus'
    # 通过 Nacos 服务发现(自动发现新启动的服务)
    nacos_sd_configs:
      - server: 'nacos-server:8848'
        namespace_id: 'production'
        group: 'DEFAULT_GROUP'
    relabel_configs:
      # 只抓取健康实例
      - source_labels: [__meta_nacos_healthy]
        regex: 'true'
        action: keep
      # 用服务名替换 instance
      - source_labels: [__meta_nacos_service]
        target_label: app

  # 如果没有 Nacos,也可以用静态配置
  - job_name: 'static-targets'
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets:
          - 'order-service:8080'
          - 'user-service:8080'
        labels:
          env: 'production'

  # MySQL Exporter(需要单独部署)
  - job_name: 'mysql'
    static_configs:
      - targets: ['mysql-exporter:9104']

  # Node Exporter(服务器指标:CPU、内存、磁盘、网络)
  - job_name: 'node'
    static_configs:
      - targets:
          - 'server1:9100'
          - 'server2:9100'

4.5 告警规则

# alerts/app-alerts.yml
groups:
  - name: application
    rules:
      # 规则1:服务宕机
      - alert: ServiceDown
        expr: up == 0                           # up=0 表示服务不可达
        for: 1m                                 # 持续 1 分钟才告警(避免抖动)
        labels:
          severity: critical
        annotations:
          summary: "服务 {{ $labels.app }} 宕机"
          description: "服务 {{ $labels.app }} 超过 1 分钟无心跳"

      # 规则2:HTTP 错误率过高
      - alert: HighErrorRate
        expr: |
          sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m]))
          /
          sum(rate(http_server_requests_seconds_count[5m]))
          > 0.05                                # 5xx 错误率超过 5%
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "{{ $labels.app }} 错误率超过 5%"
          description: "当前 5xx 错误率: {{ $value | humanizePercentage }}"

      # 规则3:响应时间 P99 过高
      - alert: HighLatency
        expr: |
          histogram_quantile(0.99,
            rate(http_server_requests_seconds_bucket{uri!="/actuator/.*"}[5m])
          ) > 1                                  # P99 > 1 秒
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "{{ $labels.app }} {{ $labels.uri }} P99 延迟 > 1 秒"
          description: "当前 P99: {{ $value }}s"

      # 规则4:JVM 堆内存使用率过高
      - alert: HighHeapMemory
        expr: |
          sum(jvm_memory_used_bytes{area="heap"}) by (app)
          /
          sum(jvm_memory_max_bytes{area="heap"}) by (app)
          > 0.85                                # 堆内存 > 85%
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "{{ $labels.app }} 堆内存使用率 > 85%"

      # 规则5:数据库连接池耗尽
      - alert: ConnectionPoolExhausted
        expr: |
          hikaricp_connections_pending > 0     # 有等待中的连接
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "{{ $labels.app }} 数据库连接池耗尽"
          description: "等待队列长度: {{ $value }}"

4.6 Alertmanager 配置

# alertmanager.yml
global:
  resolve_timeout: 5m       # 告警恢复后 5 分钟才发通知

# 告警路由:不同级别发不同渠道
route:
  receiver: 'default'
  group_by: ['alertname', 'app']
  group_wait: 10s            # 等 10s 聚合同组告警
  group_interval: 5m         # 同组新告警的间隔
  repeat_interval: 4h        # 重复告警间隔(4h 发一次)

  routes:
    # 严重告警 → 钉钉 + 电话
    - match:
        severity: critical
      receiver: 'critical-receiver'
      continue: true
    # 警告 → 只发钉钉
    - match:
        severity: warning
      receiver: 'warning-receiver'

receivers:
  # 钉钉通知
  - name: 'dingtalk'
    webhook_configs:
      - url: 'https://oapi.dingtalk.com/robot/send?access_token=xxx'
        send_resolved: true    # 恢复时也发通知
        message: |
          {{ range .Alerts }}
          ## {{ if eq .Status "firing" }}🔥 故障{{ else }}✅ 恢复{{ end }}
          - 告警名称: {{ .Labels.alertname }}
          - 服务名称: {{ .Labels.app }}
          - 详情: {{ .Annotations.description }}
          - 时间: {{ .StartsAt.Format "2006-01-02 15:04:05" }}
          {{ end }}

  # 邮件通知
  - name: 'email'
    email_configs:
      - to: 'ops-team@example.com'
        from: 'alertmanager@example.com'
        smarthost: 'smtp.example.com:587'
        auth_username: 'alertmanager@example.com'
        auth_password: 'xxxx'

4.7 Grafana 看板

// 常用 PromQL 查询

// 1. QPS(每秒请求数)
//    按应用、URI 分组
sum(rate(http_server_requests_seconds_count[1m])) by (app, uri)

// 2. P99 响应时间(按 URI)
histogram_quantile(0.99,
  sum(rate(http_server_requests_seconds_bucket[5m])) by (le, app, uri)
)

// 3. 错误率
sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m]))
/
sum(rate(http_server_requests_seconds_count[5m]))
* 100

// 4. CPU 使用率
100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)

// 5. JVM 堆内存使用率
sum(jvm_memory_used_bytes{area="heap"}) by (app)
/
sum(jvm_memory_max_bytes{area="heap"}) by (app)
* 100

// 6. GC 停顿时间(1 分钟平均)
rate(jvm_gc_pause_seconds_sum[1m])
/
rate(jvm_gc_pause_seconds_count[1m])

// 7. 数据库连接池使用率
sum(hikaricp_connections_active) by (pool)  // 活跃连接数
/
sum(hikaricp_connections_max) by (pool)     // 最大连接数
* 100

4.8 看板布局建议

Grafana Dashboard 推荐布局:

第一行(黄金指标):
  [QPS]  [P99延迟]  [错误率]  [可用性%]

第二行(资源指标):
  [CPU使用率]  [JVM堆内存]  [GC频率]  [线程数]

第三行(应用指标):
  [DB连接池]  [Redis连接]  [Feign调用成功率]  [MQ消费lag]

第四行(明细表格):
  [Top 10 慢接口]  [最近告警列表]

五、链路追踪(SkyWalking / Jaeger)

5.1 整体架构

请求 → Gateway → A服务 → B服务 → C服务
         │        │       │       │
         │   TraceID: "abc123"     │
         │        │       │       │
         ▼        ▼       ▼       ▼
      SpanA    SpanB   SpanC   SpanD
     (1ms)   (50ms)  (200ms)  (30ms)    ← 每个 Span 记录耗时
         │        │       │       │
         └────────┴───────┴───────┘
                      │
                      ▼
              SkyWalking Server
                      │
                      ▼
              SkyWalking UI(可视化)

5.2 SkyWalking 部署(Java Agent 无侵入)

# 1. 下载 SkyWalking APM 和 Agent
wget https://dlcdn.apache.org/skywalking/9.5.0/apache-skywalking-apm-9.5.0.tar.gz
tar -xzf apache-skywalking-apm-9.5.0.tar.gz

# 2. 修改 Agent 配置
# skywalking-agent/config/agent.config
agent.service_name=${SW_SERVICE_NAME:order-service}
collector.backend_service=${SW_SERVICE_ADDR:127.0.0.1:11800}

# 3. 启动应用时附加 Agent(无代码侵入)
java -javaagent:/opt/skywalking-agent/skywalking-agent.jar \
     -DSW_SERVICE_NAME=order-service \
     -jar order-service.jar

5.3 SkyWalking 能看到什么

功能 说明
拓扑图 自动绘制服务调用关系,一眼看出循环依赖
链路追踪 输入 TraceID,看一个请求经过的所有服务和耗时分布
慢 SQL 自动检测慢 SQL,包括 SQL 文本和参数
服务指标 自动采集 QPS、响应时间、错误率
告警 内置告警规则(响应时间 > 1s、错误率 > 10%)

5.4 代码中获取 TraceID

@RestController
public class OrderController {

    @GetMapping("/orders/{id}")
    public Result<OrderVO> getOrder(@PathVariable Long id) {
        // SkyWalking 自动注入 TraceID 到 MDC
        // 通过 MDC 获取当前请求的 TraceID
        String traceId = org.apache.skywalking.apm.toolkit.trace.TraceContext.traceId();

        log.info("查询订单, orderId={}, traceId={}", id, traceId);

        // 业务逻辑...
        return Result.ok(order);
    }
}

// 返回给前端,方便用户反馈问题时附带 TraceID
@GetMapping("/trace")
public Map<String, String> getTrace() {
    return Map.of(
        "traceId", TraceContext.traceId(),
        "segmentId", TraceContext.segmentId()
    );
}

5.5 手动埋点(记录自定义 Span)

// 场景:有一段业务逻辑耗时不确定,想单独看它的耗时

@Service
public class OrderService {

    // 方式1:SkyWalking @Trace 注解(无方法体侵入)
    @Trace(operationName = "createOrder")
    @Tag(key = "orderId", value = "${returnedObj.orderId}")   // 打标签
    public Order createOrder(OrderDTO dto) {
        // 在 SkyWalking UI 中能看到这个方法的耗时
        return orderMapper.create(dto);
    }

    // 方式2:手动创建 Span
    public void complexLogic() {
        // 创建一个自定义 Span
        ActiveSpan span = SkyWalkingTracer.startSpan("complexLogic");

        try {
            // 打标签
            span.setTag("step", "step1");

            // 业务逻辑...
            doStep1();
        } catch (Exception e) {
            span.log(e);     // 记录异常
            span.setTag("error", true);
            throw e;
        } finally {
            span.stop();     // 必须 stop,否则 Span 一直存在
        }
    }
}

六、最佳实践

6.1 日志打点规范

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    public Order createOrder(OrderDTO dto) {
        // 1. 入口日志:记录请求参数
        log.info("创建订单开始, userId={}, productId={}, amount={}",
            dto.getUserId(), dto.getProductId(), dto.getAmount());

        try {
            // 2. 关键步骤日志
            //    DEBUG:详细的中间过程(生产默认关闭)
            log.debug("校验库存, productId={}", dto.getProductId());

            if (!inventoryService.checkStock(dto.getProductId(), 1)) {
                // 3. 业务异常:WARN 级别(不需要运维处理)
                log.warn("库存不足, productId={}", dto.getProductId());
                throw new BizException("库存不足");
            }

            Order order = new Order();
            order.setUserId(dto.getUserId());
            order.setProductId(dto.getProductId());
            order.setAmount(dto.getAmount());
            orderMapper.insert(order);

            // 4. 成功日志:带关键 ID
            log.info("创建订单成功, orderId={}, userId={}", order.getId(), dto.getUserId());
            return order;

        } catch (BizException e) {
            // 业务异常:WARN
            log.warn("创建订单业务异常, userId={}, msg={}", dto.getUserId(), e.getMessage());
            throw e;

        } catch (Exception e) {
            // 5. 系统异常:ERROR + 完整堆栈
            log.error("创建订单系统异常, userId={}, productId={}",
                dto.getUserId(), dto.getProductId(), e);
            throw new SystemException("创建订单失败", e);
        }
    }
}

6.2 日志级别使用原则

级别 什么时候用 生产默认
ERROR 需要人工介入的系统异常(DB 挂了、Redis 不通) 开启
WARN 业务逻辑异常(库存不足、余额不够)、降级触发 开启
INFO 关键业务节点(订单创建、支付成功)、服务启动 开启
DEBUG 开发调试、详细参数 关闭(排查时临时开启)
TRACE 极其详细的每一步 关闭

6.3 告警设置原则

原则1:只告警"严重"问题
  - 可能导致用户受影响
  - 表示某个服务要失败
  - 立即需要人工干预

原则2:告警应可观测
  - 收到告警时,应该能通过监控看到证据
  - 不要用隐性指标作为告警触发源
  - 告警信息包含足够的上下文

原则3:告警应可回应
  - 告警发出时,工程师应该能采取行动
  - 如果无法做任何事,就不要告警
  - 每个告警都应该有对应的 Runbook

原则4:告警应该有时效性
  - Critical 告警:5 分钟内响应
  - Warning 告警:1 小时内响应
  - 设置合理的 for 持续时间,避免抖动告警

6.4 SLO 概念

定义 SLO 的三个要素:

1. 可用性 SLO:99.9%(允许中断时间 = 3.15分钟/天)
   - 目标:99.95% 支付请求成功
   - 错误预算:0.05% * 总请求数
   - 当错误预算耗尽时,停止新功能发布

2. 响应时间 SLO:
   - 目标:P99 延迟 < 500ms
   - 测量窗口:滚动 30 天
   - 违规处理:性能优化优先级提升

3. 错误率 SLO:
   - 目标:错误率 < 0.1%
   - 分类统计:区分系统错误和用户错误
   - 根因分析:每个 SLO 违规都需要复盘

6.5 告警疲劳处理

问题:告警太多导致忽视
解决方案:
  - 只告警真正重要的指标(遵循 Google SRE 原则)
  - 设置合理的告警阈值(基于历史数据动态调整)
  - 告警级别分类:Critical、Warning、Info
  - 告警分组:相同根因的告警合并通知

问题:误告警率高
解决方案:
  - 使用多指标关联校验(一个指标异常不告警,多个同时异常才告警)
  - 告警确认机制(需要确认才真正告警)
  - 机器学习异常检测(基于历史模式识别真实异常)
  - 告警抑制:上游服务故障时抑制下游服务告警

七、生产环境常见问题

7.1 日志太多撑爆磁盘

方案1:Filebeat 只采集 WARN 及以上
方案2:ES ILM 自动删除 7 天前日志
方案3:Kafka 消息过期 72 小时
方案4:生产关 DEBUG(通过 Nacos 动态调整级别,不用重启)

7.2 告警太多(告警疲劳)

原则:
1. 严重告警(服务挂了、DB 不通)→ 立刻通知(钉钉 + 电话)
2. 警告(CPU 80%、响应慢)→ 聚合通知(每 4h 一次)
3. 信息(磁盘 60%)→ 不发通知,只在看板展示
4. 夜间告警 → 只发严重级别

7.3 调用链不全(丢 Trace)

原因1:线程池切换(@Async、CompletableFuture)→ TraceID 丢失
解决:手动传递 TraceID

原因2:MQ 消费者 → 新线程消费,TraceID 丢失
解决:生产者写 TraceID 到消息体,消费者取出来设置到 MDC

八、面试必背要点

Q:微服务下怎么排查一个慢请求?

1. 前端/网关获取 TraceID
2. 在 SkyWalking/Zipkin 输入 TraceID
3. 看拓扑图,定位到最慢的 Span(如订单服务 Span 耗时 2 秒)
4. 看该 Span 的详情:慢在哪里(DB查询?Feign调用?代码逻辑?)
5. 如果慢 SQL → EXPLAIN 看执行计划 → 加索引
6. 如果慢 Feign → 查下游服务 → 追踪下一个 Span

Q:日志里为什么需要 TraceID?

同一个请求经过 N 个服务,每个服务都打日志
如果没有 TraceID → 每个服务的日志孤岛,无法串联
有了 TraceID → 在 Kibana 搜 TraceID=abc123 → 一个请求的全链路日志全部出来

Q:Prometheus 和 Grafana 的关系?

Prometheus:时序数据库 + 数据采集 + 告警规则引擎
Grafana:数据可视化(从 Prometheus 查数据画图)
两者互补:Prometheus 管存,Grafana 管看

Q:为什么 Prometheus 用 Pull 模式而不是 Push?

Pull 优势:
1. 不需要关心应用是否挂了(拉不到就是 down)
2. 不需要配置复杂的推送地址
3. Prometheus 自己控制频率和重试
4. Service Discovery 自动发现新应用

Q:什么是 SLO、SLI、SLA?

- SLI(Service Level Indicator):服务质量指标,如可用性百分比
- SLO(Service Level Objective):服务质量目标,如99.9%可用性
- SLA(Service Level Agreement):服务等级协议,违反SLO的商业后果
- 关系:SLI是测量值,SLO是目标值,SLA是合同条款

最后更新:2026/05/13