什么是 StatefulSet?

StatefulSet 是 Kubernetes 提供一种资源对象。它适合用来管理我们的用状态服务。管理的内容包括部署,升级,扩容等。

StatefulSet 和 Deployment 有什么不同?

其实 StatefulSet 和 Deployment 都是 Kubernetes 提供的资源对象。但是,Deployment 更适合帮我们管理无状态服务。所以,两者的区别主要是在于管理 Pod 的方式上。说的更加明确一点,两者行为上的不同主要是因为 Controller 对同一个操作实现逻辑的不同(部署,升级,扩容等)。

另外一个实现细节上面的不同之处是,StatefulSet 直接管辖的是 Pod,而 Deployment 和 Pod 之间还会有一个 ReplicaSet。

StatefulSet 为什么适合管理有状态服务?

我们先来看看有状态服务和无状态服务有什么区别。

有状态服务

「状态」指的是一些应用在对客户提供服务的过程中必要的一些信息,并且这些信息是需要被保存的。可以将它简单理解为一个计算的「中间结果」。有状态服务指的就是依赖这些信息对外提供服务的应用。

无状态服务

无状态的应用在对外提供服务的时候,将不会依赖任何持久化的数据。绝大部分它需要的信息都来自于网络请求,或者说内存中的一些信息。即使应用在对外服务的过程当中有重启行为,内存或者请求的信息丢失,也不需要担心。

如何管理

StatefulSet 为有状态服务提供了以下几个机制来保证它们部署,扩容和升级的正常进行:

  1. 稳定的存储
  2. 集群唯一的标识符
  3. 操作的顺序性

其中第一个措施主要是用来解决「状态」的存储问题的。在 StatefulSet 中,若一个 Pod 创建一个存储设备。即使之后 Pod 和 Statefulset 被删除,这个存储设备已经它里面保存的「状态」信息仍旧会被保存下来。除非我们手动的将这些存储设备删除(存储设备包含 PVC,PV,以及底层的存储实例)。

第二个措施是和 Deployment 区别比较大的一点。对于一个无状态服务的集合来说,哪一个实例对外提供服务都是 ok 的,因为他们提供的服务不依赖任何状态。但是有状态服务就不一样了:比如 A 实例依赖 B 实例的输出,B 实例又依赖 C 实例的输出。所以很多时候,有状态服务接收的流量可能仅来自于其中一个实例。既然有这种需求,那么我们就必须要有一种明确的方式来标识 Statefulset 中管理的每一个实例(Pod)。这种标识符的生成有一套特殊的规则。

第三个措施主要是针对有状态服务部署,升级,销毁三个操作的。有状态服务一般都对实例部署的顺序和销毁的顺序有一些要求。比如,A 如果不启动,B 就无法启动,因为 B 在启动时依赖了 A 写入的一些状态信息。同理,升级和销毁操作也是这样。

StatefulSet Pod 标识符的生成规则

假设现在有一个构造了3副本的 StatefulSet 对象,该对象的名字为 web。那么StatefulSet 首先会为这三个副本都标一个序号,从0~N-1。

  • web-0
  • web-1
  • web-2

Kubernetes 将其称为 Ordinal Index。

根据我们上面说过的,一个 StatefulSet 中不同的副本可能同一时间只能有一个实例对外提供服务,其余的两个都作为冗余,旨在为服务提供高可用的能力。所以,在网络通信的场景下, 这三个 Pod 也需要一个唯一的网络标识,这个唯一的网络标识,就是就是 Kubernetes 集群内部的 Pod DNS Name。它的生成规则如下:

1
web{0-N-1}.$(service name).$(namespace).svc.cluster.local

其中 service name 来自于 StatefulSet 的 Service,namespace 也是根据 StatefulSet 这个对象所在的 ns 来确定的。

Kubernetes 将其称为 Stable Network ID。

除此之外,如果在 Pod.Spec 中还写入了 PVC 创建存储设备。那么 StatefulSet 还会为每一个 Pod 的 PVC 以及 PV 也生成一个唯一的标识。生成的规则如下:

1
pvcName-web{0-N-1}

部署与升级

StatefulSet 的部署顺序将严格遵循以下几个原则:

  1. 部署顺序和 Pod Ordinal Index 的顺序一致,从 0~N-1
  2. 删除顺序和 Pod Ordinal Index 的顺序相反,从 N-1~0
  3. 一个 Pod 开始部署之前,它前面的 Pod 必须处于 Running 和 Ready 状态。相反,一个 Pod 中止之前,它前面的 Pod 必须已经被中止

对于第三点来说,这种执行的顺序性不仅限于 Ordinal Index 相邻的两个 Pod。如一个三副本的 StatefulSet 部署,pod-0,pod-1 已经部署成功。但是在 pod-2 部署的时候,pod-0 挂了,此时 Pod-2 不会开始部署,因为它的前置 Pod 有异常。

StatefulSet 自身提供了两种升级策略:OnDelete, RollingUpdate

OnDelete

当我们给 StatefulSet 配置了这种升级策略的时候,即使我们改动了 StatefulSet 的 template 中的信息,它也不会自动进行升级操作。我们需要手动的删除 Pod。这些 Pod 在重建之后,会自动应用最新的配置。这种方式的灵活性是比较大的,按照什么样的顺序升级,哪个 Pod 升级,哪个 Pod 不升级都可以由我们自己来控制。

RollingUpdate

StatefulSet 将会自动的按照 Pod 终止的顺序进行升级(从 N-1~0)。另外,在 RollingUpdate 这个策略上,StatefulSet 还额外的提供了一个 Partition 的功能,用于分阶段的升级。Partition 是一个 Pod Ordinal Index,它是 StatefulSet.Spec.updateStrategy.rollingUpdate 下的一个字段。

假设我们现在有一个三副本的 StatefulSet,当 partition 指定为1的时候,那么只有 pod-1 和 pod-2 才会因为 template 中的信息的更新而进行升级,升级顺序还是按照 Ordinal Index 的逆序。而 pod-0 仍然会以旧的版本运行,即使发生重启。如果你的 partition 设置的值已经超过了 Ordinal Index 的范围,那么将没有 Pod 会升级。