Loading

有状态服务

七、有状态服务

了解完数据卷之后,你知道了如何运行一个单实例pod和无状态的多副本pod,还有如何通过持久化存储运行一个有状态pod。可以运行几个多副本的web服务pod实例,运行一个提供持久化存储的单数据库pod实例,这个持久化存储可以是简单的pod卷,也可以是一个绑定到持久卷上的持久卷声明。但是是否可以通过ReplicaSet来复制数据库pod呢?

1 运行每个实例都有单独存储的多副本

ReplicaSet通过一个pod模板创建多个pod副本。这些副本除了它们的名字和IP地址不同外,没有别的差异。如果pod模板里描述了一个关联到特定持久卷声明的数据卷,那么ReplicaSet的所有副本都将共享这个持久卷声明,也就是绑定到同一个声明的持久卷。如下图:
image

ReplicaSet里的所有pod共享相同的持久卷声明和持久卷。因为是在pod模板里关联声明的,又会依据pod模板创建多个pod副本,则不能对每个副本都指定独立的持久卷声明。那如何运行一个pod的多个副本,让每个pod都有独立的存储卷?

1.1 手动创建pod

可以手动创建多个pod,每个pod使用一个独立的持久卷声明,但是因为没有一个ReplicaSet在后面对应它们,所以需要手动管理它们。当有的pod消失后(比如节点故障),需要手动创建它们。因此这不是一个好的选择。

1.2 一个pod实例对应一个ReplicaSet

与直接创建不同,可以创建多个ReplicaSet,每个ReplicaSet的副本数设为1,做到pod和ReplicaSet的一一对应,为每个ReplicaSet的pod模板关联一个专属的持久卷声明,这样就可以让ReplicaSet对pod进行管理。但是与单个ReplicaSet相比,它还是显得比较笨重的。例如,在这种情况下要如何伸缩pod?扩容的话,必须重新创建新的ReplicaSet。

1.3 使用同一数据卷中的不同目录

还有一个方法是所有pod共享同一数据卷,但是每个pod在数据卷中使用不同的数据目录。
image

因为不能在一个pod模板中差异化配置pod副本,所以不能指定一个实例使用哪个特定目录。但是可以让每个实例自动选择(或创建) 一个别的实例还没有使用的数据目录。这种方案要求实例之间相互协作,其正确性很难保证,同时共享存储也会成为整个应用的性能瓶颈。

2 每个pod都提供稳定的标识

除了上面说的存储需求,集群应用也会要求每一个实例拥有生命周期内唯一标识。pod可以随时被删掉,然后被新的pod替代。当一个ReplicaSet中的pod被替换时,尽管新的pod也可能使用被删掉pod数据卷中的数据,但它却是拥有全新主机名和IP的崭新pod。在一些应用中,当启动的实例拥有完全新的网络标识,但还使用旧实例的数据时,很可能引起问题。

因此一些应用需要维护一个稳定的网络标识,这个需求在有状态的分布式应用中很普遍。这类应用要求管理者在每个集群成员的配置文件中列出所有其他集群成员和它们的IP地址(或主机名)。但是在Kubernetes中,每次重新调度一个pod,这个新的pod就有一个新的主机名和IP地址,这样就要求当集群中任何一个成员被重新调度后,整个应用集群都需要重新配置。

你可以利用前面讲到的Service,针对集群中的每个成员,都创建一个独立的服务来提供稳定的网络地址。因为服务IP是固定的,可以在配置文件中指定集群成员对应的服务IP而不是pod IP。

这种解决方案可行,但是会十分麻烦,因此便出现了StatefulSet资源。

3 StatefulSet介绍

在第四章中,提到过StatefulSet是一种资源控制器,它用来管理某pod集合的部署和扩缩,并为这些pod提供持久存储和持久标识符。它和Deployment很相似,但不同的是,StatefulSet为它们的每个pod维护了一个有粘性的ID。这些pod是基于相同的配置来创建的,但不能相互替换:无论怎么调度,每个pod都有一个永久不变的ID。

要很好地理解StatefulSet的用途, 最好先与RS或者RC对比一下。首先拿一个通用的类比来解释它们。

StatefulSet最初被称为PetSet,这个名字来源于宠物与牛的类比。

