1. 故障背景与影响:支付接口超时率飙升、用户投诉、业务损失复盘
1.1 故障现象与时间线
2024年6月18日大促期间,核心支付网关接口在14:32分开始出现大面积超时,持续时长约47分钟。监控系统显示,支付确认接口(/v1/pay/confirm)的P99延迟从基线45ms飙升至12.8s,超时率从0.02%骤升至23.7%。
| 指标项 | 故障前(基线) | 故障峰值 | 恢复后 |
|---|---|---|---|
| P99 延迟 | 45 ms | 12,800 ms | 52 ms |
| 超时率 | 0.02% | 23.7% | 0.03% |
| QPS | 2,300 | 3,100 | 2,450 |
| 错误数/分钟 | 0.5 | 734 | 0.7 |
关键发现: 故障期间数据库连接池活跃连接数达到上限(200),大量goroutine阻塞在获取数据库连接上,形成级联雪崩。
1.2 用户投诉与业务影响
故障期间共收到用户投诉2,847条,主要集中为三类:
- 支付成功但订单状态未更新(占比62%):用户已扣款,但页面显示“支付中”,导致重复支付尝试。
- 支付超时提示(占比28%):接口返回504,用户无法确认是否扣款成功。
- 退款异常(占比10%):部分用户发起退款后,退款状态卡在“处理中”超过2小时。
直接业务损失估算:
| 损失类型 | 金额(万元) | 说明 |
|---|---|---|
| 订单流失 | ≈ 320 | 超时导致用户放弃支付,转化率下降18% |
| 客诉赔付 | ≈ 45 | 重复扣款、退款延迟的补偿 |
| 技术应急人力 | ≈ 12 | 6名工程师紧急排查+数据库扩容 |
| 合计 | ≈ 377 | 未计入品牌信誉损失 |
1.3 故障根因全景图
下图展示了故障发生的完整链路与根因关系:
1.4 故障复盘关键数据
通过事后日志与APM追踪,我们提取了故障期间的核心性能数据:
| 维度 | 具体指标 | 故障值 | 健康阈值 |
|---|---|---|---|
| 数据库 | 慢查询数/分钟 | 1,240 | < 10 |
| 行锁等待平均时长 | 4.7 s | < 100 ms | |
| 应用 | goroutine 阻塞数 | 8,600 | < 200 |
| DB 连接获取等待 | 3.2 s | < 10 ms | |
| 业务 | 支付成功率 | 76.3% | > 99.9% |
| 重复支付订单数 | 1,247 | 0 |
⚠️ 重要警示: 本次故障中,
order_no 字段的 SELECT ... FOR UPDATE 语句在无索引情况下触发了全表扫描,每次扫描耗时约 2.3s。并发 200 个请求时,数据库立即进入“锁风暴”状态,这是本次超时飙升的直接导火索。
1.5 故障链路的代码级还原
以下是故障发生时支付确认接口的核心代码片段,问题集中在 无索引查询 + 行锁 的组合上:
// 故障版本:支付确认接口
func (s *PayService) ConfirmPay(ctx context.Context, req *PayConfirmReq) error {
// 问题1:order_no 字段无索引,触发全表扫描
// 问题2:FOR UPDATE 锁住所有匹配行,高并发下互相等待
query := `SELECT id, status, amount FROM orders
WHERE order_no = ? FOR UPDATE`
tx, _ := s.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted})
row := tx.QueryRowContext(ctx, query, req.OrderNo)
var order Order
err := row.Scan(&order.ID, &order.Status, &order.Amount)
if err != nil {
tx.Rollback()
return err
}
// 模拟第三方支付网关调用(耗时约 200ms)
if err := s.thirdPartyPay(ctx, order); err != nil {
tx.Rollback()
return err
}
// 更新订单状态
_, err = tx.ExecContext(ctx, `UPDATE orders SET status = 'paid' WHERE id = ?`, order.ID)
if err != nil {
tx.Rollback()
return err
}
return tx.Commit()
}