Alan Hou的个人博客

云原生系列Kubernetes篇 ReplicaSets

本文来自正在规划的Go语言&云原生自我提升系列,欢迎关注后续文章。

我们已经讲解了如何以Pod运行单个容器,但这些Pod基本上是一次性的单体。大部分时候我们会想要在某个时间运行一个容器的多个副本,原因有:

冗余性:通过运行多实例实现容错。

可伸缩性:通过运行多实例实现更高的请求处理容量。

分片(Sharding):不同的副本可以并行处理运算的不同部分。

当然,我们可以通过多个不同的Pod(其实大部分内容相近)声明手动创建多个Pod的拷贝,但这样费力又易错。逻辑上,用户管理一个副本集时把它们看作定义和管理的单个实体,这也正是ReplicaSet的本质。副本集作为集群范围的Pod管理器,确保相应类型和数量的Pod一直能保持运行状态。

因ReplicaSet令我们创建和管理Pod的副本集变简单,它们是常见应用部署模式的基石,并且是基础设施级别的自愈应用。由副本集管理的Pod自动按某些错误条件重新调度,比如节点失败或网络中断。

理解副本集最简单的方式是它将饼切和所需饼干数量集成到了一个API对象中。在定义副本集时,我们会定义一个想要创建Pod的规格(饼切)以及希望的副本数量。此外我们需要定义一种查找副本集所控制的Pod的方式。实际管理副本Pod的动作是一种调谐循环(reconciliation loop)。这种循环是Kubernetes中大部分设计和实现的基础。

调谐循环

调谐循环背后的中心理念是期望状态与观测状态或当前状态比较的概念。期望状态是你所期望的状态。对于副本集,是指期望的副本数量和复制的Pod定义。例如,“期望状态是运行kuard服务端Pod的三个副本。”对应地,当前状态是系统的当前的观测状态。例如,“仅有两个当前运行的kuard Pod。”

调谐循环保持运行状态,观测当前状态并会尝试使用观测状态匹配期望状态。例如,前面的例子中,调谐循环会创建新的kuard Pod以使三个副本的观测状态与期望状态匹配。

使用调谐循环来管理状态有诸多好处。它是一个内在目标驱动、自愈的系统,但通常也可以轻松地用几行代码进行表述。例如,副本集的调谐循环是单循环,但处理用户操作扩容或缩容副本集以及节点失败或是在集群失联时进行节点重连。

在本系列中我们会看到使用调谐循环的例子。

关联Pod及副本集

解耦是Kubernetes中的一个核心主题。具体来说,Kubernetes中所有的核心概念都是彼此都是格式化的并且可由其它组件交换及替代,这点非常重要。本着这一精神,副本集与Pod之间为松耦合的。虽然副本集创建并管理Pod,它们并不拥有所创建的Pod。副本集使用标签查询来识别它们应管理的Pod集。然后它们使用与Pods一章中直接使用相同的Pod API来创建所管理的Pod。”走正门”是Kubernetes中的别一个中心设计理念。以类似的解耦方式,创建多个Pod的副本集以及这些Pod的负载均衡服务也是完全隔离的,将API与对象进行解耦。除了支持模块化,将Pod与副本集进行隔离还带来了其它重要功能,下面小节中将会讨论。

采用已有容器

虽然声明式配置很有价值,有时使用命令式单独进行构建会更容易。尤其是在早期可能只是通过容器镜像部署单个Pod且不需要ReplicaSet进行管理。甚至还会定义一个负载均衡器来对单个Pod提供服务。

但有时可能会希望将单体容器扩展为副本服务并创建管理一组类似的容器。如果副本集拥有它们所创建的Pod,那么开始复制Pod的唯一方式是先删除再通过ReplicaSet重新启动。这可能是破坏性的,因为有一个期间没有运行中的容器拷贝。但是,因为副本集与其所管理的Pod解耦了,只需要创建采用已有Pod的副本集并扩充这些容器的拷贝。这样,可以无缝地将单个指令式Pod切换为由ReplicaSet管理的Pod副本集。

隔离容器

通常在服务器出现问题时,Pod级别的健康会自动重启该Pod。但如果健康检查未完成,Pod可能会出问题但仍为副本集的一部分。在这种情况下,虽然可以直接杀死Pod,但那样只能给开发人员留下日志以供调试问题。其实可以修改出问题的Pod上的标签集。这样会取消与副本集(及服务)的关联,这样就可以调试Pod。副本集控制器会注意到缺少了一个Pod并创建新的拷贝,但因为Pod仍处于运行状态,开发者可以进行交互调试,比干巴巴地调试日志要更有价值。

