<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>controller | 伪架构师</title>
    <link>/tags/controller/</link>
      <atom:link href="/tags/controller/index.xml" rel="self" type="application/rss+xml" />
    <description>controller</description>
    <generator>Source Themes Academic (https://sourcethemes.com/academic/)</generator><language>zh</language><lastBuildDate>Mon, 13 Apr 2020 16:21:32 +0800</lastBuildDate>
    <image>
      <url>/img/logo-wide.png</url>
      <title>controller</title>
      <link>/tags/controller/</link>
    </image>
    
    <item>
      <title>自己的 Kubernetes 控制器（3）——改进和部署</title>
      <link>/post/your-own-k8s-controller-3/</link>
      <pubDate>Mon, 13 Apr 2020 16:21:32 +0800</pubDate>
      <guid>/post/your-own-k8s-controller-3/</guid>
      <description>

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

&lt;h2 id=&#34;集群内外&#34;&gt;集群内外&lt;/h2&gt;

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

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

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

&lt;h2 id=&#34;控制器的容器化&#34;&gt;控制器的容器化&lt;/h2&gt;

&lt;p&gt;给 Java 应用进行容器化的最直接方式就是使用 &lt;a href=&#34;https://github.com/GoogleContainerTools/jib&#34; target=&#34;_blank&#34;&gt;Jib 插件&lt;/a&gt;。这个插件在 Maven 和 Gradle 中可用，兼容于普通应用、Spring Boot 和 Micronaut 应用；它生成的镜像会分为不同的层次：最上层是业务类，下面则是依赖库。这种构建方式加快了更新镜像的构建速度：当业务更新时，只需要更换最上面的层就可以了。&lt;/p&gt;

&lt;p&gt;Jib 配置样例：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-xml&#34;&gt;&amp;lt;plugin&amp;gt;
    &amp;lt;groupId&amp;gt;com.google.cloud.tools&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;jib-maven-plugin&amp;lt;/artifactId&amp;gt;
    &amp;lt;version&amp;gt;1.8.0&amp;lt;/version&amp;gt;
    &amp;lt;configuration&amp;gt;
        &amp;lt;from&amp;gt;
            &amp;lt;image&amp;gt;gcr.io/distroless/java:debug&amp;lt;/image&amp;gt;
        &amp;lt;/from&amp;gt;
        &amp;lt;to&amp;gt;
            &amp;lt;image&amp;gt;jvm-operator:${project.version}&amp;lt;/image&amp;gt;
        &amp;lt;/to&amp;gt;
    &amp;lt;/configuration&amp;gt;
    &amp;lt;executions&amp;gt;
        &amp;lt;execution&amp;gt;
            &amp;lt;phase&amp;gt;compile&amp;lt;/phase&amp;gt;
            &amp;lt;goals&amp;gt;
                &amp;lt;goal&amp;gt;dockerBuild&amp;lt;/goal&amp;gt;
            &amp;lt;/goals&amp;gt;
        &amp;lt;/execution&amp;gt;
    &amp;lt;/executions&amp;gt;
&amp;lt;/plugin&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;缺省镜像没有 Shell，为了方便调试，提供一个 &lt;code&gt;debug&lt;/code&gt; Tag&lt;/li&gt;
&lt;li&gt;目标镜像的标签来自于 POM&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;compile&lt;/code&gt; 阶段会运行插件。注意镜像并没有进行打包操作，因此 &lt;code&gt;package&lt;/code&gt; 阶段可以跳过&lt;/li&gt;
&lt;li&gt;可用的目标包括 &lt;code&gt;build&lt;/code&gt; 和 &lt;code&gt;dockerBuild&lt;/code&gt;。前者无需本地 Docker，并把镜像上传到 DockerHub；后者会把镜像构建到本地 Docker 中&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;到了这一步，写个 Kubernetes 配置就很容易了。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;deploy.yml&lt;/p&gt;
&lt;/blockquote&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1
kind: Pod
metadata:
  namespace: jvmoperator
  name: custom-operator
spec:
  containers:
    - name: custom-operator
      image: jvm-operator:1.10
      imagePullPolicy: Never
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上边的代码段偷懒声明了一个简单的 &lt;code&gt;Pod&lt;/code&gt;。真实世界的配置会用 &lt;code&gt;Deployment&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kubectl apply -f deploy.yml&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;不幸的是，这个命令会失败，输出下列内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-text&#34;&gt;java.net.ProtocolException: Expected HTTP 101 response but was &#39;403 Forbidden&#39;
  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)
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;鉴权&#34;&gt;鉴权&lt;/h2&gt;

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

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  namespace: jvmoperator
  name: operator-example
