高可用系统复盘:用日志、指标和调用链还原故障
故障复盘不是写一篇故事,而是还原时间线和因果证据。发布变更、指标拐点、日志样本与 Trace 应能相互对齐;没有证据的推断标为待验证。这样留下的改进项才可验收。
故障定位证据链:日志、Metrics 与 Trace 的交叉验证
复盘先整理可核对的时间线。讨论出现分歧时,回到发布记录、指标原始值、日志样本和 Trace,不用结论性形容词替代证据。
对于调用链较长的系统,应把指标(Metrics)、链路(Trace)与日志(Logs)放到同一时间轴,并保留原始查询链接与数据版本,方便复核。
可以按下面三类证据组织复盘材料:
证据一:Metrics 指标异常
证据二:Trace 链路断点
{ "trace_id": "a4f89b21e09c4d12", "span_id": "c789123a", "name": "Redis Exec Lua Script", "duration_us": 8501230, "attributes": { "db.system": "redis", "db.statement": "EVALSHA 4a9bc811...", "error": true, "error.kind": "CommandTimeoutException" } }证据三:底层慢日志证据
在 Redis Cluster 侧拉取SLOWLOG GET 10,证据锤实:
1) 1) (integer) 4821 2) (integer) 1757372401 3) (integer) 1250000 # 执行耗时 1.25 秒 4) 1) "EVALSHA" 2) "4a9bc81123..." 3) "1" 4) "lock:order:sku_89123"KEYS *会在线性扫描期间占用 Redis 主线程;数据量越大,其他请求等待得越久。不要预设会积压多少连接,应用LATENCY DOCTOR、SLOWLOG和网关连接数共同确认阻塞是否向上游扩散。
从漏洞到防护:代码与架构级别的落地改进
复盘不能停留在“以后注意”这类表态上。每个已验证原因都要对应具体改动、责任人、验收方式和复查日期。
针对 Lua 脚本阻塞与 Redis 命令滥用,可以在架构层引入两项物理硬约束:
- 禁用所有不带
LIMIT或含有KEYS/FLUSHALL的命令,在 CI/CD 静态代码扫描中增加 AST 规则拦截。 - 对所有的分布式锁与 Redis Lua 脚本调用,强制封装硬超时机制与降级兜底路径。
优化后的分布式锁操作封装代码:
package com.architecture.highavailability.lock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import java.util.Collections; import java.util.concurrent.*; public class SafeDistributedLock { private static final Logger log = LoggerFactory.getLogger(SafeDistributedLock.class); private final StringRedisTemplate redisTemplate; // 独立设立执行线程池,隔离 Redis 阻塞风险 private final ExecutorService lockExecutor = new ThreadPoolExecutor( 8, 32, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000), new ThreadPoolExecutor.DiscardPolicy() ); public SafeDistributedLock(StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } public boolean acquireLockWithHardTimeout(String lockKey, String requestId, int expireSeconds, long timeoutMillis) { Future<Boolean> future = lockExecutor.submit(() -> { String script = "if redis.call('set', KEYS[1], ARGV[1], 'NX', 'EX', ARGV[2]) then return 1 else return 0 end"; DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class); Long result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), requestId, String.valueOf(expireSeconds)); return Long.valueOf(1L).equals(result); }); try { // 物理限制最长等待耗时,超期直接强行放弃争抢并降级 return future.get(timeoutMillis, TimeUnit.MILLISECONDS); } catch (TimeoutException e) { log.error("[ALERT] 锁争抢强行超时中断, LockKey: {}, 等待上限: {}ms", lockKey, timeoutMillis); future.cancel(true); return false; // 返回 false 走业务降级分支 } catch (Exception e) { log.error("[ERROR] Redis 锁执行异常, Key: {}", lockKey, e); return false; } } }故障复盘产出物清单
一次合格的复盘,最终应落盘为以下四项具体产出,缺一不可:
| 产出物类别 | 改进内容细节 | 验收标准与落地责任人 |
|---|---|---|
| 代码规则库 (Lint Rules) | 增加 Sonar/ArchUnit 自定义规则,禁止在业务层直接调用eval未审查的 Lua 脚本 | CI 编译流程自动拦截并阻断 Build |
| 监控面板 (Grafana) | 增加 Redis 延迟、阻塞命令与连接等待面板 | 指标可按 Trace 时间窗交叉查询;指定维护人 |
| 预案手册 (Runbook) | 写明 Redis 异常时的降级条件、操作与回滚 | 演练人员能按步骤执行并恢复;指定维护人 |
| 故障演练 (Chaos Eng) | 注入 Redis 延迟与命令超时 | 告警、降级和恢复均留下时间戳;指定复查日期 |
总结:对待故障的态度决定系统的上限
复盘的终点是“系统韧性(Resilience)的提升”。
高可用不等于不会故障。每次复盘都应留下可验证的改进项,例如新增告警、调整超时或补充回滚演练,并在约定日期复查。