为何容器内存占用居高不下,频频 OOM

最近我在回顾思考(写 PPT),整理了现状,发现了这个问题存在多时,通过一番波折,最终肯定了元凶和相对可行的解决方案,所以也在这里分享一下排查历程。git

时间线:github

  • 在上 Kubernetes 的前半年,只是用 Kubernetes,开发没有权限,业务服务极少,忙着写新业务,风平浪静。golang

  • 在上 Kubernetes 的后半年,业务服务较少,偶尔会阶段性被运维唤醒,问之 “为何大家的服务内存占用这么高,赶忙查”。此时你们还在为新业务冲刺,猜想也许是业务代码问题,但没有调整代码去尝试解决。安全

  • 在上 Kubernetes 的第二年,业务服务逐渐增多,广泛增长了容器限额 Limits,出现了好几个业务服务是内存小怪兽,所以若是不限制的话,服务过分占用会致使驱逐,所以反馈语也就变成了:“为何大家的服务内存占用这么高,老被 OOM Kill,赶忙查”。据闻也有几个业务大佬有去排查(由于 OOM 反馈),彷佛没得出最终解决方案。bash

不由让咱们思考,为何个别 Go 业务服务,Memory 老是提示这么高,常常达到容器限额,以致于被动 OOM Kill,是否是有什么安全隐患?并发

现象

内存居高不下

发现个别业务服务内存占用挺高,触发告警,且经过 Grafana 发如今凌晨(没有什么流量)的状况下,内存占用量依然拉平,没有打算降低的样子,高峰更是不得了,像是个内存炸弹:运维

image

而且我所观测的这个服务,早年还只是 100MB。如今随着业务迭代和上升,目前已经稳步 4GB,容器限额 Limits 纷纷给它开道,但我想总不能是无休止的增长资源吧,这是一个大问题。工具

进入重启怪圈

有的业务服务,业务量小,天然也就没有调整容器限额,所以得不到内存资源,又超过额度,就会进入疯狂的重启怪圈:post

image

重启将近 300 次,很是不正常了,更不用提所接受到的告警通知。优化

排查

猜测一:频繁申请重复对象

出现问题的个别业务服务都有几个特色,那就是基本为图片处理类的功能,例如:图片解压缩、批量生成二维码、PDF 生成等,所以就怀疑是否在量大时频繁申请重复对象,而 Go 自己又没有及时释放内存,所以致使持续占用。

sync.Pool

基本上想解决 “频繁申请重复对象”,咱们大多会采用多级内存池的方式,也能够用最多见的 sync.Pool,这里可参考全成所借述的《Go 夜读》上关于 sync.Pool 的分享,关于这类状况的场景:

当多个 goroutine 都须要建立同⼀个对象的时候,若是 goroutine 数过多,致使对象的建立数⽬剧增,进⽽致使 GC 压⼒增大。造成 “并发⼤-占⽤内存⼤-GC 缓慢-处理并发能⼒下降-并发更⼤”这样的恶性循环。

验证场景

在描述中关注到几个关键字,分别是并发大,Goroutine 数过多,GC 压力增大,GC 缓慢。也就是须要知足上述几个硬性条件,才能够认为是符合猜测的。

经过拉取 PProf goroutine,可得知 Goroutine 数并不高:

image

另外在凌晨长达 6 小时,没有什么流量的状况下,也不符合并发大,Goroutine 数过多的状况,若要更进一步确认,可经过 Grafana 落实其量的高低。

从结论上来说,我认为与其没有特别直接的关系,但猜测其所对应的业务功能到致使的间接关系应当存在。

猜测二:不知名内存泄露

内存居高不下,其中一个反应就是猜想是否存在泄露,而咱们的容器中目前只跑着一个 Go 进程,所以首要看看该 Go 应用是否有问题。这时候能够借助 PProf heap(可使用 base -diff):

image

显然其提示的内存使用不高,那会不会是 PProf 出现了 BUG 呢。接下经过命令也可肯定 Go 进程的 RSS 并不高,但 VSZ 却相对 “高” 的惊人,我在 19 年针对此写过一篇《Go 应用内存占用太多,让排查?(VSZ篇)》 ,此次 VSZ 太高也给我留下了一个念想。

