StatefulSet中的Stateful是有状态的意思。其中的状态即指:StatefulSet接管的每个Pod有稳定的网络标识(包括Pod的名称和主机名,是顺序索引值)、稳定的专属存储。进一步解释:
StatefulSet替换旧Pod时,新Pod与其有相同的名称、网络标识和状态;
StatefulSet管理的Pod可以有各自独立的存储卷;
StatefulSet创建的Pod的名字是有规律的,不是随机生成的;
StatefulSet接管下的Pod的架构可以用下图表示:
StatefulSet的at-most-one语义:StatefulSet必须保证不会有两个及两个以上的具有相同名称(和主机名)、绑定了相同PVC的Pod实例运行。原因可想而知。
StatefulSet与ReplicaSet对比
把StatefulSet比作宠物,ReplicaSet比作牛。
对于有状态应用,即StatefulSet接管的Pod,就像宠物一样,若一只宠物死掉,若要找一个替换它,除非找到完全一模一样的(现实中当然是不可能的),否则用户不可能感知不到。
对于无状态应用,即ReplicaSet接管的Pod,就像农场中的牛一样,若一只牛死掉,农场主完全可以找另一个牛来替代,不用一模一样,对于用户(食客)没有什么差别。
稳定的网络标识
在前面提到过,StatefulSet创建的Pod的名称和主机名都是固定的(因此称之为稳定的网络标识),因此在使用StatefulSet时通常都会创建一个用来记录每个Pod网络标识的headless Service(即Service资源的spec.clusterIP=None
)。通过这个Service资源对象,每个Pod都有自己专属的DNS记录,这样分布式应用中的其他Pod可以方便的通过主机名找到该Pod。例如,一个Pod的名称(主机名)为A-0,Service的名字为foo
,属于default
这个命名空间,那么a-0.foo.default.svc.cluster.local
就是这个Pod的专属域名。
紧跟着上面这个例子,我们通过访问foo.default.svc.cluster.local
也可以获得其对应的所有SRV记录,从而获得了StatefulSet管理的所有Pod的名称和主机名。
稳定的专属存储
StatefulSet创建的Pod都有自己的专属存储,这是依赖PVC(持久卷声明)实现的,PVC与PV是一一对应的关系,而Pod与PVC是一一对应的关系,即PVC会在Pod创建之前创建出来,并绑定至Pod上。
当StatefulSet删除Pod时,会保留PVC和PV,当创建新的同规格的Pod时,会将该保留的PVC再绑定至该Pod上。
扩缩容
扩容
扩容一个StatefulSet会使用下一个还没用到的顺序索引值创建一个新的Pod。比如,现有2个Pod,它们的索引值为0和1,则新创建的Pod的索引值为2。
在扩容时,会根据Pod的PVC模板创建它专属的PV。
缩容
缩容一个StatefulSet会最先删除最高索引值,因此StatefulSet的缩容结果是可预见的。比如,现有3个Pod,则最先被删除的Pod的索引值为2。
StatefulSet的缩容是线性的(即删除完一个,再删下一个),所以在有Pod不健康的情况下,是不允许做缩容操作的。
在缩容时,Pod会被直接删除,但是PV和PVC不会被删除。
创建StatefulSet
通过StatefulSet部署应用时,总共需要部署两个(或三个)不同的资源类型对象:
持久卷(PersistentVolume):存储应用的数据文件(若集群不支持持久卷动态供应时,才需要手动创建)
headless Service:为Pod提供依靠主机名访问应用中其他Pod的能力(通常访问应用不会通过该Service,而是另外再建一个Service将应用的API接口地址暴露出去);
部署持久卷
kind: List # List类型,其实和使用---将资源分割效果一样,这里是在items中指定其他资源类型
apiVersion: v1
items:
- apiVersion: v1 # 第一个PV
kind: PersistentVolume # 类型
metadata:
name: pv-a # PV名
spec:
capacity: # PV容量
storage: 1Mi
accessModes: # PV的访问模式
- ReadWriteOnce
persistentVolumeReclaimPolicy: Recycle # 回收策略,当PVC释放后,空间会被回收并清空,从而可以被其他PVC继续访问再利用(记住在删除Pod时是不会删除它的PVC的,因为如果删除了PVC,则空间会被回收,则该Pod的状态就没了,即使创建新Pod,也不会有原数据了,所以缩容是不会删除PVC的)
hostPath: # 存储策略,使用hostpath
path: /tmp/pv-a # (必须)宿主机路径
- apiVersion: v1 # 第二个PV
kind: PersistentVolume
metadata:
name: pv-b
spec:
capacity:
storage: 1Mi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Recycle
hostPath:
path: /tmp/pv-b
- apiVersion: v1 # 第三个PV
kind: PersistentVolume
metadata:
name: pv-c
spec:
capacity:
storage: 1Mi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Recycle
hostPath:
path: /tmp/pv-c
部署headless Service
apiVersion: v1
kind: Service
metadata:
name: kubia
spec:
clusterIP: None # headless Service需要指定ClusterIP为None
selector: # 所有标签为app=kubia的Pod都属于这个Service
app: kubia
ports:
- name: http
port: 80 # (必须)该Service向外暴露的端口
部署StatefulSet
apiVersion: apps/v1
kind: StatefulSet # 资源类型为StatefulSet
metadata:
name: kubia
spec:
replicas: 2 # 副本数
serviceName: kubia # (必须)指定控制Service的名字,该Service必须在该资源创建之前就已经存在
selector: # (必须)标签选择器,匹配需要接管的Pod的标签。可以使用matchExpressions或matchLabels
matchLabels:
app: kubia
template: # (必须)Pod模板
metadata:
labels:
app: kubia
spec:
containers:
- name: kubia
image: luksa/kubia-pet
ports:
- name: http
containerPort: 8080
volumeMounts: # Pod中的容器会把PVC数据集嵌入指定目录
- name: data # (必须)需要挂载的卷的名字,这里是声明的PVC模板的名字
mountPath: /var/data # (必须)Pod内部的挂载点
volumeClaimTemplates: # PVC模板,这里没有指定StorageClass
- metadata:
name: data
spec:
resources:
requests:
storage: 1Mi
accessModes:
- ReadWriteOnce
storageClassName: "" # 为了避免使用Minikube默认的SC,动态创建PV,这里我们要使用的是我们事先创建好的三个PV
部署结果
部署结果如下:
# 查看Pods
$ get pod --show-labels
NAME READY STATUS RESTARTS AGE LABELS
kubia-0 1/1 Running 0 18m app=kubia,controller-revision-hash=kubia-c94bcb69b,statefulset.kubernetes.io/pod-name=kubia-0
kubia-1 1/1 Running 0 18m app=kubia,controller-revision-hash=kubia-c94bcb69b,statefulset.kubernetes.io/pod-name=kubia-1
# 查看PV
$ get pv -o wide
>>> get pv -o wide
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE VOLUMEMODE
pv-a 1Mi RWO Recycle Bound default/data-kubia-0 40m Filesystem
pv-b 1Mi RWO Recycle Bound default/data-kubia-1 40m Filesystem
pv-c 1Mi RWO Recycle Available 40m Filesystem
# 查看PVC
$ get pvc --show-labels
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE LABELS
data-kubia-0 Bound pv-a 1Mi RWO 22m app=kubia
data-kubia-1 Bound pv-b 1Mi RWO 22m app=kubia
# 查看Service
$ get service -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
kubia ClusterIP None <none> 80/TCP 34m app=kubia
# 查看StatefulSet
$ get statefulset -o wide
NAME READY AGE CONTAINERS IMAGES
kubia 2/2 21m kubia luksa/kubia-pet
发现应用中其他Pod节点
在之前提到过,在部署StatefuSet资源对象时,需要创建一个headless Service资源对象,该对象可以为应用中的Pod提供发现其他Pod的能力。
假设在default
命名空间中,有一个名为kubia
的headless Service,那么应用中的其他Pod可以使用dig
命令,访问kubia.default.svc.cluster.local
就可以通过SRV记录,获得该Service的所有后端的Pod:
$ dig SRV kubia.default.svc.cluster.local
...
;; ANSWER SECTION:
kubia.default.svc.cluster.local. 30 IN SRV 0 50 80 kubia-0.kubia.default.svc.cluster.local.
kubia.default.svc.cluster.local. 30 IN SRV 0 50 80 kubia-1.kubia.default.svc.cluster.local.
;; ADDITIONAL SECTION:
kubia-0.kubia.default.svc.cluster.local. 30 IN A 172.17.0.8
kubia-1.kubia.default.svc.cluster.local. 30 IN A 172.17.0.9
...
kubia-0.kubia.default.svc.cluster.local
是一个Pod的DNS域名,其地址为172.17.0.8
kubia-1.kubia.default.svc.cluster.local
是另一个Pod的DNS域名,其地址为172.17.0.9
通过该机制,可以得到一个简易的分布式数据存储服务的操作流程,如下图所示,图中的每一个Pod都有自己的存储空间,用户的请求会随机打到任意一个Pod上,该Pod会从其他Pod上获取数据,并与自己本地数据汇总,然后再返回给用户:
处理Node失效
由于StatefulSet需要保证不会拥有两个相同标记和存储的Pod同时运行,因此在明确知道一个Pod不再运行之前,它不会去创建一个替换Pod。
当Node失效时,需要管理员手动通知Kubernetes删除失效的Pod,StatefulSet才会去创建替换Pod。