rules:
  - apiGroups:
      - &amp;quot;&amp;quot;
    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
&lt;/code&gt;&lt;/pre&gt;

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

&lt;p&gt;提交上述代码后，这个 Pod 就能够使用新的 ServiceAccount 运行了——只要做一点简单的修改：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;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
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;容器化-jvm-应用的隐患&#34;&gt;容器化 JVM 应用的隐患&lt;/h2&gt;

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

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

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;REPOSITORY            TAG          IMAGE ID            CREATED             SIZE
jvm-operator          1.8          bdaa419c75e2        50 years ago        141MB
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;综上所述，JVM 并非容器化应用的好对象。&lt;/p&gt;

&lt;h2 id=&#34;克服-jvm-的限制&#34;&gt;克服 JVM 的限制&lt;/h2&gt;

&lt;p&gt;有两种方式能够克服上述的 JVM 问题&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;使用 Java 9 中引入的模块系统，JDK 提供了一个思路，让原生可执行文件只包含引用到的模块，抛弃其它内容。这样就见效了可执行尺寸。&lt;/li&gt;
&lt;li&gt;使用 &lt;a href=&#34;https://www.graalvm.org/&#34; target=&#34;_blank&#34;&gt;Graal VM&lt;/a&gt; 的 Substrate VM&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Substrate VM 是一个能够将 Java 预编译成可执行镜像的框架。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Graal VM 能帮助你：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;把应用打包成单一的 JAR&lt;/li&gt;
&lt;li&gt;从 JAR 创建原生可执行文件&lt;/li&gt;
&lt;li&gt;把原生可执行文件进行容器化&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;不幸的是，Jib 没有 GraalVM 的配置。因此需要使用多阶段 Dockerfile：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;构建 JAR&lt;/li&gt;
&lt;li&gt;从 JAR 构建 原生可执行文件&lt;/li&gt;

&lt;li&gt;&lt;p&gt;容器化&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-dockerfile&#34;&gt;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 \
&amp;amp;&amp;amp; native-image -jar /var/jvm-operator-$VERSION.jar \
&amp;amp;&amp;amp; mv jvm-operator-$VERSION /opt/jvm-operator-$VERSION

