Kubernetes 架构学习笔记(上)

文摘   2024-07-05 11:09   上海  

编者注:本文作者许健为eBay中国研发中心云计算开发总监。全文分上下两篇。本篇为上半部分,围绕API展开;后续推文将发布下半部分,包含控制器逻辑、架构等内容。


我把eBay 云计算部门的效率看成三个层面,第一层是代码开发的效率(包含持续集成持续发布), 第二层是架构效率,第三层是产品效率。这一篇博客主要讨论第二层架构效率。这件事情的缘起是一些架构讨论悬而不决,三年多前有一天我找到云计算部门的总架构师讨论一个设计困惑,他突然问我说许健你看过 Kubernetes 的设计原则吗?他指的就是这个链接:https://github.com/kubernetes/design-proposals-archive/blob/main/architecture/principles.md。


在过去的两年中,我协助eBay云计算架构师做eBay云计算部门系统架构设计审核会议的组织协调工作,于是萌生了用我们日常工作中实际的架构设计讨论来深入理解社区 Kubernetes Design Principle 的想法。文中例子来源于真实的设计迭代, 我仅是做了汇总。借此机会感谢所有为云计算系统设计做出贡献的同事, 向你们学习才让我这些年对Kubernetes 的设计理念有了进一步的理解。


API

所有的API 都必须是声明式的。

声明的是用户希望的终态而不是在API上暴露实现的具体步骤。先来看社区的设计:


下面,我们来对比过程式API 声明式API 的区别:在用户提交了该Spec 后,如果 my-app 的三个容器其中一个崩溃了,或者承载容器的K8s物理节点(我们叫这样的节点 K8s Node)崩溃了,在过程式API设计思维下通常我们会设计另一个组件(我们暂且叫它 master 组件)来负责自我修复,Master 知道什么叫希望的状态,Master 监控容器和 K8s Node 健康状态并指示服务器完成修复,这样的不足是有可能明天我们会发现 K8s Node出现短暂的性能抖动需要处理,后天我们发现DaemonSet 也需要做自我修复......于是Master 组件就会越来越复杂而越来越难以维护和扩展。而在声明式API 的设计思路下,一旦服务器接受了这个replicaSet 的Spec,  就相当于在提交者和接受者之间签字画押,接受者必须自行保证系统在协议时间内到达Spec 所描述的期望状态。


TFAP(Tess Federated Access Point) Virtual IP

迁移讨论心得


为了方便兼容社区API,  我们把 gateway 的Spec 定义直接放在 TFAP 的 Spec 定义里:


注意这里selector 里选定的 istio: ingressgateway-rnpci-01 , 在我们当时的实现中这里 ingressgateway-rnpci-01 可不是一个label 而是一个真实的物理实体名字的唯一标识。这是因为在投入生产环境后流量是会变化的。我们就曾碰到了某一个 Software Loadbalancer (SLB)太热的问题,网站应用流量在不同的SLB 之间分配不均,需要把给定的一个 TFAP 实例的流量迁移到另一个 SLB 上,在这里也就是迁移到不同的 Gateway 上。但是我们刚才说了 在declarative API 的世界里,已经签字画押当前 TFAP 就只能在 ingressgateway-rnpci-01 上,这代表了提交该TFAP 的用户期待,是不能随意改动的。


这时候我们注意到社区replicaSet 设计中的一个细节:template 字段,社区replicaSet Sspec 中的 Pod Spec 其实是一个 Template 而不是真实的 Pod Spec 实例。如果在这里做一个对比,在replicaSet 中的Pod Spec Template 里默认是没有Node Selector 字段来限定将Pod调度到哪个K8s Node的。于是我们改了 TFAP 中 Gateway Spec 的定位,TFAP Spec 里 Gateway Spec  不再是具体实例而是 Template。


还有一个问题,我们注意到 replicaSet 的 Pod Spec Template 里面并没有 pod name哦,那我们TFAP的Gateway Spec Template 里面的 name 还需要保留吗?


方案一:移除

方案二:保留,但是 gateways.metadata.name 已经不再被理解成 gateway 的名字,而是一个模板中用来产生真实名字的 baseName.  


方案一 的难处在于 TFAP 里面还有 Virtual Service Spec Template,而Virtual Service Spec Template 要引用Gateway Spec Template。



API 对象必须互补正交可组合,而不仅仅是一个黑盒包装器。


这个设计原则的英文原文是:API objects should be complementary and composable, not opaque wrappers.


