从生产环境遇到的一个内存暴涨问题说起
某用户生产环境的 kubernetes 节点遇到的一个问题,大概问题是这样的,用户反馈他的业务所在 pod 一在吃内存,内存占用高达 17 G 并且还是持续在增长。接到用户反馈后,我秒登 VPN ,进到用户的环境开始排查问题。
当时想的思路是这样的,既然是内存问题,那先看看这个业务所在 pod 里面到底是哪个进程在吃内存吧。
1 | kubectl exec -it pod -n xxx /bin/bash |
执行 top 命令查看下当前 pod 正在运行的进程,发现在容器里面有一个 7 号进程 VSZ 占用 6522m,这里先简单说明下 top 看到的一些和内存指标相关的参数含义:
RSS是
Resident Set Size
(常驻内存大小)的缩写,用于表示进程使用了多少内存(RAM中的物理内存),RSS不包含已经被换出的内存。RSS包含了它所链接的动态库并且被加载到物理内存中的内存。RSS还包含栈内存和堆内存。VSZ是
Virtual Memory Size
(虚拟内存大小)的缩写。它包含了进程所能访问的所有内存,包含了被换出的内存,被分配但是还没有被使用的内存,以及动态库中的内存。
但是用户反馈的是占用 17 G之多,那很显然,并不是这个进程在捣鬼,可是整个容器里面确实就只有这个进程在运行着,并且该 Java 进程还设置了分配内存的限制,最大不会超过 4g,可是内存还是一直在涨。
而且不知道大家有没有发现,容器里面执行 top 看到的信息很少,我们对比下实际操作系统的 top 命令执行结果多了很多列,例如RES、 %MEM 等等。
所以只从 top 看是不准确的,于是我们直接去查这个进程的内存占用,执行
1 | cat /proc/7/status |
查看当前 pid 的状态,其中有一个字段VmRSS 表示当前进程所使用的内存,然而我们发现用户当前进程所占用的内存才2.3G 左右。
而通过kubectl top pod
查看 pod 的内存占用 确实发现该 pod 占用 17 G ,说明并不是容器内进程内存泄露导致的问题,那这就奇怪了,是什么原因导致占用这么多内存呢?
要继续排查这个问题,我们就需要先看看容器的内存统计是如何计算的了。
众所周知,操作系统系统的内存会有一部分被buffer、cache之类占用,在 Linux 操作系统中会把这部分内存算到已使用,那么对于容器来讲,也会把某容器引发的cache占用算到容器占用的内存上,要验证这个问题,你可以启动一个容器,然后直接使用 dd 去创建一个大文件观察下内存变化。
1 | [root@8e3715641c31 /]# dd if=/dev/zero of=my_new_file count=1024000 bs=3024 |
你会发现,系统的 buff/cache 这一列会不断的增大。
1 | [root@8e3715641c31 /]# free -h |
继续回到我们上面的这个生产环境问题,会不会是 Java 程序在不停的往磁盘写文件,导致 cache 不断的增大呢?
我们执行
1 | kubectl logs -f pod-name -n namespace-name |
查看,发现整屏幕不断的输出 debug 日志。然后回到开头的图,我们会发现cache 占用了 20g 左右。
我们尝试把 cache 清掉下看看内存是否会下降,执行
1 | echo 3 > /proc/sys/vm/drop_caches //表示清空所有缓存(pagecache、dentries 和 inodes) |
proc/sys是一个虚拟文件系统,可以通过对它的读写操作做为与kernel实体间进行通信的一种手段。我们可以通过修改/proc中的文件,来对当前kernel的行为做出调整。通过调整/proc/sys/vm/drop_caches来释放内存。其默认数值为0
当执行完这条命令后,该 pod 的内存瞬间变小,同时磁盘 I/O 持续飙升,说明正是 cache 问题导致的,于是告诉用户调整日志的级别,把 debug 改成 info,发现内存问题得到解决。
如何解决生产环境内存飙升的问题
首先,合理的规划资源,对每个 Pod 限制其资源使用,kubernetes 提供了针对 pod 级别的资源限制功能,我们可以非常容易的通过 limits 来限制 Pod 的内存和 CPU ,这样一来一旦内存达到使用限制,pod 会自动重启,而不会影响到其他 pod。
1 | resources: |
再次,针对应用本身也需要加上资源使用限制,例如 Java 程序可以限制堆内存和非堆内存的使用:
堆内存分配:
- JVM 初始分配的内存由-Xms 指定,默认是物理内存的 1/64;
- JVM 最大分配的内存由-Xmx 指定,默认是物理内存的 1/4;
- 默认空余堆内存小于 40% 时,JVM 就会增大堆直到-Xmx 的最大限制;空余堆内存大于 70% 时,JVM 会减少堆直到 -Xms 的最小限制;
- 因此服务器一般设置-Xms、-Xmx 相等以避免在每次 GC 后调整堆的大小。对象的堆内存由称为垃圾回收器的自动内存管理系统回收。
非堆内存分配:
- JVM 使用-XX:PermSize 设置非堆内存初始值,默认是物理内存的 1/64;
- 由 XX:MaxPermSize 设置最大非堆内存的大小,默认是物理内存的 1/4;
- -Xmn2G:设置年轻代大小为 2G;
- -XX:SurvivorRatio,设置年轻代中 Eden 区与 Survivor 区的比值。
再次,应用本身要做好优化,例如本案例中所类似的问题要尽量在生产环境发生,即在生产环境开 debug 模式。因为频繁的写日志会把 cache 打的非常高。
最后,加强监控,针对应用本身的资源使用情况和系统的各项监控指标要完善,便于及时发现问题。