更新时间:2024-12-11 GMT+08:00

有状态负载(StatefulSet)

有状态负载(StatefulSet)

Deployment控制器下的Pod都有个共同特点,那就是每个Pod除了名称和IP地址不同,其余完全相同。需要的时候,Deployment可以通过Pod模板创建新的Pod;不需要的时候,Deployment就可以删除任意一个Pod。

但是在某些场景下,这并不满足需求,比如有些分布式的场景,要求每个Pod都有自己单独的状态时,比如分布式数据库,每个Pod要求有单独的存储,这时Deployment无法满足业务需求。

分布式有状态应用的特点主要是应用中每个部分的角色不同(即分工不同),比如数据库有主备、Pod之间有依赖,在Kubernetes中部署有状态应用对Pod有如下要求:

  • Pod能够被别的Pod找到,要求Pod有固定的标识。
  • 每个Pod有单独存储,Pod被删除恢复后,必须读取原来的数据,否则状态就会不一致。

Kubernetes提供了StatefulSet来解决这个问题,其具体如下:

  1. StatefulSet给每个Pod提供固定名称,Pod名称增加从0-N的固定后缀,Pod重新调度后Pod名称和HostName不变。
  2. StatefulSet通过Headless Service给每个Pod提供固定的访问域名。
  3. StatefulSet通过创建固定标识的PVC保证Pod重新调度后还是能访问到相同的持久化数据。
    图1 StatefulSet

创建Headless Service

如前所述,创建Statefulset需要一个Headless Service用于Pod访问。

使用如下文件描述Headless Service,其中:

  • spec.clusterIP:必须设置为None,表示Headless Service。
  • spec.ports.port:Pod间通信端口号。
  • spec.ports.name:Pod间通信端口名称。
apiVersion: v1
kind: Service       # 对象类型为Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
    - name: nginx     # Pod间通信的端口名称
      port: 80        # Pod间通信的端口号
  selector:
    app: nginx        # 选择标签为app:nginx的Pod
  clusterIP: None     # 必须设置为None,表示Headless Service

执行如下命令创建Headless Service。

# kubectl create -f headless.yaml 
service/nginx created

创建完成后可以查询Service。

# kubectl get svc
NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
nginx        ClusterIP   None         <none>        80/TCP    5s

创建Statefulset

Statefulset的YAML定义与其他对象基本相同,主要有两个差异点:

  • serviceName指定了Statefulset使用哪个Headless Service,需要填写Headless Service的名称。
  • volumeClaimTemplates是用来申请持久化声明PVC ,这里定义了一个名为data的模板,它会为每个Pod创建一个PVC,storageClassName指定了持久化存储的类型,在PV、PVC和StorageClass会详细介绍;volumeMounts是为Pod挂载存储。当然如果不需要存储的话可以删除volumeClaimTemplates和volumeMounts字段。
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: nginx
spec:
  serviceName: nginx                             # headless service的名称
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: container-0
          image: nginx:alpine
          resources:
            limits:
              cpu: 100m
              memory: 200Mi
            requests:
              cpu: 100m
              memory: 200Mi
          volumeMounts:                           # Pod挂载的存储
          - name:  data
            mountPath:  /usr/share/nginx/html     # 存储挂载到/usr/share/nginx/html
      imagePullSecrets:
        - name: default-secret
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes:
      - ReadWriteMany
      resources:
        requests:
          storage: 1Gi
      storageClassName: csi-nas                   # 持久化存储的类型

执行如下命令创建。

# kubectl create -f statefulset.yaml 
statefulset.apps/nginx created

命令执行后,查询一下StatefulSet和Pod,可以看到Pod的名称后缀从0开始到2,逐个递增。

# kubectl get statefulset
NAME    READY   AGE
nginx   3/3     107s

# kubectl get pods
NAME      READY   STATUS    RESTARTS   AGE
nginx-0   1/1     Running   0          112s
nginx-1   1/1     Running   0          69s
nginx-2   1/1     Running   0          39s

此时如果手动删除nginx-1这个Pod,然后再次查询Pod,可以看到StatefulSet重新创建了一个名称相同的Pod,通过创建时间5s可以看出nginx-1是刚刚创建的。

# kubectl delete pod nginx-1
pod "nginx-1" deleted

# kubectl get pods
NAME      READY   STATUS    RESTARTS   AGE
nginx-0   1/1     Running   0          3m4s
nginx-1   1/1     Running   0          5s
nginx-2   1/1     Running   0          1m10s

进入容器查看容器的hostname,可以看到同样是nginx-0、nginx-1和nginx-2。