这里牵涉到API 设计的问题,我们可以把这个问题看成搭积木,但我更偏向于把这个问题类比数据库表的设计,或者面向对象的设计。市面上有很多关于数据库范式设计和面向对象设计相关的书籍,里面的思想可以被参考。


正交可以理解成没有冗余信息,对象 A 里有一个字段 x, 对象B里有一个字段 y,不存在 y = f(x) 的情况。除非为了缓存,否则不建议在对象 B 中存放 y。在Kubernetes里API 就是Spec,对象A 和对象B的 Spec 中的字段对于用户来说都是公开的。如果在对象B 的 Spec 里有y 字段而用户去直接修改y 字段,因为不正交,为避免出错,我们就必须给对象 B 添加一个 admission 去检查用户修改完的 y 是不是导致 y = f(x) 不成立。而组合就是搭积木了。


正交组合的设计非常考验架构师的功底。在实际工作中我们面临的任务,项目种类很多,我们并不希望每来一个新的项目,我们都要需要大量投入,而总希望能够复用一些之前的积累。同理,如果无法复用、需要新的投入,我们也希望新的投入可以被将来复用。好,现在来了一个任务,我们可以把这个任务在实现中划分成 X1, X2, X3来做, 也可以把它划分成 Y1, Y2, Y3来做,怎么划分才可以使得 Xi 或者 Yi 将来被复用的可能性增加? 


API First Design


eBay的一个安全要求是全网每月做一次由基础架构部门发起的Security Patching。Pathcing 很多时候需要重启服务,这一挑战在于Security Patching的同时不能影响业务的高可用性。之前,我们的做法是基础架构部门提前发邮件给业务部门协商维护时间,待业务部门确认后,基础架构开始实施(比如重启服务,物理机patching 也会导致服务重启,交换器升级会导致更多服务受影响)。基于人和人的沟通是无法自动化的,基于API 的才可以自动化,我们把各个部门变成各个组件,把部门与部门之间的口头或者书面协定变成 API,如果发现为了完成一件routine 的事情,需要重复的人和人的沟通,那就是一个缺失API 的信号。


Kubernetes 的API First Design

Kubernetes 往往跟“云原生”一起出现,体现在API 设计上一个显著特点就是从应用视角设计API,哪怕任务的发起端不是应用。比如上面谈到的Patching,  是基础架构的Kubernetes Node,交换机要升级维护,业务应用为什么要关心这些呢?我们刚才把各个部门变成各个模块,那A 部门去跟B 部门谈的时候是侧重B 部门视角去谈更容易谈成还是侧重A部门自身视角更容易谈成呢?比如在eBay业务部门有很大的业务压力,基础架构最好搞定这些Patching的事情,不再消耗业务部门的精力了。Kubernetes 的设计是复合这一个思路的。这里给一个例子:


在eBay,应用A 通过AccessPoint 访问应用B, 这里AccessPoint 是eBay 自己定义的一个在Service 之上的高阶对象。AccessPoint 为了性能考量,如果应用A的 Pod 和B的 Pod 在同一个Cluster 内部,A的 Pod 其实是通过 Kube Proxy 直接访问B 的Pod 的;而如果不在同一个Cluster 内部,那A 的Pod 是通过 应用B 的 Load Balancer VIP(负载均衡器的Virtual IP ) 访问。但是在某些情况下,比如应用B 因为安全合规,规定必须HTTPS, 而HTTPS Termination 在 Load Balancer 上完成, 我们就需要即使在同一个集群内部,应用A 的 Pod 不应该使用 KubeProxy.


基础架构视角的设计

在应用B 的AccessPoint Spec 里面添加 skip-kubeproxy 的字段或者 annotation

应用友好视角的设计

如果AccessPoint Spec 里说是HTTPS, 由AccessPoint Controller 推导出skip-kubeproxy 的行为。


即使为了Kubernetes 组件件协作方便还是需要添加skip-kubeproxy 的annotation, 这个annotation 也是由AccessPoint Controller 添加而不是要求客户(Spec的创建者)添加


这个问题的出现,其实是因为基础架构团队把实现做成了基础架构视角,同时也给自己带来了不少客服问题。业务端的同事不熟悉什么叫kube proxy , 客服问题多了以后还需要基础架构团队做客户培训专题会。所以,更好的办法是让这些事情都不要发生。 


API Design 的人性考量

API 本质上是一个模块间的Contract。在团队中,我们需要考虑怎样做才能激发协作积极性。我们虽然通过API 把团队协作变成了模块交互,但很多事情不是一蹴而就的,比如timeout 参数什么设定?比如服务不可用的时候承诺的恢复时间是多少?以每一个月的Patching 来说:


