跳转到内容

可观测性与故障处理

可观测性不是“日志很多”,而是能从外部现象推断内部状态:用户为什么失败,请求在哪一层变慢,哪个版本引入了变化,系统还能不能继续承受流量。故障处理则是在信息不完整、时间有限的情况下,先恢复服务,再查明原因。

没有正常基线,监控只能靠感觉。先从用户视角定义服务指标(SLI):

  • 请求成功率。
  • 响应延迟的 p50、p95、p99。
  • 关键业务流程完成率。
  • 数据新鲜度和队列延迟。
  • 错误预算与可用性。

再把目标写成 SLO,例如“过去 30 天,99.9% 的登录请求在 1 秒内成功”。SLO 不是装饰承诺,它决定何时需要停止发布、优先修复稳定性,而不是继续加功能。

若目标可用性 99.9%,30 天允许约 0.1% 不可用,即大约 43 分钟。错误预算用来做决策:

  • 预算充足时,可以推进有风险但回报高的变更。
  • 预算快速消耗时,先降低发布频率,修复主要故障。
  • 不能因为预算还没用完,就忽略局部用户体验。

指标必须区分“服务器返回 200”和“用户真的成功”。异步任务失败、前端脚本崩溃、第三方依赖不可用,都可能让用户失败,但服务器指标看起来正常。

指标是随时间聚合的数值,适合告警和趋势,不适合保存每条详情。

常见类型:

  • 计数器:只增不减,如请求总数、错误总数。
  • 仪表:当前值,如连接数、队列长度。
  • 直方图:按桶统计分布,用于延迟分位数。
  • 摘要:预计算分位数,跨实例聚合时要小心。

高基数标签会爆炸。用户 ID、请求 ID、完整 URL 和随机参数不应作为普通指标标签;应使用有限维度和采样/trace 定位单个请求。

日志是离散事件记录,适合解释“发生了什么”。结构化日志通常比自由文本更容易检索:

{
"time": "2026-10-31T12:00:00Z",
"level": "error",
"service": "api",
"requestId": "req_123",
"route": "POST /orders",
"code": "DB_TIMEOUT",
"durationMs": 5032
}

日志分级要克制。大量 debug 会拖慢系统并增加存储成本。敏感字段必须脱敏;完整请求体、Cookie、令牌和个人信息不应默认写入。

trace 描述一个请求经过哪些服务、每段耗时多少。一个 trace 由多个 span 组成:

HTTP POST /checkout 320 ms
├── auth.validate 5 ms
├── inventory.reserve 120 ms
│ └── db.query 90 ms
├── payment.charge 180 ms
│ └── external API 170 ms
└── order.persist 12 ms

span 应带服务名、操作名、状态、耗时和必要属性。不要把敏感数据塞进 span。跨服务传播 trace context,需要统一标准和采样策略。

全量采集可能成本很高。可以:

  • 对成功请求低比例采样。
  • 对错误、慢请求和高价值业务提高采样率。
  • 在入口生成 request ID,贯穿日志、指标 exemplar 和 trace。
  • 让客户端错误、服务端日志和后端 trace 能通过同一 ID 关联。

采样意味着不能从“没看到错误”推断没有错误。告警应优先使用聚合指标,定位时再查具体 trace。

仪表盘应服务具体问题,而不是把所有图放一页。

  • 请求量。
  • 成功率与各错误码。
  • 延迟分位数。
  • 饱和度:CPU、内存、连接池、队列、磁盘。
  • 依赖健康。
  • 发布和配置变更标记。
  • 下单成功率。
  • 支付失败分类。
  • 注册漏斗。
  • 任务积压与完成时间。

按依赖、实例、版本、地区、租户维度查看异常。图上要显示时间范围、单位和数据来源,避免“看起来涨了”但无法解释。

好的告警满足:

  • 对应真实用户影响或明确的容量风险。
  • 有可执行动作。
  • 有足够上下文和链接。
  • 避免重复和抖动。
  • 有负责人和升级路径。

