Kubernetes生产化实践之路
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

3.3 控制平面的高可用保证

聚焦到Kubernetes 集群本身,要想实现Kubernetes 高可用的目标,首要任务是确保控制平面组件高可用。控制平面的基础核心组件包括etcd、API Server、Scheduler 和Controller Manger。它们之间的任意一个组件不可用,都可能造成用户的请求无法得到及时处理,业务可能因此受到影响。更具挑战性的是,分布式系统由众多组件联动完成不同的控制操作,如果部分组件失效,很可能导致其他组件行为异常。假设Controller Manager 出现问题导致Endpoint 对象无法及时更新,并且kube-proxy 设置的转发规则也无法及时更新,那么将造成业务数据流向异常,进而影响业务的可用性。

控制平面高可用的开发和部署,与Kubernetes 的应用程序类似,应贯穿于组件开发和运维的整个生命周期,遵循类似的设计原则,采用类似的高可用手段,例如合理的故障域规划、业务拆分、冗余设计和负载均衡等。

针对大规模的集群,应该为控制平面组件划分单独节点,减少业务容器对控制平面容器或守护进程的干扰和资源抢占。控制平面所在的节点,应确保在不同机架上,以防止因为某些机架的交换机或电源出问题,造成所有的控制面节点都无法工作。保证控制平面的每个组件有足够的CPU、内存和磁盘资源,过于严苛的资源限制会导致系统效率低下,降低集群可用性。

应尽可能地减少或消除外部依赖。在Kubneretes 初期版本中存在较多Cloud Provider API 的调用,导致在运营过程中,当Cloud Provider API 出现故障时,会使得Kubernetes集群也无法正常工作。不仅如此,Kubernetes 控制器的Reconcile 机制还会反复重试Cloud Provider API 调用。大量的重试请求到达Cloud Provider,使得Cloud Provider 服务很难恢复,一启动就因为请求过多而崩溃。

应尽可能地将控制平面和数据平面解耦,确保控制平面组件出现故障时,将业务影响降到最低。

除基础的控制平面组件外,Kubernetes 还有一些核心插件,是以普通的Pod 形式加载运行的,可能会被调度到任意工作节点,与普通应用竞争资源。这些插件是否正常运行也决定了集群的可用性。因此,我们可以将插件Pod 的priorityClassName 标记为system- cluster-critical 或system-node-critical,确保其在发生资源竞争时具有较高的抢占优先级。

3.3.1 etcd 高可用保证

可以说etcd 是Kubernetes 控制平面组件中唯一的有状态应用。它是Kubernetes 的唯一数据库,存储了所有资源对象的配置数据、状态和元数据。因此etcd 是否高可用直接影响到整个集群的高可用性。

etcd 本身是分布式键值存储数据库。etcd 集群可以有多个成员,成员间采用raft 一致性协议来保证自身数据的一致性和可用性。多个成员可在不同服务器上同时运行,各自都维护了集群上的所有数据。不同于传统的以表格形式存储数据的数据库,etcd 为每个记录创建一个数据页面,在更新一个记录时不会妨碍其他记录的读取和更新。如图3-5 所示,etcd 中有3 个数据,即3 个键值对,其键分别是 “/foo”“/bar/this”“/bar/that”,对应的值分别是数组[“i”,“am”,” array”]、整数42 和字符串 “i am a string”。客户端可利用restful API(通过数据的地址链接)对这些数据进行同步更新,而不会相互影响。这使etcd 非常容易应对高并发写请求的应用场景。

img

图3-5 etcd 数据存储形式

3.3.1.1 etcd 高可用拓扑

针对etcd 集群部署,社区推荐了两种可选的拓扑方案:堆叠式etcd 集群的高可用拓扑(Stacked etcd Topology)和外部etcd 集群的高可用拓扑(External etcd Topology)。在构建etcd 集群时,应充分考虑每个拓扑的优缺点。

1.堆叠式etcd 集群的高可用拓扑

如图3-6 所示,堆叠式是指etcd 堆叠在运行控制平面组件的管理节点(也就是常说的Master 节点)之上。每个Master 节点上都运行了etcd、API Server、Controller Manager 和Scheduler 的实例。所有Master 节点上的etcd 实例组成etcd 集群,但API Server 仅与此节点本地的etcd 成员通信。

img

图3-6 堆叠式etcd 集群的高可用拓扑

这种拓扑将相同节点上的控制平面和etcd 成员耦合在一起。优点在于建立起来非常容易,并且对副本的管理也更容易。但是,堆叠式存在耦合失败的风险。如果一个节点发生故障,则etcd 成员和控制平面实例都会丢失,并且集群冗余也会受到损害。可以通过添加更多控制平面节点来减轻这种风险。因此,为实现集群高可用应该至少运行三个堆叠的Master 节点。

2.外部etcd 集群的高可用拓扑

如图3-7 所示,外部etcd 集群的高可用拓扑是指etcd 集群位于运行控制平面组件的Master 节点之外。Master 节点上仅运行了API Server、Controller Manager 和Scheduler 的实例,etcd 成员则在其他单独主机上运行,组成存储集群。API Server 可与etcd 集群中任意一个etcd 成员进行通信。

img

图3-7 外部etcd 集群的高可用拓扑

这里API Server 有两种etcd 地址的配置方式:第一种是配置etcd 集群的负载均衡器的VIP 地址,具体连接到哪个etcd 实例由负载均衡策略决定;第二种是配置多个etcd 实例的地址,用逗号隔开,API Server 会按顺序依次尝试连接。如果采用第二种方式,那么为了均衡etcd 集群的流量,每个API Server 处etcd 实例地址的顺序配置应略有不同。