基础架构视角

同时考核是否在一个月内完成K8s Node, 交换机升级维护和不要因为升级维护造成站点业务不可用。

业务部门视角

考核业务高可用。


我们设计了应用视角的 Health Monitor 对象作为应用是否健康的API , 我们在 PDB (Pod Disruption Budget) 的基础上设计了应用视角的 ADB (Application Disruption Budget ) 作为应用N 个节点允许同一时间失去多少节点的API。


API

基础架构行为

应用行为

Health Monitor

Health Monitor为False时基础架构不能继续patching

如果是大数据有状态应用,假设5拷贝中有3拷贝完成数据同步就可以恢复服务,是否要等多一个拷贝完成同步?同一个时间让多少节点进行同步,并行度越高,完成同步时间越长,但并行度越高,一旦出问题受影响范围越大。

ADB

基础架构每一个批次的patching 都不能违反ADB 的约束

 ADB 设置越大,风险越高,因为在同一个时间会损失更多的应用节点。


应用行为并非黑白问题,而是一个需要权衡的灰度问题。应用端在实施过程中偏向安全是很自然的行为结果,但是其结果是基础架构会面临无法在一个月内完成全站patching。这里面的一个问题就是在 API 的设定中缺少了 “时间约束”。在这个工程中还需要考虑是由基础架构来设定时间约束还是安全合规部门设定?时间约束违反时的责任行为如何?谁来跟踪这个责任行为从而形成反馈以完成持续优化?一个团队对于一个产品的后续支持模式而由此决定的每一天的工作内容很大程度上在API 的设计期就决定了。我们仅仅在设计API 吗?不是的,我们还在设计我们的日常工作模式。


交换机升级问题


假设一个机架(Rack)上配置一交换机(Switch),交换机升级时该Rack 网络不可达。有一个应用A 使用Deployment 来描述,有40个副本,设计 PDB (Pod Disruption Budget) 的Max Unavailable replica 数目为 4。如果这4个副本碰巧都在需要进行维护的Rack上,那基础架构就永远无法升级该交换机了。于是我们知道该应用A还需要定义Anti-Affinity。但是 Kubernetes Deployment 结合 Affinity 和 Anti-Affinity 并没有原生的描述 “每一个Rack 不允许调度超过4 个应用A的Pod” 的能力,在我们面临这个问题的时候 Kubernetes 还没有 Topology Spread Constraint


我们看一下eBay 的NAP (Nudata Automation Platform ) 给的方案 (以下内容来自NAP Anti-Affnity 的文档):


在实现有状态应用程序的弹性时,容忍一台机架故障是一个重要的要求,特别是当它们部署在Tess(eBay 内部基于Kubernetes 的云平台Code name )上时。当Tess团队进行操作系统升级时,他们会逐个机架升级物理节点,这意味着应用程序需要限制当一个机架中的pod同时重新启动时数据丢失的风险。通常,我们启用机架级别的反亲和性,使一个应用程序集群的pod可以分布在尽可能多的机架中。配置为一个反亲和性组的pod将不会被调度到同一个机架上。目前的常见解决方案是将一个应用程序集群的pod配置为一个反亲和性组,但这将导致新的问题。


问题1:机架数量有限,像kafka、elasticsearch这样的应用程序在每个pod中存储分区数据。通常每个分区可能有两个或三个副本。每两个pod有可能存储同一个分区的副本。这意味着如果每两个pod同时重新启动,应用程序将面临高风险。因此,理论上需要确保每两个pod不应分布在同一个机架上。然而,每个应用程序集群可能有30-50个pod。机架总数远远小于pod数量。我们无法确保一个集群中的每两个pod被调度到两个不同的机架中。唯一的方法是依靠Tess机架反亲和性的最佳努力调度策略。


问题2:负载不平衡,pod级别的反亲和性不足,我们仍然需要考虑如何将分区数据的副本调度到pod中。一些应用程序支持机架感知功能,这意味着它将在不同的机架上分布相同分区的副本。但是,正如我们在问题1中提到的,我们只能使用最佳努力策略来分配pod。它不能确保pod将均匀分布在机架中。在某些情况下,一些机架将有多个pod被调度,但有些机架将只有极少数的pod,这意味着少数pod将需要承担更多的流量。这将导致少数pod的工作负载过高,并给应用程序带来高风险。


