请求在每一站排队: 锁 / 池 / SQL / 缓存 / 队列 — 连接满不可怕, 等待时间上涨才是真危险
应用层性能就是一张游乐园排队地图: 入口闸机 (worker 池)、热门项目门口 (连接池)、项目本身 (DB/SQL)、快速通道 (缓存) 各有自己的队伍。新手只盯着"项目好不好玩" (SQL 快不快), 高手先看队伍在哪个环节变长 — 因为连接池满本身不死人, 等待时间上涨才死人。而所有队伍共享一条铁律: 下游消化不了时上游必须减速 (backpressure), 否则等待会以 retry 为杠杆放大成雪崩。
# Go mutex profile: $ go tool pprof -top http://svc:6060/debug/pprof/mutex # 4.8s configmu.Lock ← 全局锁持有 4.8s/样本周期
# 正常 500 → 20000, QPS 没变 → 泄漏实锤 $ go tool pprof http://svc:6060/debug/pprof/goroutine # ch := make(chan int) // 只发不收, 接收者早退了
# HikariCP/Go sql.DB 判定口径: # active=100 max=100 idle=0 ← 满了但未必有事 # wait_duration ↑↑ ← 这才是 P99 元凶
# 错: DB 慢 → max_conn 100→1000 # 围殴 # 对: 找慢 SQL + 池保持 2×核数×单连接效率
rows_examined / rows_returned 才暴露坏索引: 返回 10 行扫描 100 万行 = 索引没用上。 # EXPLAIN: type=ALL rows=1024000 # WHERE nickname = 'abc' ← nickname 无索引全表扫
-- 联合索引 idx(a,b): 查 b 不走索引 -- 对: WHERE a = ? AND b = ? -- 全命中 -- 覆盖: SELECT a,b FROM t WHERE a=? -- 免回表
# 错: for u := range users { getOrder(u.ID) } # 1000 次 # 对: WHERE user_id IN (...) 一次取回, 内存组装
-- 错: 事务横跨外部调用 BEGIN; SELECT ...; call_http(); UPDATE ...; COMMIT; -- 对: 算好业务 → 短事务只做两次写
-- MySQL: 谁在持锁/等锁 SELECT * FROM information_schema.innodb_trx ORDER BY trx_started; -- 最老的事务先审
# hit_rate 98% → 20% 的瞬间: # DB QPS ×50 → 连接池满 → API P99 爆 — Redis 没挂, 系统挂了
# produce=10000/s consume=9000/s # lag += 1000/s → 一小时 360 万条 → 迟早爆盘/OOM
# strace -c: futex 92% → 只算立案 # mutex profile: configmu.Lock 累计 4.8s → 逮捕
make(chan T) 无缓冲或不设上限的内存队列: 消费跟不上时队列无上限吃内存, 最后 GC/OOM 全线崩。 # 错: ch := make(chan Job) # 生产∞消费慢 # 对: make(chan Job, 10000) + 满则拒绝/降级
P50=50ms P99=4s, CPU 30%。池指标一出, 方向立刻从"代码"转向"SQL"。
# 池指标: 请求没执行 SQL 就在排队 # active=100 max=100 wait_count=4200 wait_duration avg=180ms # 谁占着连接? 慢 SQL 日志按扫描比排序 -- 慢查询: 1.8s, rows_examined=1024000, rows_returned=12 EXPLAIN SELECT * FROM orders WHERE seller_note = 'x'; -- type=ALL ← seller_note 无索引, 全表扫, 1.8s 占着连接 # 修复: 加索引 + SELECT 只取需要的列 # 效果: wait_duration 180ms→2ms, P99 4s→80ms
链条: pool.wait → 慢 SQL → rows 比值 → EXPLAIN → 索引。
CPU 15% 但服务 5 秒一响应。strace 立案, mutex profile 逮捕。
$ strace -c -p 1234 # 10 秒 # futex 92% ← 全在等锁, 立案 $ go tool pprof -top http://svc:6060/debug/pprof/mutex # flat cum # 4.8s config.RLock ← 全局配置锁, 读多却互斥 // 修复: 配置读时无锁 (atomic.Value 快照), 写时整体替换 var cfg atomic.Value // 存不可变 Config 快照 func Get() Config { return cfg.Load().(Config) } func Reload(c Config) { cfg.Store(c) } // 只在写时替换 # 效果: 500 并发从"排队"恢复真并行, P99 5s→60ms
读多写少的数据结构, atomic.Value / COW 快照比读写锁更干净。
goroutine 从 500 涨到 20000, RSS 同步爬升。goroutine profile 一眼点名。
$ go tool pprof -top http://svc:6060/debug/pprof/goroutine # 19800 goroutines over: sync.runtime_Semacquire # app/fanout.go:42 out <- result ← 发送阻塞 // 错: 下游出错提前 return, 没人读 out 了 go func() { out <- slowWork(ctx) }() // 对: select 双路, ctx 取消能退出 go func() { select { case out <- slowWork(ctx): case <-ctx.Done(): // 泄漏闸门 } }()
上线后 goroutine 稳定 480±20, RSS 曲线同步走平。
单条 SQL 全部健康 (2ms), 页面却 2 秒 — 1+1000 次 SQL 的经典复合账单。
// 错: 每个用户一次查询 for _, u := range users { // 1000 人 orders[u.ID] = repo.ListByUser(ctx, u.ID) // 1+1000 次 SQL } // 对: 一次 IN + 内存分组 ids := make([]int64, 0, len(users)) for _, u := range users { ids = append(ids, u.ID) } rows := repo.ListByUsers(ctx, ids) // WHERE user_id IN (?...) for _, o := range rows { orders[o.UserID] = append(orders[o.UserID], o) } // 1001 次 → 2 次, 页面 2.0s → 20ms
给 ORM 关掉懒加载或显式批量预载, N+1 在代码评审就该拦住。
结算事务横跨第三方支付调用, 第三方一慢, DB 连接和行锁全程被持有。
-- 错: 事务内含 800ms 的外部调用, 高峰锁等待 300ms+ BEGIN; SELECT stock FROM sku WHERE id=1 FOR UPDATE; -- call_http(payment) 800ms ← 锁全程被持有 UPDATE sku SET stock=stock-1 WHERE id=1; COMMIT; -- 对: 事务只包 DB 一致性 -- 1) 先调支付拿到凭证 (无事务) -- 2) 短事务扣减: SELECT FOR UPDATE → UPDATE → COMMIT (3ms) -- 3) 支付失败走补偿 (幂等扣减/退单)
铁律: 事务内不许有网络调用、sleep、重计算; 跨系统一致性用 saga/补偿。
1000 QPS 打同一行 UPDATE, InnoDB 行锁把吞吐锁死在单行串行。
-- 错: 热点行, 所有请求串行等锁 UPDATE seckill SET stock = stock - 1 WHERE id = 1; -- 对: 分 16 桶, 请求随机打散 (0..15) UPDATE seckill SET stock = stock - 1 WHERE id = ? AND bucket = ? AND stock > 0; # 读时 SUM 合并, 卖完任一桶再向相邻桶借量 # 效果: 单行 1k/s → 16 桶 12k/s, 锁等待消失
热点行的兄弟还有热点 user / 热点 shard — 思路都是打散 + 合并读。
首页热点 key 过期瞬间, 10 万请求同时回源 — 一个进程内合并回源即可。
var g singleflight.Group func GetHot(ctx context.Context, key string) (*Item, error) { if v, ok := cache.Get(key); ok { return v, nil } // 同 key 并发只放一个去 DB, 其余共享结果 v, err, _ := g.Do(key, func() (interface{}, error) { return loadFromDB(ctx, key) // 10万次 → 1次 }) cache.Set(key, v, 5*time.Minute+jitter(30*time.Second)) return v.(*Item), err }
多实例部署时再叠加分布式互斥 (SETNX) 或提前逻辑过期, 双保险。
被恶意 ID 扫描: 99% 请求查不存在的数据, 全部穿透到 DB。
func GetUser(id int64) (*User, error) { if !bloom.Test([]byte(strconv.FormatInt(id, 10))) { return nil, ErrNotFound // 第一闸: 布隆挡掉 99% 假 ID } if v, ok := cache.Get(id); ok { return v, nil } u, err := db.Load(id) if err == ErrNotFound { cache.SetNX(id, nil, 60*time.Second) // 第二闸: 空值短 TTL return nil, ErrNotFound } cache.Set(id, u, 10*time.Minute) return u, nil }
布隆过滤器随用户增量重建; 空值 TTL 不宜长, 防新注册用户查不到。
凌晨批量预热 20 万 key 全部 2 小时过期, 0 点整 DB 被精确围殴。
// TTL 必加抖动: 基础 2h ± 10min 随机 ttl := 2*time.Hour + time.Duration(rand.Int63n(1200))*time.Second cache.Set(key, v, ttl) // 热点再加本地一级缓存, miss 冲击先被本地吸收 type TwoLevel struct { local *lru.Cache // 进程内, 容量 1 万, TTL 10s redis *redis.Client // 共享层 } // 兜底: Redis 不可用时限流读 DB (令牌桶), 绝不放全量
修复后 0 点 DB QPS 从 8 万回落到 9000, 与白天水位一致。
大盘看着平静, 消费速率悄悄落后生产 — lag 趋势是唯一诚实的仪表。
# Prometheus 规则: lag 1 小时线性外推将突破阈值 - alert: ConsumerLagGrowing expr: | deriv(kafka_consumergroup_lag[30m]) > 0 and predict_linear(kafka_consumergroup_lag[30m], 3600) > 100000 for: 10m # 处置 SOP: 扩消费者 (分区数内) → 批量优化 (fetch.min.bytes) # → 还压不住: 评估降级/丢弃策略, 而不是硬扛
配套: 消费者扩容上限 = 分区数, 分区规划要预留消费力余量。
# 错: pool 100→1000 (DB CPU 直接 100%) # 对: 慢 SQL 修复 + 池 100 + wait 监控
defer rows.Close() 或 panic 路径漏 Release, 池被慢慢吃光. 原因: 借还不对称. 正解: 借还写在一起, 全路径 defer。 # 错: rows := db.Query(...) # 没有 defer Close # 对: rows, err := ...; defer rows.Close()
# 错: BEGIN → call_http() → UPDATE → COMMIT # 对: 先 RPC 拿凭证 → 短事务写库 → 失败补偿
# 错: var mu sync.Mutex 包住所有方法 # 对: 读走 atomic.Value 快照, 写整体替换
# 错: out <- result # 没人收就永远阻塞 # 对: select { case out <- r: case <-ctx.Done(): }
# 错: db.Query(sql) # 无 ctx # 对: db.QueryContext(ctx, sql)
defer Body.Close(), 错误也要读完 body。 # 错: if code != 200 { return err } # 没 close # 对: defer resp.Body.Close() + io.Copy(io.Discard, body)
# 错: 只监控 avg(sql_time) # 对: middleware 计数 queries/request, 阈值告警
# 错: SELECT * FROM goods LIMIT 20 # 对: SELECT id,title,price FROM goods LIMIT 20
WHERE id > ?。 # 错: LIMIT 100 OFFSET 1000000 # 越翻越慢 # 对: WHERE id > last_id ORDER BY id LIMIT 100
DATE(created)=... 或字符串列传数字, 索引直接失效. 原因: 破坏最左原样匹配. 正解: 改成范围查询, 类型对齐。 # 错: WHERE DATE(created) = '2026-09-26' # 对: WHERE created >= '2026-09-26' AND created < '2026-09-27'
# 错: jobs := make(chan Job) # 无界 # 对: make(chan Job, 10000) + default 拒绝
# 错: miss → db.Load → 还是 nil → 不缓存 # 对: cache.SetNX(id, nil, 60s) + bloom 前置
# 错: Set(k, v, 2*time.Hour) 万条同刷 # 对: 2h + rand(0,10min) jitter
# 错: miss → loadDB() 每请求都执行 # 对: g.Do(key, loadDB) 同 key 合并为 1 次
# 错: cache.Set(v) → db.Update(v) # 对: db.Update(v) → cache.Del(k)
# 错: for i := 0; i < 3; i++ { call() } # 对: backoff 100ms×2^n + jitter + 只重试幂等
# 错: alert: consumer_error_rate > 1% # 对: predict_linear(lag[30m], 3600) > 10万
# 错: 面板只有 active connections # 对: 加 wait_duration avg/max 曲线
# 错: long_query_time = 10 # 对: long_query_time = 0.2 + pt-query-digest 日报