编者注:本文作者许健为eBay中国研发中心云计算开发总监。本文总结了eBay云计算系统架构设计思路与经验,全文分为上下两篇。上篇围绕API展开,详细请见:《Kubernetes 架构学习笔记(上)》。本篇为下半部分,包含控制器逻辑、架构等内容。
在过去的两年中,我协助eBay云计算架构师做eBay云计算部门系统架构设计审核会议的组织协调工作,于是萌生了用我们日常工作中实际的架构设计讨论来深入理解社区 Kubernetes Design Principle 的想法。文中例子来源于真实的设计迭代, 我仅是做了汇总。借此机会感谢所有为云计算系统设计做出贡献的同事, 向你们学习才让我这些年对Kubernetes的设计理念有了进一步的理解。
控制器逻辑
“
功能必须是Level 触发,也就是说系统必须根据期望状态和当前观测状态来运作而不管在此过程中错过多少中间状态。Edge 触发只能用于系统优化。
我们看一下 Kubernetes 源代码中 kubelet 处理 pod 的例子:pkg/kubelet/kubelet.go,下面是一个简化版本的代码,方面说明 Level Trigger 的思路:
再来看 Edge Trigger 的代码:
这里的handlePodAddition 就是优化,如果没有handlePodAddition,kubelet 的syncPod 周期性对比目标(Spec)和当前状态(Status) 也是可以处理 Pod Add 的,添加Edge Trigger 的 handlePodAddition 的原因主要是提供更加及时,高效(也就是代价低)的方式处理Pod。例如:syncPod 是周期性检查,edge trigger 的 handlePodAddition 可以更快地提供反应, syncPod 要遍历当前 Kubernetes Node 上kubelet 管理的所有Pod, 而 handlePodAdditions 的 Scope 就更有针对性了, 并且因为 SyncPod 要处理的 Pod 更多,由于其他 Pod 的正常和异常处理需要更多的时间,不免导致该新 Pod 创建被滞后。
但是这里需要注意的是,可以没有Edge Trigger 的handlePodAddition, 但不能没有 Level Trigger 的syncPod。在三高系统设计的技术博客中也提到过当时我们设计 Openstack 到配置管理系统的Sync(详见《分享| “三高”产品设计的这些坑,你是不是也踩过?(上)》) , 一开始我们通过监听Openstack 的事件来触发同步,这个方法虽速度快但无法杜绝漏消息,所以在 delta 的事件触发同步的基础上,我们还加上了周期性的全量同步——delta 事件同步保证及时性, 全量周期同步保证数据的完整性。
在系统设计实现Controller Loop中,高性能,高可靠性和简单性中只能同时满足两项,这一点和 CAP 的理论很相似。
eBay的Tess Load Balancer (Software Load balancer 系统) 是一个由多个POD 组成的 SLB cluster, 当前的设计是偏重于 Availablity 的, 也就是说如果在控制面层面一个 Pod 丢失了到 API Server 的链接使得无法获取最新的 IPVS rule 更新, 该POD 仍然被设计成在数据面可以服务请求。
在实际的设计实现环节,我们碰到的问题如下:
TLB(Tess Load Balancer, 其中Tess 是eBay 基于K8s 的云平台Code Name )可以同时管理 HLB (Hardware Load Balancer) 和 SLB ( Software Loadbalancer), 因为 HLB 在几十年前设计的时候并不是为 Kubernetes 这种 reconcile 场景而生的,其数据面处理能力强大,控制面不适应频繁查询和更新,所以在HLB作为 Provider 的时候我们并没有给 TLB 打开全量更新逻辑。
到了 SLB 的场景下,我们应该有全量更新但是并没有做,于是有一次 TLB 丢失到 API Server 链接并且处于假死状态,造成发到有问题的 TLB Pod 上的流量出现误转发,即使重启也无法恢复,因为重启后虽然可以恢复到 API Server 的链接,但是因为 Controller 主循环里没有 Level Trigger 的全量对比,不会pick之前丢失的更新,使得我们需要重建 Pod 才可以恢复。事后诸葛亮来说两点:
TLB 开发认为,在TLB controller 全量更新存在代码出现问题的事后刷新所有对象的所有规则,很容易扩大影响范围。我们可以考虑还是进行全量更新,或者换个词叫做全量追赶,追赶漏掉的部分。即使做Level Trigger 也可以控制可以更改的数量,如果短时间要更新很多,比如单次reconcile loop 对比数据量变化太大,可以刹车。当然,最好还是加强测试, 并且在生产环境部署多套 TLB Cluster 做 Cluster 级别HA 或者 region 级别 HA, 有问题的时候就切流量,并且保证TLB 代码部署不会同时更新两个region。
TLB 本身就是一个 TLB Cluster, 我还是觉得应该 优先选择 Consistency 而不是Availability——也就是当一个 TLB Group 中的一个POD 失去控制面的时候,宁愿选择让这个POD 停止服务,或者说 Gracefully Shutdown。
“
假设系统是开放的: 不断去验证假设是否还成立并且优雅地适配外部事件和行为。比如,我们允许用户删除Replication Controller 管理的Pod, 然后我们会重建这些被删除的Pod。
Admin Traffic Control 的设计
在eBay, 线上出现问题后的第一选项是恢复服务能力而不是找到问题发生的根本原因。恢复服务的一个救命功能叫做 Region Exit , 就是将流量彻底从问题数据中心切走,实现这个能力的服务叫做 Admin Traffic Control(简称ATC),我们来看一下ATC 的Spec:
这个Spec 设计是比较巧妙的,因为Spec 的设计很好地抽象了用户的意图而没有耦合太多基础架构的实现:
bz.feature 是 eBay 的一个概念,代表一批对于ebay 业务比较重要的应用的集合,假设这个集合有200个应用,selector = bz.feature:true 就选择了这200个应用,要把这200个应用的流量都从 AZ1 切走。当然这个selector 也可以用来选择其他的应用集合。
在设计 ATC 的时候,不同的应用架构并不同,有一些应用是 region aware 智能DNS 到应用负载均衡器的一层结构,有一些应用是智能DNS 到Web 层负载均衡器再到app 层负载均衡器的两层结构。而ATC 的Spec 里并不是从基础架构的视角出发定义的,而是从用户视角定义的,用户只关心选定应用在target scope 的流量要不要切走,至于如何转换成一层架构的操作步骤,如何转化二层架构操作步骤是ATC controller 的实现细节。
做AZ exit 或者 Region exit 切流量是eBay 出事故时候的保命措施,负责Site可用性的部门(也就是ATC 的客户)对于流量切换的时间有明确要求——必须低于30秒。如果selector选中所有应用,当时我们设计ATC 的时候按照 5000个应用, 每一个应用部署在3个Available Zone来做设计,也就是这5000个应用的流量必须在30秒内切换完成。eBay 用来管理负载均衡的 Kubernetes 对象名叫做 AccessPoint ,假设这5000个应用都是2层架构,那就要操作 5000 x 3 (AZ) = 15000 个 AccessPoint 对象,而每一个AccessPoint 对象至少有 3个 member (因为有3个AZ), 每一个 Member 叫做 TLB (Tess Load Balancer) 对象, 那就要碰 15000 x 3 = 45000 个 TLB 对象 … 这对于性能提出了更高的要求,在Kubernetes 的世界之前,上一个版本的 Region exit 工具是把所有的Load Balancer 操作Payload 用一个Batch call封装在 Load Balancer Management System 里面多线程分发给所有的负载均衡设备的, 而现在基于Kubernetes 的ATC设计,在用户指令到达负载均衡器设备之前,要更新15000个 AcceePoint 对象和45000个 TLB 对象,更别说reconcile loop 对于每一个Kubernetes 对象都有多次读取和状态同步。
当时负责云计算的架构师坚持ATC 必须经过 AccessPoint 对象,而不能绕过5000个 AccessPoint 对象直接把指令下达给负载均衡器设备。原因是他认为改变ATC 虽然是应急情况下发起的救急举措,但是也改变了应用的流量分布,在Kubernetes 的世界里必须有地方描述这个“初衷”改变,如果AccessPoint 的Spec 没有改变,那AccessPoint 的 reconcile 操作就会把流量状态改回去。
我当时问架构师是不是可以绕过AccessPoint 对象直接批量操作 TLB对象作为优化,于是有了以下对话:
架构师问,Kuberntes 有Internal API吗?也就是说 TLB的 Spec可以直接被用户操作吗?我说,Kubernetes 没有 internal API , 我们从没有说用户不可以直接操作 TLB 来管理负载均衡器, 而且通过 TLB 对象操作仍然保证系统中负载均衡器的User Intention 被表达了。
架构师继续问我,Kubernetes 假设系统是开放的:
ATC 可以在AZ exit 的时候发起变更TLB 的请求
用户A可以在同一时间通过变更AccesPoint 于是Access Point Controller 就会发起变更TLB 的请求
用户B可以在同一时间直接发起对TLB 的变更请求
TLB 的 Controller 必须能够假设系统是开放的,这些情况都会发生,并且需要不断去验证假设是否还成立并且能优雅地适配外部事件和行为,来保证对于各个用例都能正确工作。正确工作包含 TLB 的Spec 上可以有 ATC 的标记,当ATC 标记被enable 的时候,TLB 拒绝来自ATC 之外的请求。我们也可以把系统设计成 AccessPoint Controller 对于跟它相关的 TLB 状态感知,当TLB 的 ATC 被enable 的时候,拒绝普通用户的 AccessPoint 流量变更。
同样的设计理念也适配于下文要提到的 TrafficMigration Object 和 AccessPoint 对象的交互。Traffic Migration 是 Access Point 的高阶对象,当我们做 AccessPoint Controller 的设计时,我们同样需要假设系统是开放的,用户可以直接变更Access Point 对象,而在同一个时间里,TrafficMigration 对象也会发起对Access Point 对象的变更请求。Access Point Controller 必须不断去验证假设是否还成立并且优雅的适配外部事件和行为,来保证对于各个用例都能正确工作。
“
不要为具有状态转换行为的对象定义全面状态机,也不要为无法通过观察确定“假定”状态的对象定义全面的状态机。
我们来看一下Kubernetes Deployment 的设计:
Deployment 内部其实有很多状态,比如创建,删除 ,Scale up 或者 Scale down 来更新replicaSet, 但是我们在 Deployment Controller 的源代码里是找不到一个地方管理一个状态机转换逻辑的,Deployment 的设计并不会在 DeploymentStatus 里面着重暴露这些内部增删改动作,而是选择暴露可观测到的replica的不同状态的数量,也就是说 Kubernetes 选择着重展示当前的状态,而不是当前正在进行的动作。动作是在 Reconcile Loop 中通过对比当前状态和目标状态的区别推导出来的。
为什么 Kubernetes 不对Deployment 实现 Auto Rollback ?
请看 Kubernetes 官方文档中的Deployment Failure 的这个例子:如果我们给错了新的image或者无法更新新的image, deployment 新的版本永远不会成功,那这个时候为什么Kubernetes 不设计自动回滚功能,而设计成要我们使用 kubectl rollout undo 手工回滚呢?
Kubernetes 社区确实有关于Deployment自动回滚的讨论:
要实现自动回滚,首先要能够判断deployment失败了,这一点大家都同意 spec.progressDeadlineSeconds 是一个选项。
如果实现Auto Rollback, 要不要更改 Deployment Spec 里面的pod template 到上一个稳定版本?如果考虑更改,我总结一下社区的思考:
好处:清晰地展示我们新的目标状态,Spec 状态更改对用户可见。
坏处:破坏了declarative 的初衷,因为Spec 代表了用户的初衷,系统自动更改用户Spec 中的pod template 就相当于改变了用户的初衷。
于是又有人提出了Strategy的想法,添加 spec.failurePolicy 以定义在失败的时候我们是 retry 还是 rollback。
最后Brian Grant 老大发话了: At this point, I think automatic rollback needs to be a feature of higher-level deployment pipelines. Example: I want to deploy to staging. If it succeeds, I want to deploy to prod. If it fails, I want to rollback. Implementing auto rollback in K8s wouldn't fit with that.
Making lack-of-progress reporting more usable in such higher-level pipelines seems more valuable than auto-rollback in Deployment. 当前工作重点是给各个 ReplicaSet, DaemonSet, Deployment 等controller 添加进度状态,auto-rollback 的需求你们在上层controller 去搞吧,现在不要做进 Deployment。
Argo CD Rollout 中状态管理的设计
Argo CD 是一款非常实用而且是生产环境可用的CD方案,我们来看看Argo CD rollout 设计中关于状态管理的思考。Argo CD rollout 提供蓝绿发布和金丝雀发布两种方式,这两种方式都需要一种方式来判断应用在新版本上是否稳定,Argo CD 提出了一个叫做 Analysis 的概念来描述:
下面是Rollout 对象所依赖的 Analysis Template对象success-rate的定义:
abortScaleDownDelaySeconds 文档中定义了在Analysis认为rollout 失败,ArgoCD rollout 状态变成Aborted 的时候的行为模式。对于普通Canary 模式来说,ArgoCD 的默认行为是 rollback 到上一个稳定版本。现在我们假设在 Analysis 结果判定为失败和ArgoCD rollout controller 在把Aborted 状态写入 Status 之间,我们重启了Argo CD controller, ArgoCD controller 重启回来以后的逻辑如何?前面我们讲过Kubernetes 对象的状态 (Status) 必须通过观察可以被100%重建,任何保存的历史记录只能被用于系统优化而不能成为纠正系统行为的必要依赖。所以可以肯定 ArgoCD controller 一定不能依靠读取Status = Aborted 来决定下一步的rollback 动作,它可以通过读取新版本应用实例的数目和 Analysis Template 定义来获取当前应用健康状态来推导出 Status 应该是 Aborted , 并且进行rollback。
流量搬迁设计
现在来看一个在流量搬迁中的案例:首先有一个AccessPoint 对象来描述一个应用实例(ApplicationInstance) 的流量,我们定一个TrafficMigration 的 CRD 和对应Controller 来实现把流量从源ApplicationInstance 迁移到目标 ApplicationInstance。
TrafficMigration 的Spec:
TrafficMigration Controller 的 reconcile loop 伪代码:
我们来看一下状态转换图:
是不是很复杂?碰到复杂问题的时候,解题思路的第一步不应该是实施,而是尝试简化。
为什么需要 Preparing, Prepared ?
现在版本的TrafficMigration controller 中设计了Preparing 状态用来生成执行计划。Kubernetes 的 controller 设计原则是希望持续做目标状态和现实状态对比以决定下一步动作(reconcile) 而不是生成执行计划工作流。如果需要做检查以决定是否可以进行Migration, 那检查逻辑推荐放进 admission 而不是设计Prepare Failed 状态。所以我们可以尝试去除Preparing, Prepared 和 Prepare Failed 这三个状态。
在实际工作中,流量管理一旦出错会直接影响Site的可用性,进而影响业务。按照eBay的变更要求,就算是自动化也需要提交变更请求到变更系统,只不过自动化的变更请求是自动Approve的。变更请求需要说明这个变更的执行计划、影响范围和rollback plan。这里出现的问题是,Kubernetes 的设计原理是不会产生完整的变更计划的,这个时候就需要看清一个要求它背后的目的是什么?一个政策它创建的时代背景是什么?eBay 的变更政策是在手工变更还占主导的时期提出来的,写清楚影响范围是为了给审批的人看,而我们的自动化是自动审批的,那生成完整执行计划的主要作用已经从在变更发生前给审批的人看,变成了在变更后提供Auditing 的记录,而Audit 的记录可以记录在Condition 或者 kubectl describe 展示的Events 里面,或者干脆通过Events 或者Logging 送到监控平台,比如eBay的公司级监控平台Sherlock。
Migrating 和 Reverting的区别是什么?
Migration 是把流量从Source ApplicationInstance foo-a1 迁移到Destination ApplicationInstance foo-a2, 而Reverting(也就是 rollback) 是反过来,区别在于从Source 到Destination 是按照trafficRamp定义的策略(相当于update strategy)逐步搬迁的,而Reverting 也就是rollback 的时候,我们是希望以最快的速度,也就是一下子(而不是逐步)把流量切回Source。那我们怎么判断当前应该是 Migrate Forward 还是 Rollback 呢?这一点跟 ArgoCD rollout 是一模一样的判断逻辑——也就是检查应用的健康状态,在Traffic Migration Spec 里面的Destination HealthMonitor hm-a2 就起到这个作用。
一个实现细节是:有可能Health Monitor 的结果会抖动,也有可能Health Monitor 不能及时反应应用状态,这时候我们怎么办?
为什么需要Cancelled?
正常情况是不需要Cancel 的,除非用户希望 Cancel。既然是用户的初衷,那这个动作应该在TrafficMigration 的 Spec 里面,当用户修改Spec设置Cancel 的时候,Controller的行为和 Reverting 是一模一样的。
Migrated, Reverted的区别是什么?
既然Migrating, Reverting 这两个状态是可以通过应用的健康状态,用户Spec 是否设置Cancel 推导出来,那Migrated , Reverted 也同样可以。
Archived 和 Abandoned 状态
这里设计Archived的目的是当流量已经成功迁移到destination foo-a2 后,停止controller loop对当前Traffic Migration 对象的reconcile。一般Kubernetes 的设计,syncTrafficMigration controller loop 应该不停地去检查foo-a1和 foo-a2上的流量,一旦发现foo-a1上还有流量就应该继续将流量迁移到foo-a2, 直到foo-a1被删除,这时候不会引入 Archived状态;而因为流量管理非常敏感,我们设计了auto rollback(reverting), 既然这样就应该尽最大努力rollback traffic到 foo-a1。但是实际情况是:Traffic Migration 的用户把 Traffic Migration CRD 当成一个任务看待,任务完成后不希望再 reconcile, 这时他其实可以要求用户删除该TrafficMigration 对象而不是设置Archived 状态,对于 Abandoned 那我们可以参考 Deployment 设计的 .spec.progressDeadlineSeconds 设置一个时间阈值,rollback 时超过该时间仍然无法完成流量复原到 Source 往往说明系统无法自动复原,需要人工介入,这时通过 abandoned 停止rollback 继续无效尝试。
修改后的Traffic Migration 主 controller loop:
“
不要假设组件的决策不会被覆盖或拒绝,并且组件始终能够理解拒绝的原因。例如,etcd可能会拒绝写入。Kubelet可能会拒绝pod。调度程序可能无法调度pod。重试,但要后退或做出替代决策。
在资源的自动分配与释放这一节里写到:在API类型上操作的控制器通常会根据类型和/或用户提供的一些额外数据来分配资源。一个典型的例子是Service API对象,在这里,像IP和网络端口这样的资源会根据类型在API对象中设置。当用户不指定资源时,这些资源将被分配;而当用户指定了确切的值时,这些资源将被保留或拒绝。当用户选择更改鉴别器(discriminator)的值(例如,从类型X更改为类型Y)而不更改任何其他字段时,系统应该清除用于在联合体中表示类型X的字段,并释放与类型X关联的资源。无论这些值和资源是如何分配的(即,是由用户保留还是由系统自动分配),这个过程都应该自动发生。再举Service API作为具体案例。系统会分配像NodePorts和ClusterIPs这样的资源,并在服务类型为NodePort或ClusterIP(鉴别器的值)时自动填充代表它们的字段。当用户将服务类型更改为ExternalName (ExternalName Service 引用外部 DNS 地址,而不仅仅是 Pod, 这类 Service 允许应用作者引用平台外、其他集群或本地存在的服务)时,这些资源和代表它们的字段会自动被清除,因为这些资源和字段值不再适用。
在eBay 的实现中,Service 背后是 TLB (Tess LoadBalancer), 而 TLB 有Provider 字段标记是Hardware LoadBalancer (HLB) 的Provider 和 Software LoadBalancer (SLB)的 Provider。我们其实正在从HLB到SLB的迁移过程中,但是这个迁移却是在TLB 之上再写一个高阶Controller 来完成而不是允许客户更改 TLB 的 Provider 字段。原因是从 HLB 迁移到SLB的过程中本质上是创建一个新的TLB对象, 然后迁移流量,所以并不是修改原有TLB 而是用新的TLB 替换原有TLB, 当时我们曾想过引入一个叫 TrafficRollout 的对象,TLB 和 TrafficRollout 的关系 就跟社区Deployment 对象和 Deployment Rollout 对象的关系类似。这样无论是更改 TLB对象里的Provider 还是 Weight 字段,都会生成一个新的Traffic Rollout 对象,真正的操作让Traffic Rollout 的Controller 负责,这样还方便 Rollback。最后因为项目时间约束和简单起见,没有这样做,而是变成了用 Admission 阻止用户修改 TLB 的 Provider 字段。这里再次强调一下,Controller 可以拒绝用户的 Intention 但是不可以篡改。
“
系统模块应该被设计成有能力自我修复。如果缓存了对象Status, 那Status最好以比周边系统检测周期更小的时间窗口内被周期性刷新,以使得在出现错误存储或漏掉删除事件时,系统可以及时自我修正。
这一条前文在整个集群不变量这一部分已经有详细的介绍,这里主要介绍一下检测周期的问题。在Kubernetes 出现之前,eBay 操作Load Balancer 的服务叫做 LBMS (Load Balancer Management Service ), 我们在 API Server 里面构建一个LBMS 的 Cache 放在相应对象的Status 字段里, Controller 每一次调用 LBMS 都需要把相应结果写进API Server , 但是为了避免Phantom Write 的问题,在API Server 和 LBMS 之间还有一个周期性的全量Sync, Sync 周期为W1, 如果Controller 依赖Status 对象做决策,那决策需要等待一个W1。而实际情况是 LBMS 的实现也是真实设备的一个Cache, LBMS的Cache和真实设备之间的全量Sync周期是W2。那Controller 的决策逻辑需要等待多长时间呢?
还有一种取巧设计在我们的IP释放和重用逻辑中,我们把IP 的分配队列设计成先进后出,也就是说如果一个IP 被释放,该IP 会放在可用IP 队列的最后,这样给周边系统足够的时间完成Sync。
我们来看一个Istio rollout 的真实事故案例。在写本文的时候我们还没有做到不切流量实现多个Cluster 的 Istio rollout, 也就是说在多个cluster 的rollout过程中,我们会先把 Cluster 上的流量切走,再对该Cluster 进行 Istio 升级。而切流量的 Kubernetes CRD 就是在前文中介绍的 Admin Traffic Control。
我个人认为私有云的基础架构组建,设定capacity quota 的意义不是很大,早年没有quota的设计,后来有了quota就需要quota审批,然而基础架构关键服务的审批都是100%通过的,继而又搞自动审批,折腾了一圈,花了很多时间很多钱,效果跟一开始没有审批一模一样。问题的本质不在审批和自动审批,而在监控和自动伸缩。对于上面表中的事件2和事件5,在系统设计的时候就应该做自动伸缩,同时为了保证自动扩容的成功率,平时的巡检系统要保证有一定余量的额外容量。
“
系统中的组件行为应该被设计成可以优雅降级。系统过载或部分失效时系统中最关键的动作应该继续工作。
就拿上文Istio rollout 那个事故为例,其容量无法回滚根本不是设备无法完成回滚配置,而是我们各类的控制面的问题造成的。我们假设现在有一个重大事故导致eBay 网站无法服务用户必须马上回滚流量,而我们的K8s不工作了,我们会如何?结果就是直接上设备敲命令回滚,配置管理系统的数据同步等流量回复以后再去修复。当我回顾这些设计的时候,发现不是大家想不到,而是在一开始就没有规定系统交付的可用性指标,在公司里很多事情不是设计问题,而是优先级问题,是ROI 问题,是如何定义问题的问题。
架构
“
系统组件在无法拿到新的指令时应该继续执行最近拿到的指令 (比如发生网络脑裂或者组件级失效)
前文提到的 Tess Load balancer 说到,在一个TLB Cluster 中一个POD 丢失到 API Server 的链接后我们当前的默认行为模式是优先可用性,于是在该TLB Pod 上的IPVS规则可能已经过期, 即使实现了 Level Trigger 进行全量数据同步的逻辑在,如果该POD到API Server 的链接不恢复,该 TLB controller 的也是拿着过期的Spec 在进行reconsile。
我们再看 Node 上的kubelet 如果丢失到API Server 的链接:
链接丢失Node Controller 就会注意到API Server 上对应的Node 对象的心跳丢失,node-monitor-grace-period 时间后,Node Lifecycle Controller 就会把该Node 标记为unrecheable,not-ready , 同时把这个 Node 上的Pod 标记成 Not Ready。
Endpoint Controller 会把这些Pod 标记成 NotReadyAddress, 然后这些 Pod 的 DNS 记录就会消失。
pod-eviction-timeout时间后 Node Lifecycle Controller 开始进行 Eviction 操作。注意那个时候丢失到API Server 链接的Node 上的kubelet 还是无法链接API Server, 自然也无法知道需要进行 Eviction 操作,于是该kubelet 就按照丢失API Server 之前所存储的状态进行操作。
注意虽然Pod并没有被evict , 但是在第二步的时候所有直接通过 POD DNS 来访问POD的客户端就已经发现POD无法访问了。
事实上我们加了更多保护,如果一个集群中超过5%的 Kubeternetes Node 汇报Not-Ready, Node LifeCycle Controller 就暂停继续mark Node 和 Node 上的POD 到 NOT-Ready了。
“
所有的组件应在内存中保存相关的状态。只有API Server 可以写ETCD, 其他组件通过API Server 来做对象的增删改查,组件之间的通讯都只能通过API Server 进行。
这条规则就是再次强调在Kubernetes的世界里,Spec 就是API , Controller就是内部实现,不同组件之间只能通过API 进行交互,这个API 的交互就是 Kubenetes Spec , 也就是说交互的媒介只能是 API Server。整个Kubernetes 的设计都遵循这个原则,我们甚至可以把Kubernetes 默认自带的controller 都换成自研的controller。谷歌的很多系统都是设计游戏规则,游戏中的具体组件可以被替换,上到Controller, 下到 Container Runtime Interface, Container Network Interface 都是体现了这样的设计精神。
“
跟API Server 交互优先使用 Watch 而不是 Polling。
在实际工作中,大部分的代码在可以使用watch 的时候都是使用的,有一些场景也不得不使用 List ,即使这样我们在可能的情况下也都推荐使用 list-watch 机制了。我们碰到的大部分问题还是客户端在做发布重启的时候的第一次list 操作击穿 API Server, 特别是DaemonSet, 在做发布的时候情况最严重。因此我们严格审查在客户端代码中把 resource version 设成 0 跳过API Server Cache 的情况。
在2023年API Server 出现过几次事故,其中最大的一次碰巧因为各种原因让公司测试环境的两个K8s 集群的 API Server 停止了工作,而我们的CI 环境是依赖于API Server 的,造成全公司1000多个Build 受影响。为此我们做了很多的优化工作,这部分相信在2024年负责API Server 稳定性的同学会写文章发表在eBay 技术荟上。
END
参考资料:
1.Kubernetes Design Principles: Understand the WHY
2.https://www.youtube.com/watch?v=ZuIQurh_kDk&t=2s
3.https://github.com/kubernetes/design-proposals-archive/blob/main/architecture/principles.md.