简要介绍:https://www.yuque.com/tiger-swicr/cqu22z/hyi6z1/edit#J7XAk
官网介绍:https://kubernetes.io/zh/docs/concepts/workloads/controllers/statefulset/

有状态应用部署,是K8S的集大成者!

必要性

一些应用是“有状态的”:实例之间有不对等关系(主从、主备),以及实例对外部数据有依赖关系。

StatefulSet 的设计其实非常容易理解。它把真实世界里的应用状态,抽象为了两种情况:

1、拓扑状态。这种情况意味着,应用的多个实例之间不是完全对等的关系。这些应用实例,必须按照某些顺序启动,比如应用的主节点 A 要先于从节点 B 启动。而如果你把 A 和 B 两个 Pod 删除掉,它们再次被创建出来时也必须严格按照这个顺序才行。并且,新创建出来的 Pod,必须和原来 Pod 的网络标识一样,这样原先的访问者才能使用同样的方法,访问到这个新 Pod。

2、存储状态。这种情况意味着,应用的多个实例分别绑定了不同的存储数据。对于这些应用实例来说,Pod A 第一次读取到的数据,和隔了十分钟之后再次读取到的数据,应该是同一份,哪怕在此期间 Pod A 被重新创建过。这种情况最典型的例子,就是一个数据库应用的多个存储实例。


Headless

不同于CLusterIP类型的Service:Service服务名 解析为 服务IP,再转发到Pod ip;
Headless类型:Service服务名 解析到所有Pod,不存在服务IP。

  1. #clusterIP: None
  2. apiVersion: v1
  3. kind: Service
  4. metadata:
  5. name: nginx
  6. labels:
  7. app: nginx
  8. spec:
  9. ports:
  10. - port: 80
  11. name: web
  12. clusterIP: None
  13. selector:
  14. app: nginx

Pod名格式:
$(StatefulSet 名称)-$(序号)
例如web-0、web-1、web-2

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

Pod的DNS记录
<pod-name>.<svc-name>.<namespace>.svc.cluster.local

参考
深入理解StatefulSet,用Kubernetes编排有状态应用
https://mp.weixin.qq.com/s/y60q0-RMh8isd4u4PuLfUg

DNS记录
https://kubernetes.io/zh/docs/concepts/services-networking/dns-pod-service/

解析出每个Pod IP

进入一个pod,执行 **nslookup Headless记录格式地址**会解析出HeadlessService代理的两个Endpoint (Pod)对应的IP。这样客户端就能通过Headless Service拿到每个EndPoint的IP,如果有需要可以自己在客户端做些负载均衡策略。

  1. /app # nslookup app-headless-svc.default.svc.cluster.local
  2. Server: 10.96.0.10
  3. Address: 10.96.0.10:53
  4. Name: app-headless-svc.default.svc.cluster.local
  5. Address: 10.1.0.38
  6. Name: app-headless-svc.default.svc.cluster.local
  7. Address: 10.1.0.39

Pod之间互相通信

进入其中一个pod,
分别执行nslookup **stat-go-app-0**的DNS域名nslookup** stat-go-app-1**的DNS域名
结果表明,Headless会为每个Endpoint也就是Pod添加DNS域名解析,这样Pod之间能够相互通信。
如果要用StatefulSet编排一个有主从关系的应用,就可以通过DNS域名访问的方式保证相互之间的通信,即使出现Pod重新调度它在内部的DNS域名也不会改变

  1. /app # nslookup stat-go-app-0.app-headless-svc.default.svc.cluster.local
  2. Server: 10.96.0.10
  3. Address: 10.96.0.10:53
  4. Name: stat-go-app-0.app-headless-svc.default.svc.cluster.local
  5. Address: 10.1.0.46
  6. /app # nslookup stat-go-app-1.app-headless-svc.default.svc.cluster.local
  7. Server: 10.96.0.10
  8. Address: 10.96.0.10:53
  9. Name: stat-go-app-1.app-headless-svc.default.svc.cluster.local
  10. Address: 10.1.0.47

作为对比,Deployment类型的,

执行nslookup clusterip的DNS域名只会解析出ClusterIP

  1. kubectl exec -it my-go-app-69d6844c5c-gkb6z -- /bin/sh
  2. /app # nslookup app-service.default.svc.cluster.local
  3. Server: 10.96.0.10
  4. Address: 10.96.0.10:53
  5. Name: app-service.default.svc.cluster.local
  6. Address: 10.108.26.155