使用副本集设计

副本集用于表示架构内单一、可扩展的微服务。其主要我是副本集控制器中所创建的每个Pod都是异质的。一般这些Pod由Kubernetes服务的负载均衡对外暴露,将流量分发到组成服务的这些Pod上。总的来说,副本集设计为无状态(或近乎无状态)服务。它们创建的元素可互换,在副本集缩容时,会任意选中一个Pod进行删除。这种缩容行为不应影响到应用。

注:一般会看到应用使用Deployment对象,原因是它可用于管理新的发布版本。副本集是Deployment对象背后的支撑,理解它们对于碰到问题时进行debug非常重要。

ReplicaSet Spec

和所有Kubernetes对象一样,副本集使用规格(specification)进行定义。所有的副本集必须有独立的名称(使用metadata.name字段定义),spec版块中描述在指定时间内在集群中运行的Pod(副本)的数量,Pods模板描述在没达到副本数量时应创建的Pod。例9-1中展示了最小化的副本集定义。注意replicas、selector和 template版块的定义,因为它们包含了副本如何运行的重要信息。

例9-1 kuard-rs.yaml

Pod模板

前面说过,在当前状态Pod的数量少于预期状态Pod数量时,ReplicaSet控制器会使用副本集规格中的模板新建Pod。这种创建Pod的方式与前面章节中使用YAML文件创建Pod的方式一样,但除了使用文件,Kubernetes副集控制器还可以根据API服务器的Pod模板创建并提交Pod声明。以下是副本集中Pod模板的一个示例:

标签

在各种大小的集群中,很多Pod会同步运行,那么副本集调谐循环如何发现某个副本集的Pod集呢?副本集使用一组Pod标签来对Pod列表进行过滤监控集群状态并追踪集群中运行的Pod。根据查询所返回的Pod数量,副本删除或创建Pod来达到预期的副本数量。它些过滤标签在副本集spec版块中进行定义,是理解副本集如何运作的关键。

注:副本集spec中的选择应为Pod模板中标签的相应子集。

创建副本集

副本集通过将ReplicaSet对象给Kubernetes API进行创建。在本节中,我们会使用配置文件和kubectl apply命令来创建一个副本集。

例9-1中的副本集配置文件可保障在任意给定时间运行gcr.io/kuar-demo/kuard-amd64:green容器的一个拷贝。使用kubectl apply命令将kuard副本集提交给Kubernetes API:

一旦接受了kuard副本集,ReplicaSet控制器会检测到没有匹配预期状态的kuard Pod在运行,并根据Pod模板的内容新建kuard Pod:

检视副本集

和Pod和其它Kubernetes API对象一相,如果对ReplicaSet的细节有兴趣,可以使用describe命令来提供跟其状态有关的更多信息。以下是使用describe来获取此前创建的副本集的详情:

可以看到副本集的标签选择器,以及它所管理的所有副本集的状态。

通过Pod查找副本集

有时你可能会想如果Pod由副本集管理,那么是哪个副本集呢?为实现这种发现,副本集控制器为其所创建的每个Pod添加了一个ownerReferences版块。运行如下命令注意查看ownerReferences版块:

如果成功运行,会看到管理这一Pod的副本集名称。

查找副本集的一组Pod

也可以知道副本集所管理的Pod集合。首先,使用kubectl describe命令获取标签集。上例中,标签选择器为app=kuard,version=2。为查找匹配该选择器的Pod,应使用--selector标记或其简写-l

这与副本集查询当前Pod数的所执行的命令相同。

副本集扩容

可以通过更新Kubernetes中存储的ReplicaSet对象的spec.replicas键来对副本集进行扩容或缩容。在扩容副本集时,会使用副本集上定义的Pod模板将新的Pod提交给Kubernetes API。

使用kubectl scale进行指令式扩容

最简单的实现方式是使用kubectlscale命令。例如,要扩容到4个副本,可运行如下命令:

虽然这种指令式命令在演示及快速响应紧急场景时(比如负载暴增)很有用,但能够更新配置文本文件来匹配由scale命令所设置的副本数也很重要。在如下场景中会发现原因很明显。

Alice当班,此时她所负责的服务量骤升。Alice使用scale命令将服务器数量增加到10个来应对请求,这时问题解决了。但是,Alice忘记了更新源头控制的副本集配置。

