背景

  1. Deployment的短板:Deployment认为一个应用的所有Pod,是完全一样
  2. 有状态应用(Stateful Application)
    • 实例之间有不对等的关系 – 拓扑
    • 实例对外部数据有依赖关系 – 存储
  3. StatefulSet
    • Kubernetes在Deployment的基础上,扩展出对有状态应用的初步支持
    • StatefulSet是Kubernetes在作业编排的集大成者

状态抽象

  1. 分类
    • 拓扑状态:应用的多个实例之间是不完全对等的关系
    • 存储状态:应用的多个实例分别绑定了不同的存储数据
  2. 核心功能:通过某种方式记录状态,然后在Pod被重新创建时,能够为新Pod恢复这些状态

设计思想

  1. StatefulSet是一种特殊的Deployment,独特之处:为每个Pod编号(代表创建顺序、网络标识等)
  2. 编号 + Headless Service ==> 拓扑状态
  3. 编号 + Headless Service + PV/PVC ==> 存储状态

拓扑状态

  1. StatefulSet控制器使用Pod模板创建Pod时,对它们进行编号,并且按编号顺序逐一完成创建工作
  2. StatefulSet控制器进行『调谐』时,会严格按照Pod编号的顺序,逐一完成这些操作
  3. 通过Headless Service的方式,StatefulSet为每个Pod创建一个固定并且稳定的DNS记录,来作为它的访问入口

Service

Services:用来将一组Pod暴露给外界访问的一种机制

访问方式

  1. VIP(Virtual IP):访问Service的VIP,Service会把请求转发到该Service所代理的某个Pod上
  2. 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

  1. clusterIP为None,该Service被创建后不会被分配一个VIP(Headless),以DNS的方式暴露它所代理(Label Selector)的Pod
  2. Headless Service创建之后,它所代理的所有Pod的IP,都会被绑定一个DNS记录
    • Pod的唯一可解释身份:**<pod-name>.<svc-name>.<namespace>**.svc.cluster.local
svc.yaml
1
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控制器在执行控制循环时,会使用nginxHeadless Service)来保证Pod的可解释身份

statefulset.yaml
1
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
  1. StatefulSet给它所管理的所有Pod的命名进行编号,编号规则为-,编号从0开始,Pod的创建严格按照编号顺序进行
  2. 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

  1. Pod删除后,Kubernetes会按照原先编号的顺序创建出2个新的Pod,并分配了一样的网络身份,保证了Pod网络标识的稳定性
  2. Kubernetes成功地将Pod的拓扑状态,按照Pod的『名字-编号』的方式固定下来
    • Pod的拓扑状态,在StatefulSet的整个生命周期里都会保持不变(不管对应Pod删除或者重建)
  3. 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

  1. volumeClaimTemplates:凡是被StatefulSet管理的Pod,都会声明一个对应的PVC编号与Pod一致
  2. 自动创建的PVC,与PV绑定成功后,进入Bound状态(该Pod可以挂载并使用这个PV)
  3. PVC是一种特殊的Volume,而PVC具体是什么类型的Volume,需要与某个PV绑定后才知道 – 动态绑定
statefulset.yaml
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
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

  1. 删除Pod后,该Pod对应的PVC和PV并不会被删除,该Pod对应的Volume已经写入的数据,依然会存储在远程存储服务
  2. StatefulSet发现Pod消失后,会重新创建一个新的同名Pod,该新Pod对象的定义里,依然使用同名PVC
  3. 新Pod被创建出来后,Kubernetes为其查找同名PVC
    • 直接找到旧Pod遗留下来的同名PVC,进而找到跟这个PVC绑定的PV
    • 新Pod可以挂载旧Pod对应的Volume,并且获取到保存在Volume里的数据

小结

  1. StatefulSet控制器直接管理的是Pod
  2. Kubernetes通过Headless Service,为这些有编号的Pod,在DNS服务器中生成带有同样编号的DNS记录
  3. 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

参考资料

  1. 深入剖析Kubernetes