# kubectl exec nginx-0 -- sh -c 'hostname'
nginx-0
# kubectl exec nginx-1 -- sh -c 'hostname'
nginx-1
# kubectl exec nginx-2 -- sh -c 'hostname'
nginx-2

同时可以看一下StatefulSet创建的PVC,可以看到这些PVC,都以“PVC名称-StatefulSet名称-编号”的方式命名,并且处于Bound状态。

# kubectl get pvc
NAME           STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
data-nginx-0   Bound    pvc-f58bc1a9-6a52-4664-a587-a9a1c904ba29   1Gi        RWX            csi-nas        2m24s
data-nginx-1   Bound    pvc-066e3a3a-fd65-4e65-87cd-6c3fd0ae6485   1Gi        RWX            csi-nas        101s
data-nginx-2   Bound    pvc-a18cf1ce-708b-4e94-af83-766007250b0c   1Gi        RWX            csi-nas        71s

StatefulSet的网络标识

StatefulSet创建后,可以看下Pod是有固定名称的,那Headless Service是如何起作用的呢,那就是使用DNS,为Pod提供固定的域名,这样Pod间就可以使用域名访问,即便Pod被重新创建而导致Pod的IP地址发生变化,这个域名也不会发生变化。

Headless Service创建后,每个Pod的IP都会有下面格式的域名。

<pod-name>.<svc-name>.<namespace>.svc.cluster.local

例如上面的三个Pod的域名就是:

  • nginx-0.nginx.default.svc.cluster.local
  • nginx-1.nginx.default.svc.cluster.local
  • nginx-2.nginx.default.svc.cluster.local

实际访问时可以省略后面的.<namespace>.svc.cluster.local

下面命令会使用tutum/dnsutils镜像创建一个Pod,进入这个Pod的容器,使用nslookup命令查看Pod对应的域名,可以发现能解析出Pod的IP地址。这里可以看到DNS服务器的地址是10.247.3.10,这是在创建CCE集群时默认安装CoreDNS插件,用于提供DNS服务,后续在Kubernetes网络会详细介绍CoreDNS的作用。

$ kubectl run -i --tty --image tutum/dnsutils dnsutils --restart=Never --rm /bin/sh 
If you don't see a command prompt, try pressing enter.
/ # nslookup nginx-0.nginx
Server:         10.247.3.10
Address:        10.247.3.10#53
Name:   nginx-0.nginx.default.svc.cluster.local
Address: 172.16.0.31

/ # nslookup nginx-1.nginx
Server:         10.247.3.10
Address:        10.247.3.10#53
Name:   nginx-1.nginx.default.svc.cluster.local
Address: 172.16.0.18

/ # nslookup nginx-2.nginx
Server:         10.247.3.10
Address:        10.247.3.10#53
Name:   nginx-2.nginx.default.svc.cluster.local
Address: 172.16.0.19

此时如果手动删除这两个Pod,查询被StatefulSet重新创建的Pod的IP,然后使用nslookup命令解析Pod的域名,可以发现nginx-0.nginx和nginx-1.nginx仍然能解析到对应的Pod。这就保证了StatefulSet网络标识不变。

StatefulSet存储状态

上面说了StatefulSet可以通过PVC做持久化存储,保证Pod重新调度后还是能访问到相同的持久化数据,在删除Pod时,PVC不会被删除。

图2 StatefulSet的Pod重建过程

下面将通过实际操作验证这一点是如何做到的,执行下面的命令,在nginx-1的目录/usr/share/nginx/html中写入一些内容,例如将index.html的内容修改为“hello world”

# kubectl exec nginx-1 -- sh -c 'echo hello world > /usr/share/nginx/html/index.html'

修改完后,如果在Pod中访问“http://localhost”,那就会返回“hello world”

# kubectl exec -it nginx-1 -- curl localhost
hello world

此时如果手动删除nginx-1这个Pod,然后再次查询Pod,可以看到StatefulSet重新创建了一个名称相同的Pod,通过创建时间4s可以看出nginx-1是刚刚创建的。

# kubectl delete pod nginx-1
pod "nginx-1" deleted

# kubectl get pods
NAME       READY   STATUS    RESTARTS   AGE
nginx-0    1/1     Running   0          14m
nginx-1    1/1     Running   0          4s
nginx-2    1/1     Running   0          13m

再次访问该Pod的index.html页面,会发现仍然返回“hello world”,这说明这个Pod仍然是访问相同的存储。

# kubectl exec -it nginx-1 -- curl localhost
hello world