因为不能在一个pod模板中差异化配置pod副本,所以不能指定一个实例使用哪个特定目录。但是可以让每个实例自动选择(或创建)一个别的实例还没有使用的数据目录。这种方案要求实例之间相互协作,其正确性很难保证,同时共享存储也会成为整个应用的性能瓶颈。
除了上面说的存储需求,集群应用也会要求每一个实例拥有生命周期内唯一标识。pod可以随时被删掉,然后被新的pod替代。当一个ReplicaSet中的pod被替换时,尽管新的pod也可能使用被删掉pod数据卷中的数据,但它却是拥有全新主机名和IP的崭新pod。在一些应用中,当启动的实例拥有完全新的网络标识,但还使用旧实例的数据时,很可能引起问题。
因此一些应用需要维护一个稳定的网络标识,这个需求在有状态的分布式应用中很普遍。这类应用要求管理者在每个集群成员的配置文件中列出所有其他集群成员和它们的IP地址(或主机名)。但是在Kubernetes中,每次重新调度一个pod,这个新的pod就有一个新的主机名和IP地址,这样就要求当集群中任何一个成员被重新调度后,整个应用集群都需要重新配置。
你可以利用前面讲到的Service,针对集群中的每个成员,都创建一个独立的服务来提供稳定的网络地址。因为服务IP是固定的,可以在配置文件中指定集群成员对应的服务IP而不是podIP。
这种解决方案可行,但是会十分麻烦,因此便出现了StatefulSet资源。
在第四章中,提到过StatefulSet是一种资源控制器,它用来管理某pod集合的部署和扩缩,并为这些pod提供持久存储和持久标识符。它和Deployment很相似,但不同的是,StatefulSet为它们的每个pod维护了一个有粘性的ID。这些pod是基于相同的配置来创建的,但不能相互替换:无论怎么调度,每个pod都有一个永久不变的ID。
要很好地理解StatefulSet的用途,最好先与RS或者RC对比一下。首先拿一个通用的类比来解释它们。
我们倾向于把应用看作宠物,给每个实例起一个名字,细心照顾每个实例。但是也许把它们看成牛更为合适,并不需要对单独的实例有太多关心。这样就可以非常方便地替换掉不健康的实例,就跟农场主替换掉一头生病的牛一样。对于无状态的应用实例来说,行为非常像农场里的牛。一个实例挂掉后并没什么影响,可以创建一个新实例,而让用户完全无感知。另一方面,有状态的应用的一个实例更像一个宠物。若一只宠物死掉,不能买到一只完全一样的,而不让用户感知到。若要替换掉这只宠物,需要找到一只行为举止与之完全一致的宠物。对应用来说,意味着新的实例需要拥有跟旧的实例完全一致的状态和标识。
RS或RC管理的pod副本比较像牛,这是因为它们都是无状态的,任何时候它们都可以被一个全新的pod替换。然而有状态的pod需要不同的方法,当一个有状态的pod挂掉后(或者它所在的节点故障),这个pod实例需要在别的节点上重建,但是新的实例必须与被替换的实例拥有相同的名称、网络标识和状态。这就是StatefulSet如何管理pod的。
一个Statefulset创建的每个pod都有一个从零开始的顺序索引,这个会体现在pod的名称和主机名上,同样还会体现在pod对应的固定存储上。这些pod的名称则是可预知的,因为它是由Statefulset的名称加该实例的顺序索引值组成的。不同于pod随机生成一个名称,这样有规则的pod名称很方便管理:
扩容一个Statefulset会使用下一个还没用到的顺序索引值创建一个新的pod实例。比如,要把一个Statefulset从两个实例扩容到三个实例,那么新实例的索引值就会是2(现有实例使用的索引值为0和1)。
当缩容一个Statefulset时,比较好的是很明确哪个pod将要被删除。作为对比,RS的缩容操作则不同,不知道哪个实例会被删除,也不能指定先删除哪个实例。缩容一个Statefulset将会最先删除最高索引值的实例,缩容的结果是可预知的。
至于存储,一个有状态的pod需要拥有自己的存储,即使该有状态的pod被重新调度(新的pod与之前pod的标识完全一致),新的实例也必须挂载着相同的存储。那Statefulset是如何做到这一点的呢?
当你不小心缩容后,如果没有删除PVC,那么可以通过扩容的方式弥补,新的pod会运行到与之前完全一致的状态(名字也相同):
StatefulSets对于需要满足以下一个或多个需求的应用程序很有价值:
在上面描述中,稳定的意味着Pod调度或重调度的整个过程是有持久性的。如果应用程序不需要任何稳定的标识符或有序的部署、删除或伸缩,则应该使用由一组无状态的副本控制器提供的工作负载来部署应用程序,比如Deployment或者ReplicaSet可能更适用于无状态应用部署。
下面的示例演示了StatefulSet的组件。
apiVersion:v1kind:Servicemetadata:name:nginxlabels:app:nginxspec:ports:-port:80name:webclusterIP:Noneselector:app:nginx---apiVersion:apps/v1kind:StatefulSetmetadata:name:webspec:selector:matchLabels:app:nginx#必须和下面的.spec.template.metadata.labels对应serviceName:"nginx"replicas:3#默认为1template:metadata:labels:app:nginx#必须和上面的.spec.selector.matchLabels对应spec:terminationGracePeriodSeconds:10containers:-name:nginximage:k8s.gcr.io/nginx-slim:0.8ports:-containerPort:80name:webvolumeMounts:-name:wwwmountPath:/usr/share/nginx/htmlvolumeClaimTemplates:-metadata:name:wwwspec:accessModes:["ReadWriteOnce"]storageClassName:"my-storage-class"resources:requests:storage:1Gi上述例子中:
你必须设置StatefulSet的.spec.selector字段,使之匹配其在.spec.template.metadata.labels中设置的标签。在Kubernetes1.8版本之前,被忽略.spec.selector字段会获得默认设置值。在1.8和以后的版本中,未指定匹配的Pod选择器将在创建StatefulSet期间导致验证错误。
如果用户想将示例中的StatefulSet收缩为replicas=1,首先被终止的是web-2。在web-2没有被完全停止和删除前,web-1不会被终止。当web-2已被终止和删除、web-1尚未被终止,如果在此期间发生web-0运行失败,那么就不会终止web-1,必须等到web-0进入Running和Ready状态后才会终止web-1。
你可以像删除Kubernetes中的其他资源一样删除StatefulSet:使用kubectldelete命令,并按文件或者名字指定StatefulSet。
kubectldelete-f
kubectldeleteservice<服务名称>当通过kubectl删除StatefulSet时,StatefulSet会被缩容为0。属于该StatefulSet的所有Pod也被删除。如果你只想删除StatefulSet而不删除Pod,使用--cascade=orphan。
kubectldelete-f
kubectldeletepods-lapp=myapp删除StatefulSet管理的Pod并不会删除关联的卷。这是为了确保你有机会在删除卷之前从卷中复制数据。
如果要完全删除StatefulSet中的所有内容,包括关联的pods,你可以运行一系列如下所示的命令:
grace=$(kubectlgetpods