Kubernetes中限制Java容器内存资源实践

2次阅读

共计 2396 个字符,预计需要花费 6 分钟才能阅读完成。

0x00 起因

公司最近部分应用要从 Docker Swarm 迁移到 Kubernetes,而迁移到新的 Kubernetes 上的应用都要做资源的限制,否则如果 Pod 不断地占用机器资源把整个节点都拖垮了那就很糟糕了。。所以我按照 Kubernetes 的文档做了限制后,发现并没有什么卵用,容器不断的被 OOMKIILED 然后又重启,服务也一直无法访问,所以需要研究下 Java 应用到底该怎么限制内存资源。

0x01 分析

当我在 google 搜索了一波后,发现这个问题就是 JVM 无法得知容器的资源限制,所以按照 JVM 的默认规则,它分配的 Max Heap Size 是系统内存的 1 /4,所以就很容易超出 resource limits 的限制,导致容器被 kill 掉。

而造成这个问题的原因是什么呢,这就得说回 Docker 容器,我们都知道 Docker 容器本质上就是一个被隔离的用户态进程,而构成这个进程自然就少不了三驾马车:

  • cgroups 做进程的资源限制
  • namespace 做命名空间隔离
  • aufs 做联合文件挂载实现文件系统

我们要限制 Java 程序自然就与 cgroups 有关了,在 Linux 上,一切皆文件,所以系统的资源信息等也是以一种特殊的文件形式放在 /proc 目录下的,像我们常用的一些 top,free,ps 等查看系统资源的工具本质上也是从这个目录下获取的信息。但是 cgroups 限制资源不一样,它是在 /sys/fs/cgroups 目录下对指定 namespace 的做限制。

而我们的 JVM 是怎么获取到的当前进程的内存信息的呢?是通过读取挂载的 /proc/meminfo 文件了,那么由于 /proc/meminfo 里面展示的是宿主机的内存资源,从而让容器产生了自己是地主的感觉,还以为有大把的内存可以给它用,其实自己只是一个长工。。

0x02 方法

了解了这个问题的成因后,并在网上搜集了一些资料,我发现解决这个问题的方式就是在 Java 启动命令前加上 JVM_OPTS 参数,而具体加什么参数和 Java 的版本有关,总的来说呢,规则如下:

  • Java < Java8_u131: 如果低于这个版本,那么在 Java 容器的 CMD 命令里得加上具体的内存分配大小,如 "-Xms64M -Xmx256M", 注意-Xms 最好不超过 Pod 限制资源 3 /4,因为不止是 JVM 要使用内存,容器本身也是需要内存的。
  • Java < 10 : 如果 Java 版本在这个区间,那么我们就不需要明确地指定最大堆的大小了,这几个版本实际上已经可以从 cgroups 获取资源限制信息,只不过这个特性需要手动开启,需要加上参数"-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=1"
  • 如果 Java 版本大于 10:恭喜你,这个特性是默认开启的
  • 其他下游 Java 分支:不太清楚,请查看官方文档

0x03 实践

我司 Java 容器启动方式有两种,一种是通过 jar 包启动,还有一种是用 Tomcat 启动,所以我会分别介绍这两种 Java 应用的资源限制方式。

1. JAR

公司的 Java 版本有的用的是 1.7,还有的用的是 1.8 小于 131 的版本,镜像也是用的 CentOS 或者 Ubuntu 的基础镜像做的,体积大得惨不忍睹。。所以我决定使用 alpine 的镜像重新构建,并且只保留 jre,这个基础镜像的 Dockerfile 可参考 jeanblanchard/java,至于其他的步骤就很简单了,我的 Dockerfile 如下:

FROM 18.16.200.10:5000/oracle-jre8:u231
WORKDIR /home
COPY xxxx.jar .
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
 echo "Asia/Shanghai" >> /etc/timezone
CMD java $JVM_OPTS  -Duser.timezone=GMT+08 -jar /home/xxxx.jar

我的 Yaml 文件大致如下,加上 env 的环境变量和资源限制:

...
     containers:
      - name: xxxxx
        image: 18.16.200.191:5000/xxx:201910310754
        env:
        - name: JVM_OPTS
          value: "-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=1 -Xms256M"
        resources:
          requests:
            cpu: 0.1
          limits:
            cpu: 1
            memory: 1.5Gi
...

2. Tomcat

Dockerfile 文件如下:

FROM tomcat:8.5-jdk8
WORKDIR /usr/local/tomcat
COPY xxx.war webapps/ROOT.war
RUN unzip webapps/ROOT.war -d webapps/ROOT/ && rm -f webapps/ROOT.war
ENV JAVA_OPTS="-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=1 -Xms256M"
EXPOSE  8080
CMD ["/usr/local/tomcat/bin/catalina.sh", "run"]

在 tomcat 的启动文件 catalina.sh中可以通过环境变量 JAVA_OPTS 传入参数。

Yaml 文件则与 JAR 的差不多。至此,改造就全部完成了。

0x04 参考资料及延伸阅读

  1. https://www.kubernetes.org.cn/5005.html
  2. https://coolshell.cn/articles/17049.html
正文完
 0