为了解决上述问题,我们需要满足两个要求:第一,平衡有限机架中的pod;第二,将副本平衡分配到pod中。反亲和性组让我们将一个集群的pod分成几个小组。例如,如果我们有一个包含50个pod的kafka集群。在生产中,创建的kafka主题通常为每个分区提供3个副本。然后我们可以将每个3个pod分成一组,并将它们配置为相同的反亲和性组,然后这3个pod将分布在3个不同的机架中。这样,每个分区可以轻松地找到一个组来分配3个副本。然后所有副本都可以平衡地分布在所有的pod中。Kubernetes支持pod亲和性和反亲和性的功能。我们将利用pod反亲和性功能来确保一个组中的任意两个pod不会被调度到同一个机架上。"preferredDuringSchedulingIgnoredDuringExecution"表示反亲和性的“硬”要求。这个策略将确保组中的任意两个pod不会被分配到同一个机架上。


实例Spec 


现实工作中,我们面对的世界往往是不完美的。有些应用一开始都没有设置AntiAffinity,  公司也有成本因素没有足够的capacity 来支撑那么多Anti-affinity 等。因此,我们的系统需要感知线上哪些应用是不满足交换机升级要求,在monthly patching之前去创建缺失的Anti affnity,调整不满足要求的Anti affinity,而在做 Patching 的时候优先保证用户应用能够回来,所以很多时候都是设置 requiredDuringSchedulingIgnoredDuringExecution,甚至是  ignoreDuringSchedulingIgnoredDuringExecution,然后再慢慢调整,尽力而为。


Batch 和 CI As A Service 的设计挑战


eBay 有不少 Batch Job 在处理财务报表,一些Job 跑一遍需要数个小时,而且Job 并没有能做到“断点续传”,一旦被打断,就要从头算,而从头算会导致无法准时提交财务报表。CI As A Service (CIAAS) 有类似的问题,业务开发团队不希望 CI(Continuous Integration)被打断。这两类服务都要求在 当前Job 跑完以后才可以让基础架构进行Patching。


拿CIAAS 为例,eBay 是基于 Jenkins 实现的。如果我们把一个 Jenkins Instance 模型成一个 应用,因为一个Jenkins Instance 都只有一个 Jenkins Master,  所以在这个应用上 Apply Pod Disruption Budget 是没有意义的。那换一种思路,如果我们把整个CIAAS 集群模型成一个应用,  然后设置 PDB,实施上可以同时Unavailable 的 CIAAS的数量取决于我们是否有同样多的备用POD可以做灾备HA。而这个灾备HA 还是 Active Standby 模式,也就是说需要额外的Capacity 成本。



控制面透明,没有内部API


关于 Kubernetes Spec 和 Controller 的关系, 没有内部API,先来看一个 Pod Spec:


再看一下Pod 创建的流程图,特别是其中跟 Scheduler 相关的部分:


这里 Scheduler 只跟API Server 打交道。对于 Pod 来说, Scheduler 的职责就是填充 Pod Spec中的nodeName 字段,其实Pod 并不知道Scheduler的存在。我们可以替换 Kubernetes 的默认 Scheduler, 只需要保证替换模块会 Watch new Pod 然后填充 nodeName字段就可以了。而这里Pod Spec中的nodeName字段是public的,所有的API Contract 都通过API Server来实施,注意这一条设计原则正是 https://github.com/kubernetes/design-proposals-archive/blob/main/architecture/principles.md#architecture 架构那一节提到的 “Only the apiserver should communicate with etcd/store, and not other components (scheduler, kubelet, etc.)” ,即在谈 API 的时候,我们只谈论 Spec 而不谈论 Controller, 因为在这个游戏规则里,API 就是 Spec,Controller是可以被替换的。



对象状态必须通过观察可以被100%重建。任何保存的历史记录只能被用于系统优化而不能成为纠正系统行为的必要依赖。



这里 phase: approved 是不可以放在 status 字段里的,因为无法通过 wisb , wiri reconcile 重建。


Conditions


Conditions 也应该是可以被重建的。


为什么 POD 的nodeName 定义成pod spec ,而不是 pod status


nodeName 被定义在 spec 中,因为它可以被视为用户对 Pod 调度的一个期望。当用户创建一个 Pod 时,他们可以指定 nodeName 来请求调度 Pod 到特定的节点上。如果 nodeName 被设置,调度器(Scheduler)会尝试将 Pod 调度到这个特定的节点上。如果没有设置,调度器会选择一个合适的节点,并将 Pod 调度到那里。



整个集群的不变量(Invariants)很难正确保证。尽量不要使用它们。如果必须要有集群不变量,不要在master组件中原子性地执行它们,这很容易出现争用,并且在有Bug导致不变量被违反的情况下无法恢复。相反,提供一系列检查来减少违反的可能性,并使涉及到的每个组件都能够从违反不变量中恢复。


