一次阿里云上的容器oom排查

3,493 阅读5分钟

这是一篇迟到的文章。去年大促期间遇到容器偶尔oom的问题,看了半天监控却发现监控数据和现场对不上,因此学习了一下prometheus的监控采集和docker容器的资源隔离。我们的应用运行在阿里云ack上,监控使用的是阿里云上的prometheus和arms。

prometheus和docker数据的关系

cadvisor 内存相关的主要指标

cAdvisor是谷歌公司用来分析运行中的Docker容器的资源占用以及性能特性的工具,prometheus上的容器监控数据即是通过cAdvisor获取的。要弄明白prometheus上的内存监控就需要先了解下cAdvisor的内存相关的指标。

metricdescription
container_memory_cache页面缓存大小。
container_memory_rssRSS内存,即常驻内存集(Resident Set Size),是分配给进程使用实际物理内存。RSS内存包括所有分配的栈内存和堆内存,以及加载到物理内存中的共享库占用的内存空间,但不包括进入交换分区的内存。
container_memory_swap虚拟内存使用量。虚拟内存(swap)指的是用磁盘来模拟内存使用。当物理内存快要使用完或者达到一定比例,就可以把部分不用的内存数据交换到硬盘保存,需要使用时再调入物理内存。
container_memory_usage_bytes当前使用的内存量,包括所有使用的内存,不管有没有被访问 (包括 cache, rss, swap等)。
container_memory_max_usage_bytes最大内存使用量的记录。
container_memory_working_set_bytes当前内存工作集(working set)使用量。
container_memory_failcnt申请内存失败次数计数
container_memory_failures_total累计的内存申请错误次数

容器中查看cgroup内存

docker使用cgroup限制容器资源,因此可以在cgroup路径下看到容器真正使用的资源大小。 /sys/fs/cgroup/memory目录就是docker真正的内存数据。

metricdescription
memory.usage_in_bytes已使用的内存量(包含cache和buffer)(字节)
memory.limit_in_bytes限制的内存总量(字节)
memory.failcnt申请内存失败次数计数
memory.memsw.usage_in_bytes已使用的内存和swap(字节)
memory.memsw.limit_in_bytes限制的内存和swap容量(字节)
memory.memsw.failcnt申请内存和swap失败次数计数
memory.stat内存相关状态

其中,memory.stat 中包含有的内存信息

metricdescription
cache高速缓存,包括 tmpfs(shmem),单位为字节
rss匿名页和swap,包含transparent hugepages
rss_huge匿名页的transparent hugepages(动态分配内存页)
mapped_filememory-mapped 映射的文件大小,包括 tmpfs(shmem)
swap交换分区大小
pgpgin存入内存中的页数
pgpgout从内存中读出的页数
active_anon在活跃的最近最少使用(least-recently-used,LRU)列表中的匿名和 swap 缓存,包括 tmpfs(shmem)。表示 anonymous pages,用户进程中与文件无关的内存(比如进程的堆栈,用malloc申请的内存),在发生换页时,是对交换区进行读/写操作
inactive_anon不活跃的 LRU 列表中的匿名和 swap 缓存,包括 tmpfs(shmem)
active_file活跃 LRU 列表中的 file-backed 内存。表示file-backed pages,用户进程中与文件关联的内存(比如程序文件、数据文件所对应的内存页),在发生换页(page-in或page-out)时,是从它对应的文件读入或写出
inactive_file不活跃 LRU 列表中的 file-backed 内存
unevictable无法再生的内存
hierarchical_memory_limit包含 memory cgroup 的层级的内存限制
hierarchical_memsw_limit包含 memory cgroup 的层级的内存加 swap 限制

cadvisor 与 cgroup 的内存指标对应关系

cadvisor既然是采集的cgroup的数据,那两者指标之间肯定是关联的,找到对应关系有助于分析监控数据是否正确。

container_memory_cache = cache = inactive_file + active_file container_memory_rss = rss = inactive_anon + active_anon container_memory_swap = swap container_memory_usage_bytes=container_memory_rss + container_memory_cache container_memory_working_set_bytes = usage - total_inactive(inactive_anon + inactive_file)

java容器oom排查

从命名来看通常以为容器的使用内存是container_memory_usage_bytes,实际上这个指标包含了文件系统缓存,通常比container_memory_working_set_bytes 要大,不能正确反应容器的内存大小。container_memory_working_set_bytes 才是容器 oom的判断标准。

数据的差异

在k8s yaml文件中,内存的单位有两种

  • K, M, G 是1000为换算标准
  • Ki, Mi, Gi 是1024为换算标准

prometheus 配置的监控采集的数据是1000为换算单位 image.png

相对应的kubectl top展示的数据是1024为换算单位 image.png

因此同样的内存监控上显示的要比命令获取的值要大。

java进程关联内存

在java运行的容器中,可以简单用以下公式表示

RSS = Heap size + MetaSpace + OffHeap size

OffHeap由线程堆栈,直接缓冲区,映射文件(库和jar)和JVM代码组成。 简单说 java运行内存 = 堆内存 + 元空间 + 线程数 * 1m +直接缓冲区,因此,在排查是否jvm内存泄漏前需要确保容器本身有足够的内存能运行java。

以某一个应用的pod为例 prometheus 配置监控显示内存高达7.58G,换算成1024单位为7.06Gi image.png 到pod上查看java进程参数,堆内存设置最大6Gi,元空间最大512Mi,直接缓存区最大1Gi image.png 在arms上查看jvm内存实际使用情况 堆总共用了3.7Gi,元空间使用了226Mi,堆外内存使用412.8Mi image.png

查看线程数,有518个=518Mi image.png 也就是说java进程目前需要的最大内存 6Gi + 512Mi + 1Gi + 518Mi > 8Gi,使用的内存是 6Gi + 226Mi + 412.8Mi + 518Mi = 7.1Gi 与prometheus 监控基本吻合

而pod 的内存限制设置的是8G,换算成1024单位实则仅有7.45Gi image.png

因此,最佳的解决方案是调整k8s yaml内存单位使用Gi,并根据堆内存使用情况下调堆内存到5g。