该拓扑将控制平面和etcd 成员解耦。丢失一个Master 节点对etcd 成员的影响较小,不会像堆叠式拓扑那样对集群冗余产生太大影响。但是,此拓扑所需的主机数量是堆叠式拓扑的两倍。具有此拓扑的集群至少需要三个主机用于控制平面节点,三个主机用于etcd集群。

为什么不管哪种拓扑结构都至少需要3 个etcd 实例呢?原因有两个。

第一,如果 etcd 集群仅有一个成员,那么一旦这个成员出现故障,会导致整个Kubernetes 集群不可用。第二,基于raft 协议的etcd 的Leader 选举和数据写入都需要半数以上的成员投票通过确认,因此,集群最好由奇数个成员组成,以确保集群内部一定能够产生多数投票通过的场景。这也就是为什么etcd 集群至少需要3 个以上的成员。建议etcd集群成员数量不要超过7 个,推荐是3 个或5 个。个数越多,投票所需时间就越多,写的吞吐量会越低。虽然提高了读的性能和可用性,但是极大损伤了写的性能。具体应该是3 个还是5 个呢?

etcd 集群是有一定的容灾能力的,并且能够自动从临时故障中恢复(例如节点重启)。对于一个N 节点的集群,允许最多出现在 (N-1)/2 个节点发生永久性故障(比如硬件故障或磁盘损耗)之后还能正常对外服务。当永久性故障的节点个数超过 (N-1)/2 时,就会陷入不可逆的失去仲裁的境地。一旦仲裁丢失,集群就无法保证一致性,因此集群就会置为只读模式,无法再接收更新请求了。

3 个成员的etcd 集群是高可用的最低需求。etcd 实例的数目越少,其维护成本越低,并且只需2 个成员投票即可将数据写入,因此具有较高的效率。仅允许1 个成员发生故障。发生故障后,需要运维人员立即介入。如若另一个成员再发生故障,集群将会变成只读的。只读的集群表象是平台变为静止。但因为集群中所有节点的kubelet 都无法不汇报状态,所以Controller Manager 中的Pod 驱逐控制器会将所有节点上的Pod 放入驱逐队列,一旦etcd 恢复,会导致大量Pod 被同时驱逐,从而造成服务器过载、服务不可用等事故。

5 个成员的etcd 集群是一种常见的配置,此配置具有更高的可用性。只有当超过两个成员出现故障时,集群才变成只读的。因此,当第一个成员出现故障时,管理员无须立即介入。对运维响应速度的要求降低了,这是以管理复杂度换取运维响应时间的常规手段。

3.3.1.2 etcd 性能调优

etcd 的性能影响了Kubernetes 操作的吞吐量和延时,进而影响了集群的稳定性。应实时监测每个etcd 成员的性能,及时修复或移除性能差的成员。否则这些成员可能会拖慢整个etcd 集群的处理速度。另外,当发现etcd 集群性能低下时,应如何调优etcd 性能呢?影响etcd 的性能(尤其是提交延迟)的因素主要有两个:网络延迟和磁盘I/O 延迟,即etcd成员之间的网络往返时间(Round Trip Time,RTT)和将数据提交到永久存储所需的时间。

1.减少网络延迟

数据中心内的RTT 大概是数毫秒(ms),美国境内的典型RTT 约为50ms,两大洲之间的RTT 可能慢至400ms。因此,建议etcd 集群尽量实现同地域部署。

当客户端到Leader 的并发连接数量过多时,可能会导致其他Follower 节点发往Leader的请求因为网络拥塞而被延迟处理。在Follower 节点上,可能会看到如下错误:

img

可以在节点上通过流量控制工具(Traffic Control)提高etcd 成员之间发送数据的优先级来避免。

2.减少磁盘I/O 延迟

对于磁盘延迟,典型的旋转磁盘写延迟约为10ms。对于SSD(Solid State Drives,固态硬盘),延迟通常低于1ms。HDD(Hard Disk Drive,硬盘驱动器)或网盘在大量数据读写操作的情况下延时会不稳定。因此,强烈建议使用SSD。同时,为了降低其他应用程序的I/O 操作对etcd 的干扰,建议将etcd 的数据存放在单独的磁盘内。也可以将不同类型的对象存储在不同的若干etcd 集群中,比如将频繁变更的event 对象从主etcd 集群中分离出来,以保证主集群的高性能。在API Server 处这是可以通过参数进行配置的。这些etcd集群最好也能分别有一块单独的存储磁盘。

如果不可避免地,etcd 和其他业务共享存储磁盘,那么就需要通过ionice 命令对etcd服务设置更高的磁盘I/O 优先级,尽可能避免其他进程的影响,代码如下:

img

3.保持合理的日志文件大小

etcd 以日志的形式保存数据,无论是数据创建还是修改,它都将操作追加到日志文件,因此,日志文件的大小会随着数据修改的次数而呈线性增长。当Kubernetes 集群规模较大时,其对etcd 集群中的数据更改也会很频繁,集群日记文件会迅速增长。为了有效降低日志文件的大小,etcd 会以固定周期创建快照保存系统的当前状态,并移除旧日志文件。另外,当修改次数累积到一定的数量(默认是10000,通过参数 “--snapshot-count” 指定)时,etcd 也会创建快照文件。如果etcd 的内存使用和磁盘使用过高,则可以先分析是否由于数据写入频度过大导致快照频度过高,确认后可通过调低快照触发的阈值来降低其对内存和磁盘的使用。

4.设置合理的存储配额

