Contents
StatefulSet控制器¶
StatefulSet控制器是一种提供排序和唯一性保证的特殊Pod控制器。当有与部署顺序、持久数据或固定网络等相关的特殊需求时,可 以使用StatefulSet控制器来进行更细粒度的控制。
StatefulSet控制器通常与面向数据的应用程序(如数据库)关联,它即使被重新分配到一个新的节点上,还是需要访问同一个存储卷的。
StatefulSet控制器对应于有状态服务(Deployment控制器对应于无状态服务),前者的功能如下所示。
实现稳定的持久化存储:Pod重新调度后还能访问相同的持久化数据,可基于PVC来实现。
实现稳定的网络标识:Pod重新调度后其PodName和HostName不变,基于无头Service(没有Cluster IP的Service)来实现。
实现有序部署、有序伸缩:Pod是有顺序的,在部署或者扩展的时候要依据定义的顺序依次执行(即从第一个到最后一个依次部 署,在下一个Pod运行之前所有的Pod必须都处于Running或Ready状态)。
实现有序收缩、有序删除:从最后一个开始,依次删除到第一个。
创建StatefulSet资源之前,先要保证集群中存在StorageClass,并使用headless service暴露服务 StatefulSet相较于Deployment,多了volumeClaimTemplates字段,即pvc存储的配置信息
yaml模板
apiVersion: apps/v1 #必填,版本号
kind: StatefulSet #必填,资源类型
metadata: #必填,元数据
name: <name>-sts #必填,资源名称
namespace: <namespace> #Pod所属的命名空间
spec: #必填,部署的详细定义
selector: #必填,标签选择
matchLabels: #必填,标签匹配
key: <value> #必填,通过此标签匹配对应pod<key: value>
serviceName: string # Headless Service资源名称
replicas: int # 副本数量
template: #必填,应用容器模版定义
metadata: #必填,元数据
labels: # 标签
key: <value> #必填,与上面matchLabels的标签相同
spec:
containers: #此处参考pod的containers
volumeClaimTemplates: #必填,+pvc模板
- metadata: #必填,元数据
name: <name>-depolyment #必填,资源名称
spec:
accessModes: [ "ReadWriteOnce | ReadOnlyMany | ReadWriteMany" ] #必填,访问模式
storageClassName: strint #存储类名,改为集群中已存在的
resources: # 存储卷需要占用的资源量最小值
requests: # 请求空间大小
storage: 1Gi # 空间大小值
yaml示例:以nginx服务使用nfs共享存储为例
#先定义了一个名为myapp-svc的Headless Service资源,用于为关联到的每个Pod资源创建DNS资源记录。
apiVersion: v1
kind: Service
metadata:
name: myapp-svc
labels:
app: myapp-svc
spec:
ports:
- port: 80
name: web
clusterIP: None
selector:
app: myapp-pod
---
# 定义多个使用NFS存储后端的PV,空间大小为2GB,仅支持单路的读写操作。
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-pv
spec:
capacity:
storage: 2Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Recycle
storageClassName: nfs
nfs:
path: /nfs/data1
server: 172.17.0.2
---
# 定义了一个名为myapp的StatefulSet资源,它通过Pod模板创建了两个Pod资源副本,并基于volumeClaimTemplates(存储卷申请模板)向nfs存储类请求动态供给PV,从而为每个Pod资源提供大小为1GB的专用存储卷。
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: myapp-sts
spec:
selector:
matchLabels:
app: myapp-pod
serviceName: myapp-svc
replicas: 2
template:
metadata:
labels:
app: myapp-pod
spec:
containers:
- name: nginx
image: k8s.gcr.io/nginx-slim:0.8
ports:
- containerPort: 80
name: web
volumeMounts:
- name: myapp-data
mountPath: /usr/share/nginx/html
volumeClaimTemplates:
- metadata:
name: myapp-data
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "nfs"
resources:
requests:
storage: 1Gi
1.statefulset的组成¶
StatefulSet控制器由3个部分组成
无头Service:用于为Pod资源标识符生成可解析的DNS记录。
volumeClaimTemplates:基于静态或动态PV供给方式为Pod资源提供专有的固定存储。
StatefulSet:管理Pod资源。
StatefulSet控制器的组成

