王泽锋
华为 PaaS 开发部高级工程师
多年电信领域系统软件开发和性能调优经验,对深度报文解析,协议识别颇有研究。华为 PaaS 平台核心开发者,主导负责最初版本的运维系统设计和开发,华为公有云 PaaS CCE 服务的多 AZ 支持、亲和/反亲和性调度设计和开发。目前担任 Kubernetes 社区的 Committer,主导 Kubernetes 社区亲和性调度相关特性开发工作。个人对社区化的项目开源运作模式有浓厚的兴趣。
简单介绍查看图片图 1
华为作为一个全球性的公司,其 IT 系统规模非常庞大,部署在全球各个地区,业务量大,应用数量急剧增长。随着规模的愈发庞大,问题也越来越突出:
资源利用率低,虚拟机增长过快,成本激增。
跨区域多 DC 的部署维护无法拉伸,运维投入巨大。
系统重载、应用伸缩周期长。
难以支持快速迭代,业务上线效率低。
查看图片图 2
面对业务压力,Cloud Container Engine(以下简称 CCE)的出现很好的解决了以上问题。华为的 CCE 可以理解为是华为的 Kubernetes 商业版本(图 2),整体上是希望通过一套核心技术,去打通 IaaS 和 PaaS 平台,一套技术支撑多种业务场景。
图 3
2015 年底 CCE 在华为 IT 系统上线(图 3),资源利用率提升了 3 到 4 倍,当前的规模达到 2000 多个虚机。下面重点讲下近期的两大重点技术实践: Kubernetes 多集群联邦、应用间的亲和/反亲和调度。
Kubernetes 多集群联邦
查看图片图 4
图 4 是 Kubernetes 非常简单的架构图,图中可以看到如下组件:
- Pod(容器组)
- Container(容器)
- Label(标签)
- Replication Controller(副本控制器)
- Service(服务)
- Node(节点)
- Kubernetes Master(Kubernetes 主节点)
其基本调度单位是 Pod ,具体应用下面实例会提到。
查看图片图 5
从华为内部 IT 系统的某项业务估算(图 5),其机器数量在 2016 年整体容器化之后,会达到 3 万虚机和 10 万容器。华为 IT 系统的典型场景是通过多数据中心统一管理部署应用,提供跨域的应用服务发现。但是跨域的网络限制与差异较大,难以通过 k8s 单集群支持。所以如何提供跨域应用的大规模集中部署成为目前需要解决的问题。
查看图片图 6
图 6 是一个简单的多集群联邦架构示意图,图中的 3 个黑块的是在集群联邦层面加出来的三个关键组件,底下白色对应的是一个集群,这个架构跟 Kubernetes 原来的架构非常相似。原来 Kubernetes 的架构也是一个 Master 管理多个节点,这里是一个 Master 管理多个集群。如果把图中的集群换成节点,其实就是 Kubernetes 的架构。用户通过统一的 API 入口创建应用,集群联邦所做的就是把联邦级别对象应用拆分到各个子集群分别部署。Cluster controller 会维护各个集群的健康状态、监控负载情况,而 Service controller 做一个提供跨集群的服务发现的打通。
查看图片图 7
总的来说,现在集群管理架构都比较相似,都有 Controller、Scheduler 和 Agent(Kubelet),这里是利用 List-Watch(图 7)机制实现的组件间交互的解耦,体现了 Everything talk to API 的设计理念。比较类似 SOA 架构中的消息总线,但跟消息总线的差别在于对数据的存储和事件通知做了统一化处理,当集群收到新的事件的同时,可以拿到这一份新的数据,包括数据的变化是新增还是更新了某个对象。
实例说明:这时要创建一个 ReplicaSet ,第一步是集群起来后,Controller-manager(包含多个 Controller)、Scheduler 和 Kubelet 都会去发起 watch,但是 watch 的对象都是不一样的。ReplicaSet Controller 会 watch ReplicaSet,Scheduler watch 的是一个 Pod(也可以理解为一个应用的实例)。这里的 watch 是带一个条件的,destNode 为空,就是未调度的 Pod ;而 Kubelet watch 的 Pod 只是 destNode 为 Node 自身的 Pod,也就是这个 Pod 调度到相应的节点才会做相应的处理。在创建应用的时候,k8s 会先将对象保存到 etcd 中,同时存完之后会上报已创建的事件。与此同时 ReplicaSet Controller 已经事先 watch 这个事件,它就会收到通知。ReplicaSet Controller 发现有一个新的 RS 被创建,这个时候它会去做拆分,因为 RS 对应的就是一个多实例无状态的应用,所以 RS 有多少个副本,Controller 就创建多少个相同的 Pod 。
这样第一阶段创建的过程已经完成。因为 Scheduler watch 了 Pod,新创建的 Pod 是没有被调度过的,destNode 是空的,这个时候它会收到 Pod 创建的通知,并执行调度算法(把这个集群中所有可用节点的数据根据 Pod 的调度需求做一个综合计算,选出一个 Node),然后将 Pod 绑定到这个 Node 上(更新这个 Pod 的 destNode),这样 Scheduler 的流程就结束了。然后在相对应被绑定的 Node 上,Kubelet 会发现有新的 Pod 调度过来,就会按照 Pod 的定义创建并启动容器。
经过上述的流程可知,各个组件通过 List-Watch 不同的对象(即便是相同的对象,状态也是不一样的),实现天然的解耦,各个组件处理一个应用生命周期的不同阶段,同时保证了在一个异步的分布式系统里面多组件间处理流程的先后顺序问题。同时 List-Watch 每次只是做事件的监控,所以每次 watch 比如创建一个 Pod 获取的是增量信息,获取的数量交互是非常少的。我们知道通过消息总线传一个消息/事件,是有可能丢失的,这就需要有一个容错机制,来保证最终一致性。Kubernetes 内部是通过 List 来解决的。比如 ReplicaSet Controller 会周期性地对 ReplicaSet 做全量 List ,拿到的数据就是当前系统需要起的应用数据,它会检查当前各个应用的实例运行情况,做相应处理,多余的删除,不足的补齐。
查看图片图 8
图 8 是集群联邦下的应用创建流程。如图所示,用户向联邦 API Server 发请求创建应用,它会首先将对象保存起来。同时 Controller 会周期性获取各集群的监控数据,同步到 API Server。在 Scheduler watch 到创建应用之后,它会根据调度算法,按照联邦里面所有的集群监控信息包括健康情况等指标筛选出合适的集群,对应用做拆分(当前主要是各个集群的容量和监控负载情况)。这个例子里面应用被拆分到了两个集群,cluster A 是 2 个实例、cluster B 是 3 个实例,拆分后仍是把这个对象保存到 API Server,跟 Kubernetes 原生在单集群下创建的流程是一致的。这个时候 Controller 会 watch 到 sub RS 创建(实际上是 RS 对象的一个属性更新),它会去对应的集群创建实际的应用实体,实例数量为前面应用拆分时的运算值。
查看图片图 9
图 9 呈现了联邦调度器的关键机制,它 watch 的是两类对象,一个是副本集,一个是 Cluster(联邦里面有多少可用集群,它们负载的情况等)。当 RC/RS 有变化时,会将它们保存到一个本地队列,worker 会从这个本地队列中逐个读取 RC/RS 并处理。处理时根据加载的调度策略筛选出合适的目标集群,然后去计算出分别应该在这几个集群创建多少个实例。最后写回到 API Server 的数据格式如图所示,是更新 RS 的一个字段 destClusters,未调度时是空值,现在变成 [{cluster: "clusterA", replicas: 6}, ... ]这样的内容。
查看图片图 10
集群联邦中包含多个 Controller,这个例子(图 10)里主要涉及两个,一个是 Cluster Controller,一个是 Replication Controller。Cluster Controller watch 的是 cluster 对象,维护联邦中所有 Cluster 状态信息,包括周期性的健康检查、负载情况等。如果配置了安全访问,还需要确保配置(比如访问集群所用的证书文件)是正确可用的。Replication Controller 则 watch RS,刚才提到联邦调度器也会 watch RS,这里 watch 的是已经过调度器拆分处理的 RS,并负责到各个集群创建指定数量的实例。
查看图片图 11
接着前面的例子,拆分到 Cluster 的 A 是 2 个实例,拆分到 Cluster 的 B 是 3 个实例,这个实际上可以理解为一个控制面的处理,解决了一个应用在联邦下部署到多个集群的问题。接下来要解决数据面的问题,部属下去之后如何实现应用跨集群的访问(图 11)。我们知道在单集群里面是全通的网络,但是跨集群的我们还希望适配混合云的场景,这个时候底层的网络不一定是通的,因此我们直接按照集群间网络不通的场景来设计。从外部流量来说,用户请求会通过全球分布式路由被分摊到各个集群,每个集群有一个负载均衡,再把流量导到各个 Node 上去。跨集群访问的时候,集群内的访问仍可以按原来的服务发现机制处理。跨集群的时候因为是跨网络访问,网络是不通的,这里有个约束是每个集群的负载均衡器需要有 public IP,可以被所有集群内的所有 Node 访问。当应用的访问请求需要跨集群的时候,流量会被转发到对端集群的负载均衡器上,由它再做反向代理,再转到后端应用实例所在的 Node 上去。
查看图片图 12
图 12 是 Service Controller 的关键机制:一个是 watch Service 变化、一个是 watch Cluster 变化。如果某个 Cluster 挂了流量就不应该转发过去,因此要 watch 它的变化,在服务里面刷新每个服务后端(Cluster)的联通性,保证它的每一个后端都是通的。同时要 watch 服务的变化,可能是新创建服务,服务后端的实例被刷新,或者说是服务后端可能有 LB 挂掉,对于这个服务来说都是有变化的,这个时候要去做相应处理。
Cluster 处理的就是 Cluster 的状态发生变化的情况。这里简单介绍下新增服务时的处理流程。在联邦 Service Controller 中,每个集群都有服务对象,服务包含了 Endpoint,这里有两类,一个是当前集群内服务的 Pod 实例所在的 Node,另外跨集群的时候,需要访问到其它集群所在的 LB,它是把这两组信息都写到 kube-proxy 里面去。同时为了对外提供访问支持,需要按集群分别将本集群中服务实例的 Endpoint 写到对应集群的 LB 中,做反向代理。这样就支持了单集群内的通信和跨集群通信。对于外部流量,因为最前面是全球分布式路由,用户在访问的时候,可以根据用户的地域或访问时延等因素,选择一个就近的集群的 LB,以获得较为理想的访问速度。
应用间的亲和/反亲和调度图 13
容器化和微服务化的改造会出现亲和性和反亲和性(图 13)的疑问,原来一个虚机上会装多个组件,进程间会有通信。但是在做容器化拆分的时候,往往直接按进程拆分容器,比如业务进程一个容器,监控日志处理或者本地数据放在另一个容器,并且有独立的生命周期。这时如果他们分布在网络中两个较远的点,请求经过多次转发,性能会很差。所以希望通过亲和性实现就近部署,然后增强网络能力实现通信上的就近路由,减少网络的损耗。反亲和性主要是出于高可靠性考虑,尽量分散实例,某个节点故障的时候,对应用的影响只是 N 分之一或者只是一个实例。
查看图片图 14
先来看一下单应用的情况(图 14)。在这个例子里,我们希望所有的实例部署到一个 AZ 里面(即在 AZ 级别互相亲和),同时在 Node 级别互相反亲和,每个 Node 部署一个实例。这样某个 Node 挂的时候,只会影响一个实例。但是每个不同的企业,在不同的平台上,对于这种 AZ,Rack,Node 等 failure domain 的理解和命名都不一样。比如有人会希望应用在机框级别做反亲和,因为有可能一个机框都会 down 掉。但本质上它们都是 Node 上的一个 label,调度时只需要按这个特殊的 label 进行动态分组,处理亲和反亲和的关系即可。
图 15
在亲和性、反亲和性的支持上面,会实现硬性和软性两种支持(图 15),因为如果是全硬性条件,很容易因为配置不恰当导致应用部署失败。这里实质上是两类算法,但是在实现逻辑上比较接近。硬性算法做过滤,不满足的节点都全部过滤掉,保证最后留下来的一定是满足条件的。在软性的情况下,不需要这么苛刻地要求,只是 best effort,条件不满足时仍希望应用调度部署成功,这里用的是评分排序,根据亲和性和反亲和性的符合情况,符合程度高的给高分,符合程度低的给低分,选 Node 时按分数从高到低进行选择。
查看图片图 16
亲和性是相互的,这个时候就会考虑对称性的问题(图 16)。两个应用互相亲和,在应用的定义里面,比如应用 B 亲和应用 A,实际是在应用 B 里面写一条亲和的规则去亲和 A,但是 A 不知情。如果 A 先创建 B 后创建,是没有问题的,因为 B 会去找 A,但是反过来 A 不会去找 B。因此我们在做算法实现的时候给它加了一个默认行为,在调度的时候,需要自己亲和/反亲和哪些 Pod(前面的单向思路);同时又要去检查哪些 Pod 亲和/反亲和自己,以此实现对称性 。
另外一个常见的问题是被亲和的应用发生迁移的情况。需要说明的有两点,一是在算法设计上做了对称性的考虑,不管是先部署还是后部署,即便这个应用挂了,它在重建并被调度时,依然会检查当前系统里亲和哪些 Pod 或者被哪些 Pod 亲和,优先跟他们部到一起去。另外,目前 RC/RS (副本集无状态应用)只有 Node 挂掉的时候才会发生重建 Pod 的情况,Node 没有挂,异常退出是在原地自己重启的。这个从两个层面上,能够保证亲和的应用不在一起、反亲和的应用分开这种需求。
查看图片图 17
这时做了硬性和软性两种实现,在做软性的时候没问题,因为即便不满足也能调度成功,按照完全对称的思路就可以做了。这里最主要的问题就是硬性亲和的对称性。亲和其他应用的场景,如果被亲和的应用不存在(未调度),调度失败是合理的。但是如果因为不存在其他的 Pod 亲和当前待调度的 Pod 而导致失败就不合理。因此硬亲和不是完全对称的,它的反向是一个软亲和。
查看图片图 18
尽管我们在亲和性/反亲和性的实现上做了对称性考虑,但它只能在涉及调度的阶段起作用,调度后,一个 Pod 的生命周期内,不会再根据实际的情况进行调整。因此后面我们会从两个方面进一步解决前面的问题(图 18)。
第一个是限制异地重启,即 Forgiveness 。假如 Node 故障或重启导致Pod被迁移( Pod 重建后被调度到其他 Node ),假如 Pod 原先保存了一些本地数据,但是因为被迁移到了别的节点,这些数据相当于丢失。所以这个时候给它配一个 Forgiveness 策略,指定 Pod 绑定在它所运行的 Node 上,如果这个 Node 挂了,不迁移 Pod,而是等待 Node 自动恢复的时候,把这个 Pod 原地拉起。这样原来存在本地硬盘里的数据,就可以被处理。对于有实效性的本地数据,我们还引入了超时机制。比如本地暂存的日志,超过两天三天之后,这个日志再做后续处理也没有意义。这个时候 Pod 需要在两三天之内绑定在这个 Node 上,等到 Node 恢复再处理任务。如果超过两三天 Node 还没有恢复,但这些本地数据已经失去意义,这时候,我们希望 Pod 可以被迁移。在做驱逐检查的时候,没有超时的 Forgiveness Pod 就继续绑定在 Node 上,超时的正常驱逐进行迁移就可以了。
第二个是运行时迁移。在运行的过程中,集群负载、应用间亲和性的满足程度、Node 的健康度或者服务质量时时刻刻都在变化,系统应该能够根据这个当前的情况去动态地调整应用实例的分布,这就是 Rescheduling 的意义。Rescheduling 最终体现的行为是周期性地检查集群状态,并做迁移的调整。但在迁移之前,有许多要考虑的因素,比如应用的可用性问题(不能因为迁移导致某个应用的所有实例在某个时刻不可用),迁移的单向性(被迁移的 Pod 如果回到原来的 Node,就变成多此一举)和收敛性(不能因为迁移某个 Pod 而引发大量其他 Pod 被迁移)。
七牛架构师实践日是由七牛云发起的线下技术沙龙活动,联合业内资深技术大牛以及各大巨头公司和创业品牌的优秀架构师,致力于为业内开发者、架构师和决策者提供最前沿、最有深度的技术交流平台,帮助大家知悉技术动态,学习经验成果。
七牛架构师实践日第十一期【体育+直播 技术最佳实践】将于 8 月 14 日与大家在 北京 见面,目前活动正在火热报名中,点击下方「阅读原文」了解更多信息,期待你的参与。