系统可观测性实践
可观测性(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