learn-tech/专栏/周志明的架构课/49_以容器构建系统(下):韧性与弹性.md
2024-10-16 06:37:41 +08:00

13 KiB
Raw Blame History

                        因收到Google相关通知网站将会择期关闭。相关通知内容
                        
                        
                        49 _ 以容器构建系统(下):韧性与弹性
                        你好,我是周志明。今天,我们接着上节课“隔离与协作”的话题,继续来讨论容器编排的另一个目标:韧性与弹性。

我曾经看过一部电影叫做《Bubble Boy》主要讲了一个体内没有任何免疫系统的小男孩每天只能生活在无菌的圆形气球里对常人来说不值一提的细菌都会直接威胁到他的性命。小男孩尽管能够降生于世但并不能真正地与世界交流这种生命是极度脆弱的。

真实世界的软件系统,跟电影世界中的小男孩所面临的处境其实差不多。

要知道,让容器能够相互连通、相互协作,仅仅是以容器构建系统的第一步,我们不仅希望得到一个能够运行起来的系统,而且还希望得到一个能够健壮运行的系统、能够抵御意外与风险的系统。

当然在Kubernetes的支持下你确实可以直接创建Pod将应用运行起来但这样的应用就像是电影中只能存活在气球中的小男孩一样脆弱无论是软件缺陷、意外操作或者硬件故障都可能导致在复杂协作的过程中某个容器出现异常进而出现系统性的崩溃。

为了解决这个问题架构师专门设计了服务容错的策略和模式你可以回顾复习第36、37讲。而Kubernetes作为云原生时代的基础设施也尽力帮助我们以最小的代价来实现容错为系统健壮运行提供底层支持。

那么Kubernetes所提供的帮助就是指除资源模型之外的另一个核心设计理念控制器设计模式。它的其中一种重要应用就是这节课我们要探讨的主题实现具有韧性与弹性的系统。

接下来我们就从如何解决场景四的问题开始一起来探讨下为什么Kubernetes要设计这些控制器以及为什么这些控制器会被设计成现在这种样子。

编排系统如何快速调整出错的服务?

我们先来看看场景四的问题:

场景四假设有个由数十个Node、数百个Pod、近千个Container所组成的分布式系统作为管理员你想要避免该系统因为外部流量压力、代码缺陷、软件更新、硬件升级、资源分配等各种原因而出现中断的状况那么你希望编排系统能为你提供何种支持

作为用户,我们当然最希望容器编排系统能自动地把所有意外因素都消灭掉,让每一个服务都永远健康,永不出错。但永不出错的服务是不切实际的,只有凑齐七颗龙珠才可能办得到。

所以我们就只能退而求其次,让编排系统在这些服务出现问题、运行状态不正确的时候,能自动将它们调整成正确的状态。

这种需求听起来其实也挺贪心的但已经具备足够的可行性了。而且我们可以采取的应对办法在工业控制系统里已经有了非常成熟的应用它叫做控制回路Control Loop

关于控制回路的一般工作过程在Kubernetes官方文档中是以“房间里空调自动调节温度”为例来具体介绍的当你设置好了温度就是告诉空调你对温度的“期望状态”Desired State而传感器测量出的房间实际温度是“当前状态”Current State

那么,根据当前状态与期望状态的差距,控制器对空调制冷的开关进行调节控制,就能让其当前状态逐渐接近期望状态。

由此我们把这种控制回路的思想迁移应用到容器编排上自然会为Kubernetes中的资源附加上了期望状态与实际状态两项属性。

不管是已经出现在上节课的资源模型中用于抽象容器运行环境的计算资源还是没有登场的对应于安全、服务、令牌、网络等功能的资源第40讲中曾提及过如果用户想使用这些资源来实现某种需求并不能像平常编程那样去调用某个或某一组方法来达成目的。而是要通过描述清楚这些资源的期望状态由Kubernetes中对应监视这些资源的控制器来驱动资源的实际状态逐渐向期望状态靠拢才能够达成自己的目的。