存储空间的配额用于控制etcd 数据空间的大小。合理的存储配额可保证集群操作的可靠性。如果没有存储配额,也就是etcd 可以利用整个磁盘空间,那么etcd 的性能会因为存储空间的持续增长而严重下降,甚至有耗完集群磁盘空间导致不可预测集群行为发生的风险。如果设置的存储配额过小,那么当其中一个节点的后台数据库的存储空间超出存储配额时,etcd 就会触发集群范围的告警,并将集群置于只接收读和删除请求的维护模式。只有在释放足够的空间、消除后端数据库的碎片和清除存储配额告警之后,集群才能恢复正常操作。

5.自动压缩历史版本

etcd 会为每个键都保存历史版本。为了避免出现性能问题或存储空间消耗完导致写不进去的问题,这些历史版本需要进行周期性的压缩。压缩历史版本就是丢弃该键给定版本之前的所有信息,节省出来的空间可以用于后续的写操作。etcd 支持自动压缩历史版本。在启动参数中指定参数 “--auto-compaction”,其值以小时为单位。也就是说,etcd 会自动压缩该值设置的时间窗口之前的历史版本。

6.定期消除碎片化

压缩历史版本,相当于离散地抹去etcd 存储空间的某些数据,etcd 存储空间中将会出现碎片。这些碎片无法被后台存储使用,却仍占据节点的存储空间。因此,定期消除存储碎片将释放碎片化的存储空间,从而重新调整整个存储空间。

7.优化运行参数

在网络延迟和磁盘延迟固定的情况下,可以通过优化etcd 运行参数来提升集群的工作效率。etcd 基于raft 协议进行Leader 选举,当Leader 选定以后才能开始数据的读写操作,因此频繁的 Leader 选举会导致数据的读写性能显著降低。我们可以通过调整心跳周期(Heatbeat Interval)和选举超时时间(Election Timeout)来降低Leader 选举的可能性。

心跳周期是指控制 Leader 以何种频度向 Follower 发起心跳通知。心跳通知除表明Leader 的活跃状态外,还带有待写入的数据信息,Follower 依据心跳信息进行数据写入,默认心跳周期是100ms。选举超时时间定义了Follower 在多久没有收到Leader 心跳时重新发起选举,该参数的默认设置是1000ms。

如果etcd 集群的不同实例部署在延迟较低的相同数据中心,那么通常使用默认配置即可。如果不同实例部署在多数据中心或者网络延迟较高的集群环境中,则需要对心跳周期和选举超时时间进行调整。建议心跳周期参数设置为接近etcd 多个成员之间平均数据往返周期的最大值,一般是平均RTT 的0.55~1.5 倍。如果心跳周期设置得过低,则etcd 会发送很多不必要的心跳信息,从而增加CPU 和网络的负担。如果设置得过高,则会导致选举频繁超时。选举超时时间也需要根据etcd 成员之间的平均RTT 时间来设置。选举超时时间最少设置为etcd 成员之间的RTT 时间的10 倍,以便应对网络波动。

心跳间隔和选举超时时间的值必须对同一个etcd 集群的所有节点都生效,如果各个节点的配置不同,集群成员之间的协商结果就会不可预知,从而导致系统不稳定。

3.3.1.3 etcd 备份存储

声明式系统是一把双刃剑。一方面,Kubernetes 基于此机制构建出故障转移、版本发布、扩容缩容等强大的功能。另一方面,数据的破坏或丢失,会导致控制器发起Pod 删除或重建等破坏性的行为。举个例子,etcd 中的Pod 数据不小心被损坏了,kubelet 将会把正在运行的Pod 清除,以确保与当前状态及数据库中的用户期望一致。作为Kubernetes 的“首脑”,确保写入效率和防止数据丢失是规划etcd 存储的主要目标。

在etcd 的默认工作目录下会生成两个子目录:wal 和snap。wal 用于存放预写式日志,其最大的作用是记录整个数据变化的全部历程。所有数据的修改在提交前都要先写入wal中。snap 用于存放快照数据。为防止wal 文件过多,etcd 会定期(当wal 中数据超过10000条记录时,由参数 “--snapshot-count” 设置)创建快照。当快照生成后,wal 中的数据就可以被删除了。如果数据遭到破坏或错误修改需要回滚到之前某个状态时,方法只有两个:一是从快照中恢复数据主体,但是未被拍入快照的数据会丢失;二是执行所有 wal中记录的修改操作,从最原始的数据恢复到数据损坏之前的状态,但恢复的时间较长。

通常我们推荐使用SSD 本地磁盘来提高etcd 的写入效率,但在毁灭性灾难(例如失去所有etcd 实例所在节点)发生时,集群数据就可能丢失。基于网络的远端存储有较完备的备份机制,数据不易丢失,但网盘的写入效率和可靠性远低于本地磁盘,无法满足频繁读写的需求。对etcd 的灾备来说,对本地磁盘进行实时备份尤为重要。

官方推荐etcd 集群的备份方式是定期创建快照。与etcd 内部定期创建快照的目的不同,该备份方式依赖外部程序定期创建快照,并将快照上传到网络存储设备以实现 etcd数据的冗余备份。上传到网络设备的数据都应进行加密。即使所有etcd 实例都丢失了数据,也能允许etcd 集群从一个已知的良好状态的时间点在任一地方进行恢复。根据集群对etcd备份粒度的要求,可适当调节备份的周期。在生产环境中实测,拍摄快照通常会影响集群当时的性能,因此不建议频繁创建快照。但是备份周期过长,可能会导致大量数据的丢失。

