Kubernetes 状态管理与扩展

阅读数:6292 2018 年 5 月 7 日 18:29

本文通过一个具体实例介绍 Kubernetes 扩展开发,分析了 API Server 的兼容性设计;基于部分源码介绍了 Kubernetes API 聚合层原理和实现;最后还分析了 Kubernetes 提供的工具链和客户端抽象,希望为 Kubernetes 扩展开发提供一些启发 。

状态管理并非新鲜话题,它为中心化系统分发一致的状态,确保分布式系统总是朝预期的状态收敛,是中心化系统的基石之一 。

以 Pod(如下图)为例,它抽象了可以独立部署的最小容器单位,Pod 描述的变化需要反映到对应的容器上,比如修改了 Pod 的 image,它对应的容器就会使用指定的 image 来重建 。

Kubernetes 状态管理与扩展

Kubernetes 从 v1.0 开始逐渐形成了完善的 API 框架和工具链,并以此为基础实现了容器管理平台。时至今日(2018 上半年),这套 API 框架和工具链已经演变为 Go 生态圈中的通用状态管理解决方案,大大降低了建立分布式系统的难度。

可惜的是,社区尚未出现对 Kubernetes 状态管理和扩展的详细介绍,官方文档中的只言片语难以支撑复杂的扩展开发需求,本文通过一个具体实例介绍 Kubernetes 扩展开发,分析了 API Server 的兼容性设计;基于部分源码介绍了 Kubernetes API 聚合层原理和实现;最后还分析了 Kubernetes 提供的工具链和客户端抽象,希望为 Kubernetes 扩展开发提供一些启发 。

扩展开发实例

