故障预防措施
某创业公司的技术团队,在成立两年内经历了 3 次 P1 级故障之后,终于痛下决心:花一年时间,系统性地建设故障防护体系。
一年后,这家公司的故障率下降了 80%,平均故障恢复时间(MTTR)从 45 分钟缩短到 8 分钟。这一节就用他们的真实经历,讲述故障预防应该怎么做。
背景:他们的问题出在哪
在开始建设防护体系之前,这家公司的问题是典型的「创业公司早期积累」:
- 发布流程靠人肉:代码 review 全靠同事之间的口头交流,没有强制机制
- 数据库没有备份验证:有备份脚本,但从来没执行过恢复
- 监控只报警不联动:告警发到群里,值班工程师要自己判断怎么处理
- 没有限流和熔断:外部服务挂了,调用方全部超时,最终拖垮整个系统
- 测试环境形同虚设:测试环境和生产环境配置完全不同,很多问题测不出来
简单来说,这家公司的问题不是「某个技术点没做好」,而是整个非功能性保障体系几乎为零。
第一阶段(第 1~3 个月):打牢基础
1. 建立强制 Code Review 机制
Code Review 是最简单也最有效的质量保障手段,问题是很多团队把它做成了形式——「帮忙点个同意吧」。
这家公司做了一件关键的事:把 Code Review 和 CI/CD 流程绑定。没有通过 review 的代码,合并按钮是灰的,CI 流水线跑不过。
.github/workflows/pr-check.yml
name: PR Quality Gates
on:
pull_request:
branches: [main]
jobs:
quality-gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run tests
run: npm test
- name: Check branch protection
run: |
# 强制要求至少 1 人 approval
approvals=$(gh pr view ${{ github.event.pull_request.number }} --json reviews -q '.reviews[] | select(.state == "APPROVED") | .author.login' | wc -l)
if [ "$approvals" -lt 1 ]; then
echo "需要至少 1 人审批才能合并"
exit 1
fi
但真正让 Code Review 起作用的,不是工具强制,而是建立了 Review Checklist:
Code
## 必须检查的项目
### 异常处理
- [ ] 是否有 try-catch?catch 后的处理是否合理?
- [ ] 是否吞掉了异常(空的 catch 块)?
- [ ] 是否在 finally 中释放了资源?
### 并发安全
- [ ] 共享变量是否做了线程安全处理?
- [ ] 是否引入了新的线程池?配置是否合理?
- [ ] 是否有死锁风险?
### 数据库
- [ ] 是否有新 SQL?是否验证了执行计划?
- [ ] 是否有大批量操作?是否做了分批处理?
- [ ] 是否使用了事务?事务边界是否合理?
### 性能
- [ ] 是否有 N+1 查询风险?
- [ ] 是否有大对象一次性加载到内存?
- [ ] 循环内是否有数据库或网络操作?
### 安全
- [ ] 是否有 SQL 注入风险?
- [ ] 敏感数据是否做了脱敏处理?
- [ ] 是否有新的外部接口调用?是否配置了超时?
2. 完善测试环境
测试环境和生产环境的差异,是很多故障的隐藏根源。测试的时候好好的,上线就出问题——往往就是因为测试环境根本测不出问题。
这家公司花了 2 个月做测试环境对齐:
# 测试环境问题清单
问题 1:测试库数据量太小
→ 现状:测试库只有 1000 条记录
→ 真相:生产库有 5000 万条,索引在生产环境才生效
→ 修复:每月从生产库脱敏导出数据,同步到测试库
问题 2:测试环境网络配置和线上不一致
→ 现状:测试环境直连数据库,没有经过网关
→ 真相:上线后网关层的限流规则在测试环境没有覆盖
→ 修复:测试环境强制走网关链路,和生产完全一致
问题 3:测试环境配置参数和生产不一致
→ 现状:JVM 堆大小测试环境 512MB,生产 4GB
→ 真相:内存敏感的功能在测试环境表现正常,生产触发 OOM
→ 修复:测试环境和生产环境参数完全对齐
3. 建立监控体系
创业公司常见的监控问题是:有监控,但不知道看什么。
他们的策略是从业务视角出发倒推监控指标:
业务视角:用户能正常下单吗?
↓ 拆解
前端:页面能打开吗? → 监控 HTTP 5xx 率
网关:请求能进来吗? → 监控网关 QPS 和延迟
服务:业务逻辑能跑通吗? → 监控核心接口成功率
数据库:数据能读写吗? → 监控数据库连接池和慢查询
缓存:缓存能命中吗? → 监控缓存命中率
然后,他们定义了告警阈值:
alerting/prometheus_rules.yml
groups:
- name: business_health
rules:
# 核心接口成功率
- alert: CoreApiSuccessRateLow
expr: |
sum(rate(http_requests_total{status=~"2.."}[5m]))
/ sum(rate(http_requests_total[5m])) < 0.99
for: 2m
labels:
severity: critical
annotations:
summary: "核心接口成功率低于 99%"
description: "{{ $labels.job }} 接口 {{ $labels.path }} 成功率 {{ $value | humanizePercentage }}"
# 数据库慢查询
- alert: DatabaseSlowQueryHigh
expr: |
sum(rate(mysql_slow_queries_total[5m])) > 10
for: 5m
labels:
severity: warning
annotations:
summary: "数据库慢查询过多"
description: "过去 5 分钟慢查询速率 {{ $value }}/s"
# 缓存命中率
- alert: CacheHitRateLow
expr: |
sum(rate(cache_hits_total[5m]))
/ sum(rate(cache_requests_total[5m])) < 0.8
for: 10m
labels:
severity: warning
annotations:
summary: "缓存命中率低于 80%"
description: "当前命中率 {{ $value | humanizePercentage }}"
关键的一点是:告警之后必须有联动。他们接入了 PagerDuty,告警自动触发值班工程师的电话,不接听就升级。
第二阶段(第 4~6 个月):构建容错能力
4. 限流与熔断
在有一次外部支付渠道挂了导致整站瘫痪之后,他们终于意识到:必须有容错机制,否则任何一个依赖方的故障都会扩散到整个系统。
// 引入 Sentinel 作为限流和熔断组件
@Configuration
public class SentinelConfig {
@Bean
public SentinelResourceAspect sentinelResourceAspect() {
return new SentinelResourceAspect();
}
}
// 对外部支付接口配置熔断
@Service
public class PaymentService {
@SentinelResource(value = "payment",
fallback = "paymentFallback",
blockHandler = "paymentBlockHandler")
public PaymentResult pay(Order order) {
return paymentGateway.process(order);
}
// 降级方法:支付失败时的兜底处理
public PaymentResult paymentFallback(Order order, Throwable t) {
log.warn("Payment service failed, return pending: {}", t.getMessage());
// 记录为待支付状态,让用户稍后重试
return PaymentResult.pending(order.getId());
}
// 限流方法:被限流时的处理
public PaymentResult paymentBlockHandler(Order order, BlockException e) {
log.warn("Payment rate limited: {}", e.getMessage());
return PaymentResult.retryLater(order.getId());
}
}
Sentinel
sentinel:
rules:
# 支付接口限流:每秒最多 1000 次调用
flow:
- resource: /api/payment
grade: 1 # QPS 模式
count: 1000
controlBehavior: 0 # 直接拒绝
# 外部渠道熔断:当错误率 > 30% 时熔断 10 秒
degrade:
- resource: payment-gateway
grade: 1 # 错误率模式
count: 0.3
timeWindow: 10 # 熔断持续 10 秒
minRequestAmount: 10 # 至少 10 个请求才触发熔断
5. 灰度发布
发布是故障的高发节点。哪怕经过充分测试,生产环境的真实流量和测试环境仍有差异。很多问题只有在真实流量下才会暴露。
他们选择了金丝雀发布(Canary Release):
Argo
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: order-service
spec:
replicas: 10
strategy:
canary:
steps:
- setWeight: 5 # 初始 5% 流量到新版本
- pause: {duration: 10m} # 观察 10 分钟
- setWeight: 20 # 扩大到 20%
- pause: {duration: 10m}
- setWeight: 50 # 扩大到 50%
- pause: {duration: 10m}
- setWeight: 100 # 全量
canaryMetadata:
labels:
role: canary
stableMetadata:
labels:
role: stable
trafficRouting:
nginx:
stableIngress: stable-ingress
additionalIngressAnnotations:
canary-by-header: X-Canary
同时配置了自动回滚条件:
# 自动回滚触发条件
rollback:
triggers:
- type: ErrorRate
threshold: 0.05 # 错误率超过 5% 自动回滚
- type: Latency
threshold: 2000ms # p99 延迟超过 2s 自动回滚
6. 故障演练
知道系统能扛住故障,和真的验证过能扛住,是两回事。这家公司每季度做一次故障演练(Chaos Engineering)。
Chaos
# 演练场景 1:数据库连接耗尽
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
name: simulate-db-connection-exhaustion
spec:
action: delay
mode: one
selector:
namespaces:
- production
labelSelectors:
app: order-service
delay:
latency: "5000ms"
target:
mode: one
selector:
namespaces:
- production
labelSelectors:
db: mysql
---
# 演练场景 2:Redis 不可用
apiVersion: chaos-mesh.org/v1alpha1
kind: PodChaos
metadata:
name: redis-node-failure
spec:
action: pod-kill
mode: one
selector:
namespaces:
- production
labelSelectors:
app: redis
每次演练之后,他们会记录:
故障演练报告 - 2026-01-15
演练场景:Redis 单节点宕机
预期结果:Redis 自动主从切换,服务在 30 秒内恢复
实际结果:服务在 45 秒内恢复(慢了 15 秒)
差距分析:应用端没有配置 Redis 连接池重连策略,
故障发生后旧连接没有被及时释放
改进措施:应用端增加连接池自动重连逻辑
完成时间:2 月 15 日
状态:已完成
第三阶段(第 7~12 个月):持续改进
7. 数据安全体系
在有一次误删数据的惊魂之后(幸好有 binlog 恢复回来了),他们把数据安全提到了最高优先级:
- 所有生产库开启 binlog:每条数据变更都有记录
- 备份 3-2-1 原则:3 份副本、2 种介质、1 份异地
- 每月恢复演练:每个备份集都要能成功恢复
# 数据备份策略
# 每小时增量备份(binlog)
00 * * * * mysqladmin flush-logs
# 每天凌晨 3 点全量备份
0 3 * * * mysqldump --single-transaction --routines --triggers \
--master-data=2 --all-databases | gzip > /backup/mysql_$(date +%Y%m%d).sql.gz
# 每周同步到异地对象存储
0 4 * * 0 s3 sync /backup/ s3://company-backup/mysql/
8. 变更管理
他们发现,故障最常见的触发点是一次「小改动」——看起来不起眼的配置变更,往往造成灾难性后果。
于是建立了变更管理流程:
变更类型:
- 普通变更:代码提交、配置修改,需要 Code Review
- 高危变更:生产数据库写操作、高危命令执行,需要额外审批 + 变更窗口
- 紧急变更:故障修复中的临时变更,需要事后复盘
变更窗口:
- 常规发布窗口:周二、周四 14:00 - 17:00
- 禁止变更窗口:周五 17:00 后、节假日全天
9. 事后复盘机制
每一次故障,无论大小,都必须有复盘。他们的复盘机制有一个关键规则:复盘不追责,但改进措施必须有人负责。
复盘改进跟踪表
| 复盘编号 | 故障日期 | 主要改进措施 | 负责人 | 计划完成 | 状态 |
| --- | --- | --- | --- | --- | --- |
| INC-001 | 2026-01-15 | 增加 Redis 重连策略 | @张三 | 2026-02-15 | 已完成 |
| INC-002 | 2026-03-20 | 备份恢复演练标准化 | @李四 | 2026-04-20 | 进行中 |
| INC-003 | 2026-06-01 | 限流规则细化 | @王五 | 2026-07-01 | 待开始 |
一年后的结果
12 个月后,防护体系建设的成效体现在数字上:
总结
这家公司的经历说明一个道理:故障预防不是一次性工程,而是持续投入的系统性建设。
回顾他们的路径,可以总结出三层防护体系:
第一层:代码质量(最基础)
- 强制 Code Review + CI 质量门禁
- 测试环境与生产环境完全对齐
- 充分的单元测试和集成测试
第二层:容错设计(防止扩散)
- 限流 + 熔断,防止依赖方故障拖垮系统
- 灰度发布 + 自动回滚,把故障影响控制在最小范围
- 故障演练,定期验证系统的容错能力
第三层:监控与数据安全(兜底手段)
- 完整的业务监控,发现问题后第一时间告警
- 数据多级备份 + 定期恢复演练,确保数据可恢复
- 变更管理 + 事后复盘,从流程上减少人为失误
三层缺一不可。第一层做好,可以减少大部分代码质量问题;第二层做好,可以把故障影响控制在局部;第三层做好,可以在最坏的情况下依然有兜底手段。
下一节,我们来看看故障真的发生时,怎么做到快速止血、快速恢复。