Skip to content

10.8 生产环境排查案例 ​

案例1: CPU飙升问题定位 ​

问题现象 ​

监控告警:
- CPU使用率突然从20%飙升至95%
- QPS无明显变化
- 响应延迟增加3倍

排查步骤 ​

Step 1: 确认进程

bash
# 查看CPU使用最高的进程
top -c

# 输出
PID   USER  %CPU  COMMAND
12345 app   95.0  ./myapp

Step 2: 采集CPU数据

bash
# 采集30秒CPU数据
curl -o cpu.prof http://localhost:6060/debug/pprof/profile?seconds=30

# 分析
go tool pprof cpu.prof

Step 3: 定位热点

bash
(pprof) top10
     5000ms 50.00%  regexp.Compile
     2000ms 20.00%  main.validateEmail
     1000ms 10.00%  runtime.mallocgc

Step 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.malg

Step 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.prof

Step 3: 定位泄露

bash
(pprof) top
     450MB  main.RequestCache.Set
     150MB  main.ConnectionPool.Get
      50MB  main.UserSession.New

Step 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.out

Step 3: 定位慢请求

Goroutine 1234:
- HTTP Handler: 1500ms
  - Database Query: 1200ms  ❌ 瓶颈
    - SELECT * FROM orders WHERE user_id = ?
  - JSON Encode: 200ms
  - Network: 100ms

Step 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.prof

Step 2: 分析Goroutine

bash
(pprof) top
     5000  main.worker
     2000  main.handler
     1000  runtime.chan receive

Step 3: 查看堆栈

bash
(pprof) traces
-----------+-------------------------------------------------------
      5000   main.worker
             main.startWorkers
             main.main
             runtime.main

Step 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泄露
  • [ ] 掌握完整排查流程
  • [ ] 建立了监控告警

延伸阅读 ​