这里可以使用增量备份的方式。如图3-8 所示,备份程序每30min 触发一次快照的拍摄,它从快照结束的版本(Revision)开始,监听etcd 集群的事件,每隔10s 将事件保存到文件中,并将快照和事件文件上传到网络存储设备中。30min 的快照周期对集群性能影响甚微。当大灾难来临时,也至多丢失10s 的数据。至于数据修复,首先把数据从网络存储设备中下载下来,然后从快照中恢复大块数据,并在此基础上依次应用存储的所有事件。这样就可以将集群数据恢复到灾难发生前。

img

图3-8 etcd 的增量备份方案

3.3.2 API Server 高可用保证

API Server 作为Kubernetes 集群的API 网关,接收来自用户和其他Kubernetes 组件的所有restful 请求。其本身是无状态的。数据的持久化职责均由后端数据库etcd 承担。当API Server 压力较大无法支撑并发业务需求时,横向扩展实例数量是最直接有效的方式。当采用堆叠式etcd 集群的拓扑时,API Server 与etcd 一对一部署在相同的Master 节点上,为了不影响etcd 集群的成员个数,可以将API Server 单独进行横向扩容。多实例冗余与负载均衡是API Server 高可用的主要手段。

3.3.2.1 API Server 高可用配置

对于实力冗余与负载均衡,如图3-9 所示,最直接的方案是客户端及其他控制平面组件使用负载均衡器与API Server 通信。虽然这样可以均衡API Server 的负载,但是将单点故障转移到了负载均衡器上。

img

图3-9 单个负载均衡器的解决方案

因此,在这个基础上还应进行一些优化配置,以获得更高的容错性和灵活性。

1.多个负载均衡器

如图3-10 所示,在配置多个负载均衡器时,API Server 提供负载均衡层的连接冗余。通过为这些负载均衡器设置智能DNS 或虚拟IP,使得客户端及其他控制平面组件使用DNS 或虚拟IP 与API Server 通信。如果其中一个负载均衡器发生故障,那么该负载均衡器上的流量将被移除,流量将被转移到其他负载均衡器,最终转发到API Server 处。

img

图3-10 多个负载均衡器的解决方案

2.完善的健康检查

即使API Server 采用多实例运行,也很难保证每一个实例都能一直正常地运行下去。因此,在负载均衡器处应该对每个实例进行健康状态检查。当某个实例处于不健康的状态时,负载均衡器应停止向这个实例分发请求。API Server 的健康状态检查是否准确将直接影响到API Server 的高可用。如果出现误报,例如API Server 不能进行服务但状态却标记为健康,那么就会造成客户的某些请求不能及时响应。

API Server 是所有客户端访问etcd 的入口,其生命周期应与etcd 紧耦合。API Server的早期版本对etcd 所做的健康检查只依赖ping 命令。但是有这样的场景:etcd 处于假死状态,ping 的结果返回成功,但etcd 的真实请求可能已经无法执行。API Server 由于没有感知到etcd 的异常,所以会继续接收用户请求,但接收的请求无法被etcd 处理,从而导致超时。在后续的版本中,etcd 的健康检查被强化,API Server 的健康检查是基于etcd 的真实请求,如果etcd 无法处理该请求,那么API Server 会将自身状态置为 “不健康” 以避免继续提供服务。

对于集群内部的客户端,应优先访问API Server 的ClusterIP,利用kube-proxy 建立的转发规则将流量送达API Server 处。这样,当外部的负载均衡器出现问题时,不会影响集群内部客户端访问API Server,也不会对集群内的客户端产生巨大影响。

3.基于Webhook 的扩展服务

Kubernetes 最常见的扩展应用是自定义资源类型和自定义控制器。在API Server 处,Kubernetes 也提供了许多扩展其内置功能的方法,例如Admission Webhook。如图3-11 所示,Kubernetes 定义了两种类型的Admission Webhook,即Mutating Admission Webhook和Validating Admission Webhook。在API 请求进行身份验证和授权后,Mutating Admission Webhook 首先被调用,能够修改发送到API Server 的对象,填充自定义的默认值。在所有对象修改完成之后,API Server 对传入对象进行验证,然后调用 Validating Admission Webhook。Validating Admission Webhook 可以根据自定义策略允许或拒绝请求。

img

图3-11 API 请求的生命周期

Mutating Admission Webhook 可以修改其允许的对象;Validating Admission Webhook允许拒绝请求。这些都是特殊的控制器。如果没有Admission Webhook 对API Server 进行扩展,就需要将这些代码编译到API Server 中,并且只能在API Server 启动时启用。Admission Webhook 的精髓在于减少了代码耦合,可以在运行时动态配置API Server 的扩展Admission 服务,用于接收准入请求并对其进行处理,让API Server 支持你所期望的所有功能。而且Admission 服务可以单独部署在集群内或集群外,可以单独对API Server 和Admission 服务进行部署和升级,增强了集群管理的灵活性。

如图3-12 所示,通过创建ValidatingWebhookConfiguration 和MutatingWebhookConfiguration来确定动态配置哪些资源,以及要服从哪些Webhook 的服务。创建WebhookConfiguration后,系统将花费几秒钟时间来接收新配置。如果Webhook 需要验证API Server 的身份,则可以将API Server 配置使用基本认证、令牌或者证书,在启动API Server 时通过参数“--admission-control-config-file” 指定Admission Webhook 配置文件的位置。Webhook 服务处理API Server 发送的AdmissionReview 请求,并以收到的相同版本的AdmissionReview对象作为回复发回给API Server。

img

图3-12 Admission Webhook 工作原理