FROM scratch
ARG VERSION
WORKDIR /home
COPY --from=native /opt/jvm-operator-$VERSION operator
ENTRYPOINT [&amp;quot;./operator&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Graal VM 发行版中缺省是不包括 Substrate VM 的，因此首先要进行安装&lt;/li&gt;
&lt;li&gt;在前面步骤生成的 JAR 上执行 &lt;code&gt;native-image&lt;/code&gt; 过程&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;scratch&lt;/code&gt; 镜像为基础。在编译过程中使用 &lt;code&gt;--static&lt;/code&gt; 选项打包，来包含所依赖的库&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这样就缩减了镜像的尺寸：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-text&#34;&gt;REPOSITORY            TAG          IMAGE ID            CREATED             SIZE
jvm-operator          1.10         340d4d9a767e        6 weeks ago         52.7MB
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Substrate VM 包含很多配置项目，为了达到上面的效果，需要这样的一组参数：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;native-image.properties&lt;/p&gt;
&lt;/blockquote&gt;

&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;Args=  -J-Xmx3072m \
       --static \
       --allow-incomplete-classpath \
       --no-fallback \
       --no-server \
       -H:EnableURLProtocols=https \
       -H:ConfigurationFileDirectories=/var/config
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;应对反射&#34;&gt;应对反射&lt;/h2&gt;

&lt;p&gt;AOT 过程在反射基础上还有&lt;a href=&#34;https://github.com/oracle/graal/blob/master/substratevm/LIMITATIONS.md&#34; target=&#34;_blank&#34;&gt;诸多限制&lt;/a&gt;。根据底层代码的编写方式不同，可能会受到更多的影响。在不同状况之中，有不同的方法来解决这个问题。这些都将在以后的帖子中介绍：现在我们先来关注一下反射。&lt;/p&gt;

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

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

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

&lt;p&gt;根据具体应用的不同，可能还会需要额外的步骤。更多信息，请参考：&lt;a href=&#34;https://blog.frankel.ch/coping-incompatible-code-graalvm-compilation/&#34; target=&#34;_blank&#34;&gt;《How to cope with incompatible code in Graal VM AOT compilation》&lt;/a&gt;。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

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

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

&lt;p&gt;完整的源码可以在 &lt;a href=&#34;https://github.com/nfrankel/jvm-controller&#34; target=&#34;_blank&#34;&gt;Github&lt;/a&gt; 上找到&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>自己的 Kubernetes 控制器（2）——用 Java 开发</title>
      <link>/post/your-own-k8s-controller-2/</link>
      <pubDate>Mon, 13 Apr 2020 15:01:52 +0800</pubDate>
      <guid>/post/your-own-k8s-controller-2/</guid>
      <description>

&lt;p&gt;前面文章中，我们大概描述了开发自定义 Kubernetes 控制器的基础内容。其中我们提到，只要能够使用 HTTP/JSON 就可以满足开发需求。本文中就言归正传开始开发。&lt;/p&gt;

&lt;p&gt;开发使用的技术栈可以 Python、NodeJS 或者 Ruby。我的博客叫“Java Geek”，所以这里选择的是 Java。&lt;/p&gt;

&lt;p&gt;这个案例中我们使用 Sidecar 模式：每次有 Pod 调度，就生成一个并行的 Pod；当前面的 Pod 被删除，后面的 Pod 也随之删除。&lt;/p&gt;

&lt;h2 id=&#34;选择合适的工具&#34;&gt;选择合适的工具&lt;/h2&gt;

&lt;p&gt;为了在 Java 中调用 REST 接口，就首先要生成绑定的结构。有几种方式可以完成这项工作：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;最无聊的方式就是手工完成：认真对待所有请求和响应的 JSON 数据，据此开发对应的 Java 对象，选择 JSON 序列化框架，以及 HTTP 客户端。&lt;/li&gt;
&lt;li&gt;次选的方式是使用 Swagger 或者 APiary 这样的代码生成器：API 提供者需要使用某种方式来提供对应的模型，开发者使用相应工具来生成代码。&lt;/li&gt;
&lt;li&gt;最好的方式是，已经有客户端库提供了绑定结构。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kubernetes 属于第三种——它已经为多种语言提供了绑定代码。只不过这种语言封装和 REST API 非常相近，不太符合我的习惯。例如获取所有命名空间下所有 Pod 的代码：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-java&#34;&gt;ApiClient client = Config.defaultClient();
CoreV1Api core = new CoreV1Api(client);
V1PodList pods =
    core.listPodForAllNamespaces(null, null, null, null, null, null, null, null);
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;所有 &lt;code&gt;null&lt;/code&gt; 都需要传递&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这就是我所说的 &lt;code&gt;和 REST API 非常相近&lt;/code&gt;，幸运的是，还有其他选项：Fabric8 在 &lt;a href=&#34;https://github.com/fabric8io/kubernetes-client&#34; target=&#34;_blank&#34;&gt;Github&lt;/a&gt; 上提供了 Java API。等价代码：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-java&#34;&gt;KubernetesClient client = new DefaultKubernetesClient();
PodList pods = client.pods().inAnyNamespace().list();
&lt;/code&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;p&gt;不再需要无用的 &lt;code&gt;null&lt;/code&gt; 参数。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&#34;fabric8-概述&#34;&gt;Fabric8 概述&lt;/h2&gt;

&lt;p&gt;简单说来，Fabric8 API 里面，在 &lt;code&gt;KubernetesClient&lt;/code&gt; 示例中可以获取所有 Kubernetes 资源：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;client.namespaces()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;client.services()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;client.nodes()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;等等&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;根据资源的特性，可以使用命名空间进行过滤：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;client.pods().inAnyNamespace()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;client.pods().inNamespace(&amp;quot;ns&amp;quot;)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;列出所有命名空间的所有 Pod：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-java&#34;&gt;client.pods().inAnyNamespace().list();
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;删除命名空间 &lt;code&gt;ns&lt;/code&gt; 中的所有 Pod：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-java&#34;&gt;client.pods().delete(client.pods().inNamespace(&amp;quot;ns&amp;quot;).list().getItems());
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;创建一个名为 &lt;code&gt;ns&lt;/code&gt; 的命名空间：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-java&#34;&gt;client.namespaces()
  .createNew()
    .withApiVersion(&amp;quot;v1&amp;quot;)
    .withNewMetadata()
      .withName(&amp;quot;ns&amp;quot;)
    .endMetadata()
  .done();
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;实现控制回路&#34;&gt;实现控制回路&lt;/h2&gt;

&lt;p&gt;Kubernetes 控制器只是一个控制回路，它会监视集群状态，并尝试将其调整为目标状态。为了跟进调度和删除事件，就需要实现观察者模式。应用订阅事件，在事件发生时，调用相关的回调。&lt;/p&gt;

&lt;p&gt;下面是一个简化版的类图：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/watcher-api.svg&#34; alt=&#34;watcher&#34; /&gt;&lt;/p&gt;

&lt;p&gt;实际实现代码：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-java&#34;&gt;public class DummyWatcher implements Watcher&amp;lt;Pod&amp;gt; {

  @Override
  public void eventReceived(Action action, Pod pod) {
    switch (action) {
      // 新 Pod
      case ADDED:
        break;
      // Pod 修改
      case MODIFIED:
        break;
      // Pod 删除
      case DELETED:
        break;
      // Pod 出错
      case ERROR:
        break;
    }
  }

  // 删除所有资源。如果客户端正确关闭，`cause` 为 `null`
  @Override
  public void onClose(KubernetesClientException cause) {

  }
}

