线上故障排查
问题
Go 服务出现线上故障(接口报错、服务不可用、数据异常),如何快速定位和恢复?
答案
应急处理流程
核心原则
先止血,后定因。线上故障的第一优先级是恢复服务,而不是找根本原因。
第一步:止血
| 止血手段 | 适用场景 | 操作 |
|---|---|---|
| 代码回滚 | 近期有发布 | 回滚到上一个稳定版本 |
| 限流 | 流量激增 | Sentinel/Nginx 限流 |
| 降级 | 非核心功能故障 | Feature Flag 关闭故障功能 |
| 切换 | 依赖服务故障 | 切备用链路/降级缓存 |
| 扩容 | 容量不足 | K8s HPA 扩容 |
// Feature Flag 降级
func GetRecommendation(ctx context.Context, userID string) ([]Item, error) {
// 推荐服务故障时降级到热门列表
if featureFlag.IsDisabled("recommendation") {
return getHotItems(ctx)
}
return recommendService.Get(ctx, userID)
}
第二步:定位
信息收集顺序
监控面板 → 错误日志 → 链路追踪 → pprof → 代码 diff
2.1 监控面板
快速看 RED 指标(Grafana):
- Rate:QPS 是否异常
- Errors:错误率是否飙升
- Duration:P99 延迟是否增加
同时关注:
- CPU / 内存 / Goroutine 数
- 数据库连接池状态
- 下游服务健康状况
2.2 错误日志
# 按时间排序查看错误日志
grep -i "error\|panic" /var/log/app/app.log | tail -100
# 如果是结构化日志(JSON),用 jq 过滤
cat app.log | jq 'select(.level=="error")' | tail -50
# ELK 查询
# level:error AND service:user-service AND @timestamp >= "2024-01-15T10:00:00"
2.3 链路追踪
# 从错误日志中提取 traceID
# 在 Jaeger/Zipkin 中搜索 traceID
# 看整条链路哪个环节耗时或报错
2.4 pprof(性能问题)
# CPU 飙高
go tool pprof http://pod-ip:6060/debug/pprof/profile?seconds=30
# 内存增长
go tool pprof http://pod-ip:6060/debug/pprof/heap
# Goroutine 泄漏
go tool pprof http://pod-ip:6060/debug/pprof/goroutine
2.5 代码 Diff
# 查最近的代码变更
git log --oneline -10
git diff HEAD~1 HEAD --stat
常见故障类型 & 排查重点
| 故障类型 | 排查方向 |
|---|---|
| 接口大量 5xx | 错误日志 → panic 栈 → 下游依赖状态 |
| 接口超时 | 链路追踪 → 慢查询 → 连接池满 → GC STW |
| CPU 飙高 | pprof CPU → 热点函数 → 死循环/正则 |
| 内存 OOM | pprof heap → Goroutine 泄漏 → 大对象缓存 |
| 数据不一致 | 并发操作 → 缓存一致性 → 分布式事务 |
| 部分用户异常 | 灰度策略 → 特定参数 → 数据问题 |
第三步:修复
// Hotfix 注意事项
// 1. 修复代码尽量小,只改必要部分
// 2. 本地测试通过 + Code Review
// 3. 灰度发布验证
// 4. 如果不确定修复是否正确,先用降级止血
第四步:验证
# 验证清单
# ✅ 接口响应正常(curl 测试)
# ✅ 错误率恢复到正常水平
# ✅ 延迟 P99 恢复
# ✅ 监控面板所有指标正常
# ✅ 日志无新增异常
# ✅ 观察至少 15~30 分钟
第五步:复盘
## 故障复盘模板
### 基本信息
- 故障等级:P0/P1/P2
- 影响范围:xxx 接口、xx 用户
- 故障时长:10:05 ~ 10:35(30 分钟)
### Timeline
- 10:05 告警触发:接口错误率 > 5%
- 10:08 值班同学开始排查
- 10:12 确认是数据库连接池耗尽
- 10:15 回滚到上一版本止血
- 10:20 错误率恢复
- 10:35 确认服务稳定
### 根因分析
- 新版本引入了一个查询,忘了 rows.Close() 导致连接泄漏
### 改进措施
- [ ] 增加 db 连接池监控告警
- [ ] CI 加 rows.Close() 的 lint 检查
- [ ] 开发 Code Review checklist
Go 服务必备应急工具
| 工具 | 用途 |
|---|---|
| pprof | CPU/内存/Goroutine 分析 |
| Prometheus + Grafana | 实时监控 |
| Jaeger/Zipkin | 链路追踪 |
| ELK / Loki | 日志查询 |
| Sentry | 错误聚合告警 |
| kubectl | K8s 操作(回滚、扩容) |
常见面试问题
Q1: 线上出现 panic 怎么办?
答案:
- Recovery 中间件确保服务不挂:
func RecoveryMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if r := recover(); r != nil {
stack := debug.Stack()
log.Printf("PANIC: %v\n%s", r, stack)
// 上报 Sentry
c.AbortWithStatus(500)
}
}()
c.Next()
}
}
- 根据 panic 栈信息定位具体代码行
- 通常原因:nil pointer、index out of range、map 并发读写
Q2: 服务偶发超时但大部分正常怎么排查?
答案:
- 看超时请求的链路追踪,找到慢环节
- 可能原因:GC STW、锁竞争、数据库慢查询、DNS 解析
- 收集多个超时 case 寻找共性
- 注意 P99 vs P50 的差异
Q3: 如何做到一分钟内发现故障?
答案:
- Prometheus 采集间隔 15s + 告警规则
for: 1m - 关键接口用即时告警(error rate > 5% 持续 30s)
- 多渠道:钉钉/飞书/电话/短信
- 值班机制 + 自动 oncall 轮转
Q4: 回滚有哪些方式?
答案:
- 代码回滚:K8s
kubectl rollout undo deployment/xxx - 配置回滚:配置中心回退版本
- 数据库回滚:谨慎,通常只能补偿而非回滚
- 流量回滚:灰度流量切回旧版本