执行nslookup <pod的DNS域名> ,则无法直接解析出Pod名对应的IP

  1. /app # nslookup my-go-app-69d6844c5c-gkb6z.app-service.default.svc.cluster.local
  2. Server: 10.96.0.10
  3. Address: 10.96.0.10:53
  4. ** server can't find my-go-app-69d6844c5c-gkb6z.app-service.default.svc.cluster.local: NXDOMAIN

StatefulSet

首先定义一个Headless类型的服务发现

  1. #clusterIP: None
  2. apiVersion: v1
  3. kind: Service
  4. metadata:
  5. name: nginx
  6. labels:
  7. app: nginx
  8. spec:
  9. ports:
  10. - port: 80
  11. name: web
  12. clusterIP: None
  13. selector:
  14. app: nginx
  1. apiVersion: apps/v1
  2. kind: StatefulSet
  3. metadata:
  4. name: web
  5. spec:
  6. serviceName: "nginx"
  7. replicas: 2
  8. selector:
  9. matchLabels:
  10. app: nginx
  11. template:
  12. metadata:
  13. labels:
  14. app: nginx
  15. spec:
  16. containers:
  17. - name: nginx
  18. image: nginx:1.9.1
  19. ports:
  20. - containerPort: 80
  21. name: web

与Deployment相比,多了一个 serviceName=nginx 字段。

这个字段的作用,就是告诉 StatefulSet 控制器,在执行控制循环(Control Loop)的时候,请使用 nginx 这个 Headless Service 来保证 Pod 的“可解析身份”。

拓扑状态

  1. # -w 监视整个过程
  2. [root@zm statefulset]# kubectl -n cka get pod -w
  3. NAME READY STATUS RESTARTS AGE
  4. dns-test 1/1 Running 0 4m6s
  5. ready-if-service-ready 1/1 Running 0 4d4h
  6. safari-5f5865b8c4-kn8ph 1/1 Running 0 4d5h
  7. web-0 0/1 ContainerCreating 0 8s
  8. web-0 1/1 Running 0 26s
  9. web-1 0/1 Pending 0 0s
  10. web-1 0/1 Pending 0 0s
  11. web-1 0/1 ContainerCreating 0 0s
  12. web-1 1/1 Running 0 1s

发现,StatefulSet类型,启动时,会等到第一个pod变为running,且ready后,再启动第二个pod,以此类推。

  1. #启动一个临时pod。镜像为busybox,注意 不能选择高版本的,nslookup会失败。这是个大坑。
  2. #--rm代表pod退出后就会被删除
  3. #--restart=Never代表永不重启
  4. [root@zm ~]# kubectl -n cka run dns-test --image=busybox:1.28.4 --rm -it --restart=Never -- /bin/sh
  5. If you don't see a command prompt, try pressing enter.
  6. / # nslookup nginx
  7. Server: 10.96.0.10
  8. Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
  9. Name: nginx
  10. Address 1: 10.244.0.16 web-0.nginx.cka.svc.cluster.local
  11. Address 2: 10.244.0.17 web-1.nginx.cka.svc.cluster.local
  12. / # nslookup web-0.nginx
  13. Server: 10.96.0.10
  14. Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
  15. Name: web-0.nginx
  16. Address 1: 10.244.0.16 web-0.nginx.cka.svc.cluster.local
  17. / # nslookup web-1.nginx
  18. Server: 10.96.0.10
  19. Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
  20. Name: web-1.nginx
  21. Address 1: 10.244.0.17 web-1.nginx.cka.svc.cluster.local

Kubernetes 就成功地将 Pod 的拓扑状态(比如:哪个节点先启动,哪个节点后启动),按照 Pod 的“名字 + 编号”的方式固定了下来。此外,Kubernetes 还为每一个 Pod 提供了一个固定并且唯一的访问入口,即:这个 Pod 对应的 DNS 记录。

小结:
StatefulSet 如何保证应用实例之间“拓扑状态”的稳定性:

StatefulSet 这个控制器的主要作用之一,就是使用 Pod 模板创建 Pod 的时候,对它们进行编号,并且按照编号顺序逐一完成创建工作。而当 StatefulSet 的“控制循环”发现 Pod 的“实际状态”与“期望状态”不一致,需要新建或者删除 Pod 进行“调谐”的时候,它会严格按照这些 Pod 编号的顺序,逐一完成这些操作。

存储状态

StatefulSet 对存储状态的管理机制。这个机制,主要使用的是一个叫作 Persistent Volume Claim 的功能。

为什么要有PVC ?

如果你并不知道有哪些 Volume 类型可以用,要怎么办呢?更具体地说,作为一个应用开发者,我可能对持久化存储项目(比如 Ceph、GlusterFS 等)一窍不通,也不知道公司的 Kubernetes 集群里到底是怎么搭建出来的,我也自然不会编写它们对应的 Volume 定义文件。

