Container OOM
首先,Kubernetes Pod里面容器上的内存遵守的是“用多了就会被干掉”的原则。这和物理机/虚拟机里的内存使用体验完全不一样。在物理机/虚拟机中,内存不够了 -> 申请内存失败 -> 程序正常运行,处理错误。在Kubernetes的container里面,内存超了-> 申请内存成功 -> 程序被杀掉。这就需要我们在Kubernetes上运行内存需求比较大的程序时要额外留心。
在这次出问题的data service里面,与内存限制有关的配置在两个地方。一个是服务Pod的container的memory limit,另一个是Pod挂载的内存volume,即emptyDir的size。对于这两个,Kubernetes处理方式也不一样。
Kubernetes POD Qos
先说一下Pod container的memory。我们知道,Kubernetes的POD可以有不同级别:Guaranteed/Burstable/BestEffort。
- BestEffort:容器的memory不设置limit。所以理论上,容器中的程序可以用完整个机器上的内存。只要机器内存压力不大,Kubernetes不会去管这个容器,但是如果机器内存资源紧张,这一类的POD优先被干掉。
- Burstable:容器的limit大于request。容器内存使用可以超过请求值,但不应该超过给定限制。当机器内存资源紧张时,在BestEffort被干完后,优先干内存使用超过request的POD
- Guranteed:容器的limit等于request。即容器服务内存使用不应该超过请求值。
上面三种服务级别,会导致机器资源紧张时Kubernetes处理方式不同。下面我们来看,资源不紧张时候,如果容器内存用超怎么办。
可以通过 kubectl top pod 来查看容器内存占用,

Cgroup Memory
第一个问题是,我们怎么判断容器用了多少内存。
对于常用的linux命令,例如free,是无法反应出容器的内存使用的。如果你exec到一个Kubernetes上的容器里面,看到free的结果实际上是整个机器(物理机或虚拟机)的内存。
Kubernetes容器资源隔离上基于cgroup,所以我们要看cgroup的内存使用。在Linux里面,查看容器cgroup内存使用可以在***/sys/fs/cgroup/memory/memory.usage_in_bytes***这个文件里面看到。
Virtual Memory / RES Memory
这里还有一个点可以讲一下。在通过例如top命令,查看出来的内存往往是很大的。例如这个例子:
|
|
这个进程的总虚拟内存有14.56g。但实际进程并没有用到那么大空间。这里面包含了并不实际再内存中的,或者共享的空间,例如shared library等。实际进程使用的空间要看RES那一列,也就是说,这个进程实际上使用了3.21g的内存。
也可以通过 ps aux 查看进程的内存占用情况,RES 是进程的内存

Cache
Kuberetes是不是只看RES的内存使用呢?答案是no。除了RES内存,还有一块内存被算到了container头上。
如果把当前容器的进程中所有RES加起来,还是小于memory.usage_in_bytes这个文件中的值。我们看下这个文件***/sys/fs/cgroup/memory/memory.state***。其中有两行,加起来正好是memory.usage_in_bytes的值。