client.pods()
  .inAnyNamespace()
  .watch(DummyWatcher());
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&#34;细枝末节&#34;&gt;细枝末节&lt;/h2&gt;

&lt;p&gt;我们已经准备好实现 Sidecar 模式了。我不会贴出所有代码，毕竟有 &lt;a href=&#34;https://github.com/nfrankel/jvm-controller&#34; target=&#34;_blank&#34;&gt;Github&lt;/a&gt;，只会贴出一些必要内容。&lt;/p&gt;

&lt;h3 id=&#34;标记-sidecar&#34;&gt;标记 Sidecar&lt;/h3&gt;

&lt;p&gt;我们的控制器要在 Pod 新建世加入 Sidecar，并在 Pod 移除时也删除 Sidecar。这个逻辑有一点问题：如果 Sidecar pod 被调度，就会触发监控事件，就会加入新的 Sidecar，这个过程会不断重复下去。因此有必要对 Sidecar Pod 进行标记。在带有标记的 Pod 被创建时，不会触发创建逻辑。&lt;/p&gt;

&lt;p&gt;有几种方式来对 Sidecar Pod 进行标记：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;给 Pod 加入后缀，比如 &lt;code&gt;sidecar&lt;/code&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;添加特定标签：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-java&#34;&gt;client.pods()
.inNamespace(&amp;quot;ns&amp;quot;)
.createNew()
.withNewMetadata()
  .addToLabels(&amp;quot;sidecar&amp;quot;, &amp;quot;true&amp;quot;)
.endMetadata()
.done();
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&#34;和-pod-一起删除-sidecar&#34;&gt;和 Pod 一起删除 Sidecar&lt;/h3&gt;

&lt;p&gt;Pod 应该有且只有一个 Sidecar，并且随 Pod 的创建和销毁同步进行创建和销毁。&lt;/p&gt;

&lt;p&gt;因此 Sidecar 数据结构中需要有一个指向主 Pod 的引用。这样在 Pod 删除时，如果它不是 Sidecar Pod，我们就能找到它的 Sidecar 并删除。&lt;/p&gt;