本节介绍一个简单的扩展开发项目来介绍 Kubernetes 扩展的开发模式(代码生成基于 https://github.com/kubernetes-incubator/apiserver-builder ),这是典型的 C/S 架构,分为 APIServer 和客户端两部分,APIServer 提供了高可用的状态存储,而客户端提供了资源的 CRUD 和可靠的状态分发接口。

先来初始化整个项目框架(这个工具的实现目前还不稳定,不同版本的 apiserver-boot 生成的代码可能存在差异)。

Kubernetes 状态管理与扩展

后续章节将介绍详细介绍扩展开发的过程。

API Server

项目初始化完成之后就可以定义状态了,为了让例子更具一般性,本文将定义多个版本的状态并在后续章节介绍 APIServer 的兼容性设计。

执行如下命令生成第一个版本的状态定义模板:v1alpha1.Bee。 

复制代码
$ apiserver-boot create resource --group alpha --version v1alpha1 --kind Bee

然后为生成的 v1alpha1.Bee 添加自定义的信息(如图 1.1.1)。

Kubernetes 状态管理与扩展

运行以下命令将修改后的状态定义变化应用到整个项目中去。 

复制代码
$ apiserver-boot build generated

生成代码时会把所有的子命令打印出来(如图 1.1.2),随后可以利用局部代码生成来对生成代码进行微调。

Kubernetes 状态管理与扩展

然后将这个自定义的 API Server 部署到一个 Kubernetes 集群中,注意这里部署的 API Server 是一个实现了 Kubernetes API 规范的独立的 Web 服务,与集群 Master 是完全独立的,仅仅是集群中的一个普通的 Pod 而已,可以有完全独立于 Master API Server 的 ETCD 存储。

Kubernetes 状态管理与扩展

由于此处的 APIServer 实现了 Kubernetes API 规范,可以通过 kubectl 来直接写入一个 v1alpha1.Bee,然后再用 kubectl 读取刚刚写入的 v1alpha1.Bee(如图 1.1.3),也可以用 kubectl 来修改图 1.1.3 中写入的 Bee 的 location 信息(如图 1.1.4)。

Kubernetes 状态管理与扩展

Kubernetes 状态管理与扩展

有了基本的存储之后就需要考虑状态修改时的校验,可以为状态描述实现任意形式的扩展逻辑,图 1.1.5 中为 v1alpha1.Bee 添加自定义校验,限定 location 必须以 China 结尾。

Kubernetes 状态管理与扩展

添加了图 1.1.5 中的校验之后,图 1.1.3 中给出的 Bee 描述已经不再合法,这时如果尝试再次写入该数据就会出现校验失败(如图 1.1.6)。

Kubernetes 状态管理与扩展

图 1.1.7 将 location 更正为 China 结尾后就可以正常写入 v1alpha1.Bee 了。

Kubernetes 状态管理与扩展

以上是定义一个 v1alpha1.Bee 状态描述的基本过程。

细心的读者可能已经发现,在写入一个 Bee 之后,如果修改了 Bee 的校验规则,那么前面已经写入的 Bee 可能已经不合法了,就会出现脏数据,这个问题怎么解决呢?工程上我们应该规避这种情况,RESTful API 中一个基本设计原则就是“不可变资源”,这里所谓的“不可变”指的是资源涉及的各种上下文,包括 Schema、校验、安全、SLA 等等,如果出现上下文改变,应该为资源提供新的版本。

为了介绍状态定义的兼容性设计,图 1.1.8 为 Bee 这个状态增加一个新版本 v1alpha2.Bee,Spec 还需要增加一个额外的字段 Gender(作者按:虽然蜜蜂分男女有点扯,这里仅仅是为了表达在一个新版本的 Bee 中添加字段,后文不再解释)。

Kubernetes 状态管理与扩展

图 1.1.9 中使用 kubectl 同时写入 v1alpha1.Bee 和 v1alpha2.Bee。

Kubernetes 状态管理与扩展

客户端

扩展客户端连接到前文实现的 API Server 来监听 Bee 的变化,进而在客户端实现自定义扩展逻辑,图 1.2.1 中为 Bee 实现的客户端扩展的基本工作流。

Kubernetes 状态管理与扩展

前文自动生成的代码中为 Bee 生成了默认的 BeeController,如图 1.2.2 所示。

Kubernetes 状态管理与扩展

当监听到创建或更新 Bee 状态的时候,可以通过实现 Reconcile 中的逻辑来处理对 Bee 的额外扩展,现在来把 Controller 部署到集群中去。

将前面写入的 Bee 全部删除后重新写入,可以看到 Controller 打印了两条日志分别为写入的 Bee.Name,如图 1.2.3 所示。

Kubernetes 状态管理与扩展

Kubernetes 状态管理与扩展

此处提到的 Reconcile 接口只能响应存在状态修改的场景,是一种无状态的扩展模式,如果需要响应删除,可以利用后文介绍的 Informer 接口来实现,此处不赘述,只给一个简单例子(如图 1.2.4)。有趣的是,BeeController 部署到集群里面,没有进行额外配置就可以连接到对应的 APIServer 正常运转了,它是如何自动与前文部署的 API Server 建立连接呢?这在后文“API 聚合层”中会具体介绍,此处亦不赘述。

Kubernetes 状态管理与扩展

Kubernetes 状态管理与扩展

Kubernetes 状态管理与扩展

注意删除了前面写入的 v1alpha1.Bee 和 v1alpha2.Bee 两个版本的 Bee,但实际上 BeeController 是利用 v1alpha1 客户端监听 Bee 的删除事件,显然 v1alpha1 的 Informer 也可以感知到所有版本 Bee 的变化,这其实就是兼容性设计的美妙之处了,不赘述。

小结

复杂系统中,客户端的维护周期是随机的,设想一个没有兼容性的系统,在运营一段时间后,客户端扩展由于升级维护周期的差异使用了不同版本的客户端实现,这样的系统任何一个点的变化对系统中其他模块可能都存在强依赖,这种耦合可能导致系统不得不部分重启,最终这个系统将陷入举步维艰的泥潭。可以说兼容性设计是 Kubernetes 高度可扩展性的基础之一,图 1.3.1 为多版本 Bee 的兼容性设计模型。

Kubernetes 状态管理与扩展

图 1.3.1 中写入和存储 Bee 的时候支持全部版本,而 API Server 为每个版本的 Bee 的读取接口实现了兼容性适配。当写入原始状态为 v1alpha1.Bee,所有客户端都可以感知到这个状态,同理写入状态 v1alpha2.Bee 时,所有客户端也都可以感知到这个状态变化。

先往 API Server 写入 v1alpha1.Bee 和 v1alpha2.Bee 两个 Bee 状态描述,如图 1.3.2 所示 。

Kubernetes 状态管理与扩展

进入 etcd 中看实际写入的 v1alpha1.Bee 和 v1alpha2.Bee 数据(如图 1.3.3),可见状态存储和写入时的输入是一致的。

Kubernetes 状态管理与扩展

特别提一下,在 Kubernetes API 设计规范中,{Group, Namespace, metadata.name} 这个三元组是全局唯一的,尽管此处写入的两个 Bee 分别为不同的版本,还是可以从所有版本的读取接口拿到这些状态(如图 1.3.4,1.3.5),这就是为什么图 1.2.4 中只需使用 v1alpha1 客户端就可以接受到删除 v1alpha1.Bee 和 v1alpha2.Bee 的事件。

图 1.3.4 和 1.3.5 直接使用 curl 命令分别从 v1alpha1 和 v1alpha2 的 API 来获取 Bee 列表,这进一步佐证了兼容性设计是在 API Server 实现的。

Kubernetes 状态管理与扩展

Kubernetes 状态管理与扩展

在不同版本的 Bee 之间互相转换如果少了字段,比如图 1.3.5 中通过 v1alpha2 客户端读取 v1alpha1.Bee,gender 字段为空,这可能是有问题的,v1alpha2 客户端拿到这样的数据可能会出现不预期的行为,应该保证客户端取到的数据满足相应版本的约束。

这就需要 APIServer 提供默认值的支持了,下面来为 v1alpha2 添加默认值支持,将 gender 默认设置为 Unknown ,如图 1.3.6 所示。

Kubernetes 状态管理与扩展

Kubernetes 状态管理与扩展

兼容性设计是困扰很多复杂系统设计者的问题,Kubernetes 中的兼容性设计模式不仅是 RESTful 设计模式的成功应用,也为复杂系统设计提供了一种通用解决方案。

API 聚合层

前面的实例中,Controller 部署到集群中就能直接访问 Bee 资源,实际上在部署 API Server 时还为其配置了 API 聚合层,将这个独立的 API Server 整合到集群的 API Server 中去了,就好像是 Kubernetes API 提供了 Bee 这个 Resource 一样。

API 聚合层是 Kubernetes API 扩展性的基础,将自定义资源整合到 Kubernetes API 中,为容器管理平台或相关插件提供状态存储(源码分析基于:f7aafaeb404563cda07b182ad9679f54afd227fe)。

API Service

Kubernetes 在早期版本中就提供了 API Service 支持,最初是为了支持将庞大的 API Server 分散在多个独立的 API Server 中去。它定义了一组灵活的反向代理接口,只要接入的 API Server 的设计满足 Kubernetes 的 API 设计,就可以整合到 Kubernetes 的 API 中去,集群已有的组件可以直接与新加入的 API Server 来做集成,图 2.1.1 为 API Service(v1) 的定义。

Kubernetes 状态管理与扩展

图 2.1.2 中将自定义的 API Server(evangelist-apiserver) 整合到 Kubernetes API 中去。

Kubernetes 状态管理与扩展

这样就可以直接通过 kubectl 来管理该 API 扩展的状态了,图 2.1.3 中使用 kubectl 来管理 Bees。

Kubernetes 状态管理与扩展

API Service 定义中如果指定了 Service,API Aggregator 会为该 Service 添加一个反向代理配置,图 2.1.4 是 API Service 资源改动后生成反向代理的实现。

Kubernetes 状态管理与扩展

Kubernetes 状态管理与扩展

每个 API Service 对都对应了独立的 proxyHandler,是针对特定 URL 的反向代理。

Custom Resource Definition

Custom Resource Definition(CRD) 的前身是 Third Party Resource(TPR),是 API Service 的一种扩展接口,其中 TPR 已经在 v1.7 之后被废弃,v1.8 之后彻底从代码中移除,因此这里只介绍 CRD 的基本实现原理。CRD 为状态管理扩展定义了以下三种能力:

  • 定义任意类型的资源
  • 定义基于 Open API Schema 的校验
  • 定义资源(Resource)生命周期中钩子(Hook)

Kubernetes 状态管理与扩展

向集群中写入该 CRD,就在该 APIServer 中注册了一个名为 Bee 的资源,并且对资源加入和校验支持,接下来就可以用 kubectl 直接管理图 2.2.1 中定义的 Bee 资源了(如图 2.2.2)。

Kubernetes 状态管理与扩展

CRD 是基于 API Service 实现的,Kubernetes 为 CRD 自动保持一个针对 Kube API 的 API Service 配置(如图 2.2.3)。

Kubernetes 状态管理与扩展

值得一提的是,CRD 的两个特点:

  • 由 Kube API 提供服务
  • 代码里面写死了优先级(如图 2.2.4),即 versionPriority(100) 和 groupPriorityMinimum(1000)

Kubernetes 状态管理与扩展

Kubernetes 状态管理与扩展

小结

Kubernetes APIServer 的基本功能之一是反向代理,APIService 提供了动态配置接口,可以为相同的转发条件定义多个优先级不同的 APIService,这样的设计很重要,保证了 Kubernetes APIServer 在切换转发配置对客户端完全透明(Zero Downtime)。

在前文“扩展开发实例”中没有采用 CRD 来实现 APIServer 扩展,这是因为 CRD 存在两方面的局限性:

  • 没有 sub-resource 支持:因此 status 和 spec 一样都是可以被 kubectl 修改的,这其实打破了 Kubernetes API 设计的基本假设
  • 缺少兼容性支持:多版本 Schema 之间互相转化通常需要自定义代码逻辑来实现,这在未来 CRD 中也是不可能支持的

CRD 提供了一种有限的、轻量级的 APIServer 扩展,在技术选型中需要考察是否存在下面的需求来决定是否选择 CRD:

  • 避免 status 被 kubectl 篡改:在多个客户端共享状态,如果 status 被 kubectl 人为篡改可能导致系统出现不预期的行为
  • 零宕机(Zero Downtime)升级:客户端和 APIServer 需要在升级维护的任意时间点保证兼容性和正确可预期的行为

CRD 会自动保持相应的 APIService 设置,即使该 APIService 被篡改,CRD Controller 也能够自动恢复正确的 API Service 配置,这也意味着如果要用自定义 APIServer 来替换已有的 CRD 服务,需要先将 CRD 删除再写入新的 APIService 配置,否则该配置会被 CRD 自动覆盖。

代码生成

Kubernetes 提供了丰富的代码生成工具用来管理 RESTful 状态的定义和客户端代码,维护状态定义的过程就是为各模块之间状态流转定义 Service Contract。

客户端生成

本节介绍客户端代码生成工具,文中提到了 Tag 的“作用域”,在 Go 语言中没有具体的定义,这里给出一种泛泛的描述:

  • Local Tag:Struct/Field/Function 定义之前,用来约束局部代码生成规则
  • Global Tag:Package 下 doc.go 的 package 关键字前,作为整个 Package 下的默认生成规则,可以被 Local Tag 覆盖

先看 deepcopy-gen,它提供了“深复制”接口生成能力,在执行一些可能会修改对象内容的操作时,“深复制”可以保护原始对象内容,让系统中对象的边界更清晰。

如图 3.1.1 中计算容器的 Resource Limit 时,需要同时考虑节点可分配的资源,最终结果会是二者合并的结果,所以这里在计算前先将 container 深复制一个临时对象,然后合并直接在这个临时对象上进行。

Kubernetes 状态管理与扩展

k8s:deepcopy-gen:声明是否生成 DeepCopy 接口,添加的位置决定了该 Tag 的作用域,可以添加到 Package(任意文件 package 关键字前)或 Struct 前,取值相关含义如下:

  • true/false: 是否生成 DeepCopy 接口,常用于声明某些 Struct 不需要生成 DeepCopy
  • package: 只用于 Package 域,为该 Package 下所有未显式禁用 DeepCopy 的类型全部生成 DeepCopy 接口

Kubernetes 状态管理与扩展

Kubernetes 状态管理与扩展

k8s:deepcopy-gen:nonpointer-interfaces: 声明是否为某类型生成值类型的 DeepCopy 接口,只能作用于 Struct 域

  • true/false:生成值类型 DeepCopy 接口,否则为指针类型

Kubernetes 状态管理与扩展

k8s:deepcopy-gen:interfaces:指定为 Struct 生成返回任意接口类型的 DeepCopy 接口,取值为逗号分隔的类型全名。

Kubernetes 状态管理与扩展

接下来介绍 conversion-gen 和 defaulter-gen,这两个工具的作用时相辅相成的,它们为多版本 Resource 提供了服务端自动转换的能力,比如前面例子中提到的 v1alpha1.Bee 和 v1alpha2.Bee 之间的自动转换。

图 3.1.6 的例子中 v1alpha1.Bee 向 v1alpha2.Bee 转换时,缺少了 gender 字段,在 v1alpha2.Bee 中赋值为缺省值 Unknown;而 v1alpha2.Bee 向 v1alpha1.Bee 转换时,只需要将它的 location 字段赋值给 v1alpha1.Bee 即可。

Kubernetes 状态管理与扩展

再看 conversion-gen,它实现了不同版本的 Resource 在服务端自动转换的能力,这在 API 兼容性设计中起到了至关重要的作用,是笔者看来这也是最常用的代码生成工具。

k8s:conversion-gen 为“源 Package”和“目标 Package”之间的同名 Struct 生成自动转换代码:

  • true/false: 声明是否注册 Conversion 逻辑,作用域可以为 Struct 和 Field。
  • Package 名:声明为当前 Package 和传入的 Package 中同名 Struct 生成 Conversion 逻辑

Kubernetes 状态管理与扩展

k8s:conversion-gen-external-types: 指定生成 Conversion 的“当前 Package”位置,这是因为有时候当前 Package 中 Resource 定义是独立维护的。

Kubernetes 状态管理与扩展

需要注意的是,图 3.1.8 中“源类型”Package 需要保证和“目标”Package 分属不同的版本,否则生成的 Method 会出现重名的情况,无法编译。

类型转换是 Go 开发中一个比较麻烦的地方,加之语言层面反射的性能和标准库支持都限制了开发的灵活性,自动转换接口为 API 在进行兼容性的转换时做到游刃有余。

最后看 defaulter-gen,在定义了 SetDefaults 函数(图 3.1.9)之后,为之自动生成 Object/List 等使用场景的 Defaulter 函数和注册逻辑。

Kubernetes 状态管理与扩展

生成 Defaulter 注册函数后,还需要注册 RegisterDefaults(图 3.1.10)函数将该默认值规则应用于任意 Scheme 的初始化阶段,在该 Scheme 的作用范围内就会使用输入的默认值规则。

Kubernetes 状态管理与扩展

defaulter-gen 在维护状态完整中起到至关重要的作用,如果不同版本的 Resource 互相转换时,缺失的字段默认为零值,而有了初始化默认值的能力后,就能保证转换的结果始终满足对应 Resource 的语义约束,如前面例子中 v1alpha2 中增加了 Gender(性别) 后,读取 v1alpha1 数据时,Gender 字段默认给出的是空字符串,这显然在语义上是说不通的。

k8s:defaulter-gen:

  • true/false: 如果作用域为 Type,则声明是否为 Type 生成 Defaulter;如果作用于
  • Function 常用来生成明明满足 SetDefault_$Name 格式的 Function

FIELDNAME: 声明为所有含有传入的 Field 的 Type 生成 Defaulter,作用域为 Package

k8s:defaulter-gen-input:用来指定生成 Defaulter 的输入 Package。

图 3.1.11 指定 “../../../../vendor/k8s.io/api/apps/v1” 作为实际 Struct 的输入,在当前目录生成 Defaulter 注册逻辑。

Kubernetes 状态管理与扩展

Kubernetes 状态管理与扩展

此外 Kubernetes 中为构建灵活的扩展提供了丰富的高层代码生成工具,超出本文要论述的范畴,不赘述。

客户端抽象

代码生成工具还可以为扩展程序生成三种常用的客户端抽象:Clientset, Lister, Informer。

Kubernetes 状态管理与扩展

相比状态定义的代码生成工具的复杂配置,客户端代码生成通常使用默认配置即可。

其中 Clientset 封装了对 Resource 以及对应集合类型的基础 CRUD 以及常用复杂读写接口;Lister 封装了对 Resource 按照 Label 过滤的接口;Informer 提供了状态主动分发能力,让客户端能监听服务端状态的变化并执行相应的回调逻辑。Lister 和 Informer 和 API Server 通信都基于 Clientset 实现。

Kubernetes 状态管理与扩展

client.AlphaV1alpha2().Bees("default") 返回的 BeeInterface 封装了具体版本的 RESTful 接口,此处就是 v1alpha2 的 RESTful 接口,正如前文兼容性设计介绍的,v1alpha2 客户端是可以读取 v1alpha1.Bee 的 。

Kubernetes 状态管理与扩展

此处 AddEventHandler 可以传入 ResourceEventHandler 接口,允许实现三个回调函数,如图 3.2.3 所示。

Kubernetes 状态管理与扩展

  • OnAdd:创建 Resource 时
  • OnUpdate:修改 Resource 时,或定时获取最新的 Resource 状态时
  • OnDelete:删除 Resource 时

其中值得一提的是 OnUpdate 被调用时,Resource 不一定真的被修改,也可能只是定时获取 Resource 最新的状态,这个回调函数常用在启动后来自动恢复客户端的状态。

小结

代码生成除了需要结合各种需求灵活使用 Tag 之外,大部分的工具生成的代码都是可以局部自定义的,比如现在有 alpha/v1alpha2.Bee 和 beta/v1beta1.Bee 两个版本的 Bee,并为它们生成了 conversion 逻辑,有时会希望自定义生成的逻辑,只需要在 conversion 代码所在 Package 下任意代码文件添加具有相同签名的函数,代码生成工具就会忽略该函数,充分利用代码生成工具的前提下又不失其灵活性(如图 3.3.1)。

Kubernetes 状态管理与扩展

总结

自 v1.0 开始,Kubernetes API 形成一套完整的状态管理解决方案, 其高度灵活的 API Server 和客户端实现可以为任意复杂系统提供状态管理支持 。

让人眼前一亮的是它的兼容性设计,它为第三方扩展提供了稳定的接口;第三方扩展集成到平台中后,维护周期可以和平台保持相对独立。兼容性设计对微服务模块的设计和实现也具有极强的指导意义,微服务中一个重要的指导原则之一就是“自治”,模块之间的 Service Contract 不但需要做到稳定可靠,还需要做到向下兼容,否则就会出现模块之间互相影响,就与“自治”这个基本假设矛盾了。

API 聚合层不仅为 Kubernetes APIServer 提供了无限横向扩展能力,独立的 APIServer 可以拥有独立的存储(不一定是 ETCD),这意味着数据量不再是 APIServer 的瓶颈,也让客户端插件无缝集成成为可能。

还有 Kubernetes 提供的代码生成工具链也很值得借鉴, 这些工具不但极大的降低了扩展开发的成本,还为自动生成的代码提供了定制能力。

他山之石可以攻玉,Kubernetes 作为 Google 的重要开源项目之一,集中体现了 Google 优秀的分布式系统实践,也为 Go 社区提供了诸多良好的典范。

作者简介

杨谕黔,FreeWheel 基础架构部 高级软件工程师。 目前主要从事服务化框架、容器化平台相关的研发与推广。关注和感兴趣的技术主要有 Golang, Docker, Kubernetes 和它们的周边生态。

评论

发布
用户头像
非常赞的一篇文章!有两个问题请教一下:
1,CRD的主要优势是什么?既然已经有了聚合层API(而且还更灵活),为什么还有推出CRD?
2, CRD 的namespaced scope 和 cluster scope 有什么区别?如何选择?
2019 年 04 月 17 日 22:53
回复
没有更多了