Admission Webhook 配置文件的内容是一个AdmissionConfiguration 的对象,示例代码如下:

img

文件中指定了对于不同的MutatingAdmissionWebhook 和ValidatingAdmissionWebhook服务API Server 应在何处读取身份凭据,凭据存储在kubeConfigFile 中。kubeConfigFile文件中存储的是一个Config 对象,示例代码如下:

img
img

上面代码中的name 字段应设置为Admission Webhook 服务的DNS 名称。如果服务使用了非443 端口,则该端口必须包含在name 字段中。name 字段支持使用通配符‘*’,如果仅有通配符‘*’,则该项为默认配置。

与ValidatingWebhookConfiguration 类似,MutatingWebhookConfiguration 的示例代码如下:

img
img

当创建WebhookConfiguration 配置时,admissionReviewVersions 是必填字段。需要Webhook 服务支持 AdmissionReview 中的至少一个版本。 API Server 在其支持的admissionReviewVersions 列表中发送第一个AdmissionReview 版本。如果API Server 不支持列表中的所有版本,则不允许创建AdmissionReview,调用Webhook 的尝试将失败并受到失败策略(在failurePolicy 中定义)的约束。

字段rules 用于指定是否应将API Server 的请求发送给Webhook 服务。每个规则都指定一个或多个apiGroups、apiVersions、operations、resources 和scope。此示例指定了所有accountresourcequotas 及其子资源对象的Create、Update 和Delete 请求,这些请求都应发送给Webhook 服务进行处理。

当API Server 收到与其中一个规则匹配的请求时,会按照clientConfig 中指定的方式向Webhook 发送一个AdmissionReview 请求。clientConfig 中可以通过URL 或Service 引用来调用Webhook,并且可以选择自定义CA 捆绑包,用于验证TLS 连接。

failurePolicy 定义如何处理来自Webhook 服务的无法识别的错误和超时错误,允许的值为 “Ignore” 或 “Fail”。matchPolicy 定义了如何使用其规则来匹配传入的请求,允许的值是 “Exact” 或 “Equivalent”。Exact 表示仅当请求与指定规则完全匹配时,才拦截该请求。Equivalent 表示当规则中列出的资源是另一个API 组的版本时,拦截请求。

reinvocationPolicy 定义了一个API 请求Webhook 服务是否重新调用,允许的值是“Never” 和 “IfNeeded”。如果设置为 “Never”,则不得多次调用Webhook 服务;如果设置为 “IfNeeded”,则在Webhook 调用之后又被其他对象的插件修改对象时,可以再次调用Webhook。sideEffects 用于显式指定Webhook 服务针对dryRun 为true 的请求是否有副作用。副作用是指Webhook 在处理请求时所做的带外修改(out-of-band changes)。sideEffects允许的值是 “Unknown”“None”“Some” 和 “NoneOnDryRun”。timeoutSeconds 是设置Webhook 服务的超时时间,超时值必须在1 到30s 之间。如果Webhook 呼叫超时,则会根据Webhook 的failurePolicy 处理请求。

Admission Webhook 服务本质上是集群控制平面的重要部分。如果需要在生产环境中部署,一定需要谨慎编写和部署它们,对它们的可用性、性能和部署应进行全面评估,具体建议如下:

● 对Webhook 延时进行评估(通常以ms 为单位),使用较小的超时时间,否则可能造成API Server 的请求高延迟。

● 采用某种形式的负载平衡,以获得高可用性和性能优势。Webhook 服务通常来说也是无状态应用,可采用与API Server 类似的高可用方法。

● ValidatingWebhookConfiguration 应看到对象的最终状态。 在所 有MutatingWebhookConfiguration 服务修改对象之后进行验证。

● 避免死锁。如果集群中运行的Webhook 服务拦截了启动其自己的Pod 所需的资源对象,则可能会产生其自身部署的死锁。建议使用 NamespaceSelector 排除运行Webhook 服务的Namespace。

● Webhook 服务应尽可能避免副作用,这意味着该Webhook 服务仅对发送给它们的AdmissionReview 的内容起作用,并且不要进行其他更改。

● 避免对kube-system Namespace 内的对象进行操作。Kubernetes 控制平面的关键组件一般都部署在kube-system Namespace 中,例如CoreDNS 等。意外更改或拒绝操作kube-system Namespace 内对象的请求可能会导致控制平面组件停止运行或引入未知行为。

3.3.2.2 API Server 性能调优

API Server 是API 请求处理的中枢,API Server 的处理能力直接影响集群的处理能力。随着集群规模的增大,默认的API Server 参数可能无法再满足其性能要求。因此,有必要对集群的未来规模做出预判,并调整参数以适应业务发展,具体方法如下:

1.预留充足的CPU、内存资源

随着集群中节点数量不断增多,API Server 对CPU 和内存的开销也不断增大。过少的CPU 资源会降低其处理效率,过少的内存资源会导致Pod 被OOMKilled,从而直接导致服务不可用。在规划API Server 资源时,不能仅看当下的需求,还要为未来预留充分。

2.善用速率限制(RateLimit)

API Server 的参数 “--max-requests-inflight” 和 “--max-mutating-requests-inflight” 支持在给定时间内限制并行处理读请求(包括Get、List 和Watch 操作)和写请求(包括Create、Delete、Update 和Patch 操作)的最大数量。当API Server 接收的请求超过这两个参数设定的值时,再接收的请求将会被直接拒绝。通过速率限制机制,可以有效地控制API Server内存的使用。如果该值配置过低,就会经常出现请求超过限制的错误,如果配置过高,则API Server 可能会因为占用过多内存而被强制终止,因此需要根据实际的运行环境,结合实时用户请求数量和API Server 的资源配置进行调优。

