背景
- 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