如果要在 Kubernetes 集群中使用全局不变量,我们应该以非争用的方式提供多重检查,并确保每个相关组件都能够在检测到不变量违规时自我恢复。


下面是一个具体的例子来进行说明:


假设,我们在Kubernetes集群中需要确保不同部署(Deployment)的容器副本数之和不超过集群中节点总数的一定比例。比如,我们要求所有部署中容器的副本数之和不超过集群中节点数的 80%。这是一个需要执行全局不变量检查的场景。如果我们仅仅在 master 组件中执行全局不变量检查,通常会采用加锁机制来保证正确性,但是这会导致争用和性能问题。此外,如果在 master 组件中出现全局不变量检查的 bug,那么可能导致系统无法自我恢复,从而出现系统崩溃的风险。


因此,我们可以采用以下建议来弥补这些局限:每个部署都提供检查,以确保容器副本数不超过某个限制。这些检查可以使用 Kubernetes 中的资源限制来执行。


我们可以创建一个独立的仲裁组件(例如 operator),以检查每个部署中副本的总数是否超过限制,并向相关组件发送警报或者执行无害恢复操作,例如暂停一个部署的扩缩容流程。如果需要,我们可以通过配置来请求外部控件(例如 NetworkPolicy和名字中带着Policy的对象类型往往使用同样的设计思路),其可确保没有群集资源超出计算能力的负载。这样可以确保容器无法保留过多的运行资源。


现在我们来看一个在eBay 生产环境中真实发生的案例,并且这个问题历经3代云计算系统都没有被彻底解决:


事故原因


在eBay 云计算平台出现过多次IP 重复分配的事故,事故发生大多是这样的过程:


1

假设应用A的多个POD在服务VIP为VIP-A的流量请求。


2

应用A的一个POD, Pod-A-1 被删除了,相应的Pod IP: IP-A-1被释放回IPAM (IP Allocation Management 系统)。


3

可是Ip-A-1 并没有在负载均衡器设备的VIP-A 的后端  Server 流量分发配置列表中被删除。

4

IPAM把IP-A-1分配给了应用B的一个Pod: Pod-B-1。


5

我们配置在流量均衡器上VIP-A的健康检查把健康检查的请求发给 Pod-B-1的时候仍然返回健康,因为健康检查并不检查应用 ID。


6

用户发往应用A的流量被负载均衡器转发到了应用B 的后端Pod 上。


下面是具体的事故案例:

影子IP

  1. 配错了 LB Management Service 指向了 物理负载均衡器的Slave 节点而不是Master 节点

  2. 代表LB member 的K8s 对象 K8sLbMember 被 Mark 成等待删除,此时K8s 无法删除改对象,因为这个对象上有一个 物理LB的Finalizer。

  3. 调用 LB Management Service 操作物理负载均衡器删除设备上的 LB member ,  设备返回成功,于是LB Management Service 返回成功,摘除 K8s 对象 K8sLbmember 上的Finalizer , K8sLbmember 对象被删除。

  4. 物理负载均衡器的Master to Slave sync 把在Slave 上被删除的Lb member 对象又创建出来。

  5. K8sLbmember 对应的IP 被系统分配给其他应用。设备上K8sLbmember 对应的IP 成为影子IP。

模型泄漏

在 eBay 的网络模型中,跟IP 有关系的实际上是两类对象

  1. IP Allocation:   这个对象里面还有具体的IP Address 字段,这个对象上可以放置多个Finalizer 

  2. Pod,  LB Member,  LB VIP 等对象:  每一个这类对象都关联一个唯一的IP Allocation 对象。


如果任何2中的对象被删除,但是其关联在IP Allocation 对象中的Finalizer 没有被摘除,这就是一个模型泄漏,其结果是其关联的IP Allocation 成为孤儿IP,  无法被重新分配,最终导致IP耗尽。


如何避免


当涉及到 Kubernetes 集群的网络和 IP 地址分配时,就涉及到全局不变量。在eBay 我们不单单需要确保在整个集群中,每个容器都分配了唯一的 IP 地址, 而且需要确保在整个云计算平台100多个集群中POD, VIP(Virtual IP) 的IP 唯一。由于 Kubernetes 集群中有很多不同的部件和节点都需要使用 IP 地址,和100多个集群需要管理,要完全保证 IP 地址分配的唯一性是困难的。如果我们尝试在 Kubernetes 的 IPAM (IP Allocation Management)中实现一个原子的全局 IP 地址校验检查,那么这些 IPAM组件将需要处理大量的并发请求,会导致性能问题。而且,如果检查出现任何 bug,可能会导致群集停机并需要繁琐的恢复操作。