从结论上来说,也不像 Go 进程内存泄露的问题,所以也将其排除。

猜测三:madvise 策略变动

  • 在 Go1.12 之前,Go Runtime 在 Linux 上使用的是 MADV_DONTNEED 策略,可让 RSS 降低的比较快,就是效率差点。

  • 在 Go1.12 及之后,Go Runtime 专门针对其进行了优化,使用了更为高效的 MADV_FREE 策略。但这样子所带来的反作用就是 RSS 不会马上降低,要等到系统有内存压力了才会释放占用,RSS 才会降低。

查看容器的 Linux 内核版本:

$ uname -a
Linux xxx-xxx-99bd5776f-k9t8z 3.10.0-693.2.2.el7.x86_64 
复制代码

MADV_FREE 的策略改变,须要 Linux 内核在 4.5 及以上(详细可见 go/issues/23687),显然不符合,所以也将其从猜想中排除。

猜测四:监控/判别条件有问题

会不会是 Grafana 的图表错了,Kubernetes OOM Kill 的判别标准也错了呢,显然不大可能,毕竟咱们拥抱云,阿里云 Kubernetes 也运行了好几年。

image

但在此次怀疑中,我了解到 OOM 的判断标准是 container_memory_working_set_bytes 指标,所以有了下一步猜测。

猜测五:容器环境的机制

既然不是业务代码影响,也不是 Go Runtime 的直接影响,那是否与环境自己有关呢,咱们能够得知容器 OOM 的判别标准是 container_memory_working_set_bytes(当前工做集)。

而 container_memory_working_set_bytes 是由 cadvisor 提供的,对应下述指标:

image

从结论上来说,Memory 换算过来是 4GB+,石锤。接下来的问题就是 Memory 是怎么计算出来的呢,显然和 RSS 不对标。

缘由

cadvisor/issues/638 可得知 container_memory_working_set_bytes 指标的组成其实是 RSS + Cache。而 Cache 高的状况,常见于进程有大量文件 IO,占用 Cache 可能就会比较高,猜想也与 Go 版本、Linux 内核版本的 Cache 释放、回收方式有较大关系。

image

而各业务模块常见功能,如:

  • 批量图片解压缩。
  • 批量二维码生成。
  • 批量上传渲染后图片。
  • 批量 PDF 生成。
  • ...

只要是涉及有大量文件 IO 的服务,基本上是这个问题的老常客了,写这类服务基本写一个中一个,由于这是一个混合问题,像其它单纯操做为主的业务服务就很 “正常”,不会出现内存居高不下。

解决方案

在本场景中 cadvisor 所提供的判别标准 container_memory_working_set_bytes 是不可变动的,也就是没法把判别标准改成 RSS,所以咱们只能考虑掌握主动权。

首先是作好作多级内存池管理,能够缓解这个问题的症状。但这存在难度,从另一个角度来看,你怎么知道何时在哪一个集群上会忽然出现这类型的服务,况且开发人员的预期状况良莠不齐,写多级内存池写出 BUG 也是有可能的。

让业务服务无限重启,也是不现实的,被动重启,没有控制,且告警,存在风险

所以为了掌握主动权,能够在部署环境能够配合脚本作 “手动” HPA,当容器内存指标超过约定限制后,起一个新的容器替换,再将原先的容器给释放掉,就能够在预期内替换且业务稳定了。

image

总结

虽然这问题时间跨度比较长,总体来说都是阶段性排查,本质上能够说是对 Kubernetes 的不熟悉有关。但综合来说这个问题涉及范围比较大,由于内存居高不下的可能性有不少种,要一个个排查,开发权限有限,费时费力。

基本排查思路就是:

  1. 怀疑业务代码(PProf)。
  2. 怀疑其它代码(PProf)。
  3. 怀疑 Go Runtime 。
  4. 怀疑工具。
  5. 怀疑环境。

很是感谢在这大段时间内被我咨询的各位大佬们,感受就是隔了一层纱,捅穿了就很快就定位到了,你们若是有其它解决方案也欢迎随时沟通。

个人博客

原文地址:为何容器内存占用居高不下,频频 OOM

个人公众号

image
相关文章
相关标签/搜索