k8s之StatefulSet介绍(六)
复制有状态的Pod
replicaSet通过一个pod模版创建多个pod副本。这些副本除了它们的名字和IP地址不同外,没有别的差异。如果pod模版里描述了一个关联到特定持久卷声明的数据卷,那么ReplicaSet的所有副本都将共享这个持久卷声明,也就是绑定到同一个持久卷声明。
因为是在pod模版里关联持久卷声明的,又会依据pod模版创建多个副本,则不能对每个副本都指定独立的持久卷声明。所以也不能通过一个ReplicaSet来运行一个每个实例都需要独立存储的分布式数据存储服务,至少通过单个ReplicaSet是做不到的。老实说,之前你学习到的所有API对象都不能提供这样的数据存储服务,还需要一个其他的对象--StatefulSet
我们先看不使用StatefulSet的情况下有没有方法实现多个副本有自己的持久卷声明。
三种取巧的方法。
第一种方法,不使用ReplicaSet,使用Pod创建多个pod,每个pod都有独立的持久卷声明。需要手动创建它们,当有的pod消失后(节点故障),需要手动创建它们。因此不是一个好方法。
第二种方法,多个replicaSet ,每个rs只有一个pod副本。但这看起来很笨重,而且没办法扩缩容。
第三种方法,使用同一个ReplicaSet,大家也都挂载同一个持久卷声明,应用内部做好互斥,创建多个data数据目录,每一个pod用一个标记为在用,后面应用不能选被标记为在用的目录。这样做很难保证协调的一点没问题,同时大家用同一个持久卷,读写io将成为整个应用的瓶颈。
除了上面的存储需求,集群应用也会要求每一个实例拥有生命周期内唯一标识。pod可以随时被删掉,然后被新的pod替代。当一个ReplicaSet中的pod被替换时,尽管新的pod也可能使用被删除pod数据卷中的数据,但它却是拥有全新主机名和IP的崭新pod.在一些应用中,当启动的实例拥有完全新的网络标识,但还使用旧实例的数据时,很可能引起问题,比如etcd存储服务。
当然也可以创建多个service ,每一个replicaset对应一个service,那么一样很笨重,且显得很低级。辛运的是,Kubernetes为我们提供了这类需求的完美解决方案--StatefulSet.
了解StatefulSet
可以创建一个StatefulSet资源代替ReplicaSet来运行这类pod.它们是专门定制的一类应用,这类应用中每一个实例都是不可替代的个体,都拥有稳定的名字和状态。
对比StatefulSet 与 ReplicaSet 或 ReplicationController
RS或RC管理的pod副本比较像牛,它们都是无状态的,任何时候它们都可以被一个全新的pod替换。然后有状态的pod需要不同的方法,当一个有状态的pod挂掉后,这个pod实例需要在别的节点上重建,但是新的实例必须与被替换的实例拥有相同的名称、网络标识和状态。这就是StatefulSet如何管理pod的。
StatefulSet 保证了pod在重新调度后保留它们的标识和状态。它让你方便地扩容、缩容。与RS类似,StatefulSet也会指定期望的副本数,它决定了在同一时间内运行的宠物数。也是依据pod模版创建的,与RS不同的是,StatefulSet 创建的pod副本并不是完全一样的。每个pod都可以拥有一组独立的数据卷(持久化状态)。另外pod的名字都是规律的(固定的),而不是每个新pod都随机获取一个名字。
提供稳定的网络标识
StatefulSet 创建的pod的名称,按照从零开始的顺序索引,这个会体现在pod的名称和主机名称上,同样还会体现在pod对应的固定存储上。
有状态的pod与普通的pod不一样的是,有状态的pod有时候需要通过其主机名来定位,而无状态的不需要,因为无状态的都一样,随机选一个就行,但对于有状态的来说,每一个pod都不一样,通常希望操作的是特定的一个。基于这个原因,一个StatefulSet要求你创建一个用来记录每个pod网络标记的headless Service。通过这个Service,每个pod将拥有独立的DNS记录,这样集群里它的伙伴或者客户端就可以通过主机名找到它。比如说一个属于default命名空间,名为foo的控制服务,它的一个pod名称为A-0,那么完整域名为:a-0.foo.default.svc.cluster.local。而在ReplicaSet是行不通的。
此外我们可以在容器中通过dig foo.default.svc.cluster.local对应的SRV记录,获取一个StatefulSet中所有pod的名称.
StatefulSet扩缩容的特点
扩容,会按照索引进行
缩容,也会按照索引,删除索引值最大的 pod
缩容StatefulSet任何时候只会操作一个pod实例,所以会很慢,不是因为索引要顺序进行,而是为了避免数据丢失。举例来说,一个分布式存储应用副本数为2,如果同时下线两个,一份数据记录就会丢失。
基于以上原因,StatefulSet在有实例不健康的情况下是不允许进行缩容操作的。一个不健康,你又缩容一个这样相当于两个同时下线。
持久卷的创建和删除
扩容Statefulset增加一个副本,会创建两个或更多的API对象(一个pod和一个与之关联的持久卷声明)。但对于缩容来将,只会删除一个pod,而遗留下之前创建的声明。因为当一个声明被删除后,与之绑定的持久卷就会被回收或删除,其上面的数据就会丢失。基于这个原因,你需要释放特定的持久卷时,需要手动删除对应的持久卷声明。
StatefulSet的保障机制。
一个有状态的pod总会被一个完全一致的pod替换(两者相同的名称,主机名和存储等)。这个替换发生在kubernetes发现旧pod不存在时(例如手动删除这个pod).
那么当Kubernetes不能确定一个pod的状态呢?如果它创建一个完全一致的pod,那系统中就会有两个完全一致的pod在同时运行。这两个pod会绑定到相同的存储,所以这两个相同标记的进程会同时写相同的文件。
为了保证两个拥有相同标记和绑定相同持久卷声明的有状态的pod实例不会同时运行,statefulset遵循at-most-one语义。也就是说一个StatefulSet必须在准确确认一个pod不再运行后,才会去创建它的替换pod。这对如何处理节点故障有很大帮助。具体实现,内部的,暂不深入。
讲了那么多StatefulSet实现有状态pod的好处,下面看看如何创建。
我们假设使用gec创建三个pv
kind: list
apiVersion: v1
item:
- apiVersion: v1
kind: PersistenVolume
metadata:
name: pv-a
spec:
capacity:
storage: 1Mi
accessModes:
- ReadWriteOnce
persistenVolumeReclaimPolicy: Recycle 卷被声明释放后,空间会被回收再利用
gcePersistentDisk:
poName: pv-a
fsType: nfs4
- apiVersion: v1
kind: PersistenVolume
metadata:
name: pv-b
...
准备好pv后,我们接下来创建statefulset
如我们之前将到的,在部署一个StatefulSet之前,需要创建一个用于在有状态的pod之间提供网络标识的headless Service
apiVersion: v1
kind: Service
metadata:
name: kubia
spec:
clusterIP: None (StatefulSet的控制Service必须时None即headless模式)
selector:
app: kubia
ports:
- name: http
port: 80
创建StatefulSet详单
apiVersion: apps/v1beta1 kind: StatefulSet metadata: name: kubia spec: serviceName: kubia replicas: 2 template: metadta: labels: app: kubia spec: containers: - name: kubia image: luksa/kubia-pet ports: - name: kubia containerPort: 8080
volumeMounts:
- name: data
mountPath: /var/data
volumeClaimTemplates:
- metadata:
name: data
spec:
resources:
requests:
storage: 1Mi
accessModes:
- ReadWriteOnce
创建:
kubectl create -f kubia-statefulset.yaml
列出pod:
kubectl get pod
Name READY
kubia-0 0/1 ...
看到会一个个进行
kubectl get pod
Name READY
kubia-0 1/1 ...
kubia-1 0/1 ...
查看pvc
kubectl get pvc
Name STATUS VOlUME
data-kubia-0 Bound pv - c ...
data-kubia-1 Bound pv - a ...
可以看到生成的持久卷声明的名称由 volumeClaimTeplate 字段中定义的名称和每个pod的名称组成。
现在你的数据存储集群节点都已经运行,可以开始使用它们了。因为之前创建的Service处于headless模式,所以不能通过service来访问你的pod。需要直接连接每个单独的pod来访问(或者创建一个普通的Service,但是这样还是不允许你访问指定的pod)
我们来创建一个普通的service如下:
apiVersion: v1
kind: service
metadata:
name: kubia-public
spec:
selector:
app: kubia
ports:
- port: 80
targetPort: 8080
StatefulSet 已经运行起来了,那么我们看下如何更新它的pod模版,让它使用新的镜像。同时你也会修改副本数为3.通常会使用kubectl edit命令来更新StatefulSet
kubectl edit statefulset kubia
你会看到新的pod实例会使用新的镜像运行,那已经存在的两个副本呢?通过他们的寿命可以看出它们没有更新。这是符合预期的。因为,首先StatefulSet更像ReplicaSet,而不是Deployment,所以在模版被修改后,它们不会重启更新,需要手动删除这些副本,然后StatefulSet会根据新的模版重新调度启动它们。
kubectl delete po kubia-0 kubia-1
注意: 从Kubernetes1.7版本开始,statefulSet支持与Deployment和DaemonSet一样的滚动升级。通过kubectl explain 获取StatefulSet的spec.updateStrategy 相关文档来获取更多信息。
前面我们提到StatefulSet的保障机制,那么当一个节点故障了,会出现什么情况。
statefulset在明确知道一个pod不再运行之前,它不能或者不应当创建一个替换pod。只有当集群的管理者告诉它这些信息时候,它才能明确知道。为了做到这一点,管理者需要删除这个pod,或者删除整个节点。
当手动停止一个node的网卡,使用kubectl get node,会显示Status notReady
因为控制台不会再收到该节点发送的状态更新,该节点上吗的所有pod状态都会变为Unknown。
当一个pod状态为Unknown时会发生什么
若该节点过段时间正常连接,并且重新汇报它上面的pod状态,那这个pod就会重新被标记为Runing。但如果这个pod的未知状态持续几分钟后(这个时间是可以配置的),这个pod就会自动从节点上驱逐。这是由主节点(kubernetes的控制组件)处理的。它通过删除pod的资源来把它从节点上驱逐。
当kubelet发现这个pod标记为删除状态后,它开始终止运行该pod。在上面的示例中,kubelet已不能与主节点通信(因为网卡断了),这意味着这个pod会一直运行着。查看
kubectl describe po kubia-0
发现status一直为Terminating,原因是NodeLost,在信息中说明的是节点不回应导致不可达。
这时候你想要手动删除pod
kubectl delete po kubia-0
执行完成后,你的想法是会再次运行一个kubia-0
但是kubectl get po会发现kubia-0 状态为 Unknown 并且还是之前那个旧pod ,因为启动时长没变。
为什么会这样?因为在删除pod之前,这个pod已经被标记为删除。这是因为控制组件已经删除了它(把它从节点驱逐)。这时你用kubectl describe po kubia-0 查看状态依然是Terminating。
这时候只能进行强制删除
kubectl delete po kubia-0 --force --grace-period 0
你需要同时使用--force和 --grace-period 0两个选项。然后kubectl 会对你做的事发出
警告信息。如果你再次列举pod,就可以看到一个新的kubia-0 pod被创建出来。
警告: 除非你确认节点不再运行或者不会再可以访问(永远不会再可以访问),否则不要强制删除有状态的pod.
码农小明