x文章目录

  • 什么是有状态的应用
  • 一、使用 YAML 描述 StatefulSet
  • 二、在 Kubernetes 里使用 StatefulSet
  • 三、实现 StatefulSet 的数据持久化
  • 总结


什么是有状态的应用

先从 PersistentVolume 谈起,它为 Kubernetes 带来了持久化存储的功能,能够让应用把数据存放在本地或者远程的磁盘上。
有了持久化存储,应用就可以把一些运行时的关键数据落盘,相当于有了一份保险,如果 Pod 发生意外崩溃,也只不过像是按下了暂停键,等重启后挂载 Volume,再加载原数据就能够满血复活,恢复之前的状态继续运行。

这里有一个关键词——状态,应用保存的数据,实际上就是它某个时刻的运行状态。运行状态信息就很重要了,如果因为重启而丢失了状态是绝对无法接受的,这样的应用就是有状态应用

无状态应用典型的例子就是 Nginx 这样的 Web 服务器,它只是处理 HTTP 请求,本身不生产数据(日志除外),不需要特意保存状态,无论以什么状态重启都能很好地对外提供服务。

有状态与无状态的特点:

  • 有状态应用:多个实例之间可能存在依赖关系,比如 master/slave、active/passive,需要依次启动才能保证应用正常运行,外界的客户端也可能要使用固定的网络标识来访问实例,而且这些信息还必须要保证在 Pod 重启后不变。
  • 无状态应用:多个实例之间是无关的,启动的顺序不固定,Pod 的名字、IP 地址、域名也都是完全随机的,这正是“无状态应用”的特点。

所以,Kubernetes 就在 Deployment 的基础之上定义了一个新的 API 对象,名字也很好理解,就叫StatefulSet,专门用来管理有状态的应用。


提示:以下是本篇文章正文内容,下面案例可供参考

一、使用 YAML 描述 StatefulSet

首先我们还是用命令 kubectl api-resources 来查看 StatefulSet 的基本信息,可以知道它的简称是 sts,YAML 文件头信息是:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: xxx-sts

这里给出 Redis 的 StatefulSet,来看看它与 Deployment 有什么差异:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis-sts

spec:
  serviceName: redis-svc
  replicas: 2
  selector:
    matchLabels:
      app: redis-sts

  template:
    metadata:
      labels:
        app: redis-sts
    spec:
      containers:
      - image: redis:5-alpine
        name: redis
        ports:
        - containerPort: 6379

YAML 文件里除了 kind 必须是StatefulSet,在 spec 里还多出了一个serviceName字段,其余的部分和 Deployment 是一模一样的,比如 replicas、selector、template 等等。

二、在 Kubernetes 里使用 StatefulSet

我们用 kubectl apply 创建 StatefulSet 对象,用 kubectl get 先看看它是什么样的:

kubectl apply -f redis-sts.yml
kubectl get sts
kubectl get pod
[root@k8s-console statefulset]# kubectl apply -f redis-sts.yaml 
statefulset.apps/redis-sts created
[root@k8s-console statefulset]# kubectl get sts
NAME        READY   AGE
redis-sts   2/2     5s

[root@k8s-console statefulset]# kubectl get pods
NAME                           READY   STATUS      RESTARTS        AGE
redis-sts-0                    1/1     Running     0               15s
redis-sts-1                    1/1     Running     0               14s

你应该能够看到,StatefulSet 所管理的 Pod 不再是随机的名字了,而是有了顺序编号,从 0开始分别被命名为 redis-sts-0、redis-sts-1,Kubernetes 也会按照这个顺序依次创建(0 号比 1 号的 AGE 要长一点),这就解决了有状态应用的第一个问题:启动顺序

有了启动的先后顺序,应用该怎么知道自己的身份,进而确定互相之间的依赖关系呢?

Kubernetes 给出的方法是使用 hostname,也就是每个 Pod 里的主机名,让我们再用 kubectl exec 登录 Pod 内部看看:

kubectl exec -it redis-sts-0 -- sh

k8s有状态mysql k8s有状态应用重启_docker


在 Pod 里查看环境变量 $HOSTNAME 或者是执行命令 hostname,都可以得到这个 Pod 的名字 redis-sts-0

