一个线上 kubernetes 的内存增长问题

从生产环境遇到的一个内存暴涨问题说起

某用户生产环境的 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
2
3
4
[root@8e3715641c31 /]# dd if=/dev/zero of=my_new_file count=1024000 bs=3024
1024000+0 records in
1024000+0 records out
3096576000 bytes (3.1 GB, 2.9 GiB) copied, 28.7933 s, 108 MB/s

你会发现,系统的 buff/cache 这一列会不断的增大。

1
2
3
4
[root@8e3715641c31 /]# free -h
total used free shared buff/cache available
Mem: 3.7Gi 281Mi 347Mi 193Mi 3.1Gi 3.0Gi
Swap: 0B 0B 0B

继续回到我们上面的这个生产环境问题,会不会是 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
2
3
4
5
6
7
resources:
requests:
cpu: "200m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"

再次,针对应用本身也需要加上资源使用限制,例如 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 打的非常高。

最后,加强监控,针对应用本身的资源使用情况和系统的各项监控指标要完善,便于及时发现问题。