最近我在回顾思考(写 PPT),整理了现状,发现了这个问题存在多时,通过一番波折,最终肯定了元凶和相对可行的解决方案,所以也在这里分享一下排查历程。git
时间线:github
在上 Kubernetes 的前半年,只是用 Kubernetes,开发没有权限,业务服务极少,忙着写新业务,风平浪静。golang
在上 Kubernetes 的后半年,业务服务较少,偶尔会阶段性被运维唤醒,问之 “为何大家的服务内存占用这么高,赶忙查”。此时你们还在为新业务冲刺,猜想也许是业务代码问题,但没有调整代码去尝试解决。安全
在上 Kubernetes 的第二年,业务服务逐渐增多,广泛增长了容器限额 Limits,出现了好几个业务服务是内存小怪兽,所以若是不限制的话,服务过分占用会致使驱逐,所以反馈语也就变成了:“为何大家的服务内存占用这么高,老被 OOM Kill,赶忙查”。据闻也有几个业务大佬有去排查(由于 OOM 反馈),彷佛没得出最终解决方案。bash
不由让咱们思考,为何个别 Go 业务服务,Memory 老是提示这么高,常常达到容器限额,以致于被动 OOM Kill,是否是有什么安全隐患?并发
发现个别业务服务内存占用挺高,触发告警,且经过 Grafana 发如今凌晨(没有什么流量)的状况下,内存占用量依然拉平,没有打算降低的样子,高峰更是不得了,像是个内存炸弹:运维
而且我所观测的这个服务,早年还只是 100MB。如今随着业务迭代和上升,目前已经稳步 4GB,容器限额 Limits 纷纷给它开道,但我想总不能是无休止的增长资源吧,这是一个大问题。工具
有的业务服务,业务量小,天然也就没有调整容器限额,所以得不到内存资源,又超过额度,就会进入疯狂的重启怪圈:post
重启将近 300 次,很是不正常了,更不用提所接受到的告警通知。优化
出现问题的个别业务服务都有几个特色,那就是基本为图片处理类的功能,例如:图片解压缩、批量生成二维码、PDF 生成等,所以就怀疑是否在量大时频繁申请重复对象,而 Go 自己又没有及时释放内存,所以致使持续占用。
基本上想解决 “频繁申请重复对象”,咱们大多会采用多级内存池的方式,也能够用最多见的 sync.Pool,这里可参考全成所借述的《Go 夜读》上关于 sync.Pool 的分享,关于这类状况的场景:
当多个 goroutine 都须要建立同⼀个对象的时候,若是 goroutine 数过多,致使对象的建立数⽬剧增,进⽽致使 GC 压⼒增大。造成 “并发⼤-占⽤内存⼤-GC 缓慢-处理并发能⼒下降-并发更⼤”这样的恶性循环。
在描述中关注到几个关键字,分别是并发大,Goroutine 数过多,GC 压力增大,GC 缓慢。也就是须要知足上述几个硬性条件,才能够认为是符合猜测的。
经过拉取 PProf goroutine,可得知 Goroutine 数并不高:
另外在凌晨长达 6 小时,没有什么流量的状况下,也不符合并发大,Goroutine 数过多的状况,若要更进一步确认,可经过 Grafana 落实其量的高低。
从结论上来说,我认为与其没有特别直接的关系,但猜测其所对应的业务功能到致使的间接关系应当存在。
内存居高不下,其中一个反应就是猜想是否存在泄露,而咱们的容器中目前只跑着一个 Go 进程,所以首要看看该 Go 应用是否有问题。这时候能够借助 PProf heap(可使用 base -diff):
显然其提示的内存使用不高,那会不会是 PProf 出现了 BUG 呢。接下经过命令也可肯定 Go 进程的 RSS 并不高,但 VSZ 却相对 “高” 的惊人,我在 19 年针对此写过一篇《Go 应用内存占用太多,让排查?(VSZ篇)》 ,此次 VSZ 太高也给我留下了一个念想。
从结论上来说,也不像 Go 进程内存泄露的问题,所以也将其排除。
在 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 也运行了好几年。
但在此次怀疑中,我了解到 OOM 的判断标准是 container_memory_working_set_bytes 指标,所以有了下一步猜测。
既然不是业务代码影响,也不是 Go Runtime 的直接影响,那是否与环境自己有关呢,咱们能够得知容器 OOM 的判别标准是 container_memory_working_set_bytes(当前工做集)。
而 container_memory_working_set_bytes 是由 cadvisor 提供的,对应下述指标:
从结论上来说,Memory 换算过来是 4GB+,石锤。接下来的问题就是 Memory 是怎么计算出来的呢,显然和 RSS 不对标。
从 cadvisor/issues/638 可得知 container_memory_working_set_bytes 指标的组成其实是 RSS + Cache。而 Cache 高的状况,常见于进程有大量文件 IO,占用 Cache 可能就会比较高,猜想也与 Go 版本、Linux 内核版本的 Cache 释放、回收方式有较大关系。
而各业务模块常见功能,如:
只要是涉及有大量文件 IO 的服务,基本上是这个问题的老常客了,写这类服务基本写一个中一个,由于这是一个混合问题,像其它单纯操做为主的业务服务就很 “正常”,不会出现内存居高不下。
在本场景中 cadvisor 所提供的判别标准 container_memory_working_set_bytes 是不可变动的,也就是没法把判别标准改成 RSS,所以咱们只能考虑掌握主动权。
首先是作好作多级内存池管理,能够缓解这个问题的症状。但这存在难度,从另一个角度来看,你怎么知道何时在哪一个集群上会忽然出现这类型的服务,况且开发人员的预期状况良莠不齐,写多级内存池写出 BUG 也是有可能的。
让业务服务无限重启,也是不现实的,被动重启,没有控制,且告警,存在风险。
所以为了掌握主动权,能够在部署环境能够配合脚本作 “手动” HPA,当容器内存指标超过约定限制后,起一个新的容器替换,再将原先的容器给释放掉,就能够在预期内替换且业务稳定了。
虽然这问题时间跨度比较长,总体来说都是阶段性排查,本质上能够说是对 Kubernetes 的不熟悉有关。但综合来说这个问题涉及范围比较大,由于内存居高不下的可能性有不少种,要一个个排查,开发权限有限,费时费力。
基本排查思路就是:
很是感谢在这大段时间内被我咨询的各位大佬们,感受就是隔了一层纱,捅穿了就很快就定位到了,你们若是有其它解决方案也欢迎随时沟通。
原文地址:为何容器内存占用居高不下,频频 OOM