&lt;p&gt;最直白的方式就是在住 Pod 删除时直接删除 Sidecar，不过这需要做不少事。Kubernetes 中可以把两个 Pod 的生命周期使用 &lt;code&gt;ownerReference&lt;/code&gt; 关联起来。这样就可以让 Kubernetes 自行处理删除逻辑了。&lt;/p&gt;

&lt;p&gt;用 API 实现非常直观：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-java&#34;&gt;client.pods()
  .inNamespace(&amp;quot;ns&amp;quot;)
  .createNew()
    .withNewMetadata()
      .addNewOwnerReference()
        .withApiVersion(&amp;quot;v1&amp;quot;)
        .withKind(&amp;quot;Pod&amp;quot;)
        .withName(podName)
        .withUid(pod.getMetadata().getUid())
      .endOwnerReference()
    .endMetadata()
  .done();
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;保持-sidecar&#34;&gt;保持 Sidecar&lt;/h3&gt;

&lt;p&gt;添加了 Sidecar 并不意味着他会永远保持。例如属于一个 Deployment 的 Pod 会被删除，Deployment 的核心功能就是保持副本数为期望值。&lt;/p&gt;

&lt;p&gt;类似的，如果一个 Sidecar 被删除，并且主 Pod 还保持存活，就应该创建新的 Sidecar，并维持 &lt;code&gt;ownerReference&lt;/code&gt;。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;本文描述了用 Java 实现 Kubernetes 控制器的过程。有了 Fabric8 API，这个过程相当直接。主要需要解决的问题就是删除和创建逻辑。下一篇也就是最后一篇，会讲解部署和运行的过程。&lt;/p&gt;

&lt;p&gt;本文涉及的完整代码保存在 &lt;a href=&#34;https://github.com/nfrankel/jvm-controller&#34; target=&#34;_blank&#34;&gt;Github&lt;/a&gt;。&lt;/p&gt;

&lt;h2 id=&#34;相关链接&#34;&gt;相关链接&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;https://github.com/nfrankel/jvm-controller&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://github.com/fabric8io/kubernetes-client&lt;/code&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>自己的 Kubernetes 控制器（1）——工作准备</title>
      <link>/post/your-own-k8s-controller-1/</link>
      <pubDate>Mon, 13 Apr 2020 11:33:12 +0800</pubDate>
      <guid>/post/your-own-k8s-controller-1/</guid>
      <description>

&lt;p&gt;原文：&lt;a href=&#34;https://blog.frankel.ch/your-own-kubernetes-controller/1/&#34; target=&#34;_blank&#34;&gt;Your own Kubernetes controller - Laying out the work&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;作者：&lt;a href=&#34;https://twitter.com/nicolas_frankel&#34; target=&#34;_blank&#34;&gt;Nicolas Fränkel&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;时至今日，&lt;a href=&#34;https://kubernetes.io/&#34; target=&#34;_blank&#34;&gt;Kubernetes&lt;/a&gt; 已经成为容器化应用部署的首选平台，是个难以忽视的存在。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Kubernetes是一个开源系统，用于自动化部署、扩展和管理容器化应用程序。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;短短几年里，Kubernetes 在 CNCF 的大旗下高歌猛进，在 DevOps 领域已经深入人心。这其中的原因众说纷纭，其中一个非常有说服力的理由是，用户能够避免被锁定在单一云提供商的 API 上。如果你对 2000 年左右微软的桌面垄断有所了解，你可能会明白我的意思。&lt;/p&gt;

&lt;p&gt;Kubernetes 的扩展相对来说比较容易，这是它获得广泛认同的一个重要原因。很多软件供应商在 Docker 镜像之外，还会提供一或多个 Operator。&lt;/p&gt;

&lt;p&gt;我假设读者仅对 Kubernetes 有所了解，对控制器一无所知，在这个假设的基础上，我将用三篇连载来讲述如何使用 Go 以外的语言实现自己的控制器。&lt;/p&gt;

&lt;h2 id=&#34;控制器是什么&#34;&gt;控制器是什么&lt;/h2&gt;

&lt;p&gt;配置管理工具可以分为两种：&lt;/p&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;分类&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;th&gt;工具&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;

