写点什么

Kubernetes 官方 java 客户端之七:patch 操作,深入浅出 Java

作者:Java高工P7
  • 2021 年 11 月 10 日
  • 本文字数:7641 字

    阅读完需:约 25 分钟

本文内容

这是篇万字长文,所以一开始就要明确本文的核心内容:开发一个 SpringBoot 应用并部署在 kubernetes 环境,这个应用通过 kubernetes 的 java 客户端向 API Server 发请求,请求内容包括:创建名为 test123 的 deployment、对这个 deployment 进行 patch 操作,如下图:



接下来先了解一些 kubernetes 的 patch 相关的基本知识;

关于 patch

  1. 是对各种资源的增删改查是 kubernetes 的基本操作;

  2. 对于修改操作,分为 Replace 和 Patch 两种;

  3. Replace 好理解,就是用指定资源替换现有资源,replace 有个特点,就是 optimistic lock 约束(类似与转账操作,先读再计算再写入);

  4. Patch 用来对资源做局部更新,没有 optimistic lock 约束,总是最后的请求会生效,因此如果您只打算修改局部信息,例如某个属性,只要指定属性去做 patch 即可(如果用 Replace,就只能先取得整个资源,在本地修改指定属性,再用 Replace 整体替换);

  5. 更详细的信息请参考下图,来自官方文档,地址:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/


patch 的四种类型

kubernetes 的 patch 一共有四种:


  1. json patch:在请求中指定操作类型,例如:add、replace,再指定 json 内容进行操作 z,请参考:https://tools.ietf.org/html/rfc6902

  2. merge patch:合并操作,可以提交整个资源的信息,与现有信息进行合并后生效,也可以提交部分信息用于替换,请参考:https://tools.ietf.org/html/rfc7386

  3. strategic merge patch:json patch 和 merge patch 都遵守 rfc 标准,但是 strategic merge patch 却是 kubernetes 独有的,官方中文文档中称为策略性合并,也是 merge 的一种,但是真正执行时 kubernetes 会做合并还是替换是和具体的资源定义相关的(具体策略由 Kubernetes 源代码中字段标记中的 patchStrategy 键的值指定),以 Pod 的 Container 为例,下面是其源码,红框中显示其 Container 节点的 patchStrategy 属性是 merge,也就是说如果您提交了一份 strategic merge patch,里面的内容是关于 Pod 的 Container 的,那么原有的 Container 不会被替换,而是合并(例如以前只有 nginx,提交的 strategic merge patch 是 redis,那么最终 pod 下会有两个 container:nginx 和 redis):



  1. 通过源码查看资源的 patchStrategy 属性是很麻烦的事情,因此也可以通过 Kubernetes API 文档来查看,如下图,地址是:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#podspec-v1-core



  1. 第四种是 apply patch:主要是指 kubernetes 1.14 版本开始的 server-side apply,由 APIServer 做 diff 和 merge 操作,很多原本易碎的现象都得到了解决(例如 controller 和 kubectl 都在更新),另外要格外注意的是:1.14 版本默认是不开启 server-side apply 特性的,具体的开启操作在下面会详细讲解;


  • 以上是对 kubernetes 四种 patch 的简介,讲得很浅,如果您想深入了解每种 patch,建议参阅官方资料,接下来咱们聚焦 java 客户端对这些 patch 能力的实现;

