34.3 m3db-oom的内存火焰图和内存分配器加油模型源码解读

本节重点介绍 :

  • m3dbnode oom时内存火焰图追查源码调用
  • 内存分配器加油模型源码解读
  • 高基数查询导致m3db oom

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提升速度,空间换时间

核心结构 objectPool

  • 位置 D:\go_path\pkg\mod\github.com\m3db\m3@v1.1.0\src\x\pool\object.go
go 复制代码
type objectPool struct {
	opts                ObjectPoolOptions
	values              chan interface{}
	alloc               Allocator
	size                int
	refillLowWatermark  int
	refillHighWatermark int
	filling             int32
	initialized         int32
	dice                int32
	metrics             objectPoolMetrics
}
  • 初始化时调用Init 向池中注入
go 复制代码
func (p *objectPool) Init(alloc Allocator) {
	if !atomic.CompareAndSwapInt32(&p.initialized, 0, 1) {
		p.onPoolAccessErrorFn(errPoolAlreadyInitialized)
		return
	}

	p.values = make(chan interface{}, p.size)
	for i := 0; i < cap(p.values); i++ {
		p.values <- alloc()
	}

	p.alloc = alloc
	p.setGauges()
}

从池中获取对象时

  • 池中还有剩余则直接获取
  • 否则走各自的alloc分配,同时设置这个 bytes_pool_get_on_empty指标+1
go 复制代码
func (p *objectPool) Get() interface{} {
	var (
		metrics = p.metrics
		v       interface{}
	)

	select {
	case v = <-p.values:
	default:
		v = p.alloc()
		metrics.getOnEmpty.Inc(1)
	}

	if unsafe.Fastrandn(sampleObjectPoolLengthEvery) == 0 {
		// inlined setGauges()
		metrics.free.Update(float64(len(p.values)))
		metrics.total.Update(float64(p.size))
	}

	if p.refillLowWatermark > 0 && len(p.values) <= p.refillLowWatermark {
		p.tryFill()
	}

	return v
}

每次Get同时判断池水位,是否加油

go 复制代码
	if p.refillLowWatermark > 0 && len(p.values) <= p.refillLowWatermark {
		p.tryFill()
	}

加油过程

  • 用CompareAndSwapInt32做并发控制标志位
  • 加油加到refillHighWatermark
go 复制代码
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
			}
		}
	}()
}

默认池参数

go 复制代码
	defaultRefillLowWaterMark  = 0.3
	defaultRefillHighWaterMark = 0.6

总结思考

  • 默认池低水位为什么不是0:因为 从水位判断到tryFill中间的并发请求使得最后tryFill开始时低水位可能低于0.3
  • 火焰图中的tryFill消耗了40G内存不是一次性的,类比右侧thriftrpc27,属于累加内存消耗值
  • 一次性的内存消耗肯定没有这么多:每次加油时内存消耗低于初始化
  • 所以可以得到结论,oom是因为在当时byte_pool频繁的get消耗,然后tryFill频繁的加油导致内存分配
  • 所以根本原因还是查询导致的

临时解决办法:限制query资源消耗保护db

  • 首先要明确的几点,因为remote_read是链式的调用
  • 所以限制m3db前面的组件prometheusm3coordinator是没用的
  • 只能限制m3db中关于query的参数,但是这个方法不根治

上面的方法治标不治本,重要的是解决高基数/重查询的问题

本节重点总结 :

  • m3dbnode oom时内存火焰图追查源码调用
  • 内存分配器加油模型源码解读
  • 高基数查询导致m3db oom
相关推荐
是阿楷啊1 天前
Java大厂面试场景:音视频场景中的Spring Boot与微服务实战
spring boot·redis·spring cloud·微服务·grafana·prometheus·java面试
xixingzhe22 天前
Prometheus+Grafana监控服务器
grafana·prometheus
牛奶咖啡132 天前
Prometheus+Grafana构建云原生分布式监控系统(十六) _基于Alertmanager的告警机制(一)
云原生·prometheus·prometheus告警整合·prometheus告警配置·prometheus告警规则·prometheus触发告警·告警规则配置实践
码农小卡拉4 天前
Prometheus 监控 SpringBoot 应用完整教程
spring boot·后端·grafana·prometheus
牛奶咖啡134 天前
Prometheus+Grafana构建云原生分布式监控系统(十五)_Prometheus中PromQL使用(二)
云原生·prometheus·集合运算·对查询结果排序·直方图原理·统计掉线的实例·检查节点或指标是否存在
牛奶咖啡135 天前
Prometheus+Grafana构建云原生分布式监控系统(十四)_Prometheus中PromQL使用(一)
云原生·prometheus·promql·计算一个时间范围内的平均值·将相同数据整合查看整体趋势·计算时间范围内的最大最小比率·向量标量的算术运算
牛奶咖啡136 天前
Prometheus+Grafana构建云原生分布式监控系统(十三)_Prometheus数据模型及其PromQL
云原生·prometheus·prometheus数据类型·promql使用场景·promql表达式解析·promql数据类型·监控系统的方法论与指标
AC赳赳老秦7 天前
外文文献精读:DeepSeek翻译并解析顶会论文核心技术要点
前端·flutter·zookeeper·自动化·rabbitmq·prometheus·deepseek
牛奶咖啡139 天前
Prometheus+Grafana构建云原生分布式监控系统(十二)_基于DNS的服务发现
云原生·prometheus·dns·搭建自己的dns服务器·使用bind搭建dns服务器·配置正向解析·基于dns的服务发现
A-刘晨阳9 天前
Prometheus + Grafana + Alertmanager 实现邮件监控告警及配置告警信息
运维·云计算·grafana·prometheus·监控·邮件