有了这个唯一的名字,应用就可以自行决定依赖关系了,比如在这个 Redis 例子里,就可以让先启动的 0 号 Pod 是主实例,后启动的 1 号 Pod 是从实例。

解决了启动顺序和依赖关系,还剩下第三个问题网络标识,这就需要用到 Service 对象。

我们不能用命令 kubectl expose 直接为 StatefulSet 生成 Service,只能手动编写 YAML

注意:metadata.name 必须和 StatefulSet 里的 serviceName 相同,selector 里的标签也必须和 StatefulSet 里的一致:

apiVersion: v1
kind: Service
metadata:
  name: redis-svc

spec:
  selector:
    app: redis-sts

  ports:
  - port: 6379
    protocol: TCP
    targetPort: 6379

写好 Service 之后,还是用 kubectl apply 创建这个对象:

kubectl apply -f redis-svc.yaml

k8s有状态mysql k8s有状态应用重启_docker_02


StatefulSet 奥秘就在它的域名上。

还记得在【k8s】Service微服务架构的应对之道(十)里我们说过的 Service 的域名用法吗?Service 自己会有一个域名,格式是对象名. 名字空间,每个 Pod 也会有一个域名,形式是IP 地址. 名字空间。但因为 IP 地址不稳定,所以 Pod 的域名并不实用,一般我们会使用稳定的 Service 域名。

当我们把 Service 对象应用于 StatefulSet 的时候,情况就不一样了。

Service 发现这些 Pod 不是一般的应用,而是有状态应用,需要有稳定的网络标识,所以就会为 Pod 再多创建出一个新的域名,格式是Pod 名. 服务名. 名字空间.svc.cluster.local。当然,这个域名也可以简写成Pod 名. 服务名

我们用 kubectl exec 进入 Pod 内部,用 ping 命令来验证一下:

kubectl exec -it redis-sts-0 -- sh

k8s有状态mysql k8s有状态应用重启_java_03

StatefulSet 里的这两个 Pod 都有了各自的域名,也就是稳定的网络标识。那么接下来,外部的客户端只要知道了 StatefulSet 对象,就可以用固定的编号去访问某个具体的实例了,虽然 Pod 的 IP 地址可能会变,但这个有编号的域名由 Service 对象维护,是稳定不变的。到这里,通过 StatefulSet 和 Service 的联合使用,Kubernetes 就解决了有状态应用的依赖关系、启动顺序和网络标识这三个问题

注意: Service 原本的目的是负载均衡,应该由它在 Pod 前面来转发流量,但是对 StatefulSet 来说,这项功能反而是不必要的,因为 Pod 已经有了稳定的域名,外界访问服务就不应该再通过 Service 这一层了。所以,从安全和节约系统资源的角度考虑,我们可以在 Service 里添加一个字段 clusterIP: None ,告诉 Kubernetes 不必再为这个对象分配 IP 地址。

一张图展示 StatefulSet 与 Service 对象的关系,你可以参考一下它们字段之间的互相引用:

k8s有状态mysql k8s有状态应用重启_docker_04

三、实现 StatefulSet 的数据持久化

为了强调持久化存储与 StatefulSet 的一对一绑定关系,Kubernetes 为 StatefulSet 专门定义了一个字段volumeClaimTemplates,直接把 PVC 定义嵌入 StatefulSet 的 YAML 文件里。这样能保证创建 StatefulSet 的同时,就会为每个 Pod 自动创建 PVC,让 StatefulSet 的可用性更高。

volumeClaimTemplates这个字段好像有点难以理解,你可以把它和 Pod 的 template、Job 的 jobTemplate 对比起来学习,它其实也是一个套娃的对象组合结构,里面就是应用了 StorageClass 的普通 PVC 而已。

把刚才的 Redis StatefulSet 对象稍微改造一下,加上持久化存储功能

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis-pv-sts

spec:
  serviceName: redis-pv-svc

  volumeClaimTemplates:
  - metadata:
      name: redis-100m-pvc
    spec:
      storageClassName: nfs-client
      accessModes:
        - ReadWriteMany
      resources:
        requests:
          storage: 100Mi

  replicas: 2
  selector:
    matchLabels:
      app: redis-pv-sts

  template:
    metadata:
      labels:
        app: redis-pv-sts
    spec:
      containers:
      - image: redis:5-alpine
        name: redis
        ports:
        - containerPort: 6379

        volumeMounts:
        - name: redis-100m-pvc
          mountPath: /data