按症状告警,通常比按每个内部指标告警更有效。例如“支付成功率低于阈值”比“某个线程池队列长度超过 10”更接近用户影响。内部指标可以作为原因线索。

瞬时峰值不一定需要叫醒人。可以用多窗口 burn rate:短时间内错误预算快速消耗时紧急告警,长期缓慢消耗时创建工单。

告警阈值要基于基线和业务容忍度。固定 CPU > 80% 既可能误报,也可能漏掉“CPU 只有 30% 但请求全部排队”的故障。

发现 → 分诊 → 止血 → 沟通 → 根因分析
→ 修复与验证 → 复盘 → 改进项

先确认:

  • 哪些用户、地区、版本和功能受影响。
  • 开始时间和最近变更。
  • 是全部失败、部分失败还是变慢。
  • 当前影响是否仍在扩大。

不要一开始就追根因。用户正在失败时,优先恢复可用性。

常见手段:

  • 回滚最近发布。
  • 关闭功能开关。
  • 切流量到健康区域或旧版本。
  • 扩容或提高限流额度。
  • 降级非核心功能。
  • 暂停批处理或后台任务。

止血也要记录时间和操作结果。第二次操作前先确认第一次是否生效。

事故期间需要稳定更新:

  • 当前影响。
  • 已采取动作。
  • 下一次更新时间。
  • 用户需要做什么。

不确定原因时不要编造结论。清晰说明“正在验证”比错误归因更有用。

指标恢复不等于所有数据恢复。检查:

  • 错误率和延迟回到基线。
  • 队列积压是否消化。
  • 失败请求是否可重试或补偿。
  • 数据是否一致。
  • 客户端缓存和旧版本是否仍受影响。

根因不是一个孤立句子。可以使用“五个为什么”,但要避免把复杂系统故障简化为个人失误。

现象:支付成功率下降
为什么:支付服务连接数据库超时
为什么:数据库连接池耗尽
为什么:慢查询增加且连接未及时释放
为什么:新索引上线后统计信息未更新
为什么:迁移流程没有包含统计信息检查

最后一层可能是流程缺口。修复应同时覆盖代码、配置、监控和发布流程。

复盘目标不是追责,而是改变系统。至少包含:

  • 时间线:何时发现、何时决策、何时恢复。
  • 影响范围与持续时间。
  • 直接触发因素和促成条件。
  • 哪些监控有效,哪些缺失。
  • 哪些操作成功,哪些浪费了时间。
  • 可执行改进项、负责人和截止时间。

“加强培训”“提高意识”通常不可验证。更好的改进项是“为迁移加入统计信息检查”“为队列积压增加告警”“把回滚时间从 30 分钟降到 5 分钟”。

延迟升高时,先分清:

  • 请求量增加。
  • 单请求变慢。
  • 依赖变慢。
  • 资源饱和。
  • 排队或锁等待。
  • 重试放大。

观察 CPU、内存、磁盘 I/O、网络、连接池、线程/协程数、队列长度和 GC。使用 profiler 找热点,不要只凭代码直觉。

容量测试要看业务链路和真实数据分布。健康检查接口的压力测试不能代表数据库查询、文件上传或复杂页面。

每个关键服务应有简短、可执行的 runbook:

服务用途与依赖
关键指标和正常范围
常见故障与判断方法
回滚与降级步骤
数据恢复与联系方式
升级路径

runbook 要在故障前验证,而不是事故中第一次打开。命令、权限和链接过期会让文档在关键时刻失效。

  • 是否有用户视角的 SLI、SLO 和错误预算。
  • 指标、日志、trace 是否通过 request ID 关联。
  • 日志是否结构化、脱敏,是否有保留期。
  • 指标标签是否控制基数。
  • 仪表盘是否区分总览、业务和排障。
  • 告警是否对应用户影响,是否有负责人和操作说明。
  • 是否有多窗口错误预算和容量告警。
  • 故障时能否快速回滚、降级和切换流量。
  • 复盘是否产生可验证、可跟踪的改进项。
  • 关键服务是否有经过验证的 runbook 和恢复演练。