客户端在接收拒绝请求的返回值后,应等待一段时间再发起重试,无间隔的重试会加重API Server 的压力,导致性能进一步降低。针对并行处理请求数的过滤颗粒度太大,在请求数量比较多的场景,重要的消息可能会被拒绝掉,自1.18 版本开始,社区引入了优先级和公平保证(Priority and Fairness)功能,以提供更细粒度的客户端请求控制。该功能支持将不同用户或不同类型的请求进行优先级归类,保证高优先级的请求总是能够更快得到处理,从而不受低优先级请求的影响。

3.设置合适的缓存大小

API Server 与etcd 之间基于gPRC 协议进行通信,gPRC 协议保证了两者在大规模集群中的数据能够高速传输。

gPRC 协议是基于HTTP2 协议的。HTTP2 通过stream 支持了连接的多路复用。一条TCP 连接可以包含多个stream,多个stream 发送的数据互不影响。在API Server 和etcd之间,相同分组的资源对象的请求共享同一个TCP 连接,组内不同资源对象的请求由不同的stream 进行传输。多路复用会引入资源竞争,流量控制可以保证stream 之间不会严重影响彼此,但是也限制了能支持的并发请求数量。

API Server 提供了集群对象的缓存机制,当客户端发起查询请求时,API Server 默认会将其缓存直接返回给客户端。缓存区大小可以通过参数“--watch-cache-sizes”进行设置。针对访问请求比较多的对象,适当设置缓存的大小,能够降低etcd 的访问频率,节省网络调用,减少etcd 集群的读写压力,从而提高对象访问的性能。

但是API Server 也是允许客户端忽略缓存的,例如客户端请求中ListOption 中没有设置resourceVersion,这时API Server 直接从etcd 拉取最新数据返回给客户端。客户端应尽量避免此操作,应在ListOption 中设置resourceVersion 为0,API Server 将从缓存中读取数据,而不会直接访问etcd。

4.客户端尽量使用长连接

当查询请求的返回数据较大且此类请求并发量较大时,容易引发TCP 链路的阻塞,导致其他查询操作超时。因此,基于 Kubernetes 开发组件时,例如某些 DaemonSet 和Controller,在查询某类对象时,应尽量通过长连接ListWatch 监听对象变更,避免全量从API Server 获取资源。在同一应用程序中,如果有多个Informer 监听API Server 的资源变化,则可以将这些Informer 合并,减少与API Server 的长连接数,从而降低对API Server的压力。

3.3.3 控制器高可用保证

Kubernetes 提供了Leader 选举机制,用以确保多个控制器的实例同时运行,并且只有Leader 实例提供真正的服务。其他实例处于准备就绪状态,如果Leader 出现故障,则取代Leader 以保证Pod 能被及时调度。此机制以占用更多资源为代价,提升了Kubernetes控制器的可用性。

Leader 选举的核心是利用Configmap、Endpoints 或Lease 对象实现分布式资源锁。当多个实例同时启动后,在运行任何业务逻辑之前,都会尝试读取该资源锁。

以Lease 对象为例,在首次抢占过程中,该对象无任何Leader 信息,第一个尝试占有锁的实例会更新该对象的acquireTime 和holderIdentity,并以leaseDurationSeconds 为周期不断更新renewTime。控制器的判断逻辑是:只有当holderIdentity 与当前实例的Pod 名称完全匹配时,控制器的程序执行才继续,否则等待获取锁。如果Leader 能保证在固定周期内及时更新renewTime,则该锁始终被Leader 占有,任何其他实例周期性地尝试更新holderIdentity 以成为新的Leader。该机制保证Leader 实例出现故障或网络断开时,其Leader租约会到期,其他实例可抢占资源锁迅速成为新Leader。Lease 对象的示例代码如下:

img
img

资源锁可保存在Configmap、Endpoints 和Lease 三种对象中。推荐使用Lease,因为Lease 对象本身就是用来协调租约对象的,其Spec 定义与Leader 选举机制需要操控的属性是一致的。使用Configmap 和Endpoints 对象更多是为了向后兼容,伴随着一定的负面影响。以Endpoints 为例,Leader 每隔固定周期就要续约,这使得Endpoints 对象处于不断的变化中。Endpoints 对象会被每个节点的kube-proxy 等监听,任何Endpoints 对象的变更都会推送给所有节点的kube-proxy,这为集群引入了不必要的网络流量。

任何集群控制器均可基于Leader 选举机制进行开发部署。调度器是一个 “特殊” 的控制器,它基于Leader 选举机制,用于保证服务高可用。

3.3.4 集群的安全性保证

集群的安全性必须考虑以下几个目标:

(1)保证容器与容器之间、容器与主机之间隔离,限制容器对其他容器和主机的消极影响。

(2)保证组件、用户及容器应用程序都是最小权限,限制它们的权限范围。

(3)保证集群的敏感数据的传输和存储安全。

Kubernetes 提供了一系列机制来实现这些安全控制目标,其中包括细粒度控制Pod 安全上下文(Pod Security Context)、API Server 的认证、授权、审计和准入控制、数据的加密机制等。关于Pod 安全策略和API Server 的认证授权审计等将在第5 章多租户进行详细讲解。这里我们着重讨论数据传输和存储的安全。

3.3.4.1 数据加密传输

