可观测性——出了问题怎么快速定位 / Observability and Rapid Production Diagnosis
📅 创建时间:2026-05-08 🏷️ 标签:#结构化日志 #ELK #OpenTelemetry #告警收敛 #可观测性 📚 前置知识:[[00-backend-overview]] [[05-middleware]](链路追踪) 📚 相关知识:[[12-devops-basics]](监控基础)
场景:用户说"下单失败了",但你不知道哪一步错了
┌─────────────────────────────────────────────────────────────┐
│ │
│ 用户投诉:下单失败了,但我不知道是哪里出了问题。 │
│ │
│ 你开始排查: │
│ 1. 看应用日志?分散在 10 台服务器上... │
│ 2. 问前端?前端说接口返回 500 │
│ 3. 问 DBA?DBA 说数据库正常 │
│ 4. 问运维?运维说网络正常 │
│ │
│ 花了 2 小时,才发现是 Redis 连接池满了。 │
│ │
│ 如果有可观测性,应该在 Redis 满的那一刻就报警。 │
│ │
└─────────────────────────────────────────────────────────────┘可观测性 = 日志 + 指标 + 链路追踪,三者缺一不可。
第1节:结构化日志——不是 console.log
问题:非结构化日志难以分析
❌ 难以分析:
2026-05-08 10:00:00 ERROR [OrderService] order create failed, userId=123, error=timeout
2026-05-08 10:00:01 ERROR [OrderService] failed to create order for user 456: connection refused
✅ 易于分析:
{
"timestamp": "2026-05-08T10:00:00Z",
"level": "ERROR",
"service": "order-service",
"traceId": "abc123",
"spanId": "def456",
"event": "order_create_failed",
"userId": 123,
"error": "timeout",
"duration": 5000,
"metadata": {
"productId": 789,
"inventory": 0
}
}结构化日志实现
python
import logging
import json
import uuid
# 结构化日志格式
class StructuredFormatter(logging.Formatter):
def format(self, record):
log_entry = {
"timestamp": self.formatTime(record),
"level": record.levelname,
"service": getattr(record, "service", "unknown"),
"traceId": getattr(record, "traceId", uuid.uuid4().hex[:16]),
"spanId": getattr(record, "spanId", ""),
"logger": record.name,
"message": record.getMessage(),
}
# 添加 extra 字段
if hasattr(record, "extra"):
log_entry["metadata"] = record.extra
# 添加异常信息
if record.exc_info:
log_entry["exception"] = self.formatException(record.exc_info)
return json.dumps(log_entry)
# 日志记录器
def get_logger(name, service):
logger = logging.getLogger(name)
logger.setLevel(logging.INFO)
handler = logging.StreamHandler()
handler.setFormatter(StructuredFormatter())
logger.addHandler(handler)
# 注入服务名
old_factory = logging.getLogRecordFactory()
def record_factory(*args, **kwargs):
record = old_factory(*args, **kwargs)
record.service = service
record.traceId = getattr(record, "traceId", uuid.uuid4().hex[:16])
return record
logging.setLogRecordFactory(record_factory)
return logger
logger = get_logger(__name__, "order-service")
# 使用
logger.info("order created",
extra={
"orderId": 12345,
"userId": 100,
"productId": 789,
"amount": 299.99
})
logger.error("order creation failed",
extra={
"orderId": 12345,
"error": "inventory insufficient",
"availableStock": 0
})日志等级规范
┌─────────────────────────────────────────────────────────────┐
│ 日志等级规范 │
├─────────────────────────────────────────────────────────────┤
│ │
│ DEBUG(调试): │
│ → 开发时使用 │
│ → 打印变量值、函数入参、SQL 语句 │
│ → 生产环境不输出 │
│ │
│ INFO(信息): │
│ → 业务关键节点 │
│ → "用户登录"、"订单创建"、"支付成功" │
│ → 便于追踪业务流程 │
│ │
│ WARN(警告): │
│ → 异常情况,但不影响业务 │
│ → "重试第 3 次"、"缓存 miss" │
│ → 警告多了说明系统不稳定 │
│ │
│ ERROR(错误): │
│ → 影响单个请求,但不影响系统 │
│ → "下单失败"、"支付超时" │
│ → 需要关注和修复 │
│ │
│ FATAL(致命): │
│ → 影响整个系统 │
│ → "数据库连接失败"、"内存不足" │
│ → 必须立即处理 │
│ │
└─────────────────────────────────────────────────────────────┘第2节:ELK 栈——日志的存储和查询
┌─────────────────────────────────────────────────────────────┐
│ ELK 架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 应用服务(结构化 JSON 日志) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────┐ │ │
│ │ │Filebeat │ 轻量级日志收集 │ │
│ │ └────┬────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────┐ │ │
│ │ │ Logstash│ 解析、过滤、转换 │ │
│ │ └────┬────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────┐ │ │
│ │ │Elasticsearch│ 存储 + 全文搜索 │ │
│ │ └────┬────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────┐ │ │
│ │ │ Kibana │ 可视化 + 查询 │ │
│ │ └─────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘Filebeat 配置
yaml
# filebeat.yml
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/log/order-service/*.log
json.keys_under_root: true
json.add_error_key: true
json.message_key: message
processors:
- add_host_metadata:
when.not.contains.tags: forwarded
- add_cloud_metadata: ~
- add_docker_metadata: ~
output.elasticsearch:
hosts: ["elasticsearch:9200"]
index: "order-service-%{+yyyy.MM.dd}"
# 日志脱敏
processors:
- redact:
fields: ["user.password", "user.token", "user.phone"]
pattern: '\b\d{11}\b'
replacement: '****'Kibana 查询
┌─────────────────────────────────────────────────────────────┐
│ Kibana 常用查询 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 按服务查询: │
│ service: "order-service" │
│ │
│ 按 traceId 查询(链路追踪): │
│ traceId: "abc123def456" │
│ │
│ 组合查询: │
│ service: "order-service" AND level: "ERROR" │
│ service: "order-service" AND message: "timeout" │
│ │
│ 模糊查询: │
│ message: "order*failed" │
│ │
│ 聚合统计: │
│ 按 service 统计 ERROR 数量: │
│ GET /_search │
│ { │
│ "aggs": { │
│ "errors_by_service": { │
│ "terms": {"field": "service.keyword"}, │
│ "aggs": { │
│ "errors": { │
│ "filter": {"term": {"level": "ERROR"}} │
│ } │
│ } │
│ } │
│ } │
│ } │
│ │
└─────────────────────────────────────────────────────────────┘第3节:OpenTelemetry——统一埋点
问题:每种语言都要单独埋点
┌─────────────────────────────────────────────────────────────┐
│ │
│ Java: Micrometer + Prometheus │
│ Python: OpenTelemetry │
│ Go: Zipkin │
│ Node.js:Jaeger │
│ │
│ 问题:跨语言链路追踪无法串联 │
│ Java 服务 → Python 服务 → Go 服务 │
│ → 每个服务的 traceId 不一致,无法追踪完整链路 │
│ │
│ 解决:OpenTelemetry(统一标准) │
│ │
└─────────────────────────────────────────────────────────────┘OpenTelemetry 埋点
python
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.resources import Resource
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace.export import BatchSpanProcessor
# 初始化
resource = Resource.create({
"service.name": "order-service",
"service.version": "1.0.0",
"deployment.environment": "production"
})
provider = TracerProvider(resource=resource)
processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="otel-collector:4317"))
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
tracer = trace.get_tracer(__name__)
# 业务埋点
@app.route("/order", methods=["POST"])
def create_order():
with tracer.start_as_current_span("create_order") as span:
# 设置属性
span.set_attribute("order.user_id", request.user_id)
span.set_attribute("order.amount", request.amount)
try:
# 扣库存
with tracer.start_as_current_span("deduct_inventory") as inventory_span:
inventory_span.set_attribute("product.id", request.product_id)
inventory_service.deduct(request.product_id)
# 创建订单
with tracer.start_as_current_span("save_order") as order_span:
order_span.set_attribute("db.system", "mysql")
order_span.set_attribute("db.statement", "INSERT INTO orders ...")
order = order_service.create(request)
span.set_status(trace.StatusCode.OK)
return {"orderId": order.id}
except InsufficientInventoryError as e:
span.set_status(trace.StatusCode.ERROR, str(e))
span.record_exception(e)
raise第4节:告警收敛——告警风暴
问题:系统报警了,然后收到了 1000 条告警
┌─────────────────────────────────────────────────────────────┐
│ │
│ Redis 内存达到 90%。 │
│ │
│ → 触发告警:"Redis 内存 90%,告警 1" │
│ → 5 分钟后,又告警:"Redis 内存 90%,告警 2" │
│ → 又过了 5 分钟... │
│ │
│ 问题: │
│ 同一个问题触发了 100 条告警 │
│ 真正的告警被淹没 │
│ │
│ 解决:告警收敛 │
│ │
└─────────────────────────────────────────────────────────────┘Alertmanager 告警收敛
yaml
# Alertmanager 配置
global:
resolve_timeout: 5m
route:
group_by: ['alertname', 'severity']
group_wait: 30s # 等待 30 秒,聚合同组告警
group_interval: 5m # 同组告警的发送间隔
repeat_interval: 4h # 重复告警的发送间隔
receiver: 'team-notifications'
routes:
# 严重告警:立即通知
- match:
severity: critical
receiver: 'pagerduty'
group_wait: 0s # 立即告警
# 警告告警:合并后通知
- match:
severity: warning
receiver: 'slack'
continue: true
# Redis 告警:按服务聚合
- match:
service: redis
receiver: 'redis-team'
group_by: ['alertname', 'service']
# 静默规则
inhibit_rules:
# 严重告警触发时,抑制同类的警告告警
- source_match:
severity: critical
target_match:
severity: warning
equal: ['alertname', 'service']告警 SOP
┌─────────────────────────────────────────────────────────────┐
│ 告警处理 SOP │
├─────────────────────────────────────────────────────────────┤
│ │
│ 收到告警: │
│ Step 1:确认告警是否真实(排查监控) │
│ Step 2:确认影响范围(影响一个用户还是所有用户) │
│ Step 3:临时止血(降级/限流/回滚) │
│ Step 4:定位根因 │
│ Step 5:彻底修复 │
│ Step 6:复盘记录 │
│ │
│ 告警质量标准: │
│ ✅ 每个告警都有明确的处理动作 │
│ ✅ 告警数量:每天 < 100 条 │
│ ✅ 告警响应时间:P50 < 5 分钟 │
│ │
└─────────────────────────────────────────────────────────────┘第5节:可观测性体系建立
┌─────────────────────────────────────────────────────────────┐
│ 可观测性三支柱 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 日志(Logs):出了什么问题 │
│ → 结构化 JSON + traceId │
│ → ELK 存储 + Kibana 查询 │
│ → 按 traceId 串联所有日志 │
│ │
│ 2. 指标(Metrics):系统健康吗 │
│ → 黄金指标:Rate / Errors / Duration │
│ → Prometheus + Grafana │
│ → 仪表盘:服务概览 / 依赖关系 / 趋势 │
│ │
│ 3. 链路(Traces):哪一步慢了 │
│ → OpenTelemetry 统一埋点 │
│ → Jaeger / Zipkin / Tempo 可视化 │
│ → 按 traceId 追踪完整调用链 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 关联关系: │ │
│ │ 指标异常 → 找到慢的 trace → 查看日志定位问题 │ │
│ │ 日志异常 → 找到 traceId → 查看链路找到根因 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘"AI 可查 vs 必须理解"清单
AI 可查:
✅ ELK 各组件的具体配置
✅ OpenTelemetry SDK 的埋点 API
✅ Alertmanager 的配置语法
必须理解:
🔴 结构化日志的核心字段(timestamp / traceId / level / service)
🔴 ELK 三组件的职责(Filebeat / Logstash / Elasticsearch)
🔴 可观测性三支柱的关联(日志→追踪→指标)
🔴 告警收敛的原理和配置学习状态:🟡 开始学习