而这种交互风格就被叫做Kubernetes的声明式API如果你之前有过实际操作Kubernetes的经验那你日常在元数据文件中的spec字段所描述的就是资源的期望状态。

额外知识Kubernates的资源对象与控制器- 目前Kubernetes已内置支持相当多的资源对象并且还可以使用CRDCustom Resource Definition来自定义扩充你可以使用kubectl api-resources来查看它们。下面我根据用途分类给你列举了一些常见的资源-

用于描述如何创建、销毁、更新、扩缩Pod包括AutoscalingHPA、CronJob、DaemonSet、Deployment、Job、Pod、ReplicaSet、StatefulSet

用于配置信息的设置与更新包括ConfigMap、Secret

用于持久性地存储文件或者Pod之间的文件共享包括Volume、LocalVolume、PersistentVolume、PersistentVolumeClaim、StorageClass

用于维护网络通信和服务访问的安全包括SecurityContext、ServiceAccount、Endpoint、NetworkPolicy

用于定义服务与访问包括Ingress、Service、EndpointSlice

用于划分虚拟集群、节点和资源配额包括Namespace、Node、ResourceQuota-

这些资源在控制器管理框架中,一般都会有相应的控制器来管理,这里我也列举了一些常见的控制器,按照它们的启动情况进行了分类,如下:-

必须启用的控制器EndpointController、ReplicationController、PodGCController、ResourceQuotaController、NamespaceController、ServiceAccountController、GarbageCollectorController、DaemonSetController、JobController、DeploymentController、ReplicaSetController、HPAController、DisruptionController、StatefulSetController、CronJobController、CSRSigningController、CSRApprovingController、TTLController

默认启用的可选控制器可通过选项禁止TokenController、NodeController、ServiceController、RouteController、PVBinderController、AttachDetachController

默认禁止的可选控制器可通过选项启用BootstrapSignerController、TokenCleanerController

那么,与资源相对应的,只要是实际状态有可能发生变化的资源对象,就通常都会由对应的控制器进行追踪,每个控制器至少会追踪一种类型的资源。

因此为了管理众多资源控制器Kubernetes设计了统一的控制器管理框架kube-controller-manager来维护这些控制器的正常运作并设计了统一的指标监视器kube-apiserver用于在控制器工作时为它提供追踪资源的度量数据。

Kubernetes控制器模式的工作原理

那么Kubernetes具体是怎么做的呢在回答之前我想先解释下毕竟我们不是在写Kubernetes的操作手册没办法展开和详解每个控制器所以下面我就以两三种资源和控制器为代表来举例说明一下。

OK回到问题上。这里我们只要把场景四进一步具体化转换成下面的场景五就可以得到一个很好的例子了。

比如说我们就以部署控制器Deployment Controller、副本集控制器ReplicaSet Controller和自动扩缩控制器HPA Controller为例来看看Kubernetes控制器模式的工作原理。

场景五:通过服务编排,我们让任何分布式系统自动实现以下三种通用的能力:

Pod出现故障时能够自动恢复不中断服务 Pod更新程序时能够滚动更新不中断服务 Pod遇到压力时能够水平扩展不中断服务。

在这节课的一开始我提到过虽然Pod本身也是资源完全可以直接创建但由Pod直接构成的系统是十分脆弱的就像是那个气球中的小男孩所以在实际生产中并不提倡。

正确的做法是通过副本集ReplicaSet来创建Pod。

ReplicaSet也是一种资源它是属于工作负荷一类的资源代表了一个或多个Pod副本的集合你可以在ReplicaSet资源的元数据中描述你期望Pod副本的数量即spec.replicas的值

当ReplicaSet成功创建之后副本集控制器就会持续跟踪该资源一旦有Pod发生崩溃退出或者状态异常默认是靠进程返回值你还可以在Pod中设置探针以自定义的方式告诉Kubernetes出现何种情况Pod才算状态异常ReplicaSet都会自动创建新的Pod来替代异常的Pod如果因异常情况出现了额外数量的Pod也会被ReplicaSet自动回收掉。

