openjdk8镜像与cgroup兼容性问题

openjdk8镜像与cgroup兼容性问题

我们的java服务镜像,是按如下方式构建的:

  • 基于openjdk:8-jre-alpine镜像,
  • 替换软件源,时区,fonts等
  • 添加业务jar包
  • 添加启动命令

java镜像构建模板

最初的java镜像构建模板(基于java环境的业务):

Dockerfile

FROM openjdk:8-jre-alpine

ENV TZ="Asia/Shanghai"

ADD  ./app.sh  /

RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories &&\
    apk add --update ttf-dejavu curl tzdata &&\
    rm -rf /var/cache/apk/* &&\
    cp /usr/share/zoneinfo/Asia/Shanghai /etc/lcoaltime &&\
    echo 'Asia/Shanghai' > /etc/timezone &&\
    chmod 777  /app.sh

注意其中FROM openjdk:8-jre-alpine

因此这些镜像有统一的openjdk8版本,统一的底层环境(alpine发行版)。

[root@jingmin-kube-archlinux ~]# docker run --rm -m 1g openjdk:8-jre-alpine java -version
openjdk version "1.8.0_212"
OpenJDK Runtime Environment (IcedTea 3.12.0) (Alpine 8.212.04-r0)
OpenJDK 64-Bit Server VM (build 25.212-b04, mixed mode)

业务镜像构建模板

这是打server-openapi-zuul镜像的Dockerfile模板

#基础镜像
FROM  harbor.ole12138.cn/wy_spc/java:latest
#维护者信息
MAINTAINER wangjm
##配置环境变量参数
ARG CONFIG_ENV
##配置NACOS命名空间
ARG NACOS_NAMESPACE
##配置NACOS服务访问地址
ARG NACOS_SERVER
ENV CONFIG_ENV=$CONFIG_ENV
ENV NACOS_NAMESPACE=$NACOS_NAMESPACE
ENV NACOS_SERVER=$NACOS_SERVER
##系统语言环境设置
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8
#将本地文件添加到容器中,tar类型文件会自动解压(网络压缩资源不会被解压),可以访问网络资源,类似wget
ADD  ./app/app.jar /app.jar

CMD  sh  /app.sh  "-XX:+UseContainerSupport" "-XX:MaxRAMPercentage=70.0 -XX:InitialRAMPercentage=45.0 -XX:MinRAMPercentage=45.0 -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m"  "-Dconfig.profile=$CONFIG_ENV -Dspring.cloud.nacos.discovery.namespace=$NACOS_NAMESPACE -Dspring.cloud.nacos.config.namespace=$NACOS_NAMESPACE -Dspring.cloud.nacos.server-addr=$NACOS_SERVER"

这是app.sh

#!/usr/bin/env sh


######$1 $2 $3 分别指的是3个不同的占位符 针对的是dockerFile 文件最后一行sh  app.sh 按顺序三个位置参数
######$1 指的是"-XX:+UseContainerSupport"
######$2 指的是"-XX:MaxRAMPercentage=70.0 -XX:InitialRAMPercentage=45.0 -XX:MinRAMPercentage=45.0 -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m"
######$3 指的是"-Dconfig.profile=$CONFIG_ENV -Dspring.cloud.nacos.discovery.namespace=$NACOS_NAMESPACE -Dspring.cloud.nacos.config.namespace=$NACOS_NAMESPACE -Dspring.cloud.nacos.server-addr=$NACOS_SERVER"
java  -jar $1 $2 $3  /app.jar

最终,在pod中执行的命令大概是这样的

(通过kubectl exec ... 然后 ps -ef |grep java查出来的)

java -jar -XX:+UseContainerSupport -XX:MaxRAMPercentage=70.0 -XX:InitialRAMPercentage=45.0 -XX:MinRAMPercentage=45.0 -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -XX:+ExitOnOutOfMemoryError -Dconfig.profile=jtest -Dspring.cloud.nacos.discovery.namespace=765fa359-2e1b-41f3-a4b2-17c3856764fe -Dspring.cloud.nacos.config.namespace=765fa359-2e1b-41f3-a4b2-17c3856764fe -Dspring.cloud.nacos.server-addr=nacos-headless.nacos.svc.cluster.local:8848 /app.jar

k8s部署业务镜像遇到问题

但是在k8s中部署业务时,发现有莫名奇妙的问题:

直接现象:

pod总是启动失败。偶尔提示OOMKilled。

kubectl describe pod/server-openapi-zuul-xxx 也没有显示太多有用信息。

kubectl logs -f pod/server-openapi-zuul-xxx 显示:业务总是启动了一半,就退出了。

起个临时pod,手动启动业务,结果确能正常启动:

起一个临时pod (这个临时pod没有设置内存限制)

kubectl run tmp-java --rm -it --image harbor.ole12138.cn/wy_jtest/server-openapi-zuul:jenkins-server-openapi-zuul-73 --overrides='{ "spec": { "template": { "spec": { "imagePullSecrets": [{"name": "harbor-secret"}] } } } }' -- sh

在pod内,手动执行之前的java程序

java -jar -XX:+UseContainerSupport -XX:MaxRAMPercentage=70.0 -XX:InitialRAMPercentage=45.0 -XX:MinRAMPercentage=45.0 -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -XX:+ExitOnOutOfMemoryError -Dconfig.profile=jtest -Dspring.cloud.nacos.discovery.namespace=765fa359-2e1b-41f3-a4b2-17c3856764fe -Dspring.cloud.nacos.config.namespace=765fa359-2e1b-41f3-a4b2-17c3856764fe -Dspring.cloud.nacos.server-addr=nacos-headless.nacos.svc.cluster.local:8848 /app.jar

另起一个终端,查询使用的内存

kubectl exec -it tmp-java -- sh
cat /sys/fs/cgroup/memory.current

发现有3个多G

而我们的k8s启动的业务deployment(它会启动pod)时,对pod是加了限制的:

cat service-k8s.yaml

apiVersion: apps/v1
kind: Deployment
##元数据信息
#...
spec:
  #...
  template:
    #...
    spec:
      #...
      containers:
        ##容器名称
        - name: server-openapi-zuul-container
          ##容器镜像地址
          image: harbor.ole12138.cn/wy_jtest/server-openapi-zuul:jenkins-server-openapi-zuul-73
          #...
          resources:
            ##资源请求限制
            limits:
              memory: 800Mi
              cpu: 200m

#...

这就导致了,一旦超过了800M,就会导致容器和pod被k8s给kill掉。

业务镜像中,业务启动命令为(省略了一些业务相关的参数):

java -jar -XX:+UseContainerSupport -XX:MaxRAMPercentage=70.0 -XX:InitialRAMPercentage=45.0 -XX:MinRAMPercentage=45.0 -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -XX:+ExitOnOutOfMemoryError /app.jar

-XX:+UseContainerSupport是启用容器支持选项。jdk 1.8.0_191之后,-XX:+UseContainerSupport参数应该是有效的。业务程序中看到的系统内存应该只有800M才对。但前面3.3G的实际使用内存,远远超过了800M。

查询java版本

/ # java -XshowSettings:vm -version
VM settings:
    Max. Heap Size (Estimated): 13.95G
    Ergonomics Machine Class: server
    Using VM: OpenJDK 64-Bit Server VM

openjdk version "1.8.0_212"
OpenJDK Runtime Environment (IcedTea 3.12.0) (Alpine 8.212.04-r0)
OpenJDK 64-Bit Server VM (build 25.212-b04, mixed mode)
/ # java -XshowSettings:vm -XX:+UseContainerSupport -version
VM settings:
    Max. Heap Size (Estimated): 13.95G
    Ergonomics Machine Class: server
    Using VM: OpenJDK 64-Bit Server VM

查询版本是java 1.8.0_212, Max. Heap Size (Estimated): 13.95G。显然,并没有获取到容器内存

参考: https://stackoverflow.com/questions/54516988/what-does-usecontainersupport-vm-parameter-do

参考: https://www.cnblogs.com/varden/p/15999471.html

综上:

jdk 1.8.0_191之后,-XX:+UseContainerSupport参数应该是有效的,但是这里试了下无效!!!

这里默认最大堆内存还是根据主机内存算出来的,而不是根据container的内存算出来的!!!

(最初以为是alpine的兼容性问题。后来发现是,openjdk镜像和cgroup的兼容性问题)

排查与解决问题

开始以为是apline的兼容性问题,切换java镜像版本

前面java镜像构建模板中FROM openjdk:8-jre-alpine, 它使用的alpine。(一个专用于容器环境的linux版本)

它不是使用glibc的。可能会有一些兼容性问题,或者莫名奇妙的问题。

那么,需要切换一下java的基础镜像,并修改软件源

参考: docker镜像的区别–Alpine、Slim、Stretch、Buster、Jessie、Bullseye

参考: https://www.timiguo.com/archives/223/

  • buster:Debian 10
  • stretch:Debian 9
  • jessie:Debian 8

openjdk这个docker镜像,对于Java 8的镜像,目前的最新镜像是8u342。(注意到这个版本已经没有了alpine版本)

镜像希望小一点,就不要选jdk,而是jre。

debian系尽量新一点。 就选buster版本。

最终选定openjdk:8u342-jre-buster这个镜像。

参考: https://www.xiaoshu168.com/docker/378.html

参考:https://www.jianshu.com/p/b4a792945d99

参考: https://zhuanlan.zhihu.com/p/598857157

修改java基础镜像的Dockerfile文件

FROM openjdk:8u342-jre-buster

ENV TZ="Asia/Shanghai"

ADD  ./app.sh  /

RUN sed -i 's#http://deb.debian.org#http://mirrors.aliyun.com#g' /etc/apt/sources.list &&\
    apt install -y ttf-dejavu-core &&\
    cp /usr/share/zoneinfo/Asia/Shanghai /etc/lcoaltime &&\
    echo 'Asia/Shanghai' > /etc/timezone &&\
    chmod 777  /app.sh

jenkins中重新构建java镜像。

harbor私有镜像库,给对应的java镜像手动加个latest标签

docker build 的时候添加--pull选项,保证拉取最新的底层镜像。

发现还是有同样的问题。

排查docker

在archlinux 上执行(获取到了错误的Max. Heap Size -根据主机内存算出来的值)

[root@jingmin-kube-archlinux ~]# docker run --rm -m 1g openjdk:8u191-jre-alpine java -XshowSettings:vm -version
VM settings:
    Max. Heap Size (Estimated): 13.95G
    Ergonomics Machine Class: server
    Using VM: OpenJDK 64-Bit Server VM

openjdk version "1.8.0_191"
OpenJDK Runtime Environment (IcedTea 3.10.0) (Alpine 8.191.12-r0)
OpenJDK 64-Bit Server VM (build 25.191-b12, mixed mode)

[root@jingmin-kube-archlinux ~]# docker version
Client:
 Version:           24.0.5
 API version:       1.43
 Go version:        go1.20.6
 Git commit:        ced0996600
 Built:             Wed Jul 26 21:44:58 2023
 OS/Arch:           linux/amd64
 Context:           default

Server:
 Engine:
  Version:          24.0.5
  API version:      1.43 (minimum version 1.12)
  Go version:       go1.20.6
  Git commit:       a61e2b4c9c
  Built:            Wed Jul 26 21:44:58 2023
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          v1.7.5
  GitCommit:        fe457eb99ac0e27b3ce638175ef8e68a7d2bc373.m
 runc:
  Version:          1.1.9
  GitCommit:        
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

在另一台linux上执行(获取到了正确的Max. Heap Size -根据容器内存算出来的值)

[root@jingmin-kube-master1 ~]# docker run --rm -m 1g openjdk:8u191-jre-alpine java -XshowSettings:vm -version
VM settings:
    Max. Heap Size (Estimated): 247.50M
    Ergonomics Machine Class: server
    Using VM: OpenJDK 64-Bit Server VM

openjdk version "1.8.0_191"
OpenJDK Runtime Environment (IcedTea 3.10.0) (Alpine 8.191.12-r0)
OpenJDK 64-Bit Server VM (build 25.191-b12, mixed mode)
您在 /var/spool/mail/root 中有邮件
[root@jingmin-kube-master1 ~]# docker version
Client: Docker Engine - Community
 Version:           24.0.5
 API version:       1.43
 Go version:        go1.20.6
 Git commit:        ced0996
 Built:             Fri Jul 21 20:36:32 2023
 OS/Arch:           linux/amd64
 Context:           default

Server: Docker Engine - Community
 Engine:
  Version:          24.0.5
  API version:      1.43 (minimum version 1.12)
  Go version:       go1.20.6
  Git commit:       a61e2b4
  Built:            Fri Jul 21 20:35:32 2023
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.6.21
  GitCommit:        3dce8eb055cbb6872793272b4f20ed16117344f8
 runc:
  Version:          1.1.7
  GitCommit:        v1.1.7-0-g860f061
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

这两台主机,一台是archlinux,一台是centos stream 8.

docker版本相同。但是在openjdk:8u191-jre容器中获取到的最大堆内存的行为确不一致(默认应该是感知到的总内存的四分之一)!!!

archlinux的容器中获取到了主机内存的四分之一。而centos的容器中获取到了容器内存的四分之一。

百思不得其解。这里两台主机docker版本是一样的。感觉是docker下层的或虚拟化层面的问题。

出问题的只能是containerd,runc,以及linux虚拟化层面的cgroup。

排查cgroup

灵光一闪,搜索关键字java 读取 cgroup

参考: https://blog.51cto.com/u_13778063/5989061

在容器中,获取容器资源(内存)配额

cat /sys/fs/cgroup/memory/memory.limit_in_bytes

centos这台主机上执行

[root@jingmin-kube-master1 ~]# docker run --rm -m 1g openjdk:8u191-jre-alpine cat /sys/fs/cgroup/memory/memory.limit_in_bytes
1073741824

差不多1G,这是正常的。

archlinux这台主机上执行

[root@jingmin-kube-archlinux ~]# docker run --rm -m 1g openjdk:8u191-jre-alpine cat /sys/fs/cgroup/memory/memory.limit_in_bytes
cat: can't open '/sys/fs/cgroup/memory/memory.limit_in_bytes': No such file or directory

[root@jingmin-kube-archlinux ~]# docker run --rm -m 1g openjdk:8u191-jre-alpine ls /sys/fs/cgroup
...
memory.current
memory.events
memory.events.local
memory.high
memory.low
memory.max
memory.min
memory.numa_stat
memory.oom.group
memory.peak
memory.pressure
memory.reclaim
memory.stat
memory.swap.current
memory.swap.events
memory.swap.high
memory.swap.max
memory.zswap.current
memory.zswap.max
...

发现其目录结构有点不同。

突然想起来,安装k8s的时候,k8s官网好像有提到,有两个cgroup版本。高版本linux内核,默认是cgroup v2的。

参考: https://github.com/docker/for-mac/issues/6118

参考: https://github.com/ibmruntimes/ci.docker/issues/124

参考: https://github.com/eclipse-openj9/openj9/issues/14190

参考:https://developers.redhat.com/articles/2023/04/19/openjdk-8u372-feature-cgroup-v2-support#onward_to_jdk_21

高版本的openjdk镜像, 比如jdk17,好像是同时支持cgroup v1 和 cgroup v2的。

openjdk:8u342-jre的官方docker镜像试了下。还是只支持 cgroup v1。

https://github.com/docker-library/openjdk/issues/505

好像要8u372才会支持 cgroup v2

改为使用eclipse-temurin,切换java镜像版本

openjdk官方镜像,好像已经不再维护了. jdk8停在了 8u342.

DEPRECATION NOTICE 弃用通知

This image is officially deprecated and all users are recommended to find and use suitable replacements ASAP. Some examples of other Official Image alternatives (listed in alphabetical order with no intentional or implied preference):

https://hub.docker.com/_/openjdk

这里提供了几个替代的docker镜像。关于应该使用哪个替代的openjdk版本:https://whichjdk.com/

推荐eclipse-temurin

在archlinux (cgroup v2)的主机上,看下镜像是获取到了正确的Max. Heap Size

[root@jingmin-kube-archlinux ~]# docker run -m 1g --rm -it eclipse-temurin:8-jre-alpine sh
/ # java -XshowSettings:vm -version
VM settings:
    Max. Heap Size (Estimated): 247.50M
    Ergonomics Machine Class: server
    Using VM: OpenJDK 64-Bit Server VM

openjdk version "1.8.0_382"
OpenJDK Runtime Environment (Temurin)(build 1.8.0_382-b05)
OpenJDK 64-Bit Server VM (Temurin)(build 25.382-b05, mixed mode)

java版本 1.8.0_382 (8u282), 已经高于 8u372, 已经支持cgroup v2环境了. 容器分配了1g的内存,最大堆内存差不多是四分之一。没问题了。

参考:https://developers.redhat.com/articles/2023/04/19/openjdk-8u372-feature-cgroup-v2-support

那么现在的java镜像构建模板(基于java环境的业务),改为:

Dockerfile

FROM eclipse-temurin:8-jre-alpine

ENV TZ="Asia/Shanghai"

ADD  ./app.sh  /

RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories &&\
    apk add --update ttf-dejavu curl tzdata &&\
    rm -rf /var/cache/apk/* &&\
    cp /usr/share/zoneinfo/Asia/Shanghai /etc/lcoaltime &&\
    echo 'Asia/Shanghai' > /etc/timezone &&\
    chmod 777  /app.sh

jenkins中重新构建java镜像。

harbor私有镜像库,给对应的java镜像手动加个latest标签(要删一下旧镜像的latest标签)

docker build 的时候添加--pull选项,保证拉取最新的底层镜像。


评论

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注