对于数据传输,Kubernetes 及其支持组件都应使用基于SSL/TLS 的HTTPS 协议来确保传输的安全性,特别是客户与API Server、API Server 与etcd、API Server 与kubelet、etcd 成员之间这种跨节点的数据通信。如果使用HTTPS 协议,那么在组建Kubernetes 集群时,必不可少地会涉及各个组件的数字证书的制作或签发,以及证书的参数配置。为了方便大家理解,在具体介绍Kubernetes 组件的证书签发和配置之前,先从SSL/TLS 握手的流程入手,了解数字证书在SSL/TLS 中是如何使用的。图3-13 展示了SSL 的握手流程,握手的目的是安全地协商出一份对称加密的密钥。

img

图3-13 SSL 的握手流程

首先,客户端向服务端发送Client_Hello 消息,这个消息包含一个客户端生成的随机数RNc、客户端支持的加密套件和SSL 版本等信息。

然后,服务端向客户端发送Server_Hello 消息,这个消息包含一个随机数RNs 和从客户端支持的加密套件中选定的加密套件信息。这个套件决定了后续加密和生成密钥时具体使用哪些算法。至此,客户端和服务端都拥有了两个随机数(RNc 和RNs),这两个随机数会在后续生成对称密钥时用到。

接着,服务端将自己的证书下发给客户端,让客户端验证自己的身份。客户端收到服务端传来的证书后,验证证书是否过期、服务端证书的CA 机构是否可靠、返回的公钥是否能正确解开返回证书中的数字签名、服务端证书上的域名是否和服务端的实际域名相匹配等。验证通过后,客户端取出证书中的服务端公钥,否则,中止通信。同时,在一些安全性要求高的场景中,服务端也会要求客户端上报证书,这一步是可选的。

如何判定服务端证书的CA 机构是否可靠呢?这要看客户端是否安装了此CA 机构的根证书。根证书是CA 认证中心给自己颁发的证书,是信任链的起始点。安装根证书意味着对这个CA 认证中心的信任。因此,通常我们把根证书简称为CA。

服务端证书验证通过后,客户端会再生成一个随机数RNc2,并用服务端公钥非对称加密,生成PMS(Pre-Master Secret),并将这个PMS 发送给服务端,服务端再用自己的私钥解出这个 PMS,得到客户端生成的RNc2。此时,客户端和服务端都拥有三个随机数(RNc、RNs 和RNc2)。

客户端和服务端再根据前面选定的加密套件的算法就可以生成一份密钥MS(Master Secret),握手结束后的应用层数据都是使用这个密钥进行对称加密的。为什么要使用三个随机数呢?这是因为SSL/TLS 握手过程的数据都是明文传输的,通过多个随机数种子来生成密钥不容易被暴力破解出来。

服务端和客户端的证书、公钥和私钥是谁颁发的?都是向CA 机构申请来的。CA 机构在为申请者签发数字证书时,在下发一个包括了公钥、申请者信息和签名的数字证书的同时,还会下发一个与其相匹配的私钥文件。CA 机构可以是可信的第三方机构,也可以是企业自身的认证系统,也可以自己制作根证书来签发证书。

如图3-14 所示,在Kubernetes 体系中,有两套分别是etcd CA 和Kubernetes CA 颁发的证书。其中etcd 客户端证书(简称为EC)、etcd 服务端证书(简称为ES)和etcd 对等证书(简称为P)都是由etcd CA 颁发和验证的,分别用于API Server 和etcd 之间、etcd成员之间的加密通信。因此,在API Server 和etcd 所有实例处都应有一份etcd CA,也就是etcd 根证书,用于验证对方证书的有效性。至于API Server 和etcd 之间的通信,都是由API Server 发起的,因此,API Server 处配置了etcd 客户端证书,etcd 处配置了etcd 服务端证书。

客户端证书(即API Server 客户端证书,简称为AC)、kubelet 客户端证书(简称为KC)、服务端证书(即API Server 服务端证书,简称为AS)和kubelet 服务端证书(简称为KS)都是由Kubernetes CA 颁发和验证的,用于kubelet 与API Server 之间双向主动通信。当kubelet 主动连接API Server 时,API Server 作为服务端,应使用服务端证书AS,kubelet 作为客户端,应使用客户端证书AC;当API Server 主动连接kubelet 时,kubelet作为服务端,应使用服务端证书KS,API Servr 作为客户端,应使用客户端证书KC。同样,为了验证双方证书的有效性,在所有kubelet 和API Server 实例处都有一份Kubernetes CA,也就是Kubernetes 的根证书。

为什么这里至少需要两套CA(即两个根证书)来颁发证书呢?这是为了防止kubelet直接与etcd 通信,而绕过API Server 内置的所有授权机制。如果使用同一套CA,那么kubelet客户端证书能直接在etcd 服务端验证通过,也就是说kubelet 将被授予无限特权。

img

图3-14 集群的安全通信

CA 和各种数字证书在组件运行之前就应制作和签发。具体地怎么生成CA、如何利用CA 进行不同角色的证书签发,可以参考官方文档,用证书生成工具(例如cfssl)来生成与自己类型及SAN(Subject Alternative Name,也就是证书支持的域名列表)匹配的证书。对于这些证书,特别是私钥,建议上载到远程安全管理系统(Security Management System)中。既是为了存储安全,又是为了方便动态发布和更新证书及密钥。

这里分别针对API Server 连接etcd、etcd 成员连接etcd 成员、kubelet 连接API Server和API Server 连接kubelet 这四个连接请求,下面来看一下API Server、etcd、kubelet 三者与证书相关的配置参数。

1.API Server 连接etcd

即API Server 为客户端,etcd 为服务端。API Server 相关的配置参数如下:

img
img

其中参数 “--etcd-cafile” 用于指定 CA 机构的根证书;参数 “--etcd-certfile” 和“--etcd-keyfile” 分别用于指定etcd 的客户端证书和私钥。

etcd 相关的配置参数如下:

img

其中参数“--client-cert-auth”用于指定需要验证客户端的证书;参数“--trusted-ca-file” 用于指定CA 机构的根证书;参数 “--cert-file” 和 “--key-file” 分别用于指定etcd 的服务端证书和私钥。

2.etcd 成员连接etcd 成员

即etcd 既作为客户端又作为服务端。因此在生成Peer 证书时,应写明此证书需有Client Auth 和Server Auth 两种用途。etcd 相关的配置参数如下:

img

对于只需要加密通信却不需要认证的使用场景,etcd 也支持使用自动生成的自签名证书加密通信,配置参数 “--auto-ls” 和 “--peer-auto-tls” 即可。这样攻击者即使截获了数据也无法解密。由于不需要管理etcd 的证书和密钥,所以大大简化了etcd 的部署。

3.kubelet 连接API Server

kubelet 使用Kubeconfig 内的信息与API Server 建立连接,而Kubeconfig 中包含了关于证书的配置信息。事实上集群中的其他组件(例如Scheduler 和Controller Manager)也都是通过这种方式和API Server 建立连接的。这里我们以kubelet 为例来阐述如何配置Kubeconfig 中的证书信息。

API Server 处的配置参数如下:

img
img

参数 “--client-ca-file” 用于指定颁发API Server 服务端和客户端证书的CA 机构的根证书。如果设置了此参数,且请求中携带的客户端证书是 “--client-ca-file” 中任一授权机构签发的,则 API Server 将使用证书的公用名(CommonName)进行身份验证。参数“--tls-cert-file” 和 “--tls-private-key-file” 为服务端证书文件和私钥。

Kubelet 的配置参数如下:

img

参数 “--certificate-authority-data” 为CA 根证书,即API Server 处的k8sca.crt 文件基于PEM 编码的数据;参数 “--client-certificate-data” 为客户端证书文件的PEM 编码的数据;参数 “--client-key-data” 为私钥。

4.API Server 连接kubelet

即API Server 作为客户端,kubelet 作为服务端。API Server 的配置参数如下:

img
img

上面代码中,参数 “--kubelet-certificate-authority” 用于指定颁发kubelet 服务端和客户端证书的CA 机构的根证书。其中kubelet 和API Server 共用同一个CA,即k8sca.crt。在方便管理和更新证书的前提下, 也可以让 kubelet 使用不同的根证书。 参数“--kubelet-client- certificate” 和 “--kubelet-client-key” 为客户端证书文件和私钥。

kubelet 的配置参数如下:

img

参数 “--tlsCertFile” 和 “--tlsPrivateKeyFile” 为服务端证书文件和私钥。如果参数“--tlsCertFile” 和 “--tlsPrivateKeyFile” 未设置,kubelet 会自动生成自签名的证书和私钥。参数 “--clientCAFile” 为根证书路径。如果设置了此参数,且请求中携带的客户端证书是“ --client-ca-file ” 中任一授权机构签发的, 则 API Server 将使用证书的公用名(CommonName)进行身份验证。

任何事物都是有利有弊的,引入SSL/TLS 机制固然能够保证安全,但是从性能上引入了更长的延时。性能影响集中在每一个连接的开始握手阶段。SSL/TLS 的握手需要三次往返,比正常TCP 握手多加了两个往返。对于网络延时比较高的环境,应适当进行调优,例如调整拥塞窗口、尽量使用长连接保持每个连接不断开等。Kubernetes 的各个组件之间的通信都是使用长连接的方式。

3.3.4.2 数据加密存储

对于数据存储,API Server 支持在远端的etcd 上针对不同资源对象进行加密。参数“--encryption-provider-config” 用于配置不同资源对象在etcd 加密数据。加密配置示例代码如下:

img
img

上述代码中,resources 字段指定需要加密的资源对象名称列表。providers 数组是加密方法的有序列表。这里提供了identity、aesgcm、aescbc、secretbox 和kms,但是只能使用其中一种加密类型。代码中第一个方法用于加密存储的指定资源。当从存储中读取资源需要解密时,会按顺序依次调用providers 中的加密方法,直到解密数据成功。如果任何加密方法都不能解密数据,则会向客户端返回错误,阻止客户端访问该资源。

每个加密方法都支持多个密钥keys。与providers 类似,在加密时,使用第一个密钥进行加密;在解密时,加密方法会依次用这些密钥尝试解密,直到找到有效的密钥。在这个例子中,默认使用identity 进行加密,identity 是不提供加密的,也就是说etcd 中的数据是不加密的。

加密不是一项新兴技术,任何一个云平台都能非常容易做到,其难点在于密钥的管理。aesgcm、aescbc 和secretbox,与identity 相比,仅能适度改善系统的安全状况。因为密钥只是采用了原始加密存在主机的文件中,即参数 “--encryption-provider-config” 指定的文件是被放置在主机上的,属于本地管理的密钥。使用本地管理的密钥可以防止etcd 受到破坏,但不能防止主机受到破坏。熟练的攻击者可以访问该文件并提取加密密钥。因此,对于安全性极高的场景,请使用kms。kms 提供一个kms 插件,可与API Server 部署在同一主机上,负责与远程kms 服务器的所有通信。它的密钥没有存在本地,而是存在远端第三方kms 服务器上,从而提供比本地存储的加密密钥更高的安全级别。