所谓“术业有专攻”,这些关于 Volume 的管理和远程持久化存储的知识,不仅超越了开发者的知识储备,还会有暴露公司基础设施秘密的风险。

Kubernetes 项目引入了一组叫作 Persistent Volume Claim(PVC)和 Persistent Volume(PV)的 API 对象,大大降低了用户声明和使用持久化 Volume 的门槛。

开发人员要做的:
第一步:首先定义一个PVC

  1. kind: PersistentVolumeClaim
  2. apiVersion: v1
  3. metadata:
  4. name: pv-claim
  5. spec:
  6. accessModes:
  7. - ReadWriteOnce
  8. resources:
  9. requests:
  10. storage: 1Gi

accessModes: ReadWriteOnce,表示这个 Volume 的挂载方式是可读写,并且只能被挂载在一个节点上而非被多个节点共享。

第二步:在应用的 Pod 中,声明使用这个 PVC:

  1. apiVersion: v1
  2. kind: Pod
  3. metadata:
  4. name: pv-pod
  5. spec:
  6. containers:
  7. - name: pv-container
  8. image: nginx
  9. ports:
  10. - containerPort: 80
  11. name: "http-server"
  12. volumeMounts:
  13. - mountPath: "/usr/share/nginx/html"
  14. name: pv-storage
  15. volumes:
  16. - name: pv-storage
  17. persistentVolumeClaim:
  18. claimName: pv-claim

这时候,只要我们创建这个 PVC 对象,Kubernetes 就会自动为它绑定一个符合条件的 Volume。可是,这些符合条件的 Volume 又是从哪里来的呢?答案是,它们来自于由运维人员维护的 PV(Persistent Volume)对象。

Kubernetes 中 PVC 和 PV 的设计,实际上类似于“接口”和“实现”的思想。开发者只要知道并会使用“接口”,即:PVC;而运维人员则负责给“接口”绑定具体的实现,即:PV。这种解耦,就避免了因为向开发者暴露过多的存储系统细节而带来的隐患。

此外,这种职责的分离,往往也意味着出现事故时可以更容易定位问题和明确责任,从而避免“扯皮”现象的出现。

小结:

首先,StatefulSet 的控制器直接管理的是 Pod。 这是因为,StatefulSet 里的不同 Pod 实例,不再像 ReplicaSet 中那样都是完全一样的,而是有了细微区别的。比如,每个 Pod 的 hostname、名字等都是不同的、携带了编号的。而 StatefulSet 区分这些实例的方式,就是通过在 Pod 的名字里加上事先约定好的编号。

其次,Kubernetes 通过 Headless Service,为这些有编号的 Pod,在 DNS 服务器中生成带有同样编号的 DNS 记录。 只要 StatefulSet 能够保证这些 Pod 名字里的编号不变,那么 Service 里类似于 web-0.nginx.default.svc.cluster.local 这样的 DNS 记录也就不会变,而这条记录解析出来的 Pod 的 IP 地址,则会随着后端 Pod 的删除和再创建而自动更新。这当然是 Service 机制本身的能力,不需要 StatefulSet 操心。

最后,StatefulSet 还为每一个 Pod 分配并创建一个同样编号的 PVC。这样,Kubernetes 就可以通过 Persistent Volume 机制为这个 PVC 绑定上对应的 PV,从而保证了每一个 Pod 都拥有一个独立的 Volume。 在这种情况下,即使 Pod 被删除,它所对应的 PVC 和 PV 依然会保留下来。所以当这个 Pod 被重新创建出来之后,Kubernetes 会为它找到同样编号的 PVC,挂载这个 PVC 对应的 Volume,从而获取到以前保存在 Volume 里的数据。

实践案例:MySQL主从

MySQL,Redis,MongoDB,Kafka,ES等

局限性
官网:

  • 给定 Pod 的存储必须由 PersistentVolume 驱动 基于所请求的 storage class 来提供,或者由管理员预先提供。
  • 删除或者收缩 StatefulSet 不会删除它关联的存储卷。 这样做是为了保证数据安全,它通常比自动清除 StatefulSet 所有相关的资源更有价值。
  • StatefulSet 当前需要Headless来负责 Pod 的网络标识。你需要负责创建此服务。
  • 当删除 StatefulSets 时,StatefulSet 不提供任何终止 Pod 的保证。 为了实现 StatefulSet 中的 Pod 可以有序地且体面地终止,可以在删除之前将 StatefulSet 缩放为 0。
  • 在默认 Pod 管理策略(OrderedReady) 时使用 滚动更新,可能进入需要人工干预 才能修复的损坏状态。