Skip to main content

Command Palette

Search for a command to run...

自己的 Kubernetes 控制器(3)——改进和部署

Updated
3 min read

我们在前面讲述了 Kubernetes 控制器的概念。简单说来控制器就是个控制回路,用来将当前状态协调到目标状态。第二篇使用 Java 实现了一个控制器。这一篇会讲讲如何部署控制器,以及如何对控制器进行改进。

集群内外

在第一篇中提到过,控制器在集群内外都能运行,只要能够完成必要的通信过程就可以。缺省情况下,官方 Kubernetes 客户端和 Fabric8 客户端都会尝试使用 ~/.kube/config 配置中存储的凭据。也就是说只要使用 kubectl 命令能访问集群,就能运行这个控制器。

交付物可以是以下几种形式:独立的 JAR,应用服务器中部署的 WebApp,甚至是一个包含很多 Class 文件的目录。这种方法的缺点是,应该把所有与所选择的方法相关的常规任务都照顾到。

另一方面,用容器化应用的方式在 Kubernetes 集群中运行会有很多好处:自动化、监控、伸缩、自愈等。如此看来,没有不容器化的道理。因此我们要给我们的控制器进行容器化。

控制器的容器化

给 Java 应用进行容器化的最直接方式就是使用 Jib 插件。这个插件在 Maven 和 Gradle 中可用,兼容于普通应用、Spring Boot 和 Micronaut 应用;它生成的镜像会分为不同的层次:最上层是业务类,下面则是依赖库。这种构建方式加快了更新镜像的构建速度:当业务更新时,只需要更换最上面的层就可以了。

Jib 配置样例:

<plugin>
    <groupId>com.google.cloud.tools</groupId>
    <artifactId>jib-maven-plugin</artifactId>
    <version>1.8.0</version>
    <configuration>
        <from>
            <image>gcr.io/distroless/java:debug</image>
        </from>
        <to>
            <image>jvm-operator:${project.version}</image>
        </to>
    </configuration>
    <executions>
        <execution>
            <phase>compile</phase>
            <goals>
                <goal>dockerBuild</goal>
            </goals>
        </execution>
    </executions>
</plugin>
  • 缺省镜像没有 Shell,为了方便调试,提供一个 debug Tag
  • 目标镜像的标签来自于 POM
  • compile 阶段会运行插件。注意镜像并没有进行打包操作,因此 package 阶段可以跳过
  • 可用的目标包括 builddockerBuild。前者无需本地 Docker,并把镜像上传到 DockerHub;后者会把镜像构建到本地 Docker 中

到了这一步,写个 Kubernetes 配置就很容易了。

deploy.yml

apiVersion: v1
kind: Pod
metadata:
  namespace: jvmoperator
  name: custom-operator
spec:
  containers:
    - name: custom-operator
      image: jvm-operator:1.10
      imagePullPolicy: Never

上边的代码段偷懒声明了一个简单的 Pod。真实世界的配置会用 Deployment

kubectl apply -f deploy.yml

不幸的是,这个命令会失败,输出下列内容:

java.net.ProtocolException: Expected HTTP 101 response but was '403 Forbidden'
  at okhttp3.internal.ws.RealWebSocket.checkResponse(RealWebSocket.java:229)
  at okhttp3.internal.ws.RealWebSocket$2.onResponse(RealWebSocket.java:196)
  at okhttp3.RealCall$AsyncCall.execute(RealCall.java:203)
  at okhttp3.internal.NamedRunnable.run(NamedRunnable.java:32)
  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
  at java.lang.Thread.run(Thread.java:748)

鉴权

这个错误仅在集群内运行时候发生,原因是权限不足。给 Kubernetes API 发送请求是个危险行为,缺省情况下每个请求都会返回错误。因此这个容器需要有合适的授权:

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  namespace: jvmoperator
  name: operator-example
rules:
  - apiGroups:
      - ""
    resources:
      - pods
    verbs:
      - watch
      - create
      - delete
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: operator-service
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: operator-example
subjects:
  - kind: ServiceAccount
    name: operator-service
    namespace: jvmoperator
roleRef:
  kind: ClusterRole
  name: operator-example
  apiGroup: rbac.authorization.k8s.io

Kubernetes 中用 RBAC 的方式进行鉴权。这方面的主题比较复杂,想要细致学习,可以参考相关文档。

提交上述代码后,这个 Pod 就能够使用新的 ServiceAccount 运行了——只要做一点简单的修改:

apiVersion: v1
kind: Pod
metadata:
  namespace: jvmoperator
  name: custom-operator
