2.15 StatefulSet #
2.15.1 状态和应用 #
理论上任何应用都是有状态的,只是有的应用的状态信息不是很重要,即使不恢复状态也能够正常运行,这就是 “无状态应用”。“无状态应用” 典型的例子就是 Nginx 这样的 Web 服务器,它只是处理 HTTP 请求,本身不生产数据(日志除外),不需要特意保存状态,无论以什么状态重启都能很好地对外提供服务。
还有一些应用,运行状态信息很重要,如果因为重启而丢失了状态是绝对无法接受的,这样的应用是 “有状态应用”。比如 Redis、MySQL 这样的数据库,它们的 “状态” 就是在内存或者磁盘上产生的数据,是应用的核心价值所在,如果不能够把这些数据及时保存再恢复,那绝对会是灾难性的后果。
对于 Deployment 来说,多个实例之间是无关的,启动的顺序不固定,Pod 的名字、IP 地址、域名也都是完全随机的,这正是 “无状态应用” 的特点。对于 “有状态应用”,多个实例之间可能存在依赖关系,比如 master/slave、active/passive,需要依次启动才能保证应用正常运行,外界的客户端也可能要使用固定的网络标识来访问实例,而且这些信息还必须要保证在 Pod 重启后不变。
Kubernetes 定义了一个新的 API 对象 StatefulSet,专门用来管理有状态的应用。
2.15.2 描述 StatefulSet #
StatefulSet 也可以看做是 Deployment 的一个特例,它不能直接用 kubectl create 创建样板文件,它的对象描述和 Deployment 差不多,可以把 Deployment 适当修改一下,就变成了 StatefulSet 对象。以下是一个使用 Redis 的 StatefulSet 描述文件:
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。
2.15.3 使用 StatefulSet #
当使用 apply 创建 StatefulSet 对象,可以看到:

StatefulSet 所管理的 Pod 不再是随机的名字了,而是有了顺序编号,从 0 开始分别被命名为 redis-sts-0、redis-sts-1,Kubernetes 也会按照这个顺序依次创建,这其实就解决了 “有状态应用” 的第一个问题:启动顺序。
当有了启动的先后顺序,应用该怎么确定互相之间的依赖关系呢?
Kubernetes 给出的方法是使用 hostname,也就是每个 Pod 里的主机名,可以使用 kubectl exec 命令登录 Pod 内部看看:

在 Pod 里查看环境变量 $HOSTNAME 或者是执行命令 hostname,都可以得到这个 Pod 的名字 redis-sts-0。有了这个唯一的名字,应用就可以自行决定依赖关系了,比如在这个 Redis 例子里,就可以让先启动的 0 号 Pod 是主实例,后启动的 1 号 Pod 是从实例。
解决了启动顺序和依赖关系,网络标识就需要用到 Service 对象。不能用命令 kubectl expose 直接为 StatefulSet 生成 Service,只能手动编写 YAML。
在写 Service 对象的时候要注意,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 自己会有一个域名,格式是 对象名.名字空间
,每个 Pod 也会有一个域名,形式是 IP 地址.名字空间
。但因为 IP 地址不稳定,所以 Pod 的域名并不实用,一般使用稳定的 Service 域名。
当把 Service 对象应用于 StatefulSet 的时候,情况会发生变化。Service 发现这些 Pod 不是一般的应用,而是有状态应用,需要有稳定的网络标识,所以就会为 Pod 再多创建出一个新的域名,格式是 Pod 名.服务名.名字空间.svc.cluster.local
。这个域名也可以简写成 Pod 名.服务名
。
可以使用 kubectl exec 进入 Pod 内部,用 ping 命令来验证一下:

在 StatefulSet 里的两个 Pod 都有各自的域名,也就是稳定的网络标识。外部的客户端只要知道了 StatefulSet 对象,就可以用固定的编号去访问某个具体的实例,虽然 Pod 的 IP 地址可能会变,但这个有编号的域名由 Service 对象维护,是稳定不变的。
Service 原本的目的是负载均衡,应该由它在 Pod 前面来转发流量,但是对 StatefulSet 来说,这项功能反而是不必要的,因为 Pod 已经有了稳定的域名,外界访问服务就不应该再通过 Service 这一层了。从安全和节约系统资源的角度考虑,可以在 Service 里添加一个字段 clusterIP: None ,告诉 Kubernetes 不必再为这个对象分配 IP 地址。
apiVersion: v1
kind: Service
metadata:
name: redis-svc
spec:
clusterIP: None
selector:
app: redis-sts
ports:
- port: 6379
protocol: TCP
targetPort: 6379
使用了 “clusterlP:None”,没有集群 IP 地址的 Service 对象,也被称为是 “Headless Service”。

下面这张图展示了 StatefulSet 与 Service 对象的关系:

2.15.4 数据持久化 #
为了强调持久化存储与 StatefulSet 的一对一绑定关系,Kubernetes 为 StatefulSet 专门定义了一个字段 “volumeClaimTemplates”,直接把 PVC 定义嵌入 StatefulSet 的 YAML 文件里。这样能保证创建 StatefulSet 的同时,就会为每个 Pod 自动创建 PVC,让 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 的数据目录。

apply 创建后,一个带持久化功能的 “有状态应用” 就算是运行起来了。可以使用命令 kubectl get pvc 来查看 StatefulSet 关联的存储卷状态:

这两个 PVC 的命名,是有规律的,用的是 PVC 名字加上 StatefulSet 的名字组合而成,所以即使 Pod 被销毁,因为它的名字不变,还能够找到这个 PVC,再次绑定使用之前存储的数据。
用 kubectl exec 运行 Redis 的客户端,在里面添加一些 KV 数据:
kubectl exec -it redis-pv-sts-0 -- redis-cli

模拟意外事故,删除这个 Pod:
kubectl delete pod redis-pv-sts-0
由于 StatefulSet 和 Deployment 一样会监控 Pod 的实例,发现 Pod 数量少了就会很快创建出新的 Pod,并且名字、网络标识也都会和之前的 Pod 一模一样:

可以再用 Redis 客户端登录去检查一下数据是否存在:
kubectl exec -it redis-pv-sts-0 -- redis-cli

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