随着云原生概念逐渐深入人心,越来越多的用户开始接受和践行云原生的设计与理念。Kubernetes作为容器管理引擎,提供了强大的容器编排能力,支持不可变基础设施与声明式Openapi,同时隔离了底层基础设施差异,已经成为云原生的基石和事实标准。Kubernetes原生支持功能强大的控制器,例如deployment 、statefulset 、daemonset等,可以解决很多用户场景。但随着 Kubernetes 的使用范围越来越广,原生的控制器已经无法满足一些复杂和大规模场景中的业务需求。
以京东集团业务为例,因为历史原因,集团内很多基础运维业务包括日志、监控、访问策略控制、服务发现等都是以IP作为实例唯一标识,这就要求Pod实例能够支持静态IP绑定。同时,我们并不是简单地为了Kubernetes而上Kubernetes,而是需要尽量利用Kubernetes为业务提供更多的DevOps能力,比如更丰富的升级策略,故障自动迁移等。
基于这些考虑,最终我们决定基于CRD扩展(CustomResourceDefinitions,即自定义资源)的机制,通过自定义控制器的方式来提供Pod绑定静态IP功能。这就是本篇文章要讲的产品——Enhanced statefulset。
顾名思义,Enhanced statefulset就是我们在statefulset的基础上对控制器做了进一步的扩展。它主要解决Pod绑定静态IP问题,同时也解决了statefulset在升级过程中不允许同时升级多个实例的限制。另外,它还可以在节点故障时支持Pod和IP的迁移。
下面和大家分享一下Enhanced statefulset的核心功能特性:
前面已经提到了静态IP的重要场景是业务依赖的周边组件都以IP作为实例唯一标识,所以上到Kubernetes后仍然需要Pod实例保持IP不变。相信很多用户也面临着类似的问题,下面就来分享一下实现原理。
我们主要是通过对Enhanced Statefulset Controller 、 Scheduler、CNI这几个模块扩展来支持Enhanced Statefulset的Pod绑定静态IP。具体模块关系和功能如下图所示:
▲模块关系图▲
Enhanced Statefulset Controller 对静态IP的管理主要是维护更新Static IP CR来实现的。当Controller收到创建请求时,会首先检查要创建的实例是否已经有对应的static IP CR记录,若记录不存在则会创建一个新的记录。在稍后scheduler完成调度,CNI完成静态IP分配后,controller会监听Pod信息并将其更新到Static IP CR中。反之若创建实例时,对应的static IP CR记录已经存在则表示这个Pod是删除重建,Controller会将static IP CR中的信息更新到Pod上,scheduler和CNI根据pod上的配置进行亲和性调度和IP分配。
StaticIP CRD中记录了负载类型、负载名称、节点、IP和Pod信息。其中IP信息在Pod实例上以annotation taticip.jke.jdcloud.com/ip-address 方式呈现,CNI就是根据这个字段来决定要分配的IP地址。节点信息则是通过affinity属性在pod上呈现,这样scheduler就不需要感知节点和IP的拓扑信息,只要按照亲和性调度就可以将Pod调度到静态IP所在的节点上,简化了scheduler的处理逻辑。
1apiVersion: "jke.jdcloud.com/v1"
2kind: StaticIP
3metadata:
4 name: {{workload}}-{{podname}}-{{ipaddress}}
5spec:
6 Ref: {{workload-name}} // 所属 workload 实例名称,如 deployment-xxxx
7 Kind: {{workload}} // workload 类型, 如 deployment
8 Node: Node-{{name}} // node 名称, 如 node-xxxx
9 IP: {{ipaddress}} // 绑定的 ip 地址, 如 10.10.0.1
10 Pod: {{pod-name}} // pod 名称: pod-xxxxx
〈〈〈左右滑动以查看完整代码 〉〉〉
我们对scheduler做的扩展主要是解决Pod资源预留问题。正常流程中当绑定静态IP的Pod删除后,Pod所占用的资源也会被释放,如果有其他新调度的Pod到这个节点上就会占用当前节点的资源。这时如果绑定静态IP的Pod在此节点重建可能就会因为资源不足而失败。为了解决这个问题,我们对scheduler做了扩展:
新增缓存:原有 Node 缓存基础上新增 staticIPNode 缓存,用于计算和缓存 staticIPPod 资源占用情况、缓存 IP 数量、Pod cpu/内存 使用量;
新增predicate :
PodFitsResourcesWithStaticIPPodPred Node 现有资源基础上基于 staticIPPod 占用资源再次过滤,达到资源预占目的;
CheckPodAnnotationWithStaticIPPred 检查pod 是否包含 static ip 的指定 node annotation, 并仅保留指定 node 结果只 fit node 列表。
概括起来核心思路就是将静态IP Pod所占用的资源作为一种特殊资源单独标识,在调度时进行匹配调度达到资源预占目的。
CNI 在静态IP场景下主要实现以下三个功能:
按照Pod annotation上指定的IP分配,若无IP则从IP池中随机分配一个IP并将此记录更新到Pod中 ;
IP地址预留,当绑定静态IP的Pod删除重建时,CNI会检查static IP CR记录,如果记录未删除则IP地址不释放,确保重建的Pod能够拿到绑定的IP;
在大规模集群场景下,为了提高SDN网络性能,我们要求CNI必须使用IP range模式。在这种模式下,弹性网卡上绑定的是IP CIDR,例如10.0.0.0/24,而不是某一个具体IP。IP迁移,释放和申请都是以CIDR的形式进行。
最后,我们通过一个创建流程来展示一下Enhanced statefulset对象如何绑定静态IP:
用户创建一个enhanced statefulset对象,该请求首先发送到API server;
enhanced statefulset controller监听到该请求,首先查询是否有该Pod对应的CR记录,若没有则创建新的CR,若已经有CR,则将CR中的信息更新的新建的Pod中;
enhanced statefulset controller开始创建Pod;
Scheduler根据Pod的affinity信息将其调度到相应的节点上,若无affinity信息则是新建pod正常调度。同时调度时会触发资源预留逻辑确保已有的静态IP Pod的资源不被占用;
CNI查看Pod静态IP记录,如无记录则随机分配IP并将IP信息更新到Pod上,若有记录则按记录分配;
StaticIP controller监听到Pod上静态IP信息变更,并将此信息更新到CR中。
除了支持静态IP这个强需求,我们考虑的第二个重点就是尽可能将Kubernetes的DevOps能力赋能给业务场景。社区原生的 StatefulSet 在升级过程中不允许同时升级多个实例,这主要是为了某些有状态应用需要依次按序升级的需求。但这样带来的问题是效率太低,而集团业务对升级失败和顺序有一定容忍度,为了提升升级效率,我们定义了MaxUnavailable 参数,它允许应用实例被并行升级,且始终保持最大不可用的实例数不超过 MaxUnavailable 的限制数。
此外,为了保证升级足够可控,Enhanced Statefulset可以通过Partitions进行分批升级。每个批次升级完成后通过再次更新Partitions触发下一次升级,如果发现升级过程中遇到问题也可以进行Rollback回滚或Paused暂停。
通过这些优化,Enhanced Statefulset具备更好了灵活性,既可以兼容原生Statefulset规则严格按照实例顺序升级,确保有状态服务的可靠性。又可以兼具类似Deployment的能力,以更高效的方式并发升级。同时还可以分批手工触发,基本覆盖了集团业务的绝大部分场景。
下面通过一个示例来具体了解下:
用户创建了一个Enhanced Statefulset的应用,副本数为6,应用从staticip-example-0到staticip-example-5,Partitions设置为3, MaxUnavailable设置为2。
1apiVersion: jke.jdcloud.com/v1alpha1
2kind: EnhancedStatefulSet
3metadata:
4 name: staticip-example
5 annotations:
6 staticip.jke.jdcloud.com/enable: "true" #打开静态IP功能
7spec:
8 serviceName: enhanced-sts-service-example
9 replicas: 6
10 selector:
11 matchLabels:
12 apps: staticip-example
13 updateStrategy:
14 rollingUpdate:
15 maxUnavailable: 2 #最大不可用数量,允许并行升级,并且容忍副本不可用
16 partition: 3 #enhanced statefulset创建的Pod都有index,命名从0开始,例如pod-0 pod-1 所有index大于等于partition值的实例升级,通过变更partition值来实现分批升级
17 paused: false
18 podUpdatePolicy: ReCreate
19 type: RollingUpdate
20 template:
21 metadata:
22 labels:
23 apps: staticip-example
24 spec:
25 containers:
26 - image: nginx:v1 # nginx:v1 变更为 nginx:v2 触发升级
〈〈〈 左右滑动以查看完整代码 〉〉〉
当用户将镜像从v1升级到v2时,升级流程如下:
Enhanced Statefulset Controller将staticip-example-3到staticip-example-5这3个副本并发升级到v2版本,其中staticip-example-4不可用,因为MaxUnavailable当前值为2,不影响应用继续升级;
用户将Partitions设置为0,enhanced statefulset controller将剩余3个副本staticip-example-0到staticip-example-2并发升级到v2版本,其中staticip-example-2不可用;
随后用户对不可用Pod进行手工修复,所有实例均恢复正常。
在执行第二步时,如果第一步升级有两个实例不可用触发MaxUnavailable阈值,则用户在第二步即使将Partitions设置为0也不会触发再次升级。
最后,再和大家聊一下故障迁移功能。静态IP为业务上Kubernetes带来便利的同时也带来了问题,其中一个比较突出的问题就是故障迁移场景。故障迁移有几个前提条件:
静态IP Pod和其所绑定的IP需要迁移到同一个目标节点上,这样才能保证Pod迁移后IP不变;
前面已经提到在大规模集群下我们要求CNI必须配置成IP Range模式,这种模式下IP CIDR不能拆分到更细粒度迁移,一个节点绑定的一个IP CIDR只能迁移到一个目标节点。这就意味着,所有绑定静态IP的Pod也必须迁移到同一个目标。这样就带来了一个问题,怎样才能保证目标节点有足够的资源;
故障迁移后,业务希望最大程度保留原来的物理拓扑,虚机配置与规格;
针对这些问题,我们当前给出的方案是node migration。基本流程如下:
当节点处于失联状态超过容忍的时间窗口后(用户可根据业务情况配置时间窗口阈值),node operator会将此节点禁用;
node operator会创建一台与故障节点同规格同AZ的目标节点;
node operator将故障节点的IP和Pod 指定迁移到新节点重新创建,并更新元数据信息;
将故障节点删除。
Enhanced statefulset自定义控制器模式既能充分利用Kubernetes平台能力,又能结合实际支撑业务场景,满足业务需求,是我们在不断探索中找到的方向与思路。目前Enhanced statefulset在实际支持业务落地过程中还在不断打磨与完善中,在发布策略、故障迁移等场景还有进一步提升空间。未来,我们一方面会在集团上云过程中针对复杂的业务场景沉淀更多的控制器模型,另一方面已经开始规划将这些能力通过开源和产品化的方式提供给更多用户使用。