一. 简介
实例之间有不对等关系,以及实例对外部数据有依赖关系的应用,就被称为“有状态应用”(Stateful Application)
。
Kubernetes的StatefulSet组件就是为了解决如下两种情况:
- 拓扑状态
应用的多个实例之间存在不是完全对等的关系。这些应用实例,必须按照某些顺序启动,比如应用的主节点 A 要先于从节点 B 启动。而如果把 A 和 B 两个 Pod 删除掉,它们再次被创建出来时也必须严格按照这个顺序才行。并且,新创建出来的 Pod,必须和原来 Pod 的网络标识一样,这样原先的访问者才能使用同样的方法,访问到这个新 Pod。 - 存储状态
应用的多个实例分别绑定了不同的存储数据。对于这些应用实例来说,Pod A 第一次读取到的数据,和隔了十分钟之后再次读取到的数据,应该是同一份,哪怕在此期间 Pod A 被重新创建过。
StatefulSet 的核心功能,就是通过某种方式记录这些状态,然后在 Pod 被重新创建时,能够为新 Pod 恢复这些状态。
关于本文的项目的代码,都放于链接:GitHub资源
二. 拓扑状态
通过Headless Service
的方式,StatefulSet 为每个 Pod 创建了一个固定并且稳定的 DNS 记录,来作为它的访问入口。
2.1 Service
Service 类型一般有如下的三种:
- VIP(Virtual IP,即:虚拟 IP)
Service提供一个 VIP,我们访问这个IP地址时,它会把请求转发到该 Service 所代理的某一个 Pod 上。
- DNS (Normal Service)
这种情况下,访问“my-svc.my-namespace.svc.cluster.local”解析到的,正是 my-svc 这个 Service 的 VIP,后面的流程就跟 VIP 方式一致。
- DNS (Headless Service)
这种情况下,访问“my-svc.my-namespace.svc.cluster.local”解析到的,直接就是 my-svc 代理的某一个 Pod 的 IP 地址。所以,Headless Service 不需要分配一个 VIP,而是可以直接以 DNS 记录的方式解析出被代理 Pod 的 IP 地址。
2.2 Headless Service
2.2.1 案例
参考如下的demo-service.yaml
案例:
apiVersion: v1
kind: Service
metadata:
name: demo-service
labels:
app: nginx
spec:
ports:
- port: 80
name: web
clusterIP: None
selector:
app: demo-nginx
创建成功后可以查看到如下的内容:
2.2.2 clusterIP
关于于上面的这行配置clusterIP: None
。
clusterIP 是服务的IP地址,通常由主服务器随机分配还具有如下特点:
- 如果地址是手动指定的,并且未被其他人使用,则该地址将分配给服务;否则,服务创建将失败
- 无法通过更新更改此字段。
- 有效值为“ None”,空字符串(“”)或有效IP地址。
- 当不需要代理时,可以为无头服务指定“无”。
- 仅适用于ClusterIP,NodePort和LoadBalancer类型。
- 如果type为ExternalName,则忽略。
Headless Service
是一个标准 Service 的 YAML 文件。只不过,它的 clusterIP 字段的值是:None。这个 Service 被创建后并不会被分配一个 VIP,而是会以 DNS 记录的方式暴露出它所代理的 Pod。
2.2.3 DNS 记录
当按照这样的方式创建了一个Headless Service
之后,它所代理的所有 Pod 的 IP 地址,都会被绑定一个这样格式的 DNS 记录,如下所示:
<pod-name>.<svc-name>.<namespace>.svc.cluster.local
这个 DNS 记录,正是 Kubernetes 项目为 Pod 分配的唯一的“可解析身份”(Resolvable Identity)
。有了这个“可解析身份”,只需要知道了一个 Pod 的名字,以及它对应的 Service 的名字,就可以非常确定地通过这条 DNS 记录访问到 Pod 的 IP 地址。
2.3 案例
2.3.1 创建Statefulset
执行创建,demo-statefulset.yaml内容如下
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: demo-statefulset
spec:
serviceName: “demo-service”
replicas: 3
selector:
matchLabels:
app: demo-nginx
template:
metadata:
labels:
app: demo-nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80
name: nginx
这个 YAML 文件,和我们在前面文章中用到的 nginx-deployment 的唯一区别,就是多了一个 serviceName=nginx 字段。这个字段的作用,就是告诉 StatefulSet 控制器,在执行控制循环(Control Loop)的时候,请使用 nginx 这个 Headless Service 来保证 Pod 的“可解析身份”。
创建成功后,可以看到如下的状态:
2.3.2 检查状态
执行如下的指令,检查Pods相关状态
kubectl get pods -w -l app=demo-nginx
# result
NAME READY STATUS RESTARTS AGE
demo-statefulset-0 1/1 Running 0 5m17s
demo-statefulset-1 1/1 Running 0 5m16s
demo-statefulset-2 1/1 Running 0 5m14s
StatefulSet 给它所管理的所有 Pod 的名字,进行了编号。
编号规则是:statefulset.name +-+ index
。这些 Pod 的创建,也是严格按照编号顺序进行的。而且这些编号都是从 0 开始累加,与 StatefulSet 的每个 Pod 实例一一对应,绝不重复。
2.3.3 检查hostname
可以看到,这两个 Pod 的 hostname 与 Pod 名字是一致的,都被分配了对应的编号。
使用 kubectl exec 命令进入到容器中查看它们的 hostname:
kubectl exec demo-statefulset-0 -- sh -c 'hostname'
kubectl exec demo-statefulset-1 -- sh -c 'hostname'
kubectl exec demo-statefulset-2 -- sh -c 'hostname'
我们将看到如下结果:
2.3.4 检查DNS
我们启动了一个一次性的 Pod,因为–rm 意味着 Pod 退出后就会被删除掉,创建并进入Pod:
kubectl run -i --tty --image busybox:1.28.4 dns-test --restart=Never --rm /bin/sh
进入这个 Pod 的容器里面,尝试用 nslookup
命令,解析一下 Pod 对应的Headless Service
:
nslookup demo-service
# result
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: demo-service
Address 1: 10.1.3.77 demo-statefulset-1.demo-service.default.svc.cluster.local
Address 2: 10.1.3.76 demo-statefulset-0.demo-service.default.svc.cluster.local
Address 3: 10.1.3.78 demo-statefulset-2.demo-service.default.svc.cluster.local
实际执行结果可以看到如下内容:
2.4 滚动更新
滚动更新时,把这几个 Pod 删除之后,Kubernetes 会按照原先编号的顺序,创建出了两个新的 Pod。并且,Kubernetes 依然为它们分配了与原来相同的“网络身份”:demo-statefulset-0 ,demo-statefulset-1 和demo-statefulset-2。通过这种严格的对应规则,StatefulSet 就保证了 Pod 网络标识的稳定性。
可以理解为:Podname
就是唯一主键。
2.5 小结
Kubernetes 成功地将 Pod 的拓扑状态,按照 Pod 的“名字 + 编号”的方式固定了下来。此外,Kubernetes 还为每一个 Pod 提供了一个固定并且唯一的访问入口,即:这个 Pod 对应的 DNS 记录。这些状态,在 StatefulSet 的整个生命周期里都会保持不变,绝不会因为对应 Pod 的删除或者重新创建而失效。
当 StatefulSet 的“控制循环”发现 Pod 的“实际状态”与“期望状态”不一致,需要新建或者删除 Pod 进行“调谐”的时候,它会严格按照这些 Pod 编号的顺序,逐一完成这些操作。
三. 存储状态
Kubernetes 项目引入了一组叫作 Persistent Volume Claim(PVC)
和Persistent Volume(PV)
的 API 对象,大大降低了用户声明和使用持久化 Volume 的门槛。
3.1 PVC
3.1.1 定义
PVC 其实就是一种特殊的 Volume。只不过一个 PVC 具体是什么类型的 Volume,要在跟某个 PV 绑定之后才知道。这些 PVC,都以“–< 编号 >”的方式命名,并且处于 Bound 状态。这个 StatefulSet 创建出来的所有 Pod,都会声明使用编号的 PVC。
3.1.2 案例
demo-pv-pod.yam
l案例的如下:
apiVersion: v1
kind: Pod
metadata:
name: demo-pv-pod
spec:
containers:
- name: demo-nginx
image: nginx
ports:
- containerPort: 80
name: "http-server"
volumeMounts:
- mountPath: "/usr/share/nginx/html"
name: pv-storage
volumes:
- name: pv-storage
persistentVolumeClaim:
claimName: pv-claim
- persistentVolumeClaim
在这个 Pod 的 Volumes 定义中,只需要指定 PVC 的名字,而完全不必关心 Volume 本身的定义。
3.2 PV
3.2.1 定义
PV来自于由运维人员维护的 PV(Persistent Volume)对象。PVC 和 PV 的设计,实际上类似于“接口”和“实现”的思想。
- 开发者只要知道并会使用“接口”,即:PVC。
- 而运维人员则负责给“接口”绑定具体的实现,即:PV。
3.2.2 案例
参考demo-pv.yaml
相关代码:
apiVersion: v1
kind: PersistentVolume
metadata:
name: demo-pv
labels:
type: local
spec:
capacity:
storage: 5Gi
accessModes:
- ReadWriteOnce
rbd:
monitors:
- 'IPs'
pool: kube
image: foo
fsType: ext4
readOnly: true
user: admin
keyring: /etc/ceph/keyring
PV 对象的spec.rbd
字段,是Ceph RBD Volume
的详细定义。而且,它还声明了这个 PV 的容量是 5 GiB。这样,Kubernetes 就会为我们刚刚创建的 PVC 对象绑定这个 PV。
3.2.3 PV状态
PersistentVolume(PV)的状态:
- Available(可用): 块空闲资源还没有被任何声明绑定
- Bound(已绑定): 卷已经被声明绑定, 注意:但是不一定不能继续被绑定,看accessModes而定
- Released(已释放): 声明被删除,但是资源还未被集群重新声明
- Failed(失败): 该卷的自动回收失败
3.3 volumeClaimTemplates
3.3.1 定义
凡是被这个 StatefulSet 管理的 Pod,都会声明一个对应的 PVC;而这个 PVC 的定义,就来自于 volumeClaimTemplates
这个模板字段。
3.3.2 案例
参考‘demo-statefulset-v2.yaml’相关代码:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: demo-statefulset
spec:
serviceName: "demo-service"
replicas: 3
selector:
matchLabels:
app: demo-nginx
template:
metadata:
labels:
app: demo-nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80
name: web
volumeMounts:
- name: tmp
mountPath: /usr/share/nginx/html
volumeClaimTemplates:
- metadata:
name: tmp
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
这个自动创建的 PVC,与 PV 绑定成功后,就会进入 Bound 状态,这就意味着这个 Pod 可以挂载并使用这个 PV 了。
执行如下查询指令:
kubectl get pvc -l app=demo-nginx
成功后的情况如下:
这个 PVC 的名字,会被分配一个与这个 Pod 完全一致的编号。
3.3.3 滚动更新
当进行滚动更新或者手动删除一个Pod时,PVC 维持有状态的经历如下的流程:
- 删除一个Pod
这个 Pod 对应的 PVC 和 PV,并不会被删除,而这个 Volume 里已经写入的数据,也依然会保存在远程存储服务里。 - 创建一个‘新的且同名’的Pod
StatefulSet 控制器就会重新创建一个‘新的且同名’的Pod,“纠正”这个不一致的情况。 - 基于
volumeClaimTemplates
模版创建
在这个新的 Pod 对象的定义里,它声明使用的 PVC 的名字与旧 PVC名称一致。这个 PVC 的定义,还是来自于 PVC 模板(volumeClaimTemplates),这是 StatefulSet 创建 Pod 的标准流程。 - PVC与PV绑定
在这个新的 Pod 被创建出来之后,Kubernetes 为它查找对应 PVC 时,就会直接找到旧 Pod 遗留下来的同名的 PVC,进而找到跟这个 PVC 绑定在一起的 PV。
基于上面的流程,新的 Pod 就可以挂载到旧 Pod 对应的那个 Volume,并且获取到保存在 Volume 里的数据。
通过这种方式,Kubernetes 的 StatefulSet 就实现了对应用存储状态的管理。
3.3.4 命名规则
根据volumeClaimTemplates
,为每个Pod 创建一个pvo,pvc的命名规则匹配模式:
(volumeClaimTemplates.name)-(pod_name)
比如上面的 volumeMounts.name=tmp, Podname=demo-statefulset-[0-2],因此创建出来的PVC是 tmp-demo-statefulset-0、tmp-demo-statefulset-1、 tmp-demo-statefulset-2。
3.4 小结
PVC 和 PV 的设计,就是“接口”和“实现”的思想,‘volumeClaimTemplates’进行了再一层的封装抽象,提供了更加模版的开发方式,降低开发与运维之间的耦合性。
四. 总结
4.1 启停顺序
- 有序部署
部署Statefulset时,如果有多个Pod副本,它们会被顺序地创建(从0到N-1)并且,在下一个Pod运行之前所有之前的Pod必须都是Running和Ready状态。 - 有序删除
当Pod被删除时,它们被终止的顺序是从N-1到0。 - 有序扩展
当对Pod执行扩展操作时,与部署一样,它前面的Pod必须都处于Running和Ready状态。
4.2 使用场景
- 稳定的持久化存储
即Pod重新调度后还是能访问到相同的持久化数据,基于PVC 来实现。 - 稳定的网络标识符
即Pod 重新调度后其iPodName 和 HostName不变。 - 有序部署,有序扩展
基于init containers 来实现。 - 有序收缩
最后,StatefulSet 其实是一种特殊的 Deployment,只不过这个“Deployment”的每个 Pod 实例的名字里,都携带了一个唯一并且固定的编号。这个编号的顺序,固定了 Pod 的拓扑关系;这个编号对应的 DNS 记录,固定了 Pod 的访问方式;这个编号对应的 PV,绑定了 Pod 与持久化存储的关系。所以,当 Pod 被删除重建时,这些“状态”都会保持不变。
Reference
https://www.cnblogs.com/baoshu/p/13281876.html
https://time.geekbang.org/column/article/41154?utm_campaign=guanwang&utm_source=baidu-ad&utm_medium=ppzq-pc&utm_content=title&utm_term=baidu-ad-ppzq-title
https://kubernetes.io/zh/docs/tutorials/stateful-application/basic-stateful-set/
https://blog.csdn.net/renfeigui0/article/details/101214865