我们倾向于把应用看作宠物,给每个实例起一个名字,细心照顾每个实例。但是也许把它们看成牛更为合适,并不需要对单独的实例有太多关心。这样就可以非常方便地替换掉不健康的实例,就跟农场主替换掉一头生病的牛一样。对于无状态的应用实例来说,行为非常像农场里的牛。一个实例挂掉后并没什么影响,可以创建一个新实例,而让用户完全无感知。
另一方面,有状态的应用的一个实例更像一个宠物。若一只宠物死掉,不能买到一只完全一样的,而不让用户感知到。若要替换掉这只宠物,需要找到一只行为举止与之完全一致的宠物。对应用来说,意味着新的实例需要拥有跟旧的实例完全一致的状态和标识。

RS或RC管理的pod副本比较像牛,这是因为它们都是无状态的,任何时候它们都可以被一个全新的pod替换。然而有状态的pod需要不同的方法,当一个有状态的pod挂掉后(或者它所在的节点故障),这个pod实例需要在别的节点上重建,但是新的实例必须与被替换的实例拥有相同的名称、网络标识和状态。这就是StatefulSet如何管理pod的。

Statefulset保证了pod在重新调度后保留它们的标识和状态。它让你方便地扩容、缩容。与ReplicaSet类似,Statefulset也会指定期望的副本个数,它决定了在同一时间内运行的宠物的数量。pod也是依据Statefulset的pod模板创建的。与ReplicaSet不同的是,Statefulset创建的pod副本并不是完全一样的。每个pod都可以拥有一组独立的数据卷,另外宠物pod的名字都是规律的(固定的),而不是每个新pod都随机获取一个名字。

一个Statefulset创建的每个pod都有一个从零开始的顺序索引,这个会体现在pod的名称和主机名上,同样还会体现在pod对应的固定存储上。这些pod的名称则是可预知的,因为它是由Statefulset的名称加该实例的顺序索引值组成的。不同于pod随机生成一个名称,这样有规则的pod名称很方便管理:

image

扩容一个Statefulset会使用下一个还没用到的顺序索引值创建一个新的pod实例。比如,要把一个Statefulset从两个实例扩容到三个实例,那么新实例的索引值就会是2(现有实例使用的索引值为0和1)。

当缩容一个Statefulset时,比较好的是很明确哪个pod将要被删除。作为对比,RS的缩容操作则不同,不知道哪个实例会被删除,也不能指定先删除哪个实例。缩容一个Statefulset将会最先删除最高索引值的实例,缩容的结果是可预知的。

image

至于存储,一个有状态的pod需要拥有自己的存储,即使该有状态的pod被重新调度(新的pod与之前pod的标识完全一致),新的实例也必须挂载着相同的存储。那Statefulset是如何做到这一点的呢?

在前面介绍过持久卷和持久卷声明,通过在pod中关联一个持久卷声明的名称,就可以为pod提供持久化存储。因为持久卷声明与持久卷是一对一的关系,所以每个Statefulset的pod都需要关联到不同的持久卷声明,与独自的持久卷相对应。

像Statefulset创建pod一样,Statefulset也需要创建持久卷声明。所以一个Statefulset可以拥有一个或多个卷声明模板,这些持久卷声明会在创建pod前创建出来,绑定到一个pod实例上:
image

扩容StatefulSet增加一个副本数时,会创建两个或更多的API 对象(一个pod和与之关联的一个或多个持久卷声明)。但是对缩容来说,则只会删除一个pod,而遗留下之前创建的声明。当你需要释放特定的持久卷时,需要手动删除对应的持久卷声明。

当你不小心缩容后,如果没有删除PVC,那么可以通过扩容的方式弥补,新的pod会运行到与之前完全一致的状态(名字也相同):

image

4 StatefulSet的使用场景

StatefulSets 对于需要满足以下一个或多个需求的应用程序很有价值:

  • 稳定的、唯一的网络标识符。
  • 稳定的、持久的存储。
  • 有序的、优雅的部署和缩放。
  • 有序的、自动的滚动更新。

在上面描述中,稳定的意味着 Pod 调度或重调度的整个过程是有持久性的。如果应用程序不需要任何稳定的标识符或有序的部署、删除或伸缩,则应该使用由一组无状态的副本控制器提供的工作负载来部署应用程序,比如Deployment或者ReplicaSet可能更适用于无状态应用部署。

5 创建StatefulSet

下面的示例演示了StatefulSet的组件。

apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None
  selector:
    app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  selector:
    matchLabels:
      app: nginx # 必须和下面的 .spec.template.metadata.labels 对应
  serviceName: "nginx"
  replicas: 3    # 默认为 1
  template:
    metadata:
      labels:
        app: nginx # 必须和上面的 .spec.selector.matchLabels 对应
    spec:
      terminationGracePeriodSeconds: 10
      containers:
      - name: nginx
        image: k8s.gcr.io/nginx-slim:0.8
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: "my-storage-class"
      resources:
        requests:
          storage: 1Gi

上述例子中:

  • 名为 nginx 的 Headless Service 用来控制网络域名。
  • 名为 web 的 StatefulSet 有一个 Spec,它表明将在独立的 3 个 Pod 副本中启动 nginx 容器。
  • volumeClaimTemplates 将通过持久卷驱动提供的PV来提供稳定的存储。

你必须设置 StatefulSet 的 .spec.selector 字段,使之匹配其在 .spec.template.metadata.labels 中设置的标签。在 Kubernetes 1.8 版本之前, 被忽略 .spec.selector 字段会获得默认设置值。 在 1.8 和以后的版本中,未指定匹配的 Pod 选择器将在创建 StatefulSet 期间导致验证错误。

在上面的 nginx 示例被创建后,会按照 web-0、web-1、web-2 的顺序部署三个 Pod。 在 web-0 进入 Running 和 Ready 状态前不会部署 web-1。在 web-1 进入 Running 和 Ready 状态前不会部署 web-2。 如果 web-1 已经处于 Running 和 Ready 状态,而 web-2 尚未部署,在此期间发生了 web-0 运行失败,那么 web-2 将不会被部署,要等到 web-0 部署完成并进入 Running 和 Ready 状态后,才会部署 web-2。

如果用户想将示例中的 StatefulSet 收缩为 replicas=1,首先被终止的是 web-2。 在 web-2 没有被完全停止和删除前,web-1 不会被终止。 当 web-2 已被终止和删除、web-1 尚未被终止,如果在此期间发生 web-0 运行失败, 那么就不会终止 web-1,必须等到 web-0 进入 Running 和 Ready 状态后才会终止 web-1。

6 删除StatefulSet

你可以像删除 Kubernetes 中的其他资源一样删除 StatefulSet:使用 kubectl delete 命令,并按文件或者名字指定 StatefulSet。

kubectl delete -f <file.yaml>
# 或者
kubectl delete statefulsets <statefulset 名称>

删除 StatefulSet 之后,你可能需要单独删除关联的无头服务。

kubectl delete service <服务名称>

当通过 kubectl 删除 StatefulSet 时,StatefulSet 会被缩容为 0。 属于该 StatefulSet 的所有 Pod 也被删除。 如果你只想删除 StatefulSet 而不删除 Pod,使用 --cascade=orphan

kubectl delete -f <file.yaml> --cascade=orphan

通过将 --cascade=orphan 传递给 kubectl delete,在删除 StatefulSet 对象之后, StatefulSet 管理的 Pod 会被保留下来。如果 Pod 具有标签 app=myapp,则可以按照 如下方式删除它们:

kubectl delete pods -l app=myapp

删除 StatefulSet 管理的 Pod 并不会删除关联的卷。这是为了确保你有机会在删除卷之前从卷中复制数据。

如果要完全删除 StatefulSet 中的所有内容,包括关联的 pods,你可以运行 一系列如下所示的命令:

grace=$(kubectl get pods <stateful-set-pod> --template '{{.spec.terminationGracePeriodSeconds}}')
kubectl delete statefulset -l app=myapp
sleep $grace
kubectl delete pvc -l app=myapp

在上面的例子中,Pod 的标签为 app=myapp;适当地替换你自己的标签。

关于强制删除:

  • 如果你发现 StatefulSet 的某些 Pod 长时间处于 'Terminating' 或者 'Unknown' 状态, 则可能需要手动干预以强制从 API 服务器中删除这些 Pod。
  • StatefulSet 不应将 pod.Spec.TerminationGracePeriodSeconds 设置为 0。 这种做法是不安全的,要强烈阻止。更多的解释请参考 强制删除 StatefulSet Pod
posted @ 2022-01-07 15:15  yyyz  阅读(276)  评论(0编辑  收藏  举报