共计 1583 个字符,预计需要花费 4 分钟才能阅读完成。
m3dbnode oom
oom 时排查
- 内存火焰图: 80G 内存
- bytes_pool_get_on_empty qps 很高
- db read qps 增长 80%
- node cpu kernel 暴涨
看图论断
- m3dbnode 内存 oom 过程很短,很激烈:总工夫不超过 7 分钟
- 内存从 27G 增长到 250G
- 节点 sys 态 cpu 暴涨:因为大量的 mem_alloca sys_call
- 内存增长曲线和 db_read_qps 曲线和 bytes_pool_get_on_empty 曲线高度吻合
- 内存火焰图:27G 的 rpc 40G 的 pool.(*objectPool).tryFill
查看代码,追踪火焰图中这个 tryFill
内存分配器
目标很简略: 本人治理内存,防止频繁的 mem_allocate sys_call 晋升速度,空间换工夫
外围构造
- 初始化时调用 init 向池中注入
type objectPool struct {
opts ObjectPoolOptions
values chan interface{}
alloc Allocator
size int
refillLowWatermark int
refillHighWatermark int
filling int32
initialized int32
dice int32
metrics objectPoolMetrics
}
for i := 0; i < cap(p.values); i++ {p.values <- p.alloc()
}
从池中获取对象时
- 池中还有残余则间接获取否则走各自的 alloc 调配
- 同时设置 bytes_pool_get_on_empty
func (p *objectPool) Get() interface{} {if atomic.LoadInt32(&p.initialized) != 1 {fn := p.opts.OnPoolAccessErrorFn()
fn(errPoolGetBeforeInitialized)
return p.alloc()}
var v interface{}
select {
case v = <-p.values:
default:
v = p.alloc()
p.metrics.getOnEmpty.Inc(1)
}
p.trySetGauges()
return v
}
同时判断池水位,是否加油
if p.refillLowWatermark > 0 && len(p.values) <= p.refillLowWatermark {p.tryFill()
}
加油过程
- 用 CompareAndSwapInt32 做并发管制标记位
- 加油加到 refillHighWatermark
func (p *objectPool) tryFill() {if !atomic.CompareAndSwapInt32(&p.filling, 0, 1) {return}
go func() {defer atomic.StoreInt32(&p.filling, 0)
for len(p.values) < p.refillHighWatermark {
select {case p.values <- p.alloc():
default:
return
}
}
}()}
默认池参数
defaultRefillLowWaterMark = 0.3
defaultRefillHighWaterMark = 0.6
总结思考
- 默认池低水位为什么不是 0: 因为 从水位判断到 tryFill 两头的并发申请使得最初 tryFill 开始时低水位可能低于 0.3
- 火焰图中的 tryFill 耗费了 40G 内存不是一次性的,类比右侧 thriftrpc27,属于累加内存耗费值
- 一次性的内存耗费必定没有这么多:每次加油时内存耗费低于初始化
- 所以能够失去论断,oom 是因为在过后 byte_pool 频繁的 get 耗费,而后 tryFill 频繁的加油导致内存调配
- 所以根本原因还是查问导致的
解决办法: 限度 query 资源耗费爱护 db
批改 m3coordinator 参数
正文完
发表至: prometheus
2020-08-13