源码下载

  1. 如果您不想编码,可以在 GitHub 下载所有源码,地址和链接信息如下表所示(https://github.com/zq2599/blog_demos):


| 名称 | 链接 | 备注 |


| :-- | :-- | :-- |


| 项目主页 | https://github.com/zq2599/blog_demos | 该项目在 GitHub 上的主页 |


| git 仓库地址(https) | https://github.com/zq2599/blog_demos.git | 该项目源码的仓库地址,https 协议 |


| git 仓库地址(ssh) | git@github.com:zq2599/blog_demos.git | 该项目源码的仓库地址,ssh 协议 |


  1. 这个 git 项目中有多个文件夹,本章的应用在 kubernetesclient 文件夹下,如下图红框所示:


实战步骤概述

  • 接下来会创建一个 springboot 工程(该工程是 kubernetesclient 的子工程),针对四种 patch 咱们都有对应的操作;

  • 每种 patch 都会准备对应的 json 文件,提前将这些文件的内容保存在字符串变量中,在程序里用 kubernetes 客户端的 patch 专用 API,将此 json 字符串发送出去,流程简图如下:



  • 编码完成后,就来动手验证功能,具体操作如下:


  1. 部署名为 patch 的 deployment,这里面是咱们编码的 SpringBoot 工程,提供多个 web 接口;

  2. 浏览器访问/patch/deploy 接口,就会创建名为 test123 的 deployment,这个 deployment 里面是个 nginx,接下来的 patch 操作都是针对这个名为 test123 的 deployment;

  3. 浏览器访问 test123 的 nginx 服务,确保部署成功了;

  4. 浏览器访问/patch/json 接口,该接口会修改 test123 的一个属性:terminationGracePeriodSeconds

  5. 浏览器访问/patch/fullmerge 接口,该接口会提交全量 merge 请求,修改内容很少,仅增加了一个 label 属性;

  6. 接下来是对比 merge patch 和 strategic merge patch 区别,分别访问/patch/partmerge 和/patch/strategic 这两个接口,其实它们操作的是同一段 patch 内容(一个新的 container),结果 merge patch 会替换原有的 continer,而 strategic merge patch 不会动原有的 container,而是新增 container,导致 test123 这个 deployment 下面的 pod 从一个变为两个;

  7. 最后是 apply yaml patch,访问接口/patch/apply,会将 nginx 容器的标签从 1.18.0 改为 1.19.1,咱们只要在浏览器访问 test123 里面的 nginx 服务就能确定是否修改生效了;

准备工作

准备工作包括创建工程、编写辅助功能代码、初始化代码等:


  1. 打开《Kubernetes官方java客户端之一:准备 》一文创建的项目 kubernetesclient,新增名为 patch 的子工程,pom.xml 内容如下:


<?xml version="1.0" encoding="UTF-8"?>


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"


xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">


<modelVersion>4.0.0</modelVersion>


<parent>


<groupId>com.bolingcavalry</groupId>


<artifactId>kubernetesclient</artifactId>


<version>1.0-SNAPSHOT</version>


<relativePath>../pom.xml</relativePath>


</parent>


<groupId>com.bolingcavalry</groupId>


<artifactId>patch</artifactId>


<version>0.0.1-SNAPSHOT</version>


<name>patch</name>


<description>patch demo</description>


<packaging>jar</packaging>


<dependencies>


<dependency>


<groupId>org.springframework.boot</groupId>


<artifactId>spring-boot-starter-web</artifactId>


<exclusions>


<exclusion>


<groupId>org.springframework.boot</groupId>


<artifactId>spring-boot-starter-json</artifactId>


</exclusion>


</exclusions>


</dependency>


<dependency>


<groupId>org.springframework.boot</groupId>


<artifactId>spring-boot-starter-actuator</artifactId>


</dependency>


<dependency>


<groupId>org.projectlombok</groupId>


<artifactId>lombok</artifactId>


<optional>true</optional>


</dependency>


<dependency>


<groupId>io.kubernetes</groupId>


<artifactId>client-java</artifactId>


</dependency>


</dependencies>


<build>


<plugins>


<plugin>


<groupId>org.springframework.boot</groupId>


<artifactId>spring-boot-maven-plugin</artifactId>


<version>2.3.0.RELEASE</version>


<configuration>


<layers>


<enabled>true</enabled>


</layers>


</configuration>


</plugin>


</plugins>


</build>


</project>


  1. 编写一个辅助类 ClassPathResourceReader.java,作用是读取 json 文件的内容作为字符串返回:


package com.bolingcavalry.patch;


import java.io.BufferedReader;


import java.io.IOException;


import java.io.InputStreamReader;


import java.util.stream.Collectors;


import org.springframework.core.io.ClassPathResource;


public class ClassPathResourceReader {


private final String path;


private String content;


public ClassPathResourceReader(String path) {


this.path = path;


}


public String getContent() {


if (content == null) {


try {


ClassPathResource resource = new ClassPathResource(path);


BufferedReader reader = new BufferedReader(new InputStreamReader(resource.getInputStream()));


content = reader.lines().collect(Collectors.joining("\n"));


reader.close();


} catch (IOException ex) {


throw new RuntimeException(ex);


}


}


return content;


}


}


  1. 接下来新建本篇文章的核心类 PatchExample.java,首先这个类中有 main 方法,整个应用从这里启动:


public static void main(String[] args) {


SpringApplication.run(PatchExample.class, args);


}


  1. 接下来有两个常量定义,分别是 kubernetes 环境里用来测试的 deployment 名称,以及 namespace 名称:


static String DEPLOYMENT_NAME = "test123";


static String NAMESPACE = "default";


  1. 然后定义几个字符串变量,执行 patch 操作时用到的 json 内容都保存到这些字符串变量中:


static String deployStr, jsonStr, mergeStr, strategicStr, applyYamlStr;


  1. 在 resources 文件夹中放入 json 文件,稍后的初始化代码会将这些文件读取到字符串变量中,如下图,这些 json 文件的内容稍后会详细说明:



  1. 编写初始化代码(通过 PostConstruct 注解实现),主要是客户端配置,还有将 json 文件的内容读出来,保存到刚刚准备的字符串变量中:


@PostConstruct


private void init() throws IOException {


// 设置 api 配置


ApiClient client = Config.defaultClient();


Configuration.setDefaultApiClient(client);


// 设置超时时间


Configuration.getDefaultApiClient().setConnectTimeout(30000);


// 部署用的 JSON 字符串


deployStr = new ClassPathResourceReader("deploy.json").getContent();


// json patch 用的 JSON 字符串


jsonStr = new ClassPathResourceReader("json.json").getContent();


// merge patch 用的 JSON 字符串,和部署的 JSON 相比:replicas 从 1 变成 2,增加一个名为 from 的 label,值为 merge


mergeStr = new ClassPathResourceReader("merge.json").getContent();


// strategic merge patch 用的 JSON 字符串


strategicStr = new ClassPathResourceReader("strategic.json").getContent();


// server side apply 用的 JSON 字符串


applyYamlStr = new ClassPathResourceReader("applyYaml.json").getContent();


}


  • 以上就是准备工作;

创建服务

  1. 首先要开发一个部署的接口,通过调用此接口可以在 kubernetes 环境部署一个 deployment:

  2. 部署服务的 path 是/patch/deploy,代码如下,可见部署 deployment 的代码分为三步:创建 api 实例、用字符串创建 body 实例、把 body 传给 api 即可:


/**


  • 通用 patch 方法

  • @param patchFormat patch 类型,一共有四种

  • @param deploymentName deployment 的名称

  • @param namespace namespace 名称

  • @param jsonStr patch 的 json 内容

  • @param fieldManager server side apply 用到

  • @param force server side apply 要设置为 true

  • @return patch 结果对象转成的字符串

  • @throws Exception


*/


private String patch(String patchFormat, String deploymentName, String namespace, String jsonStr, String fieldManager, Boolean force) throws Exception {


// 创建 api 对象,指定格式是 patchFormat


ApiClient patchClient = ClientBuilder


.standard()


.setOverridePatchFormat(patchFormat)


.build();


log.info("start deploy : " + patchFormat);


// 开启 debug 便于调试,生产环境慎用!!!


patchClient.setDebugging(true);


// 创建 deployment


ExtensionsV1beta1Deployment deployment = new ExtensionsV1beta1Api(patchClient)


.patchNamespacedDeployment(


deploymentName,


namespace,


new V1Patch(jsonStr),


null,


null,


fieldManager,


force


);


log.info("end deploy : " + patchFormat);


return new GsonBuilder().setPrettyPrinting().create().toJson(deployment);


}


  1. body 实例用到的 json 字符串来自 deploy.json 文件,内容如下,很简单,只有 nginx 的 1.18.0 版本的 pod:


{


"kind":"Deployment",


"apiVersion":"extensions/v1beta1",


"metadata":{


"name":"test123",


"labels":{


"run":"test123"


}


},


"spec":{


"replicas":1,


"selector":{


"matchLabels":{


"run":"test123"


}


},


"template":{


"metadata":{


"creationTimestamp":null,


"labels":{


"run":"test123"


}


},


"spec":{


"terminationGracePeriodSeconds":30,


"containers":[


{


"name":"test123",


"image":"nginx:1.18.0",


"ports":[


{


"containerPort":80


}


],


"resources":{


}


}


]


}


},


"strategy":{


}


},


"status":{


}


}


  1. 如此一来,web 浏览器只要访问/patch/deploy 就能创建 deployment 了;

发起 patch 请求的通用方法

  • 通过 kubernetes 的客户端发起不同的 patch 请求,其大致步骤都是相同的,只是参数有所不同,我这里做了个私有方法,发起几种 patch 请求的操作都调用此方法实现(只是入参不同而已),可见都是先建好 ApiClient 实例,将 patch 类型传入,再创建 V1Patch 实例,将 patch 字符串传入,最后执行 ExtensionsV1beta1Api 实例的 patchNamespacedDeployment 方法即可发送 patch 请求:


/**


  • 通用 patch 方法

  • @param patchFormat patch 类型,一共有四种

  • @param deploymentName deployment 的名称

  • @param namespace namespace 名称

  • @param jsonStr patch 的 json 内容

  • @param fieldManager server side apply 用到

  • @param force server side apply 要设置为 true

  • @return patch 结果对象转成的字符串

  • @throws Exception


*/


private String patch(String patchFormat, String deploymentName, String namespace, String jsonStr, String fieldManager, Boolean force) throws Exception {


// 创建 api 对象,指定格式是 patchFormat


ApiClient patchClient = ClientBuilder


.standard()


.setOverridePatchFormat(patchFormat)


.build();


log.info("start deploy : " + patchFormat);


// 开启 debug 便于调试,生产环境慎用!!!


patchClient.setDebugging(true);


// 创建 deployment


ExtensionsV1beta1Deployment deployment = new ExtensionsV1beta1Api(patchClient)


.patchNamespacedDeployment(


deploymentName,


namespace,


new V1Patch(jsonStr),


null,


null,


fieldManager,


force


);


log.info("end deploy : " + patchFormat);


return new GsonBuilder().setPrettyPrinting().create().toJson(deployment);


}


  • 上述代码中,有一行代码要格外重视,就是 patchClient.setDebugging(true)这段,执行了这一行,在 log 日志中就会将 http 的请求和响应全部打印出来,是我们调试的利器,但是日志内容过多,生产环境请慎用;

  • 上述 patch 方法有六个入参,其实除了 patch 类型和 patch 内容,其他参数都可以固定下来,于是再做个简化版的 patch 方法:


/**


  • 通用 patch 方法,fieldManager 和 force 都默认为空

  • @param patchFormat patch 类型,一共有四种

  • @param jsonStr patch 的 json 内容

  • @return patch 结果对象转成的字符串

  • @throws Exception


*/


private String patch(String patchFormat, String jsonStr) throws Exception {


return patch(patchFormat, DEPLOYMENT_NAME, NAMESPACE, jsonStr, null, null);


}


  • 入参 patchFormat 的值是四种 patch 类型的定义,在 V1Patch.java 中,其值如下所示:



  • 接下来可以轻松的开发各种类型 patch 的代码了;

执行 json patch

  1. 首先来看 json patch 要提交的内容,即 json.json 文件的内容,这些内容在应用启动时被保存到变量 jsonStr,如下所示,非常简单,修改了 terminationGracePeriodSeconds 属性的值,原来是 30,这个属性在停止 pod 的时候用到,是等待 pod 的主进程的最长时间:


[


{


"op":"replace",


"path":"/spec/template/spec/terminationGracePeriodSeconds",


"value":27


}


]


  1. 接下来就是 web 接口的代码,可见非常简单,仅需调用前面准备好的 patch 方法:


/**


  • JSON patch 格式的关系

  • @return

  • @throws Exception


*/


@RequestMapping(value = "/patch/json", method = RequestMethod.GET)


public String json() throws Exception {


return patch(V1Patch.PATCH_FORMAT_JSON_PATCH, jsonStr);


}

merge patch(全量)

  1. 先尝试全量的 merge patch,也就是准备好完整的 deployment 内容,修改其中一部分后进行提交,下图是 json 文件 merge.json 的内容,其内容前面的 deploy.json 相比,仅增加了红框处的内容,即增加了一个 label:



  1. 代码依然很简单:


@RequestMapping(value = "/patch/fullmerge", method = RequestMethod.GET)


public String fullmerge() throws Exception {


return patch(V1Patch.PATCH_FORMAT_JSON_MERGE_PATCH, mergeStr);


}

merge patch(增量)

  1. 前面曾提到 merge patch 和 strategic merge patch 的区别:merge patch 提交一个 container 时做的是替换,而 strategic merge patch 提交一个 container 时做的是合并,为了展示这两种 patch 的不同,这里我们就用同一个 json 内容,分别执行 merge patch 和 strategic merge patch,看看结果有什么区别,这是最直观的学习方法;

  2. 这个 json 对应的文件是 strategic.json,内容如下:


{


"spec":{


"template":{


"spec":{


"containers":[


{


"name":"test456",


"image":"tomcat:7.0.105-jdk8"


}


]


}


}


}


}


  1. 增量 merge 的代码如下:


@RequestMapping(value = "/patch/partmerge", method = RequestMethod.GET)


public String partmerge() throws Exception {


return patch(V1Patch.PATCH_FORMAT_JSON_MERGE_PATCH, strategicStr);


}

strategic merge patch

  • strategic merge patch 用的 json 内容和前面的增量 merge patch 是同一个,代码如下:


@RequestMapping(value = "/patch/strategic", method = RequestMethod.GET)


public String strategic() throws Exception {


return patch(V1Patch.PATCH_FORMAT_STRATEGIC_MERGE_PATCH, strategicStr);


}

apply yaml patch

  1. apply yaml patch 与其他 patch 略有不同,调用 ExtensionsV1beta1Api 的 patchNamespacedDeployment 方法发请求时,fieldManager 和 force 字段不能像之前那样为空了:


@RequestMapping(value = "/patch/apply", method = RequestMethod.GET)


public String apply() throws Exception {


return patch(V1Patch.PATCH_FORMAT_APPLY_YAML, DEPLOYMENT_NAME, NAMESPACE, applyYamlStr, "example-field-manager", true);


}


  1. 上面的代码中,如果 force 字段不等于 true,可能会导致 patch 失败,在官方文档也有说明,如下图红框:



  1. apply yaml patch 的 json 字符串来自文件 applyYaml.json,其内容是从 deploy.json 直接复制的,然后改了下图两个红框中的内容,红框 1 修改了 nginx 的版本号,用来验证 patch 是否生效(原有版本是 1.18),红框 2 是 kubernetes1.16 之前的一个问题,protocol 字段必填,否则会报错,问题详情请参考:https://github.com/kubernetes-sigs/structured-merge-diff/issues/130



  1. 以上就是所有代码和 patch 的内容了,接下来部署到 kubernetes 环境实战吧

制作镜像并且部署

  1. 在 patch 工程目录下执行以下命令编译构建:


mvn clean package -U -DskipTests


  1. 在 patch 工程目录下创建 Dockerfile 文件,内容如下:

指定基础镜像,这是分阶段构建的前期阶段

FROM openjdk:8u212-jdk-stretch as builder

执行工作目录

WORKDIR application

配置参数

ARG JAR_FILE=target/*.jar

将编译构建得到的 jar 文件复制到镜像空间中

COPY ${JAR_FILE} application.jar

通过工具 spring-boot-jarmode-layertools 从 application.jar 中提取拆分后的构建结果

RUN java -Djarmode=layertools -jar application.jar extract

用户头像

Java高工P7

关注

还未添加个人签名 2021.11.08 加入

还未添加个人简介

评论

发布
暂无评论
Kubernetes官方java客户端之七:patch操作,深入浅出Java