K8s实践练习7_有状态应用

官网链接

本教程将使用StatefulSet部署一个简单的 Web 应用。

  1. 创建 StatefulSet
  2. 了解StatefulSet怎样管理它的 Pod
  3. 删除 StatefulSet
  4. 对StatefulSet 进行扩容/缩容
  5. 更新一个StatefulSet的Pod

前置准备

搭建好一套k8s集群,可以参考我写的这篇教程:搭建k8s集群

k8s官方的镜像站在国内是拉不下来的,有几种方法解决:

  1. 在拉取镜像的虚拟机/服务器上科学上网
  2. 配置k8s的镜像源,目前国内只有阿里云支持改版后的k8s镜像源(registry.k8s.io)。
  3. 需要拉取镜像的时候,指定拉取策略为本地拉取(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.ioregistry.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查看集群存储类。有的话可以跳过此节

  1. 在三个节点上创建存储基础目录(xcall是自建的脚本)
1
2
xcall sudo mkdir -p /opt/local-path-provisioner
xcall sudo chmod 755 /opt/local-path-provisioner

以后想换路径可以修改local-path-config这个ConfigMap里的nodePathMap配置

  1. 安装 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
  1. 确认安装成功,检查 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
  1. 把 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记录

  1. 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 记录

  1. 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 可能变,但主机名对应关系不变。
  1. 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绑定,才会真正删除

网站总访客数:Loading
网站总访问量:Loading
使用 Hugo 构建
主题 StackJimmy 设计