几天后,Bob在准备每周轮转。Bob编辑了版本控制中存储的ReplicaSet配置,使用新的容器镜像,但他没有注意到当前文件中的副本量为5,而不是Alice进行响应时增加到的10。Bob进行了轮转,既更新了容器镜像也将副本数做了减半。这会马上导致过载,进而产生故障。

这个虚构的案例研究说明了需要保障指令式修改立即在源码控制中进行声明式的修改。而且如果不是很急迫的话,通常建议只按照下面小节中描述的那样做声明式修改。

使用kubectl apply进行声明式扩容

在声明式的世界中,修改通过在版本控制中编辑配置文件然后将修改应用到集群中。要对kuard副本集进行扩容,编辑kuard-rs.yaml 配置文件,然后将replicas的数量设置为3

在多用户场景中,很可能会对这一修改添加存档的代码审核,最后将修改添加到版本控制中。不管哪种方式,都可以使用kubectl apply命令提交更新后的kuard副本集至API服务端:

现在有了更新后的kuard副本集,ReplicaSet控制器会监测到期望Pod的数量发生了改变,需要采取行动达成期望状态。如果使用了前一小节中的指令式scale命令,ReplicaSet控制器会销毁一个Pod来让数量达到3个。否则,它会使用kuard副本集中定义的模板来将两个新的Pod提交给Kubernetes API。不管如何,使用kubectl get pods命令来列出运行中的kuardPod。应当会看到类似下面这三个运行中状态Pod的输出,其中两个生命更短,因为是新添加的:

副本集自动扩缩容

虽然有时会希望显式控制副本集中副本的数量,通常需要的是“足够的”副本。根据副本集中容器的需求定义会不一样。例如,对于像NGINX这样的web服务端,可能要根据CPU用量来进行扩容。对于内存缓存,可能要根据内存消耗来进行扩容。在某些场景下,可能会根据自定义应用指标进行扩容。Kubernetes可通过横向Pod自动扩缩容(HPA)处理所有这些场景。

横向Pod自动扩缩容有些拗口,读者可能会奇怪为什么不直接称之为“自动扩缩容”。Kubernetes区分了横向扩缩容,这是对Pod创建额外副本,以及纵向扩缩容,它是对具体Pod进行资源的增加(比如增加Pod所需的CPU)。很多方案还带有集群扩缩容,根据资源需求对集群中机器的数量进行扩缩容,但这种方案不在本章的讨论范畴内。

注:自动扩容要求在集群中有metrics-servermetrics-server记录指标并提供在决定做扩缩容时HPA用于消费指标的API。Kubernetes的大部分安装都默认包含有metrics-server。可以通过在kube-system命名空间中列出Pod验证其是否存在:

应该会在列表中看到以metrics-server开头的Pod。如果没看到,自动扩缩容无法正确运行。

根据CPU用量进行扩容是Pod扩容中最常见的情况。还可以按内容使用来进行扩容。基于CPU扩容对于根据收到的请求等比消耗CPU而内存用量相对稳定的系统最为有用。

可使用如下命令对副本集进行扩容:

这一命令创建一个自动扩容器,扩容范围为2到5个副本,CPU的阈值为80%。要浏览、修改或删除该资源,可以使用kubectl命令和horizontalpodautoscalers资源。输入horizontalpodautoscalers还是很费事的,可缩写为hpa

警告:因为Kubernetes的解耦特性,HPA和副本集没有直接关联。虽然模块化和组合很棒,但也产生了反模式。具体来讲,通过指令式或声明式管理副本数量合并自动扩缩容不是好主意。如果你和自动扩容器都在修改副本数量,很可能会崩溃,产生预期外的行为。

删除副本集

在不再需要副本集时,可以使用kubectl delete命令删除它。默认,它还会邮件由副本集管理的Pod:

运行kubectl get pods命令显示所有由kuard副本集创建的kuard Pod也被删除了:

如果不希望删除副本集所管理的Pod,可将--cascade标记设置为false来确保删除的是副本集对象而非Pod:

小结

使用副本集创建Pod为构建具备容错的健壮应用提供了基础,并通过启用扩容缩和良好的部署模式让应用部署轻而易举。对于重要的Pod使用副本集,即使只有一个Pod。有些人甚至默认使用副本集而不是Pod。典型的集群拥有多个副本集,所以请在有效范围内自由使用。

退出移动版