官网链接
本教程将使用StatefulSet部署一个简单的 Web 应用。
- 创建 StatefulSet
- 了解StatefulSet怎样管理它的 Pod
- 删除 StatefulSet
- 对StatefulSet 进行扩容/缩容
- 更新一个StatefulSet的Pod
前置准备
搭建好一套k8s集群,可以参考我写的这篇教程:搭建k8s集群
k8s官方的镜像站在国内是拉不下来的,有几种方法解决:
- 在拉取镜像的虚拟机/服务器上科学上网
- 配置k8s的镜像源,目前国内只有阿里云支持改版后的k8s镜像源(registry.k8s.io)。
- 需要拉取镜像的时候,指定拉取策略为本地拉取(
imagePullPolicy:Never),每次需要拉取镜像前都手动拉取/上传一份镜像到服务器上再导入镜像
这里给出阿里云镜像源的配置教程:
旧版的k8s直接修改/etc/containerd/config.toml里的mirror信息,添加上阿里云的镜像站就行。但是新版的不支持inline或者说暂时兼容,未来不支持。所以这里就只给出新版k8s镜像源配置教程。
修改/etc/containerd/config.yaml,填入下列信息(如果你已经有了config.yaml且这个配置文件是从containerd默认配置里生成的,那直接备份,然后使用下面的内容)。sudo vim /etc/containerd/config.yaml
1
2
3
4
5
6
7
8
9
10
|
version = 2
[plugins."io.containerd.grpc.v1.cri"]
sandbox_image = "registry.aliyuncs.com/google_containers/pause:3.9"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
runtime_type = "io.containerd.runc.v2"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
SystemdCgroup = true
[plugins."io.containerd.grpc.v1.cri".registry]
config_path = "/etc/containerd/certs.d"
|
创建/etc/containerd/certs.d目录,在这个目录填入docker.io和registry.k8s.io的镜像源。
注意:k8s里修改镜像源之后,使用kubectl describe pod <pod_name> 查看时还是显示的docker.io和registry.k8s.io。配置镜像源只物理修改从哪里修改,不改镜像拉取的逻辑源。所以改好镜像源之后也不太好验证成功,随便拉个镜像sudo crictl pull nginx:1.14.2,能拉下来就是成了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
# Docker Hub 加速
sudo mkdir -p /etc/containerd/certs.d/docker.io
sudo tee /etc/containerd/certs.d/docker.io/hosts.toml << 'EOF'
server = "https://registry-1.docker.io"
[host."https://docker.m.daocloud.io"]
capabilities = ["pull", "resolve"]
EOF
# K8s 镜像加速
sudo mkdir -p /etc/containerd/certs.d/registry.k8s.io
sudo tee /etc/containerd/certs.d/registry.k8s.io/hosts.toml << 'EOF'
server = "https://registry.k8s.io"
[host."https://registry.cn-hangzhou.aliyuncs.com/google_containers"]
capabilities = ["pull", "resolve"]
override_path = true
EOF
|
到这里,镜像源就配置好了,如果不出意外,文件目录应该是下面这样:
1
2
3
4
5
6
7
|
rust@k8s1:/etc/containerd$ ll
总计 28
drwxr-xr-x 3 root root 4096 2月 4 10:31 ./
drwxr-xr-x 144 root root 12288 2月 2 17:01 ../
drwxr-xr-x 4 root root 4096 2月 2 16:44 certs.d/
-rw-r--r-- 1 root root 423 2月 2 19:02 config.toml
-rw-r--r-- 1 root root 886 12月 19 02:48 config.toml.dpkg-dist
|
修改完配置文件后需要重启containerd:
1
2
|
sudo systemctl restart containerd
sudo systemctl status containerd
|
给本地自建k8s集群添加默认存储类
K8s “产品”本身不自带 StorageClass;
但“常见的集群环境”(云厂商、本地发行版)一般都会预置一个或多个 StorageClass,并把其中一个标为默认。例如:
- minikube:通常有一个 standard StorageClass,并标记为 (default)。
- GKE:一般会自带 standard-rwo(或类似名字)默认类。
- EKS:AWS EBS CSI 会安装 gp2 / gp3 类并设为默认
kubectl get sc查看集群存储类。有的话可以跳过此节
- 在三个节点上创建存储基础目录(xcall是自建的脚本)
1
2
|
xcall sudo mkdir -p /opt/local-path-provisioner
xcall sudo chmod 755 /opt/local-path-provisioner
|
以后想换路径可以修改local-path-config这个ConfigMap里的nodePathMap配置
- 安装 local-path-provisioner(自带一个 StorageClass:local-path)
直接用官方的 YAML(以当前稳定版 v0.0.34 为例)
1
|
kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.34/deploy/local-path-storage.yaml
|
这个 YAML 里面已经包含:
- Namespace:local-path-storage
- RBAC(ServiceAccount / Role / ClusterRole / RoleBinding / ClusterRoleBinding)
- Deployment:local-path-provisioner
- 一个 StorageClass:名字叫 local-path,provisioner 是 rancher.io/local-path
- 一个 ConfigMap:local-path-config,里面配置了默认路径 /opt/local-path-provisioner
- 确认安装成功,检查 Pod 是否 Running,存储类是否已经存在(名字是local-path)
1
2
3
4
5
6
7
8
|
kubectl get pods -n local-path-storage
kubectl describe pod <pod_name> -n local-path-storage
kubectl get sc
rust@k8s1:~/k8s_try$ kubectl get sc
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
local-path rancher.io/local-path Delete WaitForFirstConsumer false 4m40s
|
- 把 local-path 设置为默认StorageClass。
Kubernetes 默认 StorageClass 是通过注解 storageclass.kubernetes.io/is-default-class 来标记的。
local-path-provisioner 的 YAML 里默认没有给它打这个注解,
直接用 kubectl patch 打注解:
1
2
3
4
5
6
7
8
9
10
|
kubectl patch storageclass local-path \
-p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
rust@k8s1:~/k8s_try$ kubectl patch storageclass local-path \
-p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
storageclass.storage.k8s.io/local-path patched
# 打上注解后再查看存储类,发现local-path以标记为默认存储类
rust@k8s1:~/k8s_try$ kubectl get sc
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
local-path (default) rancher.io/local-path Delete WaitForFirstConsumer false 9m22s
|
现在集群就可以自动制取持久卷了。
创建 StatefulSet
创建一个Headless Service nginx用来发布StatefulSet web中的Pod的IP地址。
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
39
40
41
42
43
44
45
46
47
|
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
ports:
- port: 80
name: web
clusterIP: None
selector:
app: nginx
---
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
# 官方文档里用的瘦身版,拉不下来就用普通的nginx就行
image: nginx:1.25
ports:
- containerPort: 80
name: web
volumeMounts:
- name: www
mountPath: /usr/share/nginx/html
volumeClaimTemplates:
- metadata:
name: www
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 1Gi
|
开启两个终端,一个终端用来监控StatefulSet状态,另一个用来创建StatefulSet。
1
2
3
4
5
6
|
# 在第一个终端监控StatefulSet状态
kubectl get pods --watch -l app=nginx
# 在第二个终端创建StatefulSet
vim web.yaml
kubectl apply -f web.yaml
|
web.yaml创建了两个 Pod,每个都运行了一个 NGINX Web 服务器。查看pod、service、statefulset运行状态:
1
2
3
4
5
6
7
8
9
10
11
|
rust@k8s1:~/k8s_try$ kubectl get pods
NAME READY STATUS RESTARTS AGE
web-0 1/1 Running 0 90s
web-1 1/1 Running 0 90s
rust@k8s1:~/k8s_try$ kubectl get service nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx ClusterIP None <none> 80/TCP 26m
rust@k8s1:~/k8s_try$ kubectl get statefulset web
NAME READY AGE
web 2/2 26m
|
StatefulSet 默认以严格的顺序创建其 Pod。
对于一个拥有 n 个副本的 StatefulSet,Pod 被部署时是按照 {0..n-1} 的序号顺序创建的。
从刚刚开启的监控终端可以看到statefulSet创建过程中直到第一个pod(web-0)处于Running并ready状态时,第二个pod才开始创建。
1
2
3
4
5
6
7
8
9
|
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 19s
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 18s
|
StatefulSet 中的 Pod
检查 Pod 的顺序索引
StatefulSet中的每个Pod拥有一个唯一的顺序索引和稳定的网络身份标识。
StatefulSet 中的每个Pod拥有一个具有黏性的、独一无二的身份标志。这个标志基于StatefulSet控制器分配给每个 Pod 的唯一顺序索引。 Pod 名称的格式为 <statefulset 名称>-<序号索引>。web StatefulSet拥有两个副本,所以它创建了两个Pod:web-0 和 web-1。
每个 Pod 都拥有一个基于其顺序索引的稳定的主机名。使用kubectl exec在每个 Pod 中查看hostname
1
2
3
4
5
6
7
|
rust@k8s1:~$ kubectl get pods -l app=nginx
NAME READY STATUS RESTARTS AGE
web-0 1/1 Running 1 (9m28s ago) 13h
web-1 1/1 Running 1 (9m23s ago) 13h
rust@k8s1:~$ for i in 0 1; do kubectl exec "web-$i" -- sh -c 'hostname'; done
web-0
web-1
|
使用稳定的网络身份标识
使用 kubectl run 运行一个提供 nslookup 命令的容器,该命令来自于 dnsutils 包。 通过对 Pod 的主机名执行 nslookup,检查这些主机名在集群内部的 DNS 地址:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
# 打开dns-test容器终端
kubectl run -i --tty --image busybox:1.28 dns-test --restart=Never --rm
# 在终端中查看dns,可以看到两个pod的dns server地址相同
nslookup web-0.nginx
nslookup web-1.nginx
rust@k8s1:~$ kubectl run -i --tty --image busybox:1.28 dns-test --restart=Never --rm
If you don't see a command prompt, try pressing enter.
/ # nslookup web-0.nginx
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: web-0.nginx
Address 1: 172.16.109.124 web-0.nginx.default.svc.cluster.local
/ # nslookup web-1.nginx
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: web-1.nginx
Address 1: 172.16.219.51 web-1.nginx.default.svc.cluster.local
|
删除现有的pod,等待statefulSet控制器重建启动这些pod。再次查看这些pod的信息,会发现Pod 的序号、主机名、SRV 条目和记录名称没有改变,但和 Pod 相关联的 IP 地址可能发生了改变。 在本教程中使用的集群中它们就改变了。这就是为什么不要在其他应用中使用 StatefulSet 中特定 Pod 的 IP 地址进行连接,这点很重要(可以通过解析 Pod 的主机名来连接到 Pod)。
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
39
40
41
|
# 先开一个终端监控删除后statefulSet重建pod的过程
kubectl get pod --watch -l app=nginx
# 在第二个终端删除pod
kubectl delete pod -l app=nginx
# 删除完成后再查看pod,可以看见还是有两个pod在运行,但是age只有几秒
kubectl get pods
rust@k8s1:~$ kubectl delete pod -l app=nginx
pod "web-0" deleted
pod "web-1" deleted
rust@k8s1:~$ kubectl get pods
NAME READY STATUS RESTARTS AGE
web-0 1/1 Running 0 5s
web-1 1/1 Running 0 4s
# 第二个终端监控
rust@k8s1:~$ kubectl get pod --watch -l app=nginx
NAME READY STATUS RESTARTS AGE
web-0 1/1 Running 0 29s
web-1 1/1 Running 0 28s
web-0 1/1 Terminating 0 32s
web-1 1/1 Terminating 0 31s
web-1 1/1 Terminating 0 31s
web-0 1/1 Terminating 0 32s
web-1 0/1 Terminating 0 32s
web-0 0/1 Terminating 0 33s
web-1 0/1 Terminating 0 32s
web-1 0/1 Terminating 0 32s
web-0 0/1 Terminating 0 33s
web-0 0/1 Terminating 0 33s
web-0 0/1 Pending 0 0s
web-0 0/1 Pending 0 0s
web-0 0/1 ContainerCreating 0 0s
web-0 0/1 ContainerCreating 0 2s
web-0 1/1 Running 0 2s
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在删除重建后,再次查看主机名和集群内部的DNS表项。
Pod 的序号、主机名、SRV 条目和记录名称没有改变,但和 Pod 相关联的 IP 地址可能发生了改变
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
rust@k8s1:~$ for i in 0 1; do kubectl exec web-$i -- sh -c 'hostname'; done
web-0
web-1
rust@k8s1:~$ kubectl run -i --tty --image busybox:1.28 dns-test --restart=Never --rm
If you don't see a command prompt, try pressing enter.
/ # nslookup web-0.nginx
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: web-0.nginx
Address 1: 172.16.109.127 web-0.nginx.default.svc.cluster.local
/ # nslookup web-1.nginx
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: web-1.nginx
Address 1: 172.16.219.53 web-1.nginx.default.svc.cluster.local
/ # exit
pod "dns-test" deleted
rust@k8s1:~$
|
发现 StatefulSet 中特定的 Pod
相关概念:无头服务、Pod稳定主机名、SRV记录
- Headless Service(无头服务)就是 clusterIP: None 的 Service,比如示例里的:
1
2
3
4
5
6
7
8
9
10
11
|
apiVersion: v1
kind: Service
metadata:
name: nginx
spec:
clusterIP: None # 关键:Headless
selector:
app: nginx
ports:
- port: 80
name: web
|
对应statefulSet的:
1
2
3
4
5
6
7
8
|
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
serviceName: "nginx" # 关联到这个 Headless Service
replicas: 2
...
|
Headless Service不会有一个统一的 ClusterIP,而是给每个 Pod 生成一条 DNS 记录
- Pod 的稳定主机名
- StatefulSet 里的 Pod 名字固定、有序:web-0,web-1,web-2……
- 每个Pod 有一个“稳定的主机名”:web-0.nginx.default.svc.cluster.local、web-1.nginx.default.svc.cluster.local
- 即使 Pod 删除重建,名字不变,IP 可能变,但主机名对应关系不变。
- SRV 记录:
DNS 里除了 A 记录(域名->IP),还有 SRV 记录,用来表示“某个服务有哪些后端实例(主机:端口)”。
对 Headless Service,K8s 会给每个 Running+Ready 的 Pod 生成一条 SRV 记录
同一个 StatefulSet,有三种不同抽象层级的“服务发现方式”,分别对应“要列表 / 要固定 / 要简单”三种需求。
| 查找需求 |
DNS 查询的内容 |
返回值 |
自动健康检查 |
典型场景 |
| 当前所有健康成员列表 |
Headless Service 的 CNAME → SRV(nginx.default.svc.cluster.local) |
多个 Pod 的主机名+端口(仅 Running+Ready) |
是,由 SRV 过滤 |
数据库集群、副本集,要成员列表 |
| 固定连某个编号的 Pod |
某个 Pod 的 SRV(web-0.nginx.default.svc.cluster.local、web-1.nginx.default.svc.cluster.local) |
单个 Pod 的 IP+端口 |
不负责,靠应用自己探活 |
主从、分片,需要固定连某个序号 |
| 随便连一个健康的 Pod |
普通ClusterIP Service的 DNS 或 IP |
随机一个后端 Pod 的 IP |
是,Service 层负责 |
无状态服务、简单客户端 |
写入稳定的存储
查看web-0和web-1的PersistentVolumeClaims:
1
2
3
4
5
6
|
kubectl get pvc -l app=nginx
rust@k8s1:~$ kubectl get pvc -l app=nginx
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
www-web-0 Bound pvc-29d78e3e-a93b-4db3-880e-3a121d9af83b 1Gi RWO local-path <unset> 18h
www-web-1 Bound pvc-0b32d304-d17d-4f7d-9c38-a8d6fb62a709 1Gi RWO local-path <unset> 18h
|
StatefulSet 控制器创建了两个 PersistentVolumeClaims, 绑定到两个 PersistentVolumes。
前面配置了默认存储类,所以PV是动态创建和绑定的。
NginX Web 服务器默认会加载位于 /usr/share/nginx/html/index.html 的 index 文件。 StatefulSet spec 中的 volumeMounts 字段保证了 /usr/share/nginx/html 文件夹由一个 PersistentVolume 卷支持。
将 Pod 的主机名写入它们的 index.html 文件并验证 NginX Web 服务器使用该主机名提供服务:
1
2
3
4
5
6
7
|
for i in 0 1; do kubectl exec "web-$i" -- sh -c 'echo "$(hostname)" > /usr/share/nginx/html/index.html'; done
for i in 0 1; do kubectl exec -i -t "web-$i" -- curl http://localhost/; done
rust@k8s1:~$ for i in 0 1; do kubectl exec "web-$i" -- sh -c 'echo "$(hostname)" > /usr/share/nginx/html/index.html'; done
rust@k8s1:~$ for i in 0 1; do kubectl exec -i -t "web-$i" -- curl http://localhost/; done
web-0
web-1
|
在一个终端监视 StatefulSet 的 Pod,在另一个终端删除Pod,删除后再次验证所有Web 服务器在继续使用它们的主机名提供服务。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
# 监控终端
kubectl get pod -w -l app=nginx
# 操作终端,删除pod后检查pod状态以及提供服务情况
kubectl delete pod web-0 web-1
kubectl get pods
for i in 0 1; do kubectl exec -i -t "web-$i" -- curl http://localhost/; done
rust@k8s1:~$ kubectl delete pod web-0 web-1
pod "web-0" deleted
pod "web-1" deleted
rust@k8s1:~$ kubectl get pods
NAME READY STATUS RESTARTS AGE
web-0 1/1 Running 0 40s
web-1 1/1 Running 0 39s
rust@k8s1:~$ for i in 0 1; do kubectl exec -i -t "web-$i" -- curl http://localhost/; done
web-0
web-1
|
虽然 web-0 和 web-1 被重新调度了,但它们仍然继续监听各自的主机名,因为和它们的 PersistentVolumeClaim 相关联的 PersistentVolume 卷被重新挂载到了各自的 volumeMount 上。 不管 web-0 和 web-1 被调度到了哪个节点上,它们的 PersistentVolume 卷将会被挂载到合适的挂载点上。
扩容/缩容 StatefulSet
扩容/缩容 StatefulSet 指增加或减少它的副本数。这通过更新 replicas 字段完成(水平缩放)。
可以使用 kubectl scale 或者 kubectl patch 来扩容/缩容一个 StatefulSet。
扩容
在一个终端窗口监视 StatefulSet 的 Pod,在另一个终端窗口使用 kubectl scale 扩展副本数为 5。
1
2
3
4
|
# 监控pod状态,当 StatefulSet 有 5 个健康的 Pod 时结束此 watch
kubectl get pods --watch -l app=nginx
kubectl scale sts web --replicas=5
|
StatefulSet 控制器扩展了副本的数量。StatefulSet 按序号索引顺序创建各个 Pod,并且会等待前一个 Pod 变为 Running 和 Ready 才会启动下一个 Pod。
缩容
依旧在一个终端中监控,另一个终端中操作。
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
|
# 监控到有3个pod在运行时就可以停了
kubectl get pods --watch -l app=nginx
# 操作终端,缩容pod副本数为3个
kubectl patch sts web -p '{"spec":{"replicas":3}}'
rust@k8s1:~$ kubectl get pods --watch -l app=nginx
NAME READY STATUS RESTARTS AGE
web-0 1/1 Running 0 17m
web-1 1/1 Running 0 17m
web-2 1/1 Running 0 3m24s
web-3 1/1 Running 0 3m18s
web-4 1/1 Running 0 3m13s
web-4 1/1 Terminating 0 3m17s
web-4 1/1 Terminating 0 3m17s
web-4 0/1 Terminating 0 3m17s
web-4 0/1 Terminating 0 3m17s
web-4 0/1 Terminating 0 3m17s
web-3 1/1 Terminating 0 3m22s
web-3 1/1 Terminating 0 3m22s
web-3 0/1 Terminating 0 3m22s
web-3 0/1 Terminating 0 3m23s
web-3 0/1 Terminating 0 3m23s
rust@k8s1:~$ kubectl patch sts web -p '{"spec":{"replicas":3}}'
statefulset.apps/web patched
rust@k8s1:~$ kubectl get pods
NAME READY STATUS RESTARTS AGE
web-0 1/1 Running 0 18m
web-1 1/1 Running 0 18m
web-2 1/1 Running 0 3m54s
|
控制器会按照与 Pod 序号索引相反的顺序每次删除一个 Pod。在删除下一个 Pod 前会等待上一个被完全关闭。也就是后开启的先关闭(后进先出)。
获取 StatefulSet 的 PersistentVolumeClaims,PVC的生命周期和Pod是隔离的,
即使Pod被关闭或者删除(不管是手动删除还是缩容导致的删除),PVC也不会回收。
1
2
3
4
5
6
7
8
9
|
kubectl get pvc -l app=nginx
rust@k8s1:~$ kubectl get pvc -l app=nginx
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
www-web-0 Bound pvc-29d78e3e-a93b-4db3-880e-3a121d9af83b 1Gi RWO local-path <unset> 18h
www-web-1 Bound pvc-0b32d304-d17d-4f7d-9c38-a8d6fb62a709 1Gi RWO local-path <unset> 18h
www-web-2 Bound pvc-1d4e4d92-8153-4bd2-b1d0-45e3de8b477a 1Gi RWO local-path <unset> 5m46s
www-web-3 Bound pvc-cc082b06-9eec-4831-bc9a-1efa3f6c4eb7 1Gi RWO local-path <unset> 5m40s
www-web-4 Bound pvc-fbad49f8-f1b3-4869-9dba-67e1d2200324 1Gi RWO local-path <unset> 5m35s
|
更新StatefulSet
StatefulSet 控制器支持自动更新。 更新策略由 StatefulSet API 对象的 spec.updateStrategy 字段决定。这个特性能够用来更新一个 StatefulSet中Pod的容器镜像(image)、资源请求和限制(request、limit)、标签和注解(label、annotation)。
有两个有效的更新策略:RollingUpdate(默认)和 OnDelete。
滚动更新
RollingUpdate 更新策略会更新一个 StatefulSet 中的所有 Pod,采用与序号索引相反的顺序并遵循 StatefulSet 的保证。
可以通过指定.spec.updateStrategy.rollingUpdate.partition将使用RollingUpdate策略的StatefulSet的更新拆分为多个分区。
尝试一个简单的滚动更新,更新nginx镜像版本为1.14.2。
1
2
3
4
|
#在另外一个终端监控pod滚动更新
kubectl get pod -l app=nginx --watch
kubectl patch statefulset web --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value":"nginx:1.14.2"}]'
|
StatefulSet 里的 Pod 采用和序号相反的顺序更新。在更新下一个 Pod 前,StatefulSet控制器会终止最新的Pod并等待它们变成 Running 和 Ready。
请注意,虽然在顺序后继者变成 Running 和 Ready 之前 StatefulSet 控制器不会更新下一个 Pod,但它仍然会重建任何在更新过程中发生故障的 Pod,使用的是它们现有的版本。
此外,我在实验中发现假如滚动更新过程中重建pod失败,例如更新后的镜像是一个拉不下来的镜像(k8s官方镜像源的镜像),那么web-2将卡住另外两个pod的更新。这时候我再执行更新命令,更新nginx镜像为可拉取的镜像,web-2并不会执行更新而是一直堵塞在上一次更新,并且导致其他两个pod也不更新(因为最新的Pod web-2还没有完成更新)。这时候删除web-2,StatefulSet控制器会根据当前的StatefulSet配置文件创建web-2,然后再滚动更新web-1和web-0。
查看更新后的镜像,确认滚动更新是否以及完成。
1
2
3
4
5
6
|
for p in 0 1 2; do kubectl get pod "web-$p" --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'; echo; done
rust@k8s1:~$ for p in 0 1 2; do kubectl get pod "web-$p" --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'; echo; done
nginx:1.14.2
nginx:1.14.2
nginx:1.14.2
|
分段更新
指定 .spec.updateStrategy.rollingUpdate.partition 将使用 RollingUpdate 策略的 StatefulSet 的更新拆分为多个分区。
如果声明了一个分区,当 StatefulSet 的 .spec.template 被更新时,所有序号大于等于该分区序号的 Pod 都会被更新。
所有序号小于该分区序号的 Pod 都不会被更新,并且,即使它们被删除也会依据之前的版本进行重建。
如果 StatefulSet 的 .spec.updateStrategy.rollingUpdate.partition 大于它的 .spec.replicas,则对它的 .spec.template 的更新将不会传递到它的 Pod。
分段更新适用于阶段更新、执行金丝雀或执行分阶段上线。
金丝雀发布
金丝雀部署是一种部署策略,开始时有两个环境:一个有实时流量,另一个包含没有实时流量的更新代码。 流量逐渐从应用程序的原始版本转移到更新版本。 它可以从移动 1% 的实时流量开始,然后是 10%,25%,以此类推,直到所有流量都通过更新的版本运行。 企业可以在生产中测试新版本的软件,获得反馈,诊断错误,并在必要时快速回滚到稳定版本。
“金丝雀” 一词是指 “煤矿中的金丝雀” 的做法,即把金丝雀带入煤矿以保证矿工的安全。 如果出现无味的有害气体,鸟就会死亡,而矿工们知道他们必须迅速撤离。 同样,如果更新后的代码出了问题,现场交通就会被 “疏散” 回原来的版本
对 web StatefulSet 执行 Patch 操作,为 updateStrategy 字段添加一个分区,再次 Patch StatefulSet 来改变此 StatefulSet 使用的容器镜像,这次将nginx版本1.14.2->1.16.1。再删除Pod web-2,等待替代的 Pod 变成 Running 和 Ready后查看pod的镜像版本。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"type":"OnDelete", "rollingUpdate": null}}}'
kubectl patch statefulset web --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value":"nginx:1.16.1"}]'
kubectl delete pod web-2
kubectl get pod web-2 --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'
rust@k8s1:~$ kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"type":"OnDelete", "rollingUpdate": null}}}'
statefulset.apps/web patched
rust@k8s1:~$ kubectl patch statefulset web --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value":"nginx:1.16.1"}]'
statefulset.apps/web patched
rust@k8s1:~$ kubectl delete pod web-2
pod "web-2" deleted
rust@k8s1:~$ kubectl get pods
NAME READY STATUS RESTARTS AGE
web-0 1/1 Running 0 25m
web-1 1/1 Running 0 25m
web-2 1/1 Running 0 39s
rust@k8s1:~$ kubectl get pod web-2 --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'
nginx:1.16.1rust@k8s1:~$
|
监控更新的终端里显示只有web-2被更新(终止后重启),web-1和web-0没变化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
rust@k8s1:~$ kubectl get pods --watch -l app=nginx
NAME READY STATUS RESTARTS AGE
web-0 1/1 Running 0 24m
web-1 1/1 Running 0 24m
web-2 1/1 Running 0 24m
web-2 1/1 Terminating 0 24m
web-2 1/1 Terminating 0 24m
web-2 0/1 Terminating 0 24m
web-2 0/1 Terminating 0 24m
web-2 0/1 Terminating 0 24m
web-2 0/1 Pending 0 0s
web-2 0/1 Pending 0 0s
web-2 0/1 ContainerCreating 0 0s
web-2 0/1 ContainerCreating 0 0s
web-2 1/1 Running 0 1s
|
在第一个终端查看各个pod的nginx镜像版本,确实只有web-2更新了。
1
2
3
4
5
6
7
|
for p in 0 1 2; do kubectl get pod "web-$p" --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'; echo; done
rust@k8s1:~$ for p in 0 1 2; do kubectl get pod "web-$p" --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'; echo; done
nginx:1.14.2
nginx:1.14.2
nginx:1.16.1
|
金丝雀发布
现在尝试对分段的变更进行金丝雀发布。
可以通过减少上文指定的 partition 来进行金丝雀发布,以测试修改后的模板。
控制平面会触发 web-2 的替换(先优雅地删除现有 Pod,然后在删除完成后创建一个新的 Pod)。
等待新的 web-2 Pod 变成 Running 和 Ready,再查看容器的镜像版本。
改 template + 大 partition,再把 partition 减小,这样web-2由于分区比partition小,将在当前template将下被重建
1
2
3
4
5
6
7
8
9
|
# 开一个监控终端监控pod变化
kubectl get pods --watch -l app=nginx
# “partition” 的值应与 StatefulSet 现有的最高序号相匹配
kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"type":"RollingUpdate","rollingUpdate":{"partition":2}}}}'
# 再改一次镜像(或其他 template 字段)
kubectl patch statefulset web --type='json' \
-p='[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value":"nginx:1.25"}]'
|
更改template字段后,监控终端将看到web-2这个pod终止后重启,而另外两个pod由于partition比设定的partition小,将不会更新。
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
|
rust@k8s1:~$ for p in 0 1 2; do kubectl get pod "web-$p" --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'; echo; done
nginx:1.14.2
nginx:1.14.2
nginx:1.16.1
rust@k8s1:~$ kubectl patch statefulset web --type='json' \
-p='[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value":"nginx:1.25"}]'
statefulset.apps/web patched
rust@k8s1:~$ for p in 0 1 2; do kubectl get pod "web-$p" --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'; echo; done
nginx:1.14.2
nginx:1.14.2
nginx:1.25
rust@k8s1:~$ kubectl get pods --watch -l app=nginx
NAME READY STATUS RESTARTS AGE
web-0 1/1 Running 0 71m
web-1 1/1 Running 0 21m
web-2 1/1 Running 0 46m
web-2 1/1 Terminating 0 46m
web-2 1/1 Terminating 0 46m
web-2 0/1 Terminating 0 46m
web-2 0/1 Terminating 0 46m
web-2 0/1 Terminating 0 46m
web-2 0/1 Pending 0 0s
web-2 0/1 Pending 0 0s
web-2 0/1 ContainerCreating 0 0s
web-2 0/1 ContainerCreating 0 1s
web-2 1/1 Running 0 1s
|
这时候再删除web-1,由于web-1的Pod的序号小于分区,web-1重建将依据原来的template文件,也即是nginx版本还是1.14.2。
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
|
rust@k8s1:~$ kubectl delete pod web-1
pod "web-1" deleted
rust@k8s1:~$ for p in 0 1 2; do kubectl get pod "web-$p" --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'; echo; done
nginx:1.14.2
nginx:1.14.2
nginx:1.25
rust@k8s1:~$ kubectl get pods --watch -l app=nginx
NAME READY STATUS RESTARTS AGE
web-0 1/1 Running 0 71m
web-1 1/1 Running 0 21m
web-2 1/1 Running 0 46m
web-2 1/1 Terminating 0 46m
web-2 1/1 Terminating 0 46m
web-2 0/1 Terminating 0 46m
web-2 0/1 Terminating 0 46m
web-2 0/1 Terminating 0 46m
web-2 0/1 Pending 0 0s
web-2 0/1 Pending 0 0s
web-2 0/1 ContainerCreating 0 0s
web-2 0/1 ContainerCreating 0 1s
web-2 1/1 Running 0 1s
web-1 1/1 Terminating 0 24m
web-1 1/1 Terminating 0 24m
web-1 0/1 Terminating 0 24m
web-1 0/1 Terminating 0 24m
web-1 0/1 Terminating 0 24m
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 0s
web-1 0/1 ContainerCreating 0 0s
web-1 1/1 Running 0 1s
|
分阶段的发布
可以使用类似金丝雀发布的方法执行一次分阶段的发布 (例如一次线性的、等比的或者指数形式的发布)。
把分区设置为0,则web-1和web-0都将更新。
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
|
kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"type":"RollingUpdate","rollingUpdate":{"partition":0}}}}'
for p in 0 1 2; do kubectl get pod "web-$p" --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'; echo; done
rust@k8s1:~$ kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"type":"RollingUpdate","rollingUpdate":{"partition":0}}}}'
statefulset.apps/web patched
rust@k8s1:~$ for p in 0 1 2; do kubectl get pod "web-$p" --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'; echo; done
nginx:1.25
nginx:1.25
nginx:1.25
rust@k8s1:~$ kubectl get pods --watch -l app=nginx
NAME READY STATUS RESTARTS AGE
web-0 1/1 Running 0 78m
web-1 1/1 Running 0 3m8s
web-2 1/1 Running 0 6m34s
web-1 1/1 Terminating 0 3m12s
web-1 1/1 Terminating 0 3m12s
web-1 0/1 Terminating 0 3m12s
web-1 0/1 Terminating 0 3m12s
web-1 0/1 Terminating 0 3m12s
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 0s
web-1 1/1 Running 0 1s
web-0 1/1 Terminating 0 78m
web-0 1/1 Terminating 0 78m
web-0 0/1 Terminating 0 78m
web-0 0/1 Terminating 0 78m
web-0 0/1 Terminating 0 78m
web-0 0/1 Pending 0 0s
web-0 0/1 Pending 0 0s
web-0 0/1 ContainerCreating 0 0s
web-0 0/1 ContainerCreating 0 0s
web-0 1/1 Running 0 1s
|
OnDelete 策略
将.spec.template.updateStrategy.type设置为OnDelete,以使用OnDelete更新策略。
对 web StatefulSet 执行 patch 操作
1
|
kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"type":"OnDelete", "rollingUpdate": null}}}'
|
当选择这个更新策略并修改 StatefulSet 的 .spec.template 字段时,StatefulSet 控制器将不会自动更新 Pod。
需要自己手动管理发布,或使用单独的自动化工具来管理发布。
删除StatefulSet
StatefulSet 同时支持非级联和级联删除。使用非级联方式删除 StatefulSet 时,StatefulSet 的 Pod 不会被删除。使用级联删除时,StatefulSet 和它的 Pod 都会被删除。
非级联删除
在删除statefulSet时加上参数--cascade=orphan即可非级联删除。非级联删除虽然不会删除Pod,但是也没有StatefulSet管理pod,如果pod被删除或者因为异常停止退出,也不会重启。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
rust@k8s1:~$ kubectl delete statefulset web --cascade=orphan
statefulset.apps "web" deleted
rust@k8s1:~$ kubectl get pods
NAME READY STATUS RESTARTS AGE
web-0 1/1 Running 0 11m
web-1 1/1 Running 0 11m
web-2 1/1 Running 0 18m
rust@k8s1:~$ kubectl delete pod web-0
pod "web-0" deleted
rust@k8s1:~$ kubectl get pods
NAME READY STATUS RESTARTS AGE
web-1 1/1 Running 0 13m
web-2 1/1 Running 0 20m
|
重新应用StatefulSet的yaml文件,因为接下来测试级联删除还需要用(最早的yaml文件里设置的副本数为2,只是扩容缩容后变成了3)。
1
2
3
4
5
6
7
|
rust@k8s1:~/k8s_try$ kubectl apply -f web.yaml
service/nginx unchanged
statefulset.apps/web created
rust@k8s1:~/k8s_try$ kubectl get pods
NAME READY STATUS RESTARTS AGE
web-0 1/1 Running 0 5s
web-1 1/1 Running 0 15m
|
当重新创建 web StatefulSet 时,web-0 被第一个重新启动。 由于 web-1 已经处于 Running 和 Ready 状态,当 web-0 变成 Running 和 Ready 时, StatefulSet 会接收这个 Pod。由于重新创建的 StatefulSet 的 replicas 等于 2, 一旦 web-0 被重新创建并且 web-1 被认为已经处于 Running 和 Ready 状态时,web-2 将会被终止。
由于巧合,web-1的nginx版本被改回了1.25版本,也就是最早的web.yaml里的版本。如果刚才改的版本与web.yaml版本不一致,再查看pod的nginx版本,会发现web-1并没有修改为web.yaml里指定的版本。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
rust@k8s1:~$ kubectl get pods --watch -l app=nginx
web-0 1/1 Terminating 0 13m
web-0 1/1 Terminating 0 13m
web-0 0/1 Terminating 0 13m
web-0 0/1 Terminating 0 13m
web-0 0/1 Terminating 0 13m
web-2 1/1 Running 0 22m
web-1 1/1 Running 0 15m
web-0 0/1 Pending 0 0s
web-0 0/1 Pending 0 0s
web-0 0/1 ContainerCreating 0 0s
web-0 0/1 ContainerCreating 0 0s
web-0 1/1 Running 0 1s
web-2 1/1 Terminating 0 22m
web-2 1/1 Terminating 0 22m
web-2 0/1 Terminating 0 22m
web-2 0/1 Terminating 0 22m
web-2 0/1 Terminating 0 22m
|
现在再看看被 Pod 的 Web 服务器加载的 index.html 的内容:
1
2
3
4
5
|
for i in 0 1; do kubectl exec -i -t "web-$i" -- curl http://localhost/; done
rust@k8s1:~/k8s_try$ for i in 0 1; do kubectl exec -i -t "web-$i" -- curl http://localhost/; done
web-0
web-1
|
尽管同时删除了 StatefulSet 和 web-0 Pod,但它仍然使用最初写入 index.html 文件的主机名进行服务。这是因为StatefulSet不会删除PV。当你重建这个 StatefulSet并且重新启动了 web-0 时,它原本的 PersistentVolume 卷会被重新挂载。
级联删除
默认的删除模式就是级联删除(不加参数)。不过级联删除也只删除pod,不会删除关联的service。这是因为pod是由StatefulSet控制器创建的,所以删除StatefulSet时会级联删除也能级联删除Pod,但Service是由Service控制器创建的,StatefulSet不能删除Service。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
rust@k8s1:~/k8s_try$ kubectl delete statefulset web
statefulset.apps "web" deleted
rust@k8s1:~$ kubectl get pods --watch -l app=nginx
NAME READY STATUS RESTARTS AGE
web-0 1/1 Running 0 7m17s
web-1 1/1 Running 0 22m
web-1 1/1 Terminating 0 27m
web-0 1/1 Terminating 0 12m
web-1 1/1 Terminating 0 27m
web-0 1/1 Terminating 0 12m
web-1 0/1 Terminating 0 27m
web-0 0/1 Terminating 0 12m
web-0 0/1 Terminating 0 12m
web-0 0/1 Terminating 0 12m
web-1 0/1 Terminating 0 27m
web-1 0/1 Terminating 0 27m
|
这时候再重新应用web.yaml,再查看web-0和web-1的index.yaml,会发现挂载的还是原本的PVC,输出的还是更改过的主机名。
(不知是第几次call back了,这也是持久卷为什么叫持久卷。这里就不再演示了)
Pod管理策略
对于某些分布式系统来说,StatefulSet 的顺序性保证是不必要和/或者不应该的。这些系统仅仅要求唯一性和身份标志。
可以指定Pod管理策略以避免这个严格的顺序;OrderedReady(默认)或 Parallel
OrderedReady Pod管理策略
OrderedReady Pod 管理策略是 StatefulSet 的默认选项。它告诉 StatefulSet 控制器遵循上文展示的顺序性保证。
顺序就是前文中创建和删除的顺序。后进先出。严格按序
Parallel Pod管理策略
Parallel Pod管理策略告诉StatefulSet控制器并行的终止所有Pod,在启动或终止另一个 Pod 前,不必等待这些 Pod 变成 Running 和 Ready 或者完全终止状态。
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
39
40
41
42
43
44
45
46
47
48
|
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
ports:
- port: 80
name: web
clusterIP: None
selector:
app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
serviceName: "nginx"
# 关键:指定Pod管理策略为并行
podManagementPolicy: "Parallel"
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
name: web
volumeMounts:
- name: www
mountPath: /usr/share/nginx/html
volumeClaimTemplates:
- metadata:
name: www
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 1Gi
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
# 开一个终端监控pod状态
kubectl get pods --watch -l app=nginx
# 操作终端创建编辑应用yaml文件
vim web-parallel.yaml
kubectl apply -f web-parallel.yaml
rust@k8s1:~$ kubectl get pods --watch -l app=nginx
NAME READY STATUS RESTARTS AGE
web-0 0/1 Pending 0 0s
web-1 0/1 Pending 0 0s
web-0 0/1 Pending 0 0s
web-1 0/1 Pending 0 0s
web-0 0/1 ContainerCreating 0 0s
web-1 0/1 ContainerCreating 0 0s
web-0 0/1 ContainerCreating 0 0s
web-1 0/1 ContainerCreating 0 0s
web-0 1/1 Running 0 1s
web-1 1/1 Running 0 1s
|
清理现场
1
2
3
4
5
6
7
|
# 删除StatefulSet,sts 是 statefulset 的缩写
kubectl delete sts web
# 删除Sevice
kubectl delete svc nginx
# 删除PVC。
kubectl delete pvc www-web-0 www-web-1 www-web-2 www-web-3 www-web-4
|
由于动态制备的 PV 默认回收策略是Delete(reclaimPolicy: Delete),所以删除PVC之后绑定的PV也会自动删除。
如果是Retain 策略则PV 变 Released,保留数据
而直接删PV不会立刻成功,PV会进入 Terminating 状态,finalizer里会有kubernetes.io/pv-protection;要等到PV不再被任何PVC绑定,才会真正删除