在上一代云计算系统中承担这个责任的是一个叫RAS (Resource Allocation Service) 的集中控制系统,这个系统在分配IP 的时候,需要做以下检查:

  • 扫描IP 配置管理系统,找到空闲 IP;

  • 拿着步骤1 中待分配的IP 做 Ping 检查( Fast Ping) 和 反向 DNS 检查。


上了 K8s 以后,我们的系统是这样的:

  • IP Allocation Manager  (IPAM )并不是全局的,而是将整个eBay 的数据中心切成多干个Availability Zone(AZ),  每一个AZ 管理若干个 K8s Clusters,每一个AZ 一个 IPAM,每一个 IPAM 所管理的网段没有重叠。


  • 每一个 IP Allocation 对象上都有跟它关联的POD, LB VIP, LB Member 对象的Finalizer , 只有所有的Finalizer 都摘除,该 IP Alloccation 对象才能被释放并重新分配。


  • 在 IPAM 分配的时候,做类似于 上一代 Cloud 系统RAS 类似的 Fast Ping 和 反向DNS 检查,为了避免影响分配性能,可以把这个检查做成 cronjob offline 来做,发现有问题的IP 或者网段后Mark 成不可分配。


  • 构建Invariant 系统做数据一致性检查


Invariant 检查

传统数据库设计是通过范式来完成基础的数据一致性检查,再复杂点就需要程序来检查,很多业务系统都会构建Business Consistency Management 系统来检查不同系统的业务数据一致性。用Batch或者Near Realtime甚至realtime 检查数据一致性违规。为了方便扩展,很多系统引入Rule Engine 把业务检查逻辑抽象成Rule。


在eBay 的 IP 管理系统中,我们也可以抽象出以下规则: 


  1. 任何IP Allocation对象都必须有网络实体对象 (比如POD,  LB VIP,  LB Member 等)关联。

  2. 一个LB Pool 只能关联0个或者 1 个 LB VIP 对象。

  3. 一个IP Allocation 对象只能最多关联一个Service 对象。


至于实现: 

我们可以不抽象Rule,  比如用 Finalizer 来实现。比如“一个 IP Allocation 对象只能最多关联一个 Service 对象” 就可以通过在 IP Allocation 对象上放置 Service 对象的finalizer 实现。这等同于 realtime 检查。


上文中提到的巡检系统相当于 Batch 检查。我们也就可以把Batch 检查的逻辑变成 realtime 检查,相当于以牺牲性能为代价换取实时数据准确性。当然我们也可以把设备配置,配置管理系统中的对象缓存起来,付出额外的空间来避免牺牲性能。


IP 分配的冷却时间窗口

参见下文控制器逻辑部分第5条:如果缓存了对象Status, 那Status最好以比周边系统检测周期更小的时间窗口内被周期性刷新。


如何恢复


我们很早就接触了5WHY的思维模式来理解一件事情发生的根本原因,这里我想提一个5 Next 的概念来减少后续维护成本。这里假设即使有了上面提到的所有的保护,IP Leak 和 IP Duplication 还是会出现,我们会怎么做?以前我们会写一个 SOP ( Standard Operation Procedure) 指导Oncall 的同学在收到Alert 以后该如何处理。现在的思路是在系统设计的时候就把SOP 做进去。


表5.1

No.

模块

如何进行保护

如何进行恢复

A

IPAM 和 设备

对于IPAM 里面认为健康的IP 资源池进行健康巡检,把健康巡检过程中检测出来的有问题资源标记不可分配, 并对每一例这样的案例追溯根因。

巡检IP 相关对象,如果发现设备上没有,而配置管理系统里有,自动清除配置管理系统中的对象。


如果设备上有,而配置管理系统中没有,清除检测设备上的对象如果没有检测到流量。


对于每一例自动恢复的案例都自动记录,用以追溯根因。

B

Global IPAM 和 AZ IPAM

Global 是single source of truth, AZ 是为了高可用和性能考量产生的。指定Global 和 AZ 之间同步的数据质量校验策略,比如如何避免两个 AZ IPAM把相同的Subnet 从Global 同步到各自AZ?

巡检Global 和 AZ  之间的数据一致性,一旦发现数据不一致,以Global 为准进行修复。比如Global 上的 Subnet 被AZ 同步时进行引用计数以避免重复分配。


不能确认的,自动Mark 不可分配并且告警以便追溯根因。