&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;指令式&lt;/td&gt;
&lt;td&gt;指定做事方法，例如启动两个节点&lt;/td&gt;
&lt;td&gt;Ansible、SaltStack 等&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td&gt;声明式&lt;/td&gt;
&lt;td&gt;指定目标状态，例如总计五个节点&lt;/td&gt;
&lt;td&gt;Puppet、Chef 等&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;声明式的工具通常会周期性的执行以下任务：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;查询当前状态&lt;/li&gt;
&lt;li&gt;评估要从当前状态达到目标状态所需完成的步骤&lt;/li&gt;
&lt;li&gt;执行这些步骤&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这个算法描述的是一个控制回路。&lt;/p&gt;

&lt;p&gt;Kubernetes 里，已经有了这些控制回路的实现。例如 &lt;code&gt;ReplicaSet&lt;/code&gt; 和 &lt;code&gt;Deployment&lt;/code&gt;。这两个对象都可以针对特定镜像设置目标 Pod 数量。Kubernetes 会持续生成副本，直到达到预设的实例数量。如果副本数量发生变化，那么就会新建或删除副本，以达到目标副本数量。&lt;/p&gt;

&lt;p&gt;现在你可能已经猜到了，控制器就是一个控制循环的实现：检查当前状态，用现有状态计算差异，弥补差异。除了 &lt;code&gt;Deployment&lt;/code&gt; 和 &lt;code&gt;ReplicaSet&lt;/code&gt; 的控制器之外，Kubernetes 还提供了很多开箱即用的控制器。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Service&lt;/li&gt;
&lt;li&gt;DeamonSet&lt;/li&gt;
&lt;li&gt;PersistentVolume&lt;/li&gt;
&lt;li&gt;Job&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;其实大多数的 Kubernetes 资源都是由控制器管理的。&lt;/p&gt;

&lt;h2 id=&#34;初识-operator&#34;&gt;初识 Operator&lt;/h2&gt;

&lt;p&gt;对控制器感兴趣的读者，可能已经在搜索过程中偶然发现了 Operator 这个名词。如果你的时间非常有限，我建议你跳过这一部分，将这两个术语视为近义词即可。&lt;/p&gt;

&lt;p&gt;前面说到 Kubernetes 的扩展性。其中一个扩展方法就是创建控制器，这也是本文的的重点内容。另一个方式就是对 Kubernetes 模型本身进行扩展：在开箱即用的 Pod、Job 等内置资源以外，还可以使用 CRD 来提供额外的资源类型。&lt;/p&gt;

&lt;p&gt;例如下面的代码定义了一个叫做 &lt;code&gt;Hazelcast&lt;/code&gt; 的资源：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;hazelcast-crd.yml&lt;/p&gt;
&lt;/blockquote&gt;

&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: hazelcasts.hazelcast.com
spec:
  group: hazelcast.com
  names:
    kind: Hazelcast
    listKind: HazelcastList
    plural: hazelcasts
    singular: hazelcast
  scope: Namespaced
  subresources:
    status: {}
versions:
    - name: v1alpha1
      served: true
      storage: true
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;把文件提交给 API Server，让 Kubernetes 注册这个新的 &lt;code&gt;Hazelcast&lt;/code&gt; CRD。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;kubectl apply -f hazelcast-crd.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个动作完成之后，就可以像其他内置资源一样进行常用操作了：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;kubectl get hazelcasts
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;Operator&lt;/code&gt; 就是一个用于某种 CRD 的控制器。如果知道怎么实现控制器，也就能够创建 Operator 了。&lt;/p&gt;

&lt;h2 id=&#34;控制器的需求&#34;&gt;控制器的需求&lt;/h2&gt;

&lt;p&gt;现在我们看看 Kubernetes 控制器的需求。&lt;/p&gt;

&lt;h3 id=&#34;控制器的部署位置&#34;&gt;控制器的部署位置&lt;/h3&gt;

&lt;p&gt;下图是一个简化的 Kubernetes 架构图：&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;images/kubernetes-architecture.png&#34; alt=&#34;k8s arch&#34; /&gt;&lt;/p&gt;

&lt;p&gt;Kubernetes 的内置控制器是其控制平面的组成部分。然而自定义控制器是不会出现在这里（Controller Manager）的。控制器没什么限制，它可以在集群内部以 Pod 的形式运行，也可以作为独立的外部进程。&lt;/p&gt;

