背景
- Deployment的短板:Deployment认为一个应用的所有Pod,是完全一样的
- 有状态应用(Stateful Application)
- 实例之间有不对等的关系 – 拓扑
- 实例对外部数据有依赖关系 – 存储
- StatefulSet
- Kubernetes在Deployment的基础上,扩展出对有状态应用的初步支持
- StatefulSet是Kubernetes在作业编排的集大成者
状态抽象
- 分类
- 拓扑状态:应用的多个实例之间是不完全对等的关系
- 存储状态:应用的多个实例分别绑定了不同的存储数据
- 核心功能:通过某种方式记录状态,然后在Pod被重新创建时,能够为新Pod恢复这些状态
设计思想
- StatefulSet是一种特殊的Deployment,独特之处:为每个Pod编号(代表创建顺序、网络标识等)
- 编号 + Headless Service ==> 拓扑状态
- 编号 + Headless Service + PV/PVC ==> 存储状态
拓扑状态
- StatefulSet控制器使用Pod模板创建Pod时,对它们进行编号,并且按编号顺序逐一完成创建工作
- StatefulSet控制器进行『调谐』时,会严格按照Pod编号的顺序,逐一完成这些操作
- 通过Headless Service的方式,StatefulSet为每个Pod创建一个固定并且稳定的DNS记录,来作为它的访问入口
Service
Services:用来将一组Pod暴露给外界访问的一种机制
访问方式
- VIP(Virtual IP):访问Service的VIP,Service会把请求转发到该Service所代理的某个Pod上
- DNS:访问
my-svc.my-namespace.svc.cluster.local
,可以访问到Service(my-svc
)所代理的某个Pod
- Normal Service
- 访问
my-svc.my-namespace.svc.cluster.local
解析到的是my-svc
的VIP(需要转发请求)
- Headless Service
- 访问
my-svc.my-namespace.svc.cluster.local
解析到的是my-svc
代理的某个Pod的IP,并不需要分配一个VIP
svc.yaml
- clusterIP为None,该Service被创建后不会被分配一个VIP(Headless),以DNS的方式暴露它所代理(Label Selector)的Pod
- Headless Service创建之后,它所代理的所有Pod的IP,都会被绑定一个DNS记录
- Pod的唯一可解释身份:**
<pod-name>.<svc-name>.<namespace>
**.svc.cluster.local
svc.yaml1 2 3 4 5 6 7 8 9 10 11 12 13
| apiVersion: v1 kind: Service metadata: name: nginx labels: app: nginx spec: ports: - port: 80 name: web clusterIP: None selector: app: nginx
|
statefulset.yaml
serviceName: nginx
:StatefulSet控制器在执行控制循环时,会使用nginx
(Headless Service)来保证Pod的可解释身份
statefulset.yaml1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| apiVersion: apps/v1 kind: StatefulSet metadata: name: web spec: serviceName: nginx replicas: 2 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: 'nginx:1.9.1' ports: - containerPort: 80 name: web
|
创建Headless Service
1 2 3 4 5 6
| # kubectl apply -f svc.yaml service/nginx created
# kubectl get service nginx NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE nginx ClusterIP None <none> 80/TCP 17s
|
创建StatefulSet
1 2
| # kubectl apply -f statefulset.yaml statefulset.apps/web created
|
- StatefulSet给它所管理的所有Pod的命名进行编号,编号规则为
-
,编号从0开始,Pod的创建严格按照编号顺序进行
- web-0进入到Running状态并且细分状态(Conditions)成为Ready之前,web-1一直处于Pending状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| # kubectl get pods -w -l app=nginx NAME READY STATUS RESTARTS AGE web-0 0/1 Pending 0 0s web-0 0/1 Pending 0 0s web-0 0/1 ContainerCreating 0 0s web-0 1/1 Running 0 31s web-1 0/1 Pending 0 0s web-1 0/1 Pending 0 0s web-1 0/1 ContainerCreating 0 0s web-1 1/1 Running 0 2s
# kubectl describe statefulset web Name: web Namespace: default CreationTimestamp: Thu, 17 Jun 2021 08:15:06 +0000 Selector: app=nginx Labels: <none> Annotations: <none> Replicas: 2 desired | 2 total Update Strategy: RollingUpdate Partition: 0 Pods Status: 2 Running / 0 Waiting / 0 Succeeded / 0 Failed Pod Template: Labels: app=nginx Containers: nginx: Image: nginx:1.9.1 Port: 80/TCP Host Port: 0/TCP Environment: <none> Mounts: <none> Volumes: <none> Volume Claims: <none> Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal SuccessfulCreate 3m27s statefulset-controller create Pod web-0 in StatefulSet web successful Normal SuccessfulCreate 2m56s statefulset-controller create Pod web-1 in StatefulSet web successful
|
1 2 3
| # kubectl get statefulset web NAME READY AGE web 2/2 56s
|
Pod的hostname与Pod的名字一致
1 2 3 4 5 6 7 8 9 10
| # kubectl get pods NAME READY STATUS RESTARTS AGE web-0 1/1 Running 0 13m web-1 1/1 Running 0 13m
# kubectl exec web-0 -- sh -c 'hostname' web-0
# kubectl exec web-1 -- sh -c 'hostname' web-1
|
访问Headless Service
Pod是有状态的,web-0.nginx.default
对应IP为10.32.0.7,web-1.nginx.default
对应IP为10.32.0.8
1 2 3 4 5 6 7 8 9 10 11 12
| # kubectl run -i --tty --image busybox dns-test --restart=Never --rm /bin/sh / # nslookup web-0.nginx.default.svc.cluster.local Server: 10.96.0.10 Address: 10.96.0.10:53 Name: web-0.nginx.default.svc.cluster.local Address: 10.32.0.7
/ # nslookup web-1.nginx.default.svc.cluster.local Server: 10.96.0.10 Address: 10.96.0.10:53 Name: web-1.nginx.default.svc.cluster.local Address: 10.32.0.8
|
删除Pod
- Pod删除后,Kubernetes会按照原先编号的顺序创建出2个新的Pod,并分配了一样的网络身份,保证了Pod网络标识的稳定性
- Kubernetes成功地将Pod的拓扑状态,按照Pod的『名字-编号』的方式固定下来
- Pod的拓扑状态,在StatefulSet的整个生命周期里都会保持不变(不管对应Pod删除或者重建)
- Kubernetes为每个Pod提供了固定且唯一的访问入口(Pod对应的DNS记录)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| # kubectl delete pod -l app=nginx pod "web-0" deleted pod "web-1" deleted
# kubectl get pod -w -l app=nginx NAME READY STATUS RESTARTS AGE web-0 1/1 Running 0 33m web-1 1/1 Running 0 33m web-0 1/1 Terminating 0 34m web-1 1/1 Terminating 0 33m web-1 0/1 Terminating 0 33m web-0 0/1 Terminating 0 34m web-1 0/1 Terminating 0 33m web-1 0/1 Terminating 0 33m web-0 0/1 Terminating 0 34m web-0 0/1 Terminating 0 34m web-0 0/1 Pending 0 0s web-0 0/1 Pending 0 0s web-0 0/1 ContainerCreating 0 0s web-0 1/1 Running 0 3s web-1 0/1 Pending 0 0s web-1 0/1 Pending 0 0s web-1 0/1 ContainerCreating 0 0s web-1 1/1 Running 0 2s
# kubectl get pods NAME READY STATUS RESTARTS AGE web-0 1/1 Running 0 75s web-1 1/1 Running 0 72s
|
web-0.nginx.default.svc.cluster.local
本身不会变,但解析到的Pod的IP并不是固定的,因此访问Stateful应用,应该使用域名
1 2 3 4 5 6 7 8 9 10 11 12
| # kubectl run -i --tty --image busybox dns-test --restart=Never --rm /bin/sh / # nslookup web-0.nginx.default.svc.cluster.local Server: 10.96.0.10 Address: 10.96.0.10:53 Name: web-0.nginx.default.svc.cluster.local Address: 10.32.0.7
/ # nslookup web-1.nginx.default.svc.cluster.local Server: 10.96.0.10 Address: 10.96.0.10:53 Name: web-1.nginx.default.svc.cluster.local Address: 10.32.0.8
|
存储状态
PV & PVC
Persistent Volume(PV) + Persistent Volume Claim(PVC):降低用户声明和使用持久化Volume的门槛
PVC和PV ≈ 接口和实现,职责分离,避免向开发者暴露过多的存储系统细节而带来的安全隐患
定义PVC
在PVC对象里,不需要任何关于Volume细节的字段,只有描述性的属性和定义
开发人员1 2 3 4 5 6 7 8 9 10
| kind: PersistentVolumeClaim apiVersion: v1 metadata: name: pv-claim spec: accessModes: - ReadWriteOnce resources: requests: storage: 1Gi
|
Pod使用PVC
声明Volume类型为persistentVolumeClaim,并指定PVC的名字,完全不必关心Volume本身的定义
创建PVC后,Kubernetes会自动为它绑定一个符合条件的PV
开发人员1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| apiVersion: v1 kind: Pod metadata: name: pv-pod spec: containers: - name: pv-container image: nginx ports: - containerPort: 80 name: http-server volumeMounts: - mountPath: /usr/share/nginx/html name: pv-storage volumes: - name: pv-storage persistentVolumeClaim: claimName: pv-claim
|
定义PV
PV容量为10GiB,Kubernetes会将该PV对象绑定到前面的PVC对象(需要1GiB)
运维人员1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| kind: PersistentVolume apiVersion: v1 metadata: name: pv-volume labels: type: local spec: capacity: storage: 10Gi rbd: monitors: - '10.16.154.78:6789' - '10.16.154.82:6789' - '10.16.154.83:6789' pool: kube image: foo fsType: ext4 readOnly: true user: admin keyring: /etc/ceph/keyring imageformat: '2' imagefeatures: layering
|
statefulset.yaml
- volumeClaimTemplates:凡是被StatefulSet管理的Pod,都会声明一个对应的PVC(编号与Pod一致)
- 自动创建的PVC,与PV绑定成功后,进入Bound状态(该Pod可以挂载并使用这个PV)
- PVC是一种特殊的Volume,而PVC具体是什么类型的Volume,需要与某个PV绑定后才知道 – 动态绑定
statefulset.yaml1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| apiVersion: apps/v1 kind: StatefulSet metadata: name: web spec: serviceName: nginx replicas: 2 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: 'nginx:1.9.1' ports: - containerPort: 80 name: web volumeMounts: - name: www mountPath: /usr/share/nginx/html volumeClaimTemplates: - metadata: name: www spec: accessModes: - ReadWriteOnce resources: requests: storage: 1Gi
|
由于本环境目前暂未创建符合条件的PV,无法建立绑定,因此PVC的状态一直为Pending
1 2 3 4 5 6 7 8 9
| # kubectl apply -f svc.yaml service/nginx created
# kubectl apply -f statefulset.yaml statefulset.apps/web created
# kubectl get pvc -l app=nginx NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE www-web-0 Pending 18s
|
成功建立绑定后,PVC的命名规则:**<pvc.name>-<statefulset.name>-<编号>
**
1 2 3 4
| # kubectl get pvc -l app=nginx NAME STATUS VOLUME CAPACITY ACCESSMODES AGE www-web-0 Bound pvc-15c268c7-b507-11e6-932f-42010a800002 1Gi RWO 48s www-web-1 Bound pvc-15c79307-b507-11e6-932f-42010a800002 1Gi RWO 48s
|
删除Pod
- 删除Pod后,该Pod对应的PVC和PV并不会被删除,该Pod对应的Volume已经写入的数据,依然会存储在远程存储服务
- StatefulSet发现Pod消失后,会重新创建一个新的同名Pod,该新Pod对象的定义里,依然使用同名PVC
- 新Pod被创建出来后,Kubernetes为其查找同名PVC
- 直接找到旧Pod遗留下来的同名PVC,进而找到跟这个PVC绑定的PV
- 新Pod可以挂载旧Pod对应的Volume,并且获取到保存在Volume里的数据
小结
- StatefulSet控制器直接管理的是Pod
- Kubernetes通过Headless Service,为这些有编号的Pod,在DNS服务器中生成带有同样编号的DNS记录
- StatefulSet为每个Pod分配并创建一个同样编号的PVC
工程化优势
StatefulSet是对现有典型运维业务的容器化抽象
滚动更新
patch:修改API Object的指定字段
StatefulSet控制器会按照与Pod编号相反的顺序,从最后一个Pod开始,逐一更新,如果更新发生错误,滚动更新会停止
1 2
| # kubectl patch statefulset mysql --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value":"mysql:5.7.23"}]' statefulset.apps/mysql patched
|
灰度发布
应用的多个实例中被指定的一部分不会被更新到最新版本
partition=2
:当Pod模板发生变化时,只有序号≥2的Pod会被更新
1 2
| # kubectl patch statefulset mysql -p '{"spec":{"updateStrategy":{"type":"RollingUpdate","rollingUpdate":{"partition":2}}}}' statefulset.apps/mysql patched
|
参考资料
- 深入剖析Kubernetes