C

NetD (eBay K8s Node 上的网络配置模块)

K8s 在Pod调度到Node 的Schedule 过程中默认并不保证目标Node 上的 网络组件(比如Container Network Interface : CNI) 可以获得有效 Pod IP 


引入Node Network Taint Controller 巡检Node 的可分配IP,  Taint 有问题的Node.  在IP 余量恢复时再自动 Untaint 


注意:eBay 的云计算网络组和数据中心网络组隶属不同,云计算网络组通过数据中心网络组提供的API 来进行交换机/路由器配置更新或者查询。如果分配的IP 无法正确进行ARP , 无法ping 通所在subnet 的Gateway,  很有可能是设备上Subnet配置有误,不排除数据中心网络组提供API也出现后台配置数据库内容和设备之间不一致的现象。

构建Node 的自动IP refiller 来补充IP 地址。


用arp或者 ping 进行subnet 可用性检查,标记subnet 不可用并告警以便追溯根因。




我这里有一份网络组记录的各种问题和造成问题的直接原因的列表,我们可以对照并深入思考一下文中上面提到的方法在多大可能解决了问题,是否还有更好的系统性的方法来根除这类问题。


netforce:配置三层交换机的组件名称

bridge group是网络概念:为了实现不同端口之间的桥接交换,必须将这些端口归入同一个网桥组中


表5.2

No.

问题描述

如何恢复 (表5.1 No. )

1

通过neforce 添加subnet 失败,Port 已经存在,Bridge Group 删除API 不稳定

IPAM 和 设备(netforce):  表5.1 No. A

2

从global IPAM 取得可分配subnet 后调用 netforce , netforce 汇报输入subnet 和其后端数据库中的 subnet 有 overlapping

 IPAM 和 设备(netforce):  表5.1 No. A

3

Netforce 汇报调用参数无效,比如 输入参数中对象 还没有被同步到netforce DB

IPAM 和 设备(netforce):  表5.1 No. A

4

AZ IPAM 问Global IPAM 要更多subnet 的时候,Global IPAM 回答没有IP了

统一 Global IPAM 和 Netforce 补充Subnet 逻辑

5

添加 Subnet 时netforce 汇报 对应的 PTR 记录已经存在

IPAM 和 设备:  表5.1 No. A

这里的设备从三层交换机扩展到DNS

6

后端配置管理系统成功更新,在netforce 去更新设备失败

Netforce 和 设备间信息同步,可以把API设计成同步,此时直接返回失败,或者做成eventually consisitency 的异步系统,此时配置对象上应该有状态标记

7

IPAM 创建subnet 把partial success 当成 success , 设备更新没有完整完成

8

删除  Bridge Group 失败,因为待删除的BridgeGroup 里面还有Allocation

IPAM 和 设备(netforce):  表5.1 No. A


总结


在跟网络组的同事进一步讨论后,把解决方案分成以下三项:


1- 统一controller 编程风格:  提供分配IP, 回收IP 的库函数,使用Template 设计模式来操作跟IP 相关的 POD,  LB VIP,  LB Member,Service 等对象,  ,让组织内开发人员使用同样的风格来编写跟IP打交道的组件,避免出错。一个耳熟能详的做法就是在库函数内部登记对象从属关系删除父对象的时候由库函数保证级联删除,避免手工一个一个调用漏掉出错。


2-Invariants checking 


3-把自动修复做进系统成为系统本身的一部分,而不是一个事后应急的脚本。



低级别的API应该被设计成可以被高层级系统控制。高层级API应该以意向为中心,而不是以实现为中心。


Federated Deployment 的设计



Federated Deployment 是Depoyment 之上的高阶对象,以实现跨越多个Cluster的部署。


strategy.base其实就是Deployment Spec, 而strategy.placement 和strategy.rollout 用来描述意向,我们在 FederatedDeployment 的 Spec 里面并没有看到具体的每一个Cluster的 replica 的数目,也没有看到具体的rollout plan,因为设计者不认为这是大多数的用户意图。


FD-Fed 就是部署在Federate 层的FederatedDeployment Controller ,它先调用ResolvePlacement 把 strategy.placement 的抽象意图转换成具体的每一个Cluster里面到底是多少个replica, 回填到 FederatedDeployment 的Spec 里面,注意上面的用户Spec 里面spec.deployments 是一个数组,目前只有name=fcp 一个元素,ResolvePlacement 之后,spec 里面会添加如下 deployment 元素:



这里有一个权衡,就是设计的时候是否让用户直接输入placement 信息,目前eBay 的FederatedDeployment 设计是允许的,所以resolvePlacement 回填的位置是Spec而不是把Placement 的信息放到一个别的对象里,而在我们做出这个回填位置是FederatedDeployment 的Spec 的时候,意味着回填信息也变成了一个用户意图,于是就必须多引入一个ValidatePlacement的逻辑来处理spec.deployments和 spec.deployments.fcp.strategy.placement 冲突的情况。


而对于strategy.rollout,  FederatedDeployment 的设计决定是创建一个新的rollout 对象,具体的Rollout 的计划是保存在 Rollout对象而不是回填到 FederatedDeployment 的Spec里面。如果有可能,我们尽量不要给描述用户用意的Spec添加字段,因为每添加一个字段就等于添加一个约束,Kubernetes 的设计原则是可以拒绝用户Intention, 但是不可以修改用户Intention的。


这里还有一个细节,注意到我们有FD-Fed 和 FD-Cluster 两个对象,FD-Cluster 的信息是和FD-Fed一模一样的,只不过一个对象在Fed 层,一个对象在Cluster 层,而Cluster 内的Deployment 对象是由 Cluster 层的FD controller来管理的。Fed 层的rollout 对象并不会绕过Cluster 层的FD 来直接操作 Cluster 层的Deployment 对象,为什么呢?这跟当时eBay 云计算的架构师的理念有关,他想构建高可用系统,想给将来的变动留下优化空间,注意最上面的FederatedDeployment Spec 里面:



正常情况下Cluster 级别的FederatedDeployment 是由Fed 层面的Controller 管理的。这个信息明确的写在了supervisor 字段里。如果Fed 层垮了,设计上是可以允许将任何一个Cluster 的FederatedDeployment Controller 升级成supervisor 的。,为了达到这个目的,在正常情况下每一个Cluster 的 FederatedDeployment 对象都必须有全局信息,也就是说Fed和Fed 管理的所有的Cluster 中的同一个key 所指向的FederatedDeployment 对象都是eventually consistency 到一样的。,这也就是为什么有 Fed 到  Cluster,Cluster 到 Fed 的 syncStatus 操作。还有因为每一个局部都有全局信息,也方便将来Cluster 级别Controller 在操作本Cluster 的时候最初更优选择。


没有什么事情是只有好处没有代价的,让每一个Cluster 的 FederatedDeployment 都具备全局信息造成的一个代价是就是在 Fed 和 Cluster 之间造成大量status 同步操作。以至于我们需要单独创建一个 Status Controller 来完成同步操作,而且Status Controller 也是我们最忙的Controler 之一。


“Headless” Federated Deployment 的设计


这是一个取巧的设计。eBay的Federated Deployment的 Spec 还支持 dependents。


Dependents 的内容是一个数组,因为不少应用还需要secretes, configmap, PodPresets 或者其他CRD对象,这些对象需要在In Cluster Federeated Deployment 对象操作前先行从Fed 层同步到Incluster层。注意 Dependents 也是有 rollout strategy 的。有一天,框架团队找到云计算团队说因为应用配置分离的设计,线上有很多rollout 是只rollout 配置,而不用更新docker image 的。我说取巧的原因是本来想要做一个 FedereatedConfiguration的对象和Controller 来支持的,因为项目交付时间紧迫,架构师最终同意在 FederatedDeployment 的实现上打一个叫做 “Headless”的标记,controller 看到这个标记就不去碰 docker image 而只按照rollout strategy 更新dependents 了。


Strategy 的设计模式


在eBay的Kubernetes落地过程设计模式中,strategy 模式被普遍使用。我们在设计Spec的时候就需要用第一性原理逼问自己:现在的抽象够了吗?大多数情况用户并不在乎具体每一个cluster 中replica的数量,并不在乎每一个cluster 中的流量比例。用户关注更多的,是能不能流量平均,是当前Cluster 里面应用的Pod数目是不是能够承载接入的流量,是应用是否健康。一旦在Spec里面添加更加详细的信息,所有今天加进去的字段都会成为明天限制我们的枷锁。以FederatedDeployment 的 Placement 来说,真的需要允许客户自己填写Placement 吗?用户的需求背后的需求又是什么?如果我们有了基于应用健康和性能的监控数据,应用auto scale 后还需要replica 吗?如果我们进一步结合每一个 Cluster 的capacity 信息,应用auto placement 后还需要表露manual placement 吗?


(上篇讲到这里,下篇我们继续:)

eBay技术荟
eBay技术荟,与你分享最卓越的技术,最前沿的讯息,最多元的文化。
 最新文章