spec:
  serviceAccountName: operator-service
  containers:
    - name: custom-operator
      image: jvm-operator:1.8
      imagePullPolicy: Never

容器化 JVM 应用的隐患

早期版本的 JVM 会返回主机的 CPU 和内存数量,而不是容器的。JVM 尝试占用不存在的内存,会导致 OutOfMemoryError。Kubernetes 则会杀死行为异常的 Pod。如果被杀死 Pod 是 ReplicaSet 的一部分,就会新建一个 Pod。这个过程很不利联想。JDK 10 开始这个问题已经解决了(这个特性也被融合到 JDK 8 的新版本之中)。

JVM 能够根据工作负载来调整应用程序的编译代码,这是优于静态编译的原生可执行程序的。JVM 需要大量的额外内存来实现这一点。而且 JVM 的启动时间相当长。由于自适应编译后的代码需要时间,所以在启动后的一段时间内,性能都不会符合要求。这也是为什么在 JVM 上的性能指标总是要在较长的预热时间后再进行测量的原因。最后,与原生可执行文件相比,容器的大小要大得多,因为它嵌入了 JVM 本身。

REPOSITORY            TAG          IMAGE ID            CREATED             SIZE
jvm-operator          1.8          bdaa419c75e2        50 years ago        141MB

综上所述,JVM 并非容器化应用的好对象。

克服 JVM 的限制

有两种方式能够克服上述的 JVM 问题

  1. 使用 Java 9 中引入的模块系统,JDK 提供了一个思路,让原生可执行文件只包含引用到的模块,抛弃其它内容。这样就见效了可执行尺寸。
  2. 使用 Graal VM 的 Substrate VM

Substrate VM 是一个能够将 Java 预编译成可执行镜像的框架。

Graal VM 能帮助你:

  • 把应用打包成单一的 JAR
  • 从 JAR 创建原生可执行文件
  • 把原生可执行文件进行容器化

不幸的是,Jib 没有 GraalVM 的配置。因此需要使用多阶段 Dockerfile:

  1. 构建 JAR
  2. 从 JAR 构建 原生可执行文件
  3. 容器化
ARG VERSION=1.10

FROM zenika/alpine-maven:3 as build
COPY src src
COPY pom.xml pom.xml
RUN mvn package

FROM oracle/graalvm-ce:19.2.1 as native
ARG VERSION
COPY --from=build /usr/src/app/target/jvm-operator-$VERSION.jar \
                  /var/jvm-operator-$VERSION.jar
WORKDIR /opt/graalvm
RUN gu install native-image \
 && native-image -jar /var/jvm-operator-$VERSION.jar \
 && mv jvm-operator-$VERSION /opt/jvm-operator-$VERSION

FROM scratch
ARG VERSION
WORKDIR /home
COPY --from=native /opt/jvm-operator-$VERSION operator
ENTRYPOINT ["./operator"]
  • Graal VM 发行版中缺省是不包括 Substrate VM 的,因此首先要进行安装
  • 在前面步骤生成的 JAR 上执行 native-image 过程
  • 使用 scratch 镜像为基础。在编译过程中使用 --static 选项打包,来包含所依赖的库

这样就缩减了镜像的尺寸:

REPOSITORY            TAG          IMAGE ID            CREATED             SIZE
jvm-operator          1.10         340d4d9a767e        6 weeks ago         52.7MB

Substrate VM 包含很多配置项目,为了达到上面的效果,需要这样的一组参数:

native-image.properties

Args=  -J-Xmx3072m \
       --static \
       --allow-incomplete-classpath \
       --no-fallback \
       --no-server \
       -H:EnableURLProtocols=https \
       -H:ConfigurationFileDirectories=/var/config

应对反射

AOT 过程在反射基础上还有诸多限制。根据底层代码的编写方式不同,可能会受到更多的影响。在不同状况之中,有不同的方法来解决这个问题。这些都将在以后的帖子中介绍:现在我们先来关注一下反射。

在 Java 中,一些底层代码或多或少依赖于基于运行时的反射。不幸的是,Substrate VM 会删除它认为不需要的代码。不过,这可以通过JSON文件来配置。鉴于依赖反射的调用量,手动配置是一项艰巨的任务。

Substrate VM 提供了一个更好的选择:它提供了一个 Java 代理,可以在运行中的控制器的命令行中设置。这个代理会拦截控制器应用程序内部的每一个反射调用,并将其记录在一个专门的 reflect-config.json 文件中。

