ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
[TOC] 参考:[https://blog.51cto.com/newfly/2140004](https://blog.51cto.com/newfly/2140004) 参考:[https://jimmysong.io/kubernetes-handbook/concepts/statefulset.html](https://jimmysong.io/kubernetes-handbook/concepts/statefulset.html) ## 背景 用户通过 Deployment、ReplicationController 可以方便地在 kubernetes 中部署一套高可用、可扩展的**分布式无状态服务**。这类应用不在本地存储数据,通过简单的负载均衡策略可实现请求分发。随着 k8s 的普及和云原生架构的兴起,越来越多的人希望把数据库这类有状态服务也通过 k8s 进行编排。但因为有状态服务的复杂性,这一过程并不容易。本文将以最流行的开源数据库 MySQL 为例,介绍如何在 k8s 上部署运维有状态服务。本文所作的调研基于`k8s 1.18`。 ## StatefulSet 简介 [Deployment](https://link.zhihu.com/?target=https%3A//kubernetes.io/docs/concepts/workloads/controllers/deployment/)、[ReplicationController](https://link.zhihu.com/?target=https%3A//kubernetes.io/docs/concepts/workloads/controllers/replicationcontroller/)是为无状态服务而设计的,它们中 pod 的名称、主机名、存储都是不稳定的,且 pod 的启动、销毁顺序随机,并不适合数据库这样的有状态应用。为此,k8s 推出了面向有状态服务的工作负载[StatefulSet](https://link.zhihu.com/?target=https%3A//kubernetes.io/docs/concepts/workloads/controllers/statefulset/)。其管理的 pod 具有如下特点: 1. 唯一性 - 对于包含 N 个副本的 StatefulSet,每个 pod 会被分配一个 \[0,N)范围内的唯一序号。 2. 顺序性 - StatefulSet 中 pod 的启动、更新、销毁默认都是按顺序进行的。 3. 稳定的网络身份标识 - pod 的主机名、DNS 地址不会随着 pod 被重新调度而发生变化。 4. 稳定的持久化存储 - 当 pod 被重新调度后,仍然能挂载原有的 PersistentVolume,保证了数据的完整性和一致性。 StatefulSet在Headless Service的基础上又为StatefulSet控制的每个Pod副本创建了一个DNS域名,这个域名的格式为: ``` $(podname).(headless server name) FQDN: $(podname).(headless server name).namespace.svc.cluster.local ``` ## 创建 StatefulSet 作为开始,使用如下示例创建一个 StatefulSet。它和[StatefulSets](https://kubernetes.io/zh/docs/concepts/abstractions/controllers/statefulsets/)概念中的示例相似。它创建了一个[Headless Service](https://kubernetes.io/zh/docs/user-guide/services/#headless-services)`nginx`用来发布 StatefulSet`web`中的 Pod 的 IP 地址。 ``` apiVersion: v1 kind: Service metadata: name: nginx labels: app: nginx spec: ports: - port: 80 name: web clusterIP: None selector: app: nginx --- apiVersion: v1 kind: PersistentVolume metadata: name: "www-data-pv" labels: name: www-data-pv release: stable spec: capacity: storage: 5Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Recycle nfs: path: /nfs/www/data server: 192.168.84.75 --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: www-data-pvc spec: accessModes: - ReadWriteOnce resources: requests: storage: 5Gi selector: matchLabels: name: www-data-pv release: stable --- apiVersion: apps/v1 kind: StatefulSet metadata: name: web spec: serviceName: "nginx" replicas: 2 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx ports: - containerPort: 80 name: web volumeMounts: - name: www mountPath: /usr/share/nginx/html volumes: - name: www persistentVolumeClaim: claimName: www-data-pvc ``` 通过该配置文件,可看出StatefulSet的三个组成部分: * Headless Service:名为nginx,用来定义Pod网络标识( DNS domain)。 * StatefulSet:定义具体应用,名为Nginx,有三个Pod副本,并为每个Pod定义了一个域名。 * persistentVolumeClaim: 是由**用户进行存储的请求**。它类似于pod。Pod消耗节点资源,PVC消耗PV资源。Pod可以请求特定级别的资源(CPU和内存)。声明可以请求特定的大小和访问模式(例如,可以一次读/写或多次只读)。**【persistentVolumeClaim的细节请看k8s的持久化存储PVC】** >**为什么需要 headless service 无头服务?** 在用Deployment时,每一个Pod名称是没有顺序的,是随机字符串,因此是Pod名称是无序的,但是在statefulset中要求必须是有序 ,每一个pod不能被随意取代,pod重建后pod名称还是一样的。而pod IP是变化的,所以是以Pod名称来识别。pod名称是pod唯一性的标识符,必须持久稳定有效。这时候要用到无头服务,它可以给每个Pod一个唯一的名称 。 使用`kubectl apply`中的 Headless Service 和 StatefulSet。 ``` kubectl apply -f web.yaml service/nginx created statefulset.apps/web created ``` >最后三个Pod全部running且ready, 且顺序是web-0.web-1,web-2 ## 测试pod间是否相通 每个 Pod 都拥有一个基于其顺序索引的稳定的主机名。使用`kubectl exec`在每个 Pod 中执行`hostname`。 ``` $ for i in 0 1; do kubectl exec web-$i -- sh -c 'hostname'; done web-0 web-1 ``` 使用`kubectl run`运行一个提供`nslookup`命令的容器,该命令来自于`dnsutils`包。通过对 Pod 的主机名执行`nslookup`,你可以检查他们在集群内部的 DNS 地址。 ``` $ kubectl run -i --tty --image busybox:1.28 dns-test --restart=Never --rm nslookup web-0.nginx Server: 10.0.0.10 Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local Name: web-0.nginx Address 1: 10.244.1.6 nslookup web-1.nginx Server: 10.0.0.10 Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local Name: web-1.nginx Address 1: 10.244.2.6 ``` 另外一种方法是可以直接在web-0 pod中执行 nslookup + 域名 指令。 ``` kubectl exec web-0 -- nslookup web-1.nginx.svc.cluster.local 格式是:${podname}.${servername}.svc.cluster.local ``` ## 扩容/缩容 StatefulSet 扩容/缩容 StatefulSet 指增加或减少它的副本数。这通过更新`replicas`字段完成。你可以使用[`kubectl scale`](https://kubernetes.io/zh/docs/user-guide/kubectl/v1.18/#scale)或者[`kubectl patch`](https://kubernetes.io/zh/docs/user-guide/kubectl/v1.18/#patch)来扩容/缩容一个 StatefulSet。 ### 扩容 在一个终端窗口观察 StatefulSet 的 Pod。 ``` kubectl get pods -w -l app=nginx ``` 在另一个终端窗口使用`kubectl scale`扩展副本数为 5。 ``` kubectl scale sts web --replicas=5 statefulset.apps/web scaled ``` 在第一个 终端中检查`kubectl get`命令的输出,等待增加的 3 个 Pod 的状态变为 Running 和 Ready。 StatefulSet 控制器扩展了副本的数量。StatefulSet 按序号索引顺序的创建每个 Pod,并且会等待前一个 Pod 变为 Running 和 Ready 才会启动下一个 Pod。 ### 缩容 在一个终端观察 StatefulSet 的 Pod。 ``` kubectl get pods -w -l app=nginx ``` 在另一个终端使用`kubectl patch`将 StatefulSet 缩容回三个副本。 ``` kubectl patch sts web -p '{"spec":{"replicas":3}}' statefulset.apps/web patched ``` ### 顺序终止 Pod 控制器会按照与 Pod 序号索引相反的顺序每次删除一个 Pod。在删除下一个 Pod 前会等待上一个被完全关闭。 **规律总结:** * 匹配Pod name(网络标识)的模式为:**$(statefulset名称)-$(序号)**,比如上面的示例:web-0,web-1,web-2。 * StatefulSet为每个Pod副本创建了一个DNS域名,这个域名的格式为:**$(podname).(headless server name)**,也就意味着服务间是通过Pod域名来通信而非Pod IP,因为当Pod所在Node发生故障时,Pod会被飘移到其它Node上,Pod IP会发生变化,但是Pod域名不会有变化。 * StatefulSet使用Headless服务来控制Pod的域名,这个域名的FQDN为:**$(service name).$(namespace).svc.cluster.local**,其中,“cluster.local”指的是集群的域名。 * 根据volumeClaimTemplates,为每个Pod创建一个pvc,pvc的命名规则匹配模式:**(volumeClaimTemplates.name)-(pod\_name)**,比如上面的volumeMounts.name=www, Pod name=web-\[0-2\],因此创建出来的PVC是www-web-0、www-web-1、www-web-2。 * 删除Pod不会删除其pvc,手动删除pvc将自动释放pv。 ## 域名示例 关于Cluster Domain、headless service名称、StatefulSet 名称如何影响StatefulSet的Pod的DNS域名的示例: | Cluster Domain | Service (ns/name) | StatefulSet (ns/name) | StatefulSet Domain | Pod DNS | Pod Hostname | | --- | --- | --- | --- | --- | --- | | cluster.local | default/nginx | default/web | nginx.default.svc.cluster.local | web-{0..N-1}.nginx.default.svc.cluster.local | web-{0..N-1} | | cluster.local | foo/nginx | foo/web | nginx.foo.svc.cluster.local | web-{0..N-1}.nginx.foo.svc.cluster.local | web-{0..N-1} | | kube.local | foo/nginx | foo/web | nginx.foo.svc.kube.local | web-{0..N-1}.nginx.foo.svc.kube.local | web-{0..N-1} | ## 更新策略 在Kubernetes 1.7及更高版本中,通过.spec.updateStrategy字段允许配置或禁用Pod、labels、source request/limits、annotations自动滚动更新功能。 **OnDelete:**通过.spec.updateStrategy.type 字段设置为OnDelete,StatefulSet控制器不会自动更新StatefulSet中的Pod。用户必须手动删除Pod,以使控制器创建新的Pod。 **RollingUpdate:**通过.spec.updateStrategy.type 字段设置为RollingUpdate,实现了Pod的自动滚动更新,如果.spec.updateStrategy未指定,则此为默认策略。 StatefulSet控制器将删除并重新创建StatefulSet中的每个Pod。它将以Pod终止(从最大序数到最小序数)的顺序进行,一次更新每个Pod。在更新下一个Pod之前,必须等待这个Pod Running and Ready。 **Partitions:**通过指定 .spec.updateStrategy.rollingUpdate.partition 来对 RollingUpdate 更新策略进行分区,如果指定了分区,则当 StatefulSet 的 .spec.template 更新时,具有大于或等于分区序数的所有 Pod 将被更新。 具有小于分区的序数的所有 Pod 将不会被更新,即使删除它们也将被重新创建。如果 StatefulSet 的 .spec.updateStrategy.rollingUpdate.partition 大于其 .spec.replicas,则其 .spec.template 的更新将不会传播到 Pod。在大多数情况下,不需要使用分区。 ## 外部访问集群内部 ### 土方法  对pod打标签,然后映射成服务 ``` kubectl label pod web-0 webInst=0 -n sy-platform-demo kubectl expose pod web-0 --port=80--target-port=80--name=web-0 --selector=web=0 --type=NodePort -n sy-platform-demo ``` 修改web-0 的nodeport 端口号 ``` kubectl edit service web-0 -n sy-platform-demo ```