&lt;p&gt;当然 Pod 形式会享受各种 Kubernetes 上运行容器化应用的福利，例如自愈等。&lt;/p&gt;

&lt;h3 id=&#34;和-kubernetes-的通信&#34;&gt;和 Kubernetes 的通信&lt;/h3&gt;

&lt;p&gt;在 Kubernetes 中，API Server 是一个通信组件。客户端发送 HTTP 请求，API Server 处理请求后发回响应。给 &lt;code&gt;kubectl&lt;/code&gt; 加上参数就能观察到这一过程：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;$ kubectl get pods --v=8
I0209 12:36:31.330067   13717 round_trippers.go:420] GET https://192.168.99.103:8443/api/v1/namespaces/default/pods?limit=500
I0209 12:36:31.330078   13717 round_trippers.go:427] Request Headers:
I0209 12:36:31.330081   13717 round_trippers.go:431]     Accept: application/json;as=Table;v=v1beta1;g=meta.k8s.io, application/json
I0209 12:36:31.330085   13717 round_trippers.go:431]     User-Agent: kubectl/v1.17.2 (darwin/amd64) kubernetes/59603c6
I0209 12:36:31.339770   13717 round_trippers.go:446] Response Status: 200 OK in 9 milliseconds
I0209 12:36:31.339780   13717 round_trippers.go:449] Response Headers:
I0209 12:36:31.339798   13717 round_trippers.go:452]     Content-Length: 2933
I0209 12:36:31.339804   13717 round_trippers.go:452]     Date: Sun, 09 Feb 2020 11:36:31 GMT
I0209 12:36:31.339822   13717 round_trippers.go:452]     Content-Type: application/json
I0209 12:36:31.340084   13717 request.go:1017] Response Body:
{ &amp;quot;kind&amp;quot;:&amp;quot;Table&amp;quot;,
  &amp;quot;apiVersion&amp;quot;:&amp;quot;meta.k8s.io/v1beta1&amp;quot;,
  &amp;quot;metadata&amp;quot;:{
    &amp;quot;selfLink&amp;quot;:&amp;quot;/api/v1/namespaces/default/pods&amp;quot;,
    &amp;quot;resourceVersion&amp;quot;:&amp;quot;2387836&amp;quot; },
  &amp;quot;columnDefinitions&amp;quot;:[
    { &amp;quot;name&amp;quot;:&amp;quot;Name&amp;quot;,
      &amp;quot;type&amp;quot;:&amp;quot;string&amp;quot;,
      &amp;quot;format&amp;quot;:&amp;quot;name&amp;quot;,
      &amp;quot;description&amp;quot;:&amp;quot;Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names&amp;quot;,
      &amp;quot;priority&amp;quot;:0 },
    { &amp;quot;name&amp;quot;:&amp;quot;Ready&amp;quot;,
      &amp;quot;type&amp;quot;:&amp;quot;string&amp;quot;,
      &amp;quot;format&amp;quot;:&amp;quot;&amp;quot;,
      &amp;quot;description&amp;quot;:&amp;quot;The aggregate readiness state of this pod for accepting traffic.&amp;quot;,
      &amp;quot;priority&amp;quot;:0 },
    { &amp;quot;name&amp;quot;:&amp;quot;Status&amp;quot;,
      &amp;quot;type&amp;quot;:&amp;quot;string&amp;quot;,
      &amp;quot;format&amp;quot;:&amp;quot;&amp;quot;,
      &amp;quot;description&amp;quot;:&amp;quot;The aggregate status of the containers in this pod.&amp;quot;,
      &amp;quot;priority&amp;quot;:0 },
    { &amp;quot;name&amp;quot;:&amp;quot;Restarts&amp;quot;,
      &amp;quot;type&amp;quot;:&amp;quot;integer&amp;quot;,
      &amp;quot;format&amp;quot;:&amp;quot;&amp;quot;,
      &amp;quot;description&amp;quot;:&amp;quot;The number of times the containers in this pod have been restarted.&amp;quot;,
      &amp;quot;priority&amp;quot;:0 },
    { &amp;quot;name&amp;quot;:&amp;quot;Age&amp;quot;,
      &amp;quot;type&amp;quot;:&amp;quot;stri
[truncated 1909 chars]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这个通信过程的需求很简单：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;能够处理 HTTP 的请求和响应&lt;/li&gt;
&lt;li&gt;JSON 解析（或者说序列化和反序列化）&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;是的，有 JSON 和 HTTP 的处理能力就够了，所以要编写一个控制器，并不一定必须使用特定语言（例如 Go），理论上用单纯的 Shell 也是可以实现的。&lt;/p&gt;

&lt;h2 id=&#34;go-的定位&#34;&gt;Go 的定位&lt;/h2&gt;

&lt;p&gt;在进入实现细节之前，首先要看看 Kubernetes 的生态。&lt;/p&gt;

&lt;p&gt;历史上好像 Kubernetes 的祖先是用 Java 开发的，后来被移植到了 Go 上。这可能是部分代码不符合 Go 语言风格的原因。尽管 Go 具有垃圾收集功能，但它还是被称为一种低级语言，很适合运行接近于裸机的软件。这种说法是否成立，远远超出了本文的范围，也超出了我的能力。&lt;/p&gt;

&lt;p&gt;然而 Kubernetes 生态中大量软件是使用 Go 语言编写的，我想是有其原因的。&lt;/p&gt;

&lt;p&gt;如果你已经对 Go 相当了解，那么继续使用是个很好的选择——改弦易辙需要勇气。这并不只是一个语言的问题，除了语法之外，还有很多其他内容：&lt;/p&gt;

&lt;h3 id=&#34;要多久才能用新语言写出地道的代码&#34;&gt;要多久才能用新语言写出地道的代码&lt;/h3&gt;

&lt;p&gt;我记得我在学习 Java 的时候，读过 C 语言开发者写的代码。虽然语法是 Java，但是却写出了 C 语言的风格，例如在方法结束之前释放本地变量的引用。&lt;/p&gt;

&lt;h3 id=&#34;多久才能搞清楚在什么条件下使用什么库&#34;&gt;多久才能搞清楚在什么条件下使用什么库&lt;/h3&gt;

&lt;p&gt;我不了解 Go，但是我知道 Java。Java 生态的丰富是人所皆知的。例如测试的场景，就有 JUnit 4、JUnit 5 以及 TestNG 可以选择，另外需要加入断言库么？这还只是测试呢。&lt;/p&gt;

&lt;h3 id=&#34;选择正确的工具链要多久&#34;&gt;选择正确的工具链要多久&lt;/h3&gt;

&lt;p&gt;如果已经在使用 JetBrains 的产品，那么从 JetBrains IDE 之间跳转是比较容易的，例如 IDEA 和 GoLand。但是 IDE 市场非常混乱，例如微软正在推广的包含丰富插件的 VS Code。而 Java 世界中，Eclipse 仍然占据客观的市场份额。各种产品都有自己的优劣，自己的拥趸。工具的选择可能在组织内部引发圣战。&lt;/p&gt;

&lt;h3 id=&#34;新工具形成生产力要多久&#34;&gt;新工具形成生产力要多久&lt;/h3&gt;

&lt;p&gt;各种 IDE 都有各自的玩法。例如我从 Eclipse 切换到 IntelliJ 的过程中，几个星期后才停掉了频繁保存文件的习惯。除了 IDE 之外，还有除错工具等。新的语言能怎么除错？有什么先决条件么？&lt;/p&gt;

&lt;p&gt;另外前面说的几个点只是开发，如果考虑到相关的构建、集成和投产环境，其投入可能又会有数倍的增长。&lt;/p&gt;

&lt;p&gt;我希望上面几点能够让读者意识到，语言的切换事关重大。在很多情况下，沿用原有的语言可能是个更好的选择。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

&lt;p&gt;本文的第一部分，大概了解了一下 Kubernetes 控制器的基础内容。我们详细介绍了什么是控制器，以及开发控制器的需要：即能够与 HTTP/JSON 通信。在下一篇帖子中，我们将详细介绍并实际开发自己的自定义控制器。&lt;/p&gt;
</description>
    </item>
    
  </channel>
</rss>
