目录
一、Statefulset 控制器:概念、原理解读
1.1 什么是有状态服务?
1.2 什么是无状态服务?
二、 Statefulset 资源清单文件编写技巧
三、Statefulset 使用案例:部署 web 站点
3.1 StatefulSet 由以下几个部分组成:
3.2 什么是 Headless service ?
3.3 为什么要用 volumeClaimTemplate ?
3.4 举例说明 service 和 headless service 区别
3.5 Statefulset 小结
四、Statefulset 管理 pod:动态扩容、缩容、更新
4.1 Statefulset 实现 pod 的动态扩容
4.2 Statefulset 实现 pod 的动态缩容
4.3 Statefulset 实现 pod 的更新
更新策略
分区滚动更新
一、Statefulset 控制器:概念、原理解读
StatefulSet 是用来管理有状态应用的工作负载 API 对象。
StatefulSet 用来管理某 Pod 集合的部署和扩缩, 并为这些 Pod 提供持久存储和持久标识符。
和 Deployment 类似, StatefulSet 管理基于相同容器规约的一组 Pod。但和 Deployment 不同的是, StatefulSet 为它们的每个 Pod 维护了一个有粘性的 ID。这些 Pod 是基于相同的规约来创建的, 但是不能相互替换:无论怎么调度,每个 Pod 都有一个永久不变的 ID。
如果希望使用存储卷为工作负载提供持久存储,可以使用 StatefulSet 作为解决方案的一部分。 尽管 StatefulSet 中的单个 Pod 仍可能出现故障, 但持久的 Pod 标识符使得将现有卷与替换已失败 Pod 的新 Pod 相匹配变得更加容易。
1.1 什么是有状态服务?
StatefulSet 是有状态的集合,管理有状态的服务,它所管理的 Pod 的名称不能随意变化。数据持久化的目录也是不一样,每一个 Pod 都有自己独有的数据持久化存储目录。比如 MySQL 主从、redis集群等。
1.2 什么是无状态服务?
RC、Deployment、DaemonSet 都是管理无状态的服务,它们所管理的 Pod 的 IP、名字,启停顺序等都是随机的。个体对整体无影响,所有 pod 都是共用一个数据卷的,部署的 tomcat 就是无状态的服务,tomcat 被删除,在启动一个新的 tomcat,加入到集群即可,跟 tomcat 的名字无关。
官方参考文档:StatefulSet | Kubernetes
二、 Statefulset 资源清单文件编写技巧
# 用来存放这次的实验文件
[root@k8s-master01 ~]# mkdir statefulset
[root@k8s-master01 ~]# cd statefulset/
# 查看定义 Statefulset 资源需要的字段
[root@k8s-master01 ~]# kubectl explain statefulset
[root@k8s-master01 ~]# kubectl explain statefulset.metadata
[root@k8s-master01 ~]# kubectl explain statefulset.spec
[root@k8s-master01 ~]# kubectl explain statefulset.spec.template
[root@k8s-master01 ~]# kubectl explain statefulset.spec.template.metadata
[root@k8s-master01 ~]# kubectl explain statefulset.spec.template.spec
[root@k8s-master01 ~]# kubectl explain statefulset.spec.template.spec.containers
通过上面可以看到,statefulset 资源中有两个 spec 字段。第一个 spec 声明的是 statefulset定义多少个 Pod 副本(默认将仅部署1个Pod)、匹配 Pod 标签的选择器、创建 pod 的模板、存储卷申请模板;第二个 spec是 spec.template.spec,主要用于 Pod 里的容器属性等配置。
.spec.template 里的内容是声明 Pod 对象时要定义的各种属性,所以这部分也叫做PodTemplate(Pod模板)。还有一个值得注意的地方是:在 .spec.selector 中定义的标签选择器必须能够匹配到 spec.template.metadata.labels 里定义的 Pod 标签,否则 Kubernetes 将不允许创建 statefulset。
三、Statefulset 使用案例:部署 web 站点
# 编写一个 Statefulset 资源清单文件
[root@k8s-master01 statefulset]# vim statefulset.yaml
apiVersion: v1 # 定义api版本
kind: Service # 定义要创建的资源:service
metadata:
name: nginx # 定义 service 的名字
labels:
app: nginx # service 的标签
spec:
ports:
- port: 80
name: web
clusterIP: None # 创建一个没有 ip 的 service
selector:
app: nginx # 关联拥有 app=nginx 标签的 pod
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
selector:
matchLabels:
app: nginx # 必须匹配 .spec.template.metadata.labels
serviceName: "nginx" # headless service 的名字
replicas: 2 # 副本数
template: # 定义 pod 的模板
metadata:
labels:
app: nginx # 必须匹配 .spec.selector.matchLabels
spec:
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
name: web
volumeMounts:
- name: www
mountPath: /usr/share/nginx/html
volumeClaimTemplates: # 卷申领模板
- metadata:
name: www
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: "nfs" # 指定从哪个存储类申请 pv
resources:
requests:
storage: 1Gi # 需要 1G 的 pvc,会自动跟符合条件的 pv 绑定
# 前提是已经启动了 nfs 外部驱动和存储类 storageclasses。上一篇文章讲过,下面贴链接
[root@k8s-master01 statefulset]# kubectl get pods
NAME READY STATUS RESTARTS AGE
nfs-provisioner-6fc44b8b68-r8r8d 1/1 Running 0 5s
[root@k8s-master01 statefulset]# kubectl get storageclasses
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
nfs example.com/nfs Delete Immediate false 44m
# 更新资源清单文件
[root@k8s-master01 statefulset]# kubectl apply -f statefulset.yaml
# 查看 statefulset 是否创建成功
[root@k8s-master01 statefulset]# kubectl get statefulsets
NAME READY AGE
web 2/2 9m20s
[root@k8s-master01 statefulset]# kubectl get pods -o wide -l app=nginx
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
web-0 1/1 Running 0 8m28s 10.244.169.176 k8s-node2 <none> <none>
web-1 1/1 Running 0 5m4s 10.244.36.81 k8s-node1 <none> <none>
通过上面可以看到创建的 pod 名称是有序的。对于具有 N 个副本的 StatefulSet,该 StatefulSet 中的每个 Pod 将被分配一个从 0 到 N-1 的整数序号,该序号在此 StatefulSet 上是唯一的。
# 查看 pv
[root@k8s-master01 statefulset]# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-37663ba6-dbd4-4fa4-af35-30137a1c993d 1Gi RWO Delete Bound default/www-web-1 nfs 9m6s
pvc-e988891f-0dba-4ad4-ac7f-848cc667efba 1Gi RWO Delete Bound default/www-web-0 nfs 9m13s
# 查看 pvc
[root@k8s-master01 statefulset]# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
www-web-0 Bound pvc-e988891f-0dba-4ad4-ac7f-848cc667efba 1Gi RWO nfs 12m
www-web-1 Bound pvc-37663ba6-dbd4-4fa4-af35-30137a1c993d 1Gi RWO nfs 9m4s
# 查看 headless service
[root@k8s-master01 statefulset]# kubectl get svc -l app=nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx ClusterIP None <none> 80/TCP 12m
# 删除其中一个 pod
[root@k8s-master01 statefulset]# kubectl delete pods web-1
pod "web-1" deleted
# 会自动生成一个名字相同 pod
[root@k8s-master01 statefulset]# kubectl get pods -l app=nginx
NAME READY STATUS RESTARTS AGE
web-0 1/1 Running 0 70m
web-1 1/1 Running 0 27s
3.1 StatefulSet 由以下几个部分组成:
- Headless Service:用来定义 pod 网路标识,生成可解析的 DNS 记录。
- volumeClaimTemplates:存储卷申请模板,创建 pvc,指定 pvc 名称大小,自动创建 pvc,且 pvc由存储类供应。
- StatefulSet:管理 pod 。
官方参考文档:StatefulSet | Kubernetes
3.2 什么是 Headless service ?
Headless service 不分配 clusterIP,headless service 可以通过解析 service 的 DNS,返回所有 Pod 的 dns 和 ip 地址 (statefulSet 部署的 Pod 才有 DNS),普通的 service,只能通过解析service 的 DNS 返回 service 的 ClusterIP。
headless service 会为 service 分配一个域名,管理域的这个服务的格式为:<service name>.$<namespace name>.svc.cluster.local
K8s 中资源的全局 FQDN 格式:
- Service_NAME.NameSpace_NAME.Domain.LTD.
- Domain.LTD.=svc.cluster.local. # 这是默认 k8s 集群的域名。
FQDN 全称 Fully Qualified Domain Name,即全限定域名:同时带有主机名和域名的名称。FQDN = Hostname + DomainName。如主机名是 master,域名是 baidu.com,FQDN= master.baidu.com
StatefulSet 会为关联的 Pod 保持一个不变的 Pod Name。statefulset 中 Pod 的名字格式为$(StatefulSet 名称)-$(序号),如上面创建的两个名称分别为 web-0、web-1
的 Pod。
一旦每个 Pod 创建成功,就会得到一个匹配的 DNS 子域,格式为:$(pod 名称).$(所属服务的 DNS 域名),其中所属服务由 StatefulSet 的 serviceName
域来设定。
即:$<Pod Name>.$<service name>.$<namespace name>.svc.cluster.local
# 使用 kubectl run 运行一个提供 nslookup 命令的容器的,这个命令来自于 dnsutils 包,通过对 pod 主机名执行 nslookup,可以检查它们在集群内部的 DNS 地址:
[root@k8s-master01 ~]# kubectl run busybox --image docker.io/library/busybox:1.28 --image-pull-policy=IfNotPresent --restart=Never --rm -it busybox -- sh
If you don't see a command prompt, try pressing enter.
/ # nslookup web-0.nginx.default.svc.cluster.local
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: web-0.nginx.default.svc.cluster.local
Address 1: 10.244.169.176 web-0.nginx.default.svc.cluster.local # statefulset 创建的 pod 也是有 dns 记录的。解析的是 pod 的 ip 地址。
/ # nslookup nginx.default.svc.cluster.local # 查询 service dns,会把对应的 pod ip 解析出来
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: nginx.default.svc.cluster.local
Address 1: 10.244.36.81 web-1.nginx.default.svc.cluster.local
Address 2: 10.244.169.176 web-0.nginx.default.svc.cluster.local
无头服务(Headless Services)官方说明文档:服务(Service) | Kubernetes
稳定的网络 ID 官网说明文档:StatefulSet | Kubernetes
3.3 为什么要用 volumeClaimTemplate ?
对于有状态应用都会用到持久化存储,比如 mysql 主从,由于主从数据库的数据是不能存放在一个目录下的,每个 mysq 节点都需要有自己独立的存储空间。而在 deployment 中创建的存储卷是一个共享的存储卷,多个 pod 使用同一个存储卷,它们数据是同步的,而 statefulset 定义中的每一个 pod 都不能使用同一个存储卷,这就需要使用 volumeClainTemplate,当在使用statefulset 创建 pod 时,volumeClainTemplate 会自动生成一个 PVC,从而请求绑定一个 PV,每一个 pod 都有自己专用的存储卷。Pod、PVC 和 PV 对应的关系图如下:
# 进入宿主机挂载目录
[root@k8s-master01 ~]# cd /data/nfs_pro/
# 新生成两个 pod 共享目录
[root@k8s-master01 nfs_pro]# ls
archived-pvc-a7571e3f-7017-40fa-8e3f-7d96a106450a default-www-web-1-pvc-37663ba6-dbd4-4fa4-af35-30137a1c993d
default-www-web-0-pvc-e988891f-0dba-4ad4-ac7f-848cc667efba
# 分别在两个新目录下创建文件
[root@k8s-master01 nfs_pro]# cd default-www-web-0-pvc-e988891f-0dba-4ad4-ac7f-848cc667efba/
[root@k8s-master01 default-www-web-0-pvc-e988891f-0dba-4ad4-ac7f-848cc667efba]# touch web-o.txt
[root@k8s-master01 default-www-web-0-pvc-e988891f-0dba-4ad4-ac7f-848cc667efba]# cd ../default-www-web-1-pvc-37663ba6-dbd4-4fa4-af35-30137a1c993d/
[root@k8s-master01 default-www-web-1-pvc-37663ba6-dbd4-4fa4-af35-30137a1c993d]# touch web-1.txt
# 进入容器内部查询是否独立同步到对应的 pod 里
[root@k8s-master01 statefulset]# kubectl exec -it web-0 -- bash
root@web-0:/# ls /usr/share/nginx/html/
web-o.txt
[root@k8s-master01 statefulset]# kubectl exec -it web-1 -- bash
root@web-1:/# ls /usr/share/nginx/html/
web-1.txt
3.4 举例说明 service 和 headless service 区别
# 通过 deployment 创建 pod,pod 前端创建一个 service
[root@k8s-master01 statefulset]# vim deploy-service.yaml
apiVersion: v1
kind: Service
metadata:
name: my-nginx
labels:
run: my-nginx
spec:
type: ClusterIP
ports:
- port: 80 # service 的端口,暴露给 k8s 集群内部服务访问
protocol: TCP
targetPort: 80 # pod 容器中定义的端口
selector:
run: my-nginx # 选择拥有 run=my-nginx 标签的 pod
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-nginx
spec:
selector:
matchLabels:
run: my-nginx
replicas: 2
template:
metadata:
labels:
run: my-nginx
spec:
containers:
- name: my-nginx
image: busybox
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
command:
- sleep
- "3600"
# 更新资源清单文件
[root@k8s-master01 statefulset]# kubectl apply -f deploy-service.yaml
service/my-nginx created
deployment.apps/my-nginx created
# 查看 service
[root@k8s-master01 statefulset]# kubectl get svc -l run=my-nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
my-nginx ClusterIP 10.103.6.151 <none> 80/TCP 17s
# 查看 pod。可以看到 deployment 创建的 pod 名称是随机生成的:
[root@k8s-master01 statefulset]# kubectl get pods -l run=my-nginx -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
my-nginx-676b95659c-lvttl 1/1 Running 0 42s 10.244.169.178 k8s-node2 <none> <none>
my-nginx-676b95659c-lzfgk 1/1 Running 0 42s 10.244.36.82 k8s-node1 <none> <none>
# 解析域名
[root@k8s-master01 ~]# kubectl run busybox --image docker.io/library/busybox:1.28 --image-pull-policy=IfNotPresent --restart=Never --rm -it busybox -- sh
If you don't see a command prompt, try pressing enter.
/ # nslookup my-nginx.default.svc.cluster.local
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: my-nginx.default.svc.cluster.local
Address 1: 10.103.6.151 my-nginx.default.svc.cluster.local # 解析的是 service 的 ip 地址,而不是关联的 pod ip。
3.5 Statefulset 小结
- Statefulset 管理的 pod,pod 名字是有序的,由 statefulset 的名字-0、1、2这种格式组成。
- 创建 statefulset 资源的时候,必须事先创建好一个 service,如果创建的 service 没有 ip,那对这个 service 做 dns 解析,会找到它所关联的 pod ip,如果创建的 service 有 ip,那对这个service 做 dns 解析,会解析到 service 本身 ip。
- statefulset 管理的 pod,删除 pod,自动恢复创建的 pod 名字跟删除的 pod 名字是一样的。
- statefulset 具有 volumeclaimtemplate 这个字段,这个是卷申请模板,会自动创建 pv,pvc 也会自动生成,跟 pv 进行绑定,那如果创建的 statefulset 使用了 volumeclaimtemplate 这个字段,那创建 pod,数据目录是独享的。
- ststefulset 创建的 pod,是有域名的(域名组成:pod-name.svc-name.svc-namespace.svc.cluster.local)
四、Statefulset 管理 pod:动态扩容、缩容、更新
4.1 Statefulset 实现 pod 的动态扩容
如果我们觉得两个副本太少了,想要增加,只需要修改配置文件 statefulset.yaml 里的replicas 的值即可,原来 replicas: 2,现在变成 replicaset: 3,修改之后,执行如下命令更新实现动态扩容:
[root@k8s-master01 statefulset]# vim statefulset.yaml
······
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
selector:
matchLabels:
app: nginx
serviceName: "nginx"
replicas: 3
······
[root@k8s-master01 statefulset]# kubectl apply -f statefulset.yaml
[root@k8s-master01 statefulset]# kubectl get pods -l app=nginx -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
web-0 1/1 Running 0 118m 10.244.169.176 k8s-node2 <none> <none>
web-1 1/1 Running 0 48m 10.244.36.80 k8s-node1 <none> <none>
web-2 1/1 Running 0 20s 10.244.169.179 k8s-node2 <none> <none>
也可以直接编辑控制器实现扩容
# 使用 kubectl edit 编辑副本字段:把 spec 下的 replicas 后面的值改成 4,保存退出
[root@k8s-master01 statefulset]# kubectl edit sts web
······
spec:
podManagementPolicy: OrderedReady
replicas: 4
revisionHistoryLimit: 10
······
[root@k8s-master01 statefulset]# kubectl get pods -l app=nginx -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
web-0 1/1 Running 0 122m 10.244.169.176 k8s-node2 <none> <none>
web-1 1/1 Running 0 52m 10.244.36.80 k8s-node1 <none> <none>
web-2 1/1 Running 0 4m12s 10.244.169.179 k8s-node2 <none> <none>
web-3 1/1 Running 0 28s 10.244.36.86 k8s-node1 <none> <none>
4.2 Statefulset 实现 pod 的动态缩容
如果我们觉得 4 个 Pod 副本太多了,想要减少,只需要修改配置文件 statefulset.yaml 里的replicas 的值即可,把 replicaset:3 变成 replicas: 2,修改之后,执行如下命令更新:
[root@k8s-master01 statefulset]# vim statefulset.yaml
······
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
selector:
matchLabels:
app: nginx
serviceName: "nginx"
replicas: 2
······
[root@k8s-master01 statefulset]# kubectl apply -f statefulset.yaml
service/nginx unchanged
statefulset.apps/web configured
[root@k8s-master01 statefulset]# kubectl get pods -l app=nginx -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
web-0 1/1 Running 0 126m 10.244.169.176 k8s-node2 <none> <none>
web-1 1/1 Running 0 56m 10.244.36.80 k8s-node1 <none> <none>
也可以直接编辑控制器实现扩容
# 使用 kubectl edit 编辑副本字段:把 spec 下的 replicas 后面的值改成 1,保存退出
[root@k8s-master01 statefulset]# kubectl edit sts web
······
spec:
podManagementPolicy: OrderedReady
replicas: 1
revisionHistoryLimit: 10
······
[root@k8s-master01 statefulset]# kubectl get pods -l app=nginx -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
web-0 1/1 Running 0 127m 10.244.169.176 k8s-node2 <none> <none>
小结:
- 对于包含 N 个 副本的 StatefulSet,当部署 Pod 时,它们是依次创建的,顺序为
0..N-1
。 - 当删除 Pod 时,它们是逆序终止的,顺序为
N-1..0
。
扩缩 StatefulSet 官方参考文档:扩缩 StatefulSet | Kubernetes
4.3 Statefulset 实现 pod 的更新
-
更新策略
StatefulSet 的 .spec.updateStrategy
字段让你可以配置和禁用掉自动滚动更新 Pod 的容器、标签、资源请求或限制、以及注解。有两个允许的值:
OnDelete
当 StatefulSet 的 .spec.updateStrategy.type
设置为 OnDelete
时, 它的控制器将不会自动更新 StatefulSet 中的 Pod。 用户必须手动删除 Pod 以便让控制器创建新的 Pod,以此来对 StatefulSet 的 .spec.template
的变动作出反应。
RollingUpdate
RollingUpdate
更新策略对 StatefulSet 中的 Pod 执行自动的滚动更新。这是默认的更新策略。
-
分区滚动更新
[root@k8s-master01 statefulset]# vim statefulset.yaml
······
spec: # 新增内容
updateStrategy:
rollingUpdate:
partition: 1
maxUnavailable: 0
selector:
matchLabels:
app: nginx
······
spec:
containers:
- name: nginx
image: tomcat # 修改镜像
imagePullPolicy: IfNotPresent
······
[root@k8s-master01 statefulset]# kubectl apply -f statefulset.yaml
# 在另一个终端动态查看 pod
[root@k8s-master01 ~]# kubectl get pods -l app=nginx -w
web-0 1/1 Running 0 138m
web-1 0/1 Pending 0 0s
web-1 0/1 Pending 0 0s
web-1 0/1 ContainerCreating 0 0s
web-1 0/1 ContainerCreating 0 1s
web-1 1/1 Running 0 2s
从上面结果可以看出来,pod 在更新的时候,只是更新了 web-1 这个 pod, 因为 partition: 1 表示更新的时候会把 pod 序号大于等于 1 的进行更新。
-
对 partition 和 maxUnavailable 字段的官方解释:
如果更新策略是 OnDelete,那不会自动更新 pod,需要手动删除旧的 pod,重新创建 pod 才会实现更新:
[root@k8s-master01 statefulset]# vim statefulset.yaml
······
spec:
updateStrategy:
type: OnDelete # 更新策略为 OnDelete
selector:
matchLabels:
app: nginx
······
spec:
containers:
- name: nginx
image: nginx # 修改镜像
imagePullPolicy: IfNotPresent
······
# 在另一个终端动态查看信息
[root@k8s-master01 ~]# kubectl get pods -w -l app=nginx
[root@k8s-master01 statefulset]# kubectl apply -f statefulset.yaml
发现没有 pod 没有任何变化:
只有手动删除 pod 才会实现更新:
[root@k8s-master01 statefulset]# kubectl delete pods web-1
pod "web-1" deleted
[root@k8s-master01 statefulset]# kubectl delete pods web-0
pod "web-0" deleted
statefulset 更新策略官方参考文档:StatefulSet | Kubernetes
上一篇文章:【云原生 | Kubernetes 实战】13、K8s 常见的存储方案及具体应用场景分析(下)_Stars.Sky的博客-CSDN博客