10.8 生产环境排查案例
案例1: CPU飙升问题定位
问题现象
监控告警:
- CPU使用率突然从20%飙升至95%
- QPS无明显变化
- 响应延迟增加3倍排查步骤
Step 1: 确认进程
bash
# 查看CPU使用最高的进程
top -c
# 输出
PID USER %CPU COMMAND
12345 app 95.0 ./myappStep 2: 采集CPU数据
bash
# 采集30秒CPU数据
curl -o cpu.prof http://localhost:6060/debug/pprof/profile?seconds=30
# 分析
go tool pprof cpu.profStep 3: 定位热点
bash
(pprof) top10
5000ms 50.00% regexp.Compile
2000ms 20.00% main.validateEmail
1000ms 10.00% runtime.mallocgcStep 4: 查看详细代码
bash
(pprof) list main.validateEmail
. func validateEmail(email string) bool {
10ms pattern := `^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`
1000ms matched, _ := regexp.MatchString(pattern, email) // 每次编译
. return matched
. }问题根因
go
// ❌ 每次调用都编译正则表达式
func validateEmail(email string) bool {
pattern := `^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`
matched, _ := regexp.MatchString(pattern, email)
return matched
}解决方案
go
// ✅ 预编译正则表达式
var emailRegex = regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`)
func validateEmail(email string) bool {
return emailRegex.MatchString(email)
}优化效果
CPU使用率: 95% → 25%
响应延迟: 150ms → 50ms
QPS: 保持不变案例2: 内存泄露问题排查
问题现象
监控告警:
- 内存使用量持续增长
- 每2小时增长500MB
- 需要定期重启服务排查步骤
Step 1: 建立基线
bash
# 采集初始内存
curl http://localhost:6060/debug/pprof/heap > baseline.prof
# 查看当前内存
go tool pprof -inuse_space baseline.prof
(pprof) top
50MB main.init
20MB runtime.malgStep 2: 压测后采集
bash
# 压测2小时
ab -n 1000000 http://localhost:8080/api
# 采集压测后内存
curl http://localhost:6060/debug/pprof/heap > after.prof
# 对比差异
go tool pprof -base baseline.prof after.profStep 3: 定位泄露
bash
(pprof) top
450MB main.RequestCache.Set
150MB main.ConnectionPool.Get
50MB main.UserSession.NewStep 4: 查看代码
bash
(pprof) list main.RequestCache.Set
. func (c *RequestCache) Set(key string, req *Request) {
450MB c.mu.Lock()
. c.data[key] = req // 只存不删
. c.mu.Unlock()
. }问题根因
go
// ❌ 缓存只增不减
type RequestCache struct {
mu sync.Mutex
data map[string]*Request
}
func (c *RequestCache) Set(key string, req *Request) {
c.mu.Lock()
c.data[key] = req // 永远不删除
c.mu.Unlock()
}解决方案
go
// ✅ 使用LRU缓存并设置过期时间
type RequestCache struct {
mu sync.Mutex
data map[string]*cacheEntry
maxSize int
ttl time.Duration
}
type cacheEntry struct {
value *Request
expiredAt time.Time
}
func (c *RequestCache) cleanup() {
c.mu.Lock()
defer c.mu.Unlock()
now := time.Now()
for key, entry := range c.data {
if now.After(entry.expiredAt) {
delete(c.data, key)
}
}
}
// 定期清理
go func() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for range ticker.C {
c.cleanup()
}
}()优化效果
内存使用: 持续增长 → 稳定在200MB
重启频率: 每2小时 → 无需重启
服务稳定性: 显著提升案例3: 响应延迟优化
问题现象
用户反馈:
- API响应慢
- P99延迟超过1秒
- 偶发超时错误排查步骤
Step 1: 分析延迟分布
bash
# Prometheus查询
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))
# 输出
P50: 50ms
P90: 200ms
P99: 1500ms ❌Step 2: Trace分析
bash
# 采集trace
curl -o trace.out http://localhost:6060/debug/pprof/trace?seconds=10
# 可视化
go tool trace trace.outStep 3: 定位慢请求
Goroutine 1234:
- HTTP Handler: 1500ms
- Database Query: 1200ms ❌ 瓶颈
- SELECT * FROM orders WHERE user_id = ?
- JSON Encode: 200ms
- Network: 100msStep 4: 数据库分析
sql
-- 执行计划分析
EXPLAIN SELECT * FROM orders WHERE user_id = 12345;
-- 输出
type: ALL ❌ 全表扫描
rows: 1000000
Extra: Using where问题根因
go
// ❌ 无索引查询
func GetOrdersByUserID(db *sql.DB, userID int) ([]Order, error) {
rows, err := db.Query("SELECT * FROM orders WHERE user_id = ?", userID)
// user_id无索引,全表扫描
}解决方案
方案1: 添加索引
sql
-- 创建索引
CREATE INDEX idx_user_id ON orders(user_id);
-- 验证
EXPLAIN SELECT * FROM orders WHERE user_id = 12345;
-- 输出
type: ref ✅
rows: 50方案2: 添加缓存
go
// 使用Redis缓存
func GetOrdersByUserID(db *sql.DB, cache *redis.Client, userID int) ([]Order, error) {
cacheKey := fmt.Sprintf("orders:user:%d", userID)
// 先查缓存
cached, err := cache.Get(cacheKey).Result()
if err == nil {
var orders []Order
json.Unmarshal([]byte(cached), &orders)
return orders, nil
}
// 缓存未命中,查询数据库
rows, err := db.Query("SELECT * FROM orders WHERE user_id = ?", userID)
// ...
// 写入缓存(过期时间5分钟)
cache.Set(cacheKey, data, 5*time.Minute)
}优化效果
P99延迟: 1500ms → 80ms (提升18倍)
QPS: 500 → 2000 (提升4倍)
超时率: 5% → 0.1%案例4: Goroutine泄露排查
问题现象
监控告警:
- Goroutine数量持续增长
- 24小时增长到10000+
- 服务响应变慢排查步骤
Step 1: 采集Goroutine数据
bash
curl http://localhost:6060/debug/pprof/goroutine > goroutine.prof
go tool pprof goroutine.profStep 2: 分析Goroutine
bash
(pprof) top
5000 main.worker
2000 main.handler
1000 runtime.chan receiveStep 3: 查看堆栈
bash
(pprof) traces
-----------+-------------------------------------------------------
5000 main.worker
main.startWorkers
main.main
runtime.mainStep 4: 查看代码
go
// ❌ Goroutine泄露
func handler(conn net.Conn) {
for {
data := make([]byte, 1024)
n, err := conn.Read(data)
if err != nil {
log.Println(err)
// 没有return,goroutine永不退出
}
process(data[:n])
}
}解决方案
go
// ✅ 添加超时和退出机制
func handler(conn net.Conn) {
defer conn.Close()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
done := make(chan struct{})
go func() {
defer close(done)
for {
select {
case <-ctx.Done():
return
default:
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
data := make([]byte, 1024)
n, err := conn.Read(data)
if err != nil {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
continue
}
return
}
process(data[:n])
}
}
}()
select {
case <-done:
case <-ctx.Done():
}
}优化效果
Goroutine数量: 10000+ → 100
内存使用: 2GB → 500MB
服务响应: 恢复正常生产环境排查SOP
标准流程
1. 确认问题
- 查看监控告警
- 确认影响范围
- 记录问题时间点
2. 采集数据
- CPU: pprof profile
- 内存: pprof heap
- Goroutine: pprof goroutine
- Trace: pprof trace
3. 分析定位
- 使用pprof分析热点
- 查看代码详情
- 确定根因
4. 制定方案
- 修复代码
- 优化配置
- 添加监控
5. 验证效果
- 压测验证
- 对比优化前后
- 确保问题解决
6. 文档沉淀
- 记录排查过程
- 总结解决方案
- 团队分享经验学习检查点
完成本章节后,验证:
- [ ] 排查过CPU飙升问题
- [ ] 定位过内存泄露
- [ ] 优化过响应延迟
- [ ] 解决过Goroutine泄露
- [ ] 掌握完整排查流程
- [ ] 建立了监控告警