在以后的阶段,这个文件(和其他类似的文件一起)可以反馈到编译过程中,这样通过反射访问的代码就会被保留下来。一种方法是通过命令行来送入它们。另一种是将它们打包到 JAR 里面,放在一个专门的文件夹里:这允许库的提供者提供与 AOT 兼容的 JAR,应该是首选的方式。

根据具体应用的不同,可能还会需要额外的步骤。更多信息,请参考:《How to cope with incompatible code in Graal VM AOT compilation》

结论

三篇文章,我们讲述了 Kubernetes 控制器的实现方法。开发过程中我们看到,这并不是一项艰巨的任务。在这其中提到的技术基础之上,能够实现更多更好的功能。

最后我们在 Kubernetes 集群上运行了新开发的 Java 控制器。后续我们引入 Graal VM 创建了一个原生可执行文件。虽然它使构建过程更加复杂,但使用这样的原生可执行文件消除了 JVM 平台的一些限制:它大大减少了映像大小、内存消耗以及启动时间。

完整的源码可以在 Github 上找到

More from this blog

龙虾恐慌:AIOps 又要改名了?

ChatGPT 开始,把 AI 拉近到普罗大众的面前,让无数人感受到 AI 的亲民魅力。而龙虾,则把大模型驱动的自动化能力,突然间变得水灵灵、活泼泼地走进千家万户。它不只是“风口上的猪”,而是风口本身。热度高到让 Mac mini 一度断货,不知道这在不在库克的预料之内。 每代人都有每代人的鸡蛋,春节期间,我就领了我的鸡蛋。翻出古老的 MacBook Air M1,充值各种大模型。当然了,这个工具

Mar 9, 20261 min read

再见 2025

我猜不少人以为这个号废了吧?并没有,只是今年变化有点大,一直有种抄起键盘,无从说起的感觉,所以一直偷懒到今天,2025 的最后一天。 今年是我的第四个本命年,去年末一期播客里,大内说本命年不是灾年,是变化年,有危也有机。可是讲真啊,只看到危,没看到机。 各种因缘际会,从鹅厂跳槽到前东家,已经接近四年,第一个合同期已经进入尾声。除了前两年还在云原生领域嗷嗷叫,后两年基本都是些鸡零狗碎的东西了,用老东家的术语说是——偏离主航道,可谓是前景暗淡了。 一旦确定要滚蛋,反倒心思轻松起来,每天骑着我的小红车...

Jan 5, 20261 min read

辅助编程?dora 说:我知道你很急可是请你别急

从 OpenGPT 把大模型的火烧旺了之后,这三年来,相信很多组织或摩拳擦掌、或躬身入局,希望借助聪明能干的大模型,或想偿还技术宅,或想降本增效,或想弯道超车。一时间,沉寂许久的 AIxx 又活过来了,LLM Ops、Vibe Coding、中医大模型、GPT 算命等等,全都老树发新芽,焕发了勃勃生机。那么视角拉回从业者最关注的饭碗相关的领域之一——AI 辅助开发,产生了什么触动,应该如何拥抱呢? DORA 的年度报告中给出了很有意思的结论——强者恒强。 执行摘要部分总结了几个有趣的点: 问题...

Oct 6, 20251 min read

[译]dora:ai 辅助软件开发状态报告

执行摘要 在 2025 年,科技领导者面临的核心问题已不再是“是否要采用 AI”,而是“如何实现其价值”。 DORA 的研究基于超过 100 小时的定性访谈和来自全球近 5,000 名技术专业人士的问卷调查。研究揭示了一个关键事实:AI 在软件开发中的主要角色是“放大器”。它会放大高效能组织的优势,也会凸显组织的缺陷。 关键结论:AI 是放大器 AI 投资的最大回报并非来自工具本身,而是来自组织底层系统的战略性建设: 高质量的内部平台 清晰的工作流 团队的协同能力 缺少这些基础,AI ...

Oct 2, 202514 min read

僭越了,有人在用 Rust 写 Kubernetes

一个新语言问世,最爱做的事情之一,就是重写存量软件了。 云原生喝酒 SIG 重点扶持项目——rk8s(https://github.com/rk8s-dev/rk8s) 也可以归在这个范畴里,只不过这个项目重写的东西比较大,是 Kubernetes。 从 2025 年 1 月第一个 Commit 开始,到现在有了 200 多次 Commit,十几万行代码。当然距离 Kubernetes 的几百万行代码还差得远——老马就是喜欢整这种大无畏项目。 另外该项目也是国内第一个脱离 Cargo 转向使用 ...

Sep 27, 20253 min read

【伪】架构师

342 posts