|
|
其中rss就是容器中所有进程的RES。还有一个是cache,Cache被用来缓存IO数据的。例如,我们有时候会挂载一块远程存储。重复读取存储中某些数据,速度远远快于第一次读取速度,这就是cache起了作用。一般来说这块不会很大,但Kubernetes的一个功能,让这块用了很大空间。这个下面会说。
OOM kill
当容器内的进程用超了memory limit会发生什么行为呢?
Kubernetes并不会把整个POD或容器都干掉。首先,Kubernetes会把当前容器中的进程列出来。
|
|
首先会看oom score这一列,先把oom score高的干掉。如果我们想配置容器oom时候被干掉进程的顺序,可以从这个地方着手。
如果像在这个例子中,大家oom score都一样,kubernetes会把内存使用最多的进程干掉。如果这个进程kill导致了容器主进程出错,我们就可以看到container重启或退出等行为。如果主进程能很好处理这个错误,就不会导致container的错误。当然,如果直接kill了容器的主进程,那container也就完了。
EmptyDir
Kubernetes提供Memory Volume功能。你可以申请一块EmptyDir类型的volume,然后挂载到container里使用。EmptyDir也可以指定size。
EmptyDir的size限制仍然是遵守Kubernetes的风格,就是你要自己管理好用量,用超了我把你干掉。这里的干掉比较暴力,直接把整个POD弹出来了。
这里一个很有意思的点是,这块EmptyDir其实是算到了前面提到的container内存中cache那一项上。也就是说,如果你写了很多东西到EmptyDir上,如果container容器memory limit比较小,在POD Evict前,container就会触发OOM行为。
cadvisor 与 cgroup 的内存指标对应关系
cadvisor既然是采集的cgroup的数据,那两者指标之间肯定是关联的,找到对应关系有助于分析监控数据是否正确。
|
|
从命名来看通常以为容器的使用内存是 container_memory_usage_bytes,实际上这个指标包含了文件系统缓存,通常比container_memory_working_set_byte 要大,不能正确反应容器的内存大小。container_memory_working_set_bytes 才是容器 oom的判断标准。
以上为例:
- Usage: 355123200
- inactive_anon: 0
- inactive_file: 96784384
- container_memory_working_set_bytes: 355123200 - 96784384 = 258338816 B = 252284 KiB 这个与 kubectl top pod 一致
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 限制 |
升级前
|
|
Go 版本关系
RSS( Resident Set Size )常驻内存集合大小,表示相应进程在RAM中占用了多少内存,并不包含在SWAP中占用的虚拟内存。即使是在内存中的使用了共享库的内存大小也一并计算在内,包含了完整的在stack和heap中的内存。
VSZ (Virtual Memory Size),表明是虚拟内存大小,表明了该进程可以访问的所有内存,包括被交换的内存和共享库内存。
go 的 runtime 在释放内存返回到内核时,有两种系统调用
- MADV_DONTNEED:内核会在进程的页表中将这些页标记为 “未分配”,从而进程的 RSS 就会尽快释放和变小。OS 后续可以将对应的物理页分配给其他进程。
- MADV_FREE:内核只会在页表中将这些进程页面标记为可回收,在需要的时候才回收这些页面。
版本变迁:
-
Go 1.12 版本前:Go Runtime 在 Linux 上默认使用的是 MADV_DONTNEED 策略。从整体效果来看,进程 RSS 可以下降的比较快,但从性能效率上来看差点。
1 2// 没有任何奇奇怪怪的判断 madvise(v, n, _MADV_DONTNEED) -
Go 1.12 ~ 1.15 版本:当前 Linux 内核版本 >=4.5 时,Go Runtime 在 Linux 上默认使用了性能更为高效的 MADV_FREE 策略。从整体效果来看,进程RSS 不会立刻下降,要等到系统有内存压力了才会释放占用,RSS 才会下降。
1 2 3 4 5 6 7 8 9 10 11 12var advise uint32 if debug.madvdontneed != 0 { advise = _MADV_DONTNEED } else { advise = atomic.Load(&adviseUnused) } if errno := madvise(v, n, int32(advise)); advise == _MADV_FREE && errno != 0 { // MADV_FREE was added in Linux 4.5. Fall back to MADV_DONTNEED if it is // not supported. atomic.Store(&adviseUnused, _MADV_DONTNEED) madvise(v, n, _MADV_DONTNEED) }- 结合社区里所遇到的案例可得知,该次调整带来了许多问题:
- 引发用户体验的问题:Go issues 上总是出现以为内存泄露,但其实只是未满足条件,内存没有马上释放的案例。
- 混淆统计信息和监控工具的情况:在 Grafana 等监控上,发现容器进程内存较高,释放很慢,告警了,很慌。
- 导致与内存使用有关联的个别管理系统集成不良:例如 Kubernetes HPA ,或者自定义了扩缩容策略这类模式,难以评估。
- 挤压同主机上的其他应用资源:并不是所有的 Go 程序都一定独立跑在单一主机中,自然就会导致同一台主机上的其他应用受到挤压,这是难以评估的。
- 结合社区里所遇到的案例可得知,该次调整带来了许多问题:
-
从 v1.16 起,Go 在 Linux 下的默认内存管理策略会从
MADV_FREE改回MADV_DONTNEED策略。
参考资料
-
No backlinks found.