2.为什么要用StatefulSet控制器?¶
Deployment控制器下的每一个Pod都毫无区别地提供服务。
但StatefulSet控制器下的Pod则不同,虽然各个Pod的定义是一样的,但因为数据不同,所提供的服务是有差异的。
分布式存储系统就适合使用StatefulSet控制器,由Pod A存储一部分数据并提供相关服务,由Pod B存储另一部分数据并提供相关服务。
又比如有些服务会临时保存客户请求的数据,如使用服务端Session方式存放部分信息的业务网站,由于Session的不同,Pod A和Pod B能提供的服务也不尽相同,这种场景也适合使用StatefulSet控制器。
因为在上述场景下每一个有状态的Pod提供的服务都不一样,所以每一个Pod不能被随意取代,必须有序分配且必须为其分配唯一的标识。
Pod名称将是它们的唯一标识符,和Deployment控制器下的Pod不同,即使有状态的Pod发生故障并被重建,Pod名称也会和原来的一模一样。
因为各个有状态的Pod也必须要拥有一个唯一的网络标识符以访问具体的某个Pod,所以会用到无头Service,无头Service可以给每个 Pod分配一个唯一的DNS名称。
有状态的Pod都会使用到持久存储(如果没有持久存储,Pod发生故障时数据就没有了)。
如前所述,有状态的Pod的最大特点是各个Pod中的数据是不一样的,所以各个Pod无法共用同一个存储卷。
需要单独分配各自的PV和PVC
因为每个Pod要有各自专用的存储卷,所以并不是在Pod模板中定义(若在Pod模板中定义,那么每个Pod都用的同一个存储卷)StatefulSet控制器的存储卷配置,而是在StatefulSet控制器模板的volumeClaimTemplate属性中定义存储卷的申请模板,并会为每个 Pod生成不同的PVC且各自绑定PV,从而使各个Pod拥有各自专用的存储卷。
因为每个Pod都会产生各自专用的PVC及PV,所以StatefulSet控制器的存储最好通过StorageClass来动态创建。
当然,也可以通过手动创建各个预设的PV,只是这个过程会相当麻烦。
3.StatefulSet控制器的基本操作¶
定义模板文件,创建一个名为examplestatefulset.yml的模板文件
kind: Service
apiVersion: v1
metadata:
name: examplestatefulservice
spec:
selector:
example: exampleforstateful
# headless service
clusterIP: None
ports:
- protocol: TCP
port: 8080
targetPort: 80
type: ClusterIP
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: examplestatefulset
spec:
replicas: 3
serviceName: "examplestatefulservice"
selector:
matchLabels:
example: exampleforstateful
template:
metadata:
labels:
example: exampleforstateful
spec:
containers:
- name: pythonserviceforstateful
image: python:3.7
imagePullPolicy: IfNotPresent
command: ['sh', '-c']
args: ['echo "The host is $(hostname)" >> /dir/data; echo "<p>The host is $(hostname)</p>" > index.html; python -m http.server 80']
volumeMounts:
- name: statefuldata
mountPath: /dir
ports:
- name: http
containerPort: 80
volumeClaimTemplates:
- metadata:
name: statefuldata
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "managed-nfs-storage"
resources:
requests:
storage: 200Mi
这个模板主要分为3个部分。首先,创建了一个无头Service,其名称为examplestatefulservice。它会通过标签选择器关联到各个标签为
example: exampleforstateful的Pod上。
然后,创建了一个StatefulSet模板。该StatefulSet模板的前半部分的定义和Deployment模板相似,定义了3个Pod副本,其容器为 “python:3.7”镜像,其目的是搭建服务。在启动容器时,会先以追加方式向/dir/data文件写入一串文本”The host is \((hostname)", 这串文本使用\)(hostname)环境变量获取当前Pod名称。/dir目录通过volumeMounts属性映射到名为statefuldata的存储卷申请模板 上,这在写入文本时会直接写入存储卷中。
接下来,执行echo "<p>The host is $(hostname)</p> "index.html命令,将一段HTML代码插入index.html文件中,这样
在访问index.html时就可以知道访问的是哪个Pod。
另外,通过python -m http.server 80命令,搭建一个简单的Web服务,并令服务对应的端口为80。
StatefulSet模板的后半部分是存储卷申请模板,其定义的内容和PVC模板的差不多,但要注意,这里批量定义了PVC。 storageClassName属性设置为managed-nfs-storage,与上一节中创建的StorageClass名称保持一致。
requests为storage:200Mi,这表示为每一个Pod都申请200MiB的存储空间。
接下来,执行以下命令,创建StatefulSet控制器的相关资源。
$ kubectl apply -f examplestatefulset.yml
在创建过程中,在不同时段通过$ kubectl get pod进行查看,会发现Pod是按照顺序依次创建的。
Kubernetes会先创建第一个Pod,第二个Pod处于Pending状态。
第一个Pod创建完毕后创建第二个Pod,此时第三个Pod处于Pending状态。
前两个Pod创建完毕后,再创建第三个Pod。Pod的名称和Deployment控制器下的Pod不一样,名称末尾并没有生成随机字符串,而是按照数字顺序从0开始依次向上累加。
StatefulSet控制器下有序创建的各个Pod

通过以下命令,可以查看StatefulSet控制器的总体状态。
$ kubectl get statefulset
NAME READY AGE
examplestatefulset 3/3 82s
通过以下命令,可以查看StatefulSet控制器的详细信息。
$ kubectl describe statefulset examplestatefulset
Name: examplestatefulset
Namespace: default
CreationTimestamp: Mon, 18 Apr 2022 14:59:01 +0800
Selector: example=exampleforstateful
Labels: <none>
Annotations: <none>
Replicas: 3 desired | 3 total
Update Strategy: RollingUpdate
Partition: 0
Pods Status: 3 Running / 0 Waiting / 0 Succeeded / 0 Failed
Pod Template:
Labels: example=exampleforstateful
Containers:
pythonserviceforstateful:
Image: python:3.7
Port: 80/TCP
Host Port: 0/TCP
Command:
sh
-c
Args:
echo "The host is $(hostname)" >> /dir/data; echo "<p>The host is $(hostname)</p>" > index.html; python -m http.server 80
Environment: <none>
Mounts:
/dir from statefuldata (rw)
Volumes: <none>
Volume Claims:
Name: statefuldata
StorageClass: managed-nfs-storage
Labels: <none>
Annotations: <none>
Capacity: 200Mi
Access Modes: [ReadWriteOnce]
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal SuccessfulCreate 5m6s statefulset-controller create Claim statefuldata-examplestatefulset-0 Pod examplestatefulset-0 in StatefulSet e xamplestatefulset success
Normal SuccessfulCreate 5m6s statefulset-controller create Pod examplestatefulset-0 in StatefulSet examplestatefulset successful
Normal SuccessfulCreate 5m3s statefulset-controller create Claim statefuldata-examplestatefulset-1 Pod examplestatefulset-1 in StatefulSet e xamplestatefulset success
Normal SuccessfulCreate 5m3s statefulset-controller create Pod examplestatefulset-1 in StatefulSet examplestatefulset successful
Normal SuccessfulCreate 3m47s statefulset-controller create Claim statefuldata-examplestatefulset-2 Pod examplestatefulset-2 in StatefulSet e xamplestatefulset success
Normal SuccessfulCreate 3m47s statefulset-controller create Pod examplestatefulset-2 in StatefulSet examplestatefulset successful
4.PVC及PV的使用¶
我们先检查存储卷的使用情况。此时如果通过$ kubectl getpvc以及$ kubectl get pv命令进行查询,可以看到StatefulSet
控制器为每个Pod都创建了各自专用的PVC及PV。
$ kubectl get pv|grep default/statefuldata-examplestatefulset
pvc-0bd1af19-8af1-41b1-ba61-8933ac9fb8f2 200Mi RWO Delete Bound default/statefuldata-examplestatefulset-2 managed-nfs-storage 6m46s
pvc-3144e3df-cf7c-46c6-a285-d2719b9d5161 200Mi RWO Delete Bound default/statefuldata-examplestatefulset-1 managed-nfs-storage 8m2s
pvc-9fa9ae39-a778-4d31-aa38-37c84de9d7fa 200Mi RWO Delete Bound default/statefuldata-examplestatefulset-0 managed-nfs-storage 8m4s
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
statefuldata-examplestatefulset-0 Bound pvc-9fa9ae39-a778-4d31-aa38-37c84de9d7fa 200Mi RWO managed-nfs-storage 7m41s
statefuldata-examplestatefulset-1 Bound pvc-3144e3df-cf7c-46c6-a285-d2719b9d5161 200Mi RWO managed-nfs-storage 7m38s
statefuldata-examplestatefulset-2 Bound pvc-0bd1af19-8af1-41b1-ba61-8933ac9fb8f2 200Mi RWO managed-nfs-storage 6m22s
任意挑选一个PV,通过$ kubectl describe pv命令查看详情,可以看到它在NFS服务器共享目录上创建的专用目录,
$ kubectl describe pv pvc-0bd1af19-8af1-41b1-ba61-8933ac9fb8f2
Name: pvc-0bd1af19-8af1-41b1-ba61-8933ac9fb8f2
Labels: <none>
Annotations: pv.kubernetes.io/provisioned-by: fuseim.pri/ifs
Finalizers: [kubernetes.io/pv-protection]
StorageClass: managed-nfs-storage
Status: Bound
Claim: default/statefuldata-examplestatefulset-2
Reclaim Policy: Delete
Access Modes: RWO
VolumeMode: Filesystem
Capacity: 200Mi
Node Affinity: <none>
Message:
Source:
Type: NFS (an NFS mount that lasts the lifetime of a pod)
Server: 192.168.1.60
Path: /nfstest/default-statefuldata-examplestatefulset-2-pvc-0bd1af19-8af1-41b1-ba61-8933ac9fb8f2
ReadOnly: false
Events: <none>
在NFS服务器上执行以下命令,查看Pod是否已成功向该目录写入文件。
执行结果如下所示,可以看到Pod已成功向它的专属存储卷中写入数据。
$ cat /data/nfs/nfstest/default-statefuldata-examplestatefulset-0-pvc-9fa9ae39-a778-4d31-aa38-37c84de9d7fa/data
The host is examplestatefulset-0
对于其余两个Pod也是一样的,先通过$ kubectl describe pv pvName命令查看其专属目录位置,然后执行命令查看文件是否写入。
$ kubectl describe pv pvc-3144e3df-cf7c-46c6-a285-d2719b9d5161
Name: pvc-3144e3df-cf7c-46c6-a285-d2719b9d5161
Labels: <none>
Annotations: pv.kubernetes.io/provisioned-by: fuseim.pri/ifs
Finalizers: [kubernetes.io/pv-protection]
StorageClass: managed-nfs-storage
Status: Bound
Claim: default/statefuldata-examplestatefulset-1
Reclaim Policy: Delete
Access Modes: RWO
VolumeMode: Filesystem
Capacity: 200Mi
Node Affinity: <none>
Message:
Source:
Type: NFS (an NFS mount that lasts the lifetime of a pod)
Server: 192.168.1.60
Path: /nfstest/default-statefuldata-examplestatefulset-1-pvc-3144e3df-cf7c-46c6-a285-d2719b9d5161
ReadOnly: false
Events: <none>
$ cat /data/nfs/nfstest/default-statefuldata-examplestatefulset-1-pvc-3144e3df-cf7c-46c6-a285-d2719b9d5161/data
The host is examplestatefulset-1
结果:每个Pod都往各自的存储卷中写入了数据。
5.无头Service的访问¶
我们检查Service的发布情况。使用$ kubectl get svc命令可以看到已经创建了一个无头Service。
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
examplestatefulservice ClusterIP None <none> 8080/TCP 11m
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 173d
由于这个Service无法由集群内外的机器直接访问,因此只能由Pod访问,而且需要通过DNS形式来访问,具体访问形式为
{ServiceName}.{Namespace}.svc.{ClusterDomain}。
svc是Service的缩写(固定格式);
ClusterDomain表示集群域,本例中默认的集群域为cluster.local;
前面两个字段则是根据Service定义决定的,在这个例子中ServiceName为examplestatefulservice,而Namespace我们没有在yml文件中指定,默认值为Default。
在访问这个地址之前,我们先创建一个测试用的Pod,用它来尝试访问Service。命令如下。
examplepodforheadlessservice.yml
apiVersion: v1
kind: Pod
metadata:
name: examplepodforheadlessservice
spec:
containers:
- name: testcontainer
image: docker.io/appropriate/curl
imagePullPolicy: IfNotPresent
command: ['sh', '-c']
args: ['echo "test pod for headless service!"; sleep 3600']
这个Pod并没有什么特别之处,其镜像为appropriate/curl。该镜像是一种工具箱,里面存放了一些测试网络和DNS使用的工具(例 如curl和nslookup等),可用于测试现在的Service。通过sleep3600命令,可让该容器长期处于运行状态。
通过模板创建Pod。
$ kubectl apply -f examplepodforheadlessservice.yml
Pod创建完成后,就可以通过以下命令进入Pod内部,这样就可以在Pod内部执行命令行。
进入容器内部后,可以执行nslookup命令查询DNS信息,获得这个DNS下面的IP地址列表。之前已经提到,Kubernetes中的DNS资源访 问方式为{ServiceName}.{Namespace}. svc.{ClusterDomain},本例中的具体命令如下。
$ kubectl exec -it pod/examplepodforheadlessservice -- /bin/sh
/ # nslookup examplestatefulservice.default.svc.cluster.local
nslookup: can't resolve '(null)': Name does not resolve
Name: examplestatefulservice.default.svc.cluster.local
Address 1: 10.0.16.28 examplestatefulset-1.examplestatefulservice.default.svc.cluster.local
Address 2: 10.0.32.106 examplestatefulset-2.examplestatefulservice.default.svc.cluster.local
Address 3: 10.0.36.231 examplestatefulset-0.examplestatefulservice.default.svc.cluster.local
可以看到,一共返回了3个IP地址,这些IP地址正是之前创建的各个Pod的IP地址,而Kubernetes又为每个 Pod地址创建了对应的专属域名。访问这些专属域名就可以访问指定Pod提供的服务
当然,也可以直接使用无头Service的总域名来访问服务,如下面所示。通过这种方式访问的服务是随机的,这对于Deployment控制 器提供的无状态Pod没有问题,但如前所述,对于StatefulSet控制器提供的有状态Pod而言,每个Pod提供的服务都是不同的,在调用时必须指明调用哪一个Pod提供的服务。
/ # curl examplestatefulservice.default.svc.cluster.local
<p>The host is examplestatefulset-1</p>
/ # curl examplestatefulservice.default.svc.cluster.local
<p>The host is examplestatefulset-0</p>
/ # curl examplestatefulservice.default.svc.cluster.local
<p>The host is examplestatefulset-1</p>
/ # curl examplestatefulservice.default.svc.cluster.local
<p>The host is examplestatefulset-2</p>
在无头Service中,每一个Pod都会生成专属的访问域名,其访问格式为{PodName}. {ServiceName}.{Namespace}.svc. {ClusterDomain}。每个域名通过DNS查询都可以解析出Pod的IP地址,例如,使用以下命令
/ # nslookup examplestatefulset-0.examplestatefulservice.default.svc.cluster.local
nslookup: can't resolve '(null)': Name does not resolve
Name: examplestatefulset-0.examplestatefulservice.default.svc.cluster.local
Address 1: 10.0.36.231 examplestatefulset-0.examplestatefulservice.default.svc.cluster.local
/ # nslookup examplestatefulset-1.examplestatefulservice.default.svc.cluster.local
nslookup: can't resolve '(null)': Name does not resolve
Name: examplestatefulset-1.examplestatefulservice.default.svc.cluster.local
Address 1: 10.0.16.28 examplestatefulset-1.examplestatefulservice.default.svc.cluster.local
/ # nslookup examplestatefulset-2.examplestatefulservice.default.svc.cluster.local
nslookup: can't resolve '(null)': Name does not resolve
Name: examplestatefulset-2.examplestatefulservice.default.svc.cluster.local
Address 1: 10.0.32.106 examplestatefulset-2.examplestatefulservice.default.svc.cluster.local
综上所述,要访问由不同的有状态Pod提供的服务,只需要访问其专属域名即可。
/ # curl examplestatefulset-0.examplestatefulservice.default.svc.cluster.local
<p>The host is examplestatefulset-0</p>
/ # curl examplestatefulset-1.examplestatefulservice.default.svc.cluster.local
<p>The host is examplestatefulset-1</p>
/ # curl examplestatefulset-2.examplestatefulservice.default.svc.cluster.local
<p>The host is examplestatefulset-2</p>
可以看到每个域名都可以成功返回各自的结果。
6.Pod的重建¶
可以模拟Pod发生故障时的场景。假设现在examplestatefulset-1发生故障(例如,人为删除),请执行以下命令。
$ kubectl delete pod/examplestatefulset-1
因为在之前模板中replicas设置为3,这表示会保留3个稳定副本,所以Pod会重建。可以看到,Pod 重建后的名称一模一样,Pod的 IP地址会有变化(但不会有实际影响)。
如图:

执行以下命令,输出这个 Pod 专属的存储卷中文件的内容,查看是否仍然调用了同一个存储。
$ cat /data/nfs/nfstest/default-statefuldata-examplestatefulset-1-pvc-3144e3df-cf7c-46c6-a285-d2719b9d5161/data
The host is examplestatefulset-1
The host is examplestatefulset-1
因为在之前的Pod定义中Pod启动时会以追加文本的形式向文件中写入数据,所以Pod重建后,会再写一条数据。
因为重建后的Pod使用的还是同一个PVC和PV,所以仍然在同一个文件上进行编辑。
查询该文件会看到两条文本,一条是之前由被删除的Pod在启动时写的,一条是重建时写的
7.StatefulSet控制器的伸缩与更新¶
和Deployment控制器一样,StatefulSet控制器也可以实现动态伸缩,只需要修改配置模板中的replicas属性然后执行应用即可。
但与Deployment控制器不同的地方在于,Pod是有序伸缩的,就像创建StatefulSet控制器时依次创建Pod一样。在扩容时,后续新增的Pod会从前往后依次创建,创建完成后才开始下一个 Pod 的创建;
在缩容时,会先从编号最大的 Pod开始,从后往前依次删除,完全删除后才开始下一个Pod的删除。
StatefulSet控制器有两种更新策略,可以在模板中通过.spec.updateStrategy属性进行设置。
OnDelete更新策略,这是默认的向后兼容的更新策略。使用OnDelete更新策略更新StatefulSet模板后,只有在手动删除旧的Pod时才会创建新的Pod。
RollingUpdate策略。在更新StatefulSet控制器模板后,旧的Pod将被终止,并且将以受控方式自动创建新的 Pod。
7.1 sts和deployment的滚动更新差异¶
StatefulSet控制器和Deployment控制器的滚动更新,有一些细节上的差异。
因为StatefulSet控制器是有序的,所以它会从编号最大的Pod到最小的Pod依次更新,而且在更新前不会立即删除旧的Pod,而是 等新的Pod已完全创建完毕且处于Running状态时,才会替换并删除旧的Pod。
StatefulSet控制器拥有独有的更新属性.spec.updateStrategy.rollingUpdate.partition。这种方式类似于金丝雀部署,如果将partition设置为4,只有编号大于或等于4的Pod才会进行更新,编号小于partition的Pod将不会更新。如果已经更新的Pod通过验证,则再将partition改为0,更新其余Pod即可。