作者介绍
李杰:2020年2月份加入去哪儿游览酒店报价中心研发团队,云原生SIG成员,首要负责国内酒店报价实时和离线计算模块的研发和保护。喜欢钻研云原生相关开发,完结JVM参数校验功用,处理容器运用的OOM被kill等问题。
前语
自从公司 2021 年 11 月份开端全面容器化后,酒店报价中心团队快速呼应,迁移了 98% 的运用,由原来的 kvm 或实体机器到容器上,咱们的多个运用呈现了频频被 kill 的状况,首要包含两大类:
- 由于 GC 时刻过长导致 k8s 检活失利,被 kill 掉。
- 由于内存碎片的问题,导致 OOM 被 kill 掉。
本文首要介绍发现问题以及处理问题的过程。
问题一:运用长期 GC
运用 GC 时刻长导致 k8s 检活失利,k8s 会 kill 掉事务运用。
详细现象和剖析
其时咱们团队的两个运用在发布到 docker上以后,呈现了频频重启的现象,发布后一天内重启次数高达 29 次左右,提示也仅仅是 “时刻:2021-11-25T21:34:58+08:00 原因:Error kill” 看不到详细的问题,只能到对应的容器内部去寻找线索了。详细排查过程如下:
1、确以为啥会被 kill ?
首要经过 dmesg 命令,检查容器所在主机上的日志。发现是存在 OOM kill 掉的 Java 运用,可是比照了下这个 total-vm 和咱们自己的运用配置发现差别很大,不是咱们的运用进程,咱们的配置是:
放弃了这个方向后转向了 k8s 的日志(ps:为啥第一次不看这个,是由于这个日志的检查时需求权限的,前期没有权限,只能自己动手先看方便的),发现了在被 kill 之前呈现了3次 unhealthy 的 k8s 日志,且返回恳求状况不是200。
看下为什么会有检活恳求反常,检查事务拜访日志,发现这个时刻点是有接收到恳求,手动拜访也是成功的。可是其时为啥会有拜访状况不是 200 的问题呢?怀疑其时事务进程是有大量使命在跑,呼应超时问题导致, 所以开端排查事务的详细日志。
2、发现问题
在排查日志的时候发现了导致该问题的根本原因,实质是 GC 时刻过长导致。检查重启前的容器 GC 日志:
容器被重启是在 2021-11-25T21:34:58+08:00 重启的。在这个时刻点前,也便是被 kill 之前的一次 GC 时刻高达 18s + 7s 。至此,原因就很清晰了:由于运用进程的 GC 导致服务不能正常呼应 k8s 的检活恳求,k8s 以为运用“死”了,触发了 kill 和重启操作!
处理
- 经过 GC 日志,剖析首要耗时点。引荐 GC 剖析东西:gceasy.io/ ,调整 JVM参数。
- k8s 调整了检活机制,由原来超时 10s、20s,最后调整为 2min。
- 经过剖析日志发现首要的长时 GC 是由于新生代晋升失利,扩展 young 区和堆巨细优化 JVM 参数。
问题二:内存碎片
现象
问题现象是运用办理平台上呈现了容器 “OOM kill” 的提示。
JVM堆内存剖析
根据 OOM kill 的提示,开端剖析运用日志,发现堆区、栈区并没有呈现 OOM 的问题,怀疑是堆外内存内存溢出导致,因此,测验添加相应的JVM参数以调查堆外内存的运用状况。
调查是否是有堆外内存没有释放,再加上 OOM 没有显着征兆,写了脚本守时 30s 看下运用状况。
下图是在容器发动后的 1 分钟 到 容器即将被 kill 时的 JVM 内存分配比照图:
发现 JVM 的运用内存并没有显着变化(12491M→ 12705M),且整体没有超越 docker 分配的内存约束(docker limit Memory:12G),可是为什么会有 OOM 呢?哪块的内存运用升高导致了 OOM 呢?
查询了大量的材料,排查方向转向内存这块。
pmap 内存映射剖析
运用 pmap 剖析 Java 进程的内存映射关系:
pmap说明
Address:内存开端地址
Kbytes: 占用内存的字节数(KB)
RSS: 保存内存的字节数(KB)
Dirty: 脏页的字节数(包含同享和私有的)(KB)
Mode: 内存的权限:read、write、execute、shared、private (写时仿制)
Mapping: 占用内存的文件、或[anon](分配的内存)、或[stack](仓库)
Offset: 文件偏移
Device: 设备名 (major:minor)
发现可疑的当地有两个 1029712KB(1005M)的内存块和较多64M内存块,linux 默许运用的 glibc 的 ptmalloc 内存分配器,有这个问题。
Glibc为什么会有64M的内存块的问题?
引进内存分配器
在进程请求内存时,根据需求分配的内存巨细由内存分配器来想内核请求详细的内存区域,那么为什么会有内存分配器来请求内存,而不是进程直接向体系请求呢?由于体系调用的开支比较大,这样做是非常不值的。一起,在 linux下分配堆内存需求运用 brk体系调用,而这个体系调用仅仅简略地改动堆顶指针而已,也便是将堆扩展或许缩小。举个例子,进程分别请求了 M2 和 M1 两块内存,运行了一段时刻后,M2 内存不需求了,需求回收了。
运用体系处理的话,只能运用 brk 移动指针,那么 M1 也会被回收掉,这样显然是不行的。所以引进内存分配器,把 M2 的内存缓存下来,等到进程需求再次请求内存空间时不需求运用体系调用,而是直接从缓存中分配,这个动作便是由内存分配器完结的。内存分配器不只提高了运行功率,还提高了内存的运用率。
ptmalloc 解读
glibc 的内存分配器(ptmalloc)的结构如图所示,一个进程就有一个主分配区和若干个从分配区。一切的线程请求内存时,都要经过主分配区请求,多线程时就需求经过锁机制来确保分配的正确性,从分区就应运而生了,ptmalloc 根据体系对分配区的争用状况动态添加分配区的数量。
在请求内存时,glibc 每次请求新的内存时,主分配区是可以经过 brk 或许 mmap 来向体系请求的,可是非主分配的内存只能经过 mmap 请求了,在64位机器上每次请求的虚拟内存区块巨细是 64MB ,最大为8倍的 CPU 数量。且从分配区一旦创立,就不会被回收了。这个便是该问题中发现的 64MB 内存块产生的原因。进程请求内存的简略过程如下:
-
经过 fastbins 查找适宜内存块,
-
1没有,从 small bin 中获取,
-
2没有,从 unsorted bin 中获取,
-
3没有,从 large bin 中获取,
-
4没有,从 top chunk 中,
-
5不够,向体系请求 brk/mmap。
内存回收的简略过程如下:
-
判别是否是 mmap 映射,是直接回收
-
判别是否附近 top chunk
-
不是2,根据 size 放到不同的 bins 中
-
是2,判别 top chunk 中附近内存是否在运用 是 兼并 top chunk
-
兼并后判别 top chunk 巨细,超越阈值(默许128k),可是开端分配128k不会回收。经过翻阅材料发现,现在市道上有不少内存分配器的完成,如 tcmalloc ,jemalloc 等,在这里咱们挑选了 jemalloc。
替换内存分配器处理内存碎片问题
jemalloc
jemalloc 是一个通用的 malloc(3) 完成,着重碎片防止和可扩展的并发支撑。
防止内存碎片和性能点提高
- 线程内存池:在一个进程中每个线程会有自己的内存池,用来办理自己的内存运用,会大幅度削减并发时锁的性能丢失。
- 锁粒度:运用非公正锁,替换自旋锁,削减 CPU 空转。
性能比照
在灰度环境测验替换为 jemalloc 的内存办理器:
安装 jemalloc
也可测验安装 tcmalloc
验证下是否运用成功了
发现 ’64MB’ 的空间映射已经不存在了,且在调查时刻范围内还有一次内存的下降, 调查一周一直平稳运行,没有呈现 OOM kill 的问题。
总结
问题处理流程
发现问题,便是个人前进的一大步。要发现问题,就要抓细节,不放弃再加上有头脑的处理问题!
东西优化
- 优化 dump 体验。原来容器 dump 时会存在 dump 到一半机器就重启的问题,跟根底架构 和技能运营的同学交流后,对该部分做了优化,让事务剖析 GC 时刻过长有了实质性帮助。
- 承认监控问题。之前我们看到容器运用监控上运用重启都是由于内存翻倍运用,但实际状况是容器在重启后,监控平台把两个容器运用的内存求和了,没有单独分隔处理。
- 支撑可挑选分配器。根底架构部门对 jemalloc 和 tcmalloc 的内存分配器进行支撑。
参阅文件:
- 华庭 :《Glibc内存办理-Ptmalloc2 源代码剖析》
- JeMalloc-UncP 知乎
- jemalloc.net/