总之就是确保在任何时候集群中这个Pod副本的数量都会向期望状态靠拢。

另外我们还要清楚一点就是ReplicaSet本身就能满足场景五中的第一项能力可以保证Pod出现故障时自动恢复。但是在升级程序版本时ReplicaSet就不得不主动中断旧Pod的运行重新创建新版的Pod了而这会造成服务中断。

因此对于那些不允许中断的业务以前的Kubernetes曾经提供过kubectl rolling-update命令来辅助实现滚动更新。

所谓的滚动更新Rolling Updates是指先停止少量旧副本维持大量旧副本继续提供服务当停止的旧副本更新成功新副本可以提供服务以后再重复以上操作直至所有的副本都更新成功。我们把这个过程放到ReplicaSet上就是先创建新版本的ReplicaSet然后一边让新ReplicaSet逐步创建新版Pod的副本一边让旧的ReplicaSet逐渐减少旧版Pod的副本。

而到了现在之所以kubectl rolling-update命令会被淘汰其实是因为这样的命令式交互完全不符合Kubernetes的设计理念这是台面上的说法我觉得淘汰的根本原因主要是因为它不够好用。如果你希望改变某个资源的某种状态就应该将期望状态告诉Kubernetes而不是去教Kubernetes具体该如何操作。

所以现在新的部署资源Deployment与部署控制器就被设计出来了。具体的实现步骤是这样的我们可以由Deployment来创建ReplicaSet再由ReplicaSet来创建Pod当我们更新了Deployment中的信息以后比如更新了镜像的版本部署控制器就会跟踪到新的期望状态自动地创建新ReplicaSet并逐渐缩减旧的ReplicaSet的副本数直到升级完成后彻底删除掉旧ReplicaSet。这个工作过程如下图所示

好,我们再来看看场景五中的最后一种情况。

你可能会知道在遇到流量压力时管理员完全可以手动修改Deployment中的副本数量或者通过kubectl scale命令指定副本数量促使Kubernetes部署更多的Pod副本来应对压力。然而这种扩容方式不仅需要人工参与而且只靠人类经验来判断需要扩容的副本数量也不容易做到精确与及时。

为此Kubernetes又提供了Autoscaling资源和自动扩缩控制器它们能够自动地根据度量指标如处理器、内存占用率、用户自定义的度量值等来设置Deployment或者ReplicaSet的期望状态实现当度量指标出现变化时系统自动按照“Autoscaling→Deployment→ReplicaSet→Pod”这样的顺序层层变更最终实现根据度量指标自动扩容缩容。

小结

故障恢复、滚动更新、自动扩缩这些特性在云原生时代中常常被概括成服务的弹性Elasticity与韧性ResilienceReplicaSet、Deployment、Autoscaling的用法也是所有Kubernetes教材资料中都会讲到的“基础必修课”。

学完了这两节课我还想再说明一点如果你准备学习Kubernetes或者其他云原生的相关技术我建议你最好不要死记硬背地学习每个资源的元数据文件该如何编写、有哪些指令、有哪些功能更好的方式是站在解决问题的角度去理解为什么Kubernetes要设计这些资源和控制器理解为什么这些资源和控制器会被设计成现在这种样子。

一课一思

如果你觉得已经理解了前面几种资源和控制器的例子,那不妨思考几个问题:

假设我想限制某个Pod持有的最大存储卷数量应该如何设计 假设集群中某个Node发生硬件故障Kubernetes要让调度任务避开这个Node应该如何设计 假设一旦这个Node重新恢复Kubernetes要能尽快利用上面的资源又该如何去设计

其实只要你真正接受了资源与控制器是贯穿整个Kubernetes的两大设计理念即便不去查文档手册也应该能推想出个大概轮廓你在这个基础上再去看手册或者源码的时候想必就能够事半功倍。

好,欢迎给我留言,分享你的答案。如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。