首先 StatefulSet 对象的名字是 redis-pv-sts,表示它使用了 PV 存储。然后volumeClaimTemplates里定义了一个 PVC,名字是 redis-100m-pvc,申请了 100MB 的 NFS 存储。在 Pod 模板里用 volumeMounts 引用了这个 PVC,把网盘挂载到了 /data 目录,也就是 Redis 的数据目录。

下面的这张图就是这个 StatefulSet 对象完整的关系图:

k8s有状态mysql k8s有状态应用重启_java_05

最后使用 kubectl apply 创建这些对象,一个带持久化功能的有状态应用就算是运行起来了:

[root@k8s-console statefulset]# kubectl apply -f redis-pv-sts.yaml 
statefulset.apps/redis-pv-sts created

使用命令 kubectl get pvc 来查看 StatefulSet 关联的存储卷状态:

[root@k8s-console statefulset]# kubectl get pvc
NAME                            STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
redis-100m-pvc-redis-pv-sts-0   Bound    pvc-fb5b203f-f199-4ad4-a954-e20f5ba1b80d   100Mi      RWX            nfs-client     1m
redis-100m-pvc-redis-pv-sts-1   Bound    pvc-c6b818a3-8058-4505-a197-910098ce85c8   100Mi      RWX            nfs-client     1m

看这两个 PVC 的命名,不是随机的,是有规律的,用的是 PVC 名字加上 StatefulSet 的名字组合而成,所以即使 Pod 被销毁,因为它的名字不变,还能够找到这个 PVC,再次绑定使用之前存储的数据。

我们就来实地验证一下,用 kubectl exec 运行 Redis 的客户端,在里面添加一些 KV 数据:

kubectl exec -it redis-pv-sts-0 -- redis-cli
[root@k8s-console statefulset]# kubectl exec -it redis-pv-sts-0 -- redis-cli
127.0.0.1:6379> set a 111
OK
127.0.0.1:6379> set b 222
OK
127.0.0.1:6379> keys *
1) "b"
2) "a"
127.0.0.1:6379>

这里我设置了两个值,分别是 a=111 和 b=222。

现在我们模拟意外事故,删除这个 Pod:

[root@k8s-console statefulset]# kubectl delete pod redis-pv-sts-0
pod "redis-pv-sts-0" deleted

由于 StatefulSet 和 Deployment 一样会监控 Pod 的实例,发现 Pod 数量少了就会很快创建出新的 Pod,并且名字、网络标识也都会和之前的 Pod 一模一样:

[root@k8s-console statefulset]# kubectl get pods
NAME                           READY   STATUS             RESTARTS        AGE
redis-pv-sts-0                 1/1     Running            0               39s
redis-pv-sts-1                 1/1     Running            0               4m33s

再用 Redis 客户端登录去检查一下:

[root@k8s-console statefulset]# kubectl exec -it redis-pv-sts-0 -- redis-cli
127.0.0.1:6379> get a
"111"
127.0.0.1:6379> get b
"222"
127.0.0.1:6379> keys *
1) "a"
2) "b"

因为我们把 NFS 网络存储挂载到了 Pod 的 /data 目录,Redis 就会定期把数据落盘保存,所以新创建的 Pod 再次挂载目录的时候会从备份文件里恢复数据,内存里的数据就恢复原状了。

总结

  1. StatefulSet 的 YAML 描述和 Deployment 几乎完全相同,只是多了一个关键字段 serviceName。
  2. 要为 StatefulSet 里的 Pod 生成稳定的域名,需要定义 Service 对象,它的名字必须和 StatefulSet 里的 serviceName 一致。
  3. 访问 StatefulSet 应该使用每个 Pod 的单独域名,形式是“Pod 名. 服务名”,不应该使用 Service 的负载均衡功能。
  4. 在 StatefulSet 里可以用字段volumeClaimTemplates直接定义 PVC,让 Pod 实现数据持久化存储。