这是一篇迟到的文章。去年大促期间遇到容器偶尔oom的问题,看了半天监控却发现监控数据和现场对不上,因此学习了一下prometheus的监控采集和docker容器的资源隔离。我们的应用运行在阿里云ack上,监控使用的是阿里云上的prometheus和arms。
prometheus和docker数据的关系
cadvisor 内存相关的主要指标
cAdvisor是谷歌公司用来分析运行中的Docker容器的资源占用以及性能特性的工具,prometheus上的容器监控数据即是通过cAdvisor获取的。要弄明白prometheus上的内存监控就需要先了解下cAdvisor的内存相关的指标。
metric | description |
---|---|
container_memory_cache | 页面缓存大小。 |
container_memory_rss | RSS内存,即常驻内存集(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真正的内存数据。
metric | description |
---|---|
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 中包含有的内存信息
metric | description |
---|---|
cache | 高速缓存,包括 tmpfs(shmem),单位为字节 |
rss | 匿名页和swap,包含transparent hugepages |
rss_huge | 匿名页的transparent hugepages(动态分配内存页) |
mapped_file | memory-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为换算单位
相对应的kubectl top展示的数据是1024为换算单位
因此同样的内存监控上显示的要比命令获取的值要大。
java进程关联内存
在java运行的容器中,可以简单用以下公式表示
RSS = Heap size + MetaSpace + OffHeap size
OffHeap由线程堆栈,直接缓冲区,映射文件(库和jar)和JVM代码组成。 简单说 java运行内存 = 堆内存 + 元空间 + 线程数 * 1m +直接缓冲区,因此,在排查是否jvm内存泄漏前需要确保容器本身有足够的内存能运行java。
以某一个应用的pod为例 prometheus 配置监控显示内存高达7.58G,换算成1024单位为7.06Gi 到pod上查看java进程参数,堆内存设置最大6Gi,元空间最大512Mi,直接缓存区最大1Gi 在arms上查看jvm内存实际使用情况 堆总共用了3.7Gi,元空间使用了226Mi,堆外内存使用412.8Mi
查看线程数,有518个=518Mi 也就是说java进程目前需要的最大内存 6Gi + 512Mi + 1Gi + 518Mi > 8Gi,使用的内存是 6Gi + 226Mi + 412.8Mi + 518Mi = 7.1Gi 与prometheus 监控基本吻合
而pod 的内存限制设置的是8G,换算成1024单位实则仅有7.45Gi
因此,最佳的解决方案是调整k8s yaml内存单位使用Gi,并根据堆内存使用情况下调堆内存到5g。