kubernetes工作负载资源StatefulSet

著作:行癫 <盗版必究> ------ ## 一:StatefulSet ​ StatefulSet 是用来管理有状态应用的工作负载 API 对象;StatefulSet 用来管理某 [Pod](https://kubernetes.io/docs/concepts/workloads/pods/pod-overview/) 集合的部署和扩缩, 并为这些 Pod 提供持久存储和持久标识符;和 [Deployment](https://kubernetes.io/zh/docs/concepts/workloads/controllers/deployment/) 类似, StatefulSet 管理基于相同容器规约的一组 Pod。但不同的是, StatefulSet 为它们的每个 Pod 维护了一个有粘性的 ID。这些 Pod 是基于相同的规约来创建的, 但是不能相互替换:无论怎么调度,每个 Pod 都有一个永久不变的 ID。 ​ 使用存储卷为工作负载提供持久存储,可以使用 StatefulSet 作为解决方案的一部分。 尽管 StatefulSet 中的单个 Pod 仍可能出现故障, 但持久的 Pod 标识符使得将现有卷与替换已失败 Pod 的新 Pod 相匹配变得更加容易。 #### 1.特点 StatefulSets 对于需要满足以下一个或多个需求的应用程序很有价值: ​ 稳定的、唯一的网络标识符 ​ 稳定的、持久的存储 ​ 有序的、优雅的部署和缩放 ​ 有序的、自动的滚动更新 ​ 稳定的意味着 Pod 调度或重调度的整个过程是有持久性的。 如果应用程序不需要任何稳定的标识符或有序的部署、删除或伸缩,则应该使用 由一组无状态的副本控制器提供的工作负载来部署应用程序,比如Deployment或者ReplicaSet 可能更适用于你的无状态应用部署需要。 #### 2.限制 ​ 给定 Pod 的存储必须由 [PersistentVolume 驱动](https://github.com/kubernetes/examples/tree/master/staging/persistent-volume-provisioning/README.md) 基于所请求的 `storage class` 来提供,或者由管理员预先提供 ​ 删除或者收缩 StatefulSet 并不会删除它关联的存储卷。 这样做是为了保证数据安全 ​ StatefulSet 当前需要无头服务来负责 Pod 的网络标识。你需要负责创建此服务 ​ 当删除 StatefulSets 时,StatefulSet 不提供任何终止 Pod 的保证 ​ 为了实现 StatefulSet 中的 Pod 可以有序地且体面地终止,可以在删除之前将 StatefulSet 缩放为 0 注意: 无头服务(Headless Services): ​ 有时不需要或不想要负载均衡,以及单独的 Service IP。 遇到这种情况,可以通过指定 Cluster IP(`spec.clusterIP`)的值为 `"None"` 来创建 `Headless` Service。 ​ 使用无头 Service 与其他服务发现机制进行接口,而不必与 Kubernetes 的实现捆绑在一起 ​ 无头 Service 并不会分配 Cluster IP,kube-proxy 不会处理它们, 而且平台也不会为它们进行负载均衡和路由。 DNS 如何实现自动配置,依赖于 Service 是否定义了选择算符。 #### 3.创建StatefulSet ```shell [root@master xingdian]# cat Statefulset.yaml apiVersion: v1 kind: Service metadata: name: nginx labels: app: nginx spec: type: NodePort ports: - port: 80 name: web targetPort: 80 nodePort: 30010 selector: app: nginx --- apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: xingdian provisioner: example.com/external-nfs parameters: server: 10.0.0.230 path: /kubernetes-1 readOnly: "false" --- apiVersion: v1 kind: PersistentVolume metadata: name: xingdian-1 spec: capacity: storage: 1Gi volumeMode: Filesystem accessModes: - ReadWriteOnce storageClassName: xingdian nfs: path: /kubernetes-1 server: 10.0.0.230 --- apiVersion: v1 kind: PersistentVolume metadata: name: xingdian-2 spec: capacity: storage: 1Gi volumeMode: Filesystem accessModes: - ReadWriteOnce storageClassName: xingdian nfs: path: /kubernetes-1 server: 10.0.0.230 --- apiVersion: apps/v1 kind: StatefulSet metadata: name: web spec: selector: matchLabels: app: nginx serviceName: "nginx" replicas: 2 template: metadata: labels: app: nginx spec: terminationGracePeriodSeconds: 10 containers: - name: nginx image: nginx:1.20.1 ports: - containerPort: 80 name: web volumeMounts: - name: www mountPath: /usr/share/nginx/html volumeClaimTemplates: - metadata: name: www spec: accessModes: [ "ReadWriteOnce" ] storageClassName: "xingdian" resources: requests: storage: 1Gi ``` ​ 名为 `nginx` 的 Headless Service 用来控制网络域名 ​ 名为 `web` 的 StatefulSet 有一个 Spec,它表明将在独立的2个 Pod 副本中启动 nginx 容器 ​ `volumeClaimTemplates` 将通过 PersistentVolumes 驱动提供的 [PersistentVolumes](https://kubernetes.io/zh/docs/concepts/storage/persistent-volumes/) 来提供稳定的存储 #### 4.Pod 选择算符 ​ 你必须设置 StatefulSet 的 `.spec.selector` 字段,使之匹配其在 `.spec.template.metadata.labels` 中设置的标签。在 Kubernetes 1.8 版本之前, 被忽略 `.spec.selector` 字段会获得默认设置值。 在 1.8 和以后的版本中,未指定匹配的 Pod 选择器将在创建 StatefulSet 期间导致验证错误。 #### 5.Pod 标识 ​ StatefulSet Pod 具有唯一的标识,该标识包括顺序标识、稳定的网络标识和稳定的存储。 该标识和 Pod 是绑定的,不管它被调度在哪个节点上。 #### 6.有序索引 ​ 对于具有 N 个副本的 StatefulSet,StatefulSet 中的每个 Pod 将被分配一个整数序号, 从 0 到 N-1,该序号在 StatefulSet 上是唯一的。 #### 7.稳定的网络 ID ​ StatefulSet 中的每个 Pod 根据 StatefulSet 的名称和 Pod 的序号派生出它的主机名。 组合主机名的格式为`$(StatefulSet 名称)-$(序号)`。 上例将会创建三个名称分别为 `web-0、web-1、web-2` 的 Pod。 ​ StatefulSet 可以使用无头服务 控制它的 Pod 的网络域。管理域的这个服务的格式为: `$(服务名称).$(命名空间).svc.cluster.local`,其中 `cluster.local` 是集群域。 一旦每个 Pod 创建成功,就会得到一个匹配的 DNS 子域,格式为: `$(pod 名称).$(所属服务的 DNS 域名)`,其中所属服务由 StatefulSet 的 `serviceName` 域来设定。 ``` $(pod name).$(service name).$(namespace).svc.cluster.local ``` ​ 取决于集群域内部 DNS 的配置,有可能无法查询一个刚刚启动的 Pod 的 DNS 命名。 当集群内其他客户端在 Pod 创建完成前发出 Pod 主机名查询时,就会发生这种情况。 负缓存 (在 DNS 中较为常见) 意味着之前失败的查询结果会被记录和重用至少若干秒钟, 即使 Pod 已经正常运行了也是如此。 如果需要在 Pod 被创建之后及时发现它们,有以下选项: ​ 直接查询 Kubernetes API(比如,利用 watch 机制)而不是依赖于 DNS 查询 ​ 缩短 Kubernetes DNS 驱动的缓存时长(通常这意味着修改 CoreDNS 的 ConfigMap,目前缓存时长为 30 秒) | 集群域名 | 服务(名字空间/名字) | StatefulSet(名字空间/名字) | StatefulSet 域名 | Pod DNS | Pod 主机名 | | ------------- | --------------------- | ---------------------------- | ------------------------------- | -------------------------------------------- | ------------ | | 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} | #### 8.稳定的存储 ​ 对于 StatefulSet 中定义的每个 VolumeClaimTemplate,每个 Pod 接收到一个 PersistentVolumeClaim。在上面的 nginx 示例中,每个 Pod 将会得到基于 StorageClass `my-storage-class` 提供的 1 Gib 的 PersistentVolume。 如果没有声明 StorageClass,就会使用默认的 StorageClass。 当一个 Pod 被调度(重新调度)到节点上时,它的 `volumeMounts` 会挂载与其 PersistentVolumeClaims 相关联的 PersistentVolume。 请注意,当 Pod 或者 StatefulSet 被删除时,与 PersistentVolumeClaims 相关联的 PersistentVolume 并不会被删除。要删除它必须通过手动方式来完成。 #### 9.部署和扩缩保证 ​ 对于包含 N 个 副本的 StatefulSet,当部署 Pod 时,它们是依次创建的,顺序为 `0..N-1` ​ 当删除 Pod 时,它们是逆序终止的,顺序为 `N-1..0` ​ 在将缩放操作应用到 Pod 之前,它前面的所有 Pod 必须是 Running 和 Ready 状态 ​ 在 Pod 终止之前,所有的继任者必须完全关闭 注意: ​ StatefulSet 不应将 `pod.Spec.TerminationGracePeriodSeconds` 设置为 0;参数指定Kubernetes在强制终止pod之前等待pod正常终止的时间。 这种做法是不安全的,要强烈阻止。 ​ 在上面的 nginx 示例被创建后,会按照 web-0、web-1 的顺序部署2个 Pod。 在 web-0 进入Running 和 Ready状态前不会部署 web-1。要等到 web-0 部署完成并进入 Running 和 Ready 状态后,才会部署 web-1。 ​ 如果用户想将示例中的 StatefulSet 收缩为 `replicas=1`,首先被终止的是 web-1。 在 web-1没有被完全停止和删除前,如果在此期间发生 web-0 运行失败, 那么就不会终止 web-1,必须等到 web-0 进入 Running 和 Ready 状态后才会终止 web-1。 #### 10.无状态服务 - 是指该服务运行的实例不会在本地存储需要持久化的数据,并且多个实例对于同一个请求响应的结果是完全一致的。 - 多个实例可以共享相同的持久化数据。例如:nginx 实例,tomcat 实例等 - 相关的 k8s 资源有:ReplicaSet、ReplicationController、Deployment等,由于是无状态服务,所以这些控制器创建的 pod 序号都是随机值。并且在缩容的时候并不会明确缩容某一个pod,而是随机的,因为所有实例得到的返回值都是一样,所以缩容任何一个pod都可以。 ​ 1、 pod 之间没有依赖,数据或者配置 ​ 2、 相同服务所有pod响应的结果都是一致的 ​ 3、 相同服务的不同pod 启动,停止,更新,删除没有顺序要求, #### 11.有状态服务 - 宠物和牛的类比,农场主的牛如果病了可以丢掉再重新买一头,如果宠物主的宠物病死了是没法找到一头一模一样的宠物的。有状态服务可以说是需要数据存储功能的服务、或者指多线程类型的服务,队列等。(mysql数据库、kafka、zookeeper等) - 每个实例都需要有自己独立的持久化存储,并且在 k8s 中是通过申明模板来进行定义。持久卷申明模板在创建 pod 之前创建,绑定到 pod 中,模板可以定义多个。 - 1、pod 对数据或者配置有严格的依赖, - 2、相同服务的每个pod都是唯一的,功能不同,相应的结果不同。 - 3、相同服务的不同pod启动,停止,更新,删除有严格顺序要求,一旦顺序错乱,服务不可用,同一个时间点只能对一个pod操作 - 4、pod的启动的时候有明确的序号 ## 二:案例 #### 1.利用Statefulset构建Mysql主从集群 准备工作: ​ NFS共享存储为Kubernetes集群提供持久化存储 ​ 配置Kubernetes集群链接企业私有Harbor仓库 ​ Kubernetes集群安装NFS插件Provisioner,利用NFS存储在Kubernetes集群创建StorageClass使用 注意: ​ 部署NFS插件Provisioner方式 ​ Kuboard方式 ​ yaml文件方式(见新文档) ##### 创建共享目录 ​ 略 共享目录为 /data/ha-mysql ##### 创建命名空间 ```shell [root@xingdiancloud-native-master-a mysql]# kubectl create ns mysql ``` ##### 创建存储类 ![image-20241221163057079](https://xingdian-home.oss-cn-beijing.aliyuncs.com/imagesimage-20241221163057079.png) ![image-20241221163210967](https://xingdian-home.oss-cn-beijing.aliyuncs.com/imagesimage-20241221163210967.png) ![image-20241221163251629](https://xingdian-home.oss-cn-beijing.aliyuncs.com/imagesimage-20241221163251629.png) ##### 创建ConfigMap ​ Kuboard方式 ![image-20241221231643049](https://xingdian-home.oss-cn-beijing.aliyuncs.com/imagesimage-20241221231643049.png) ![image-20241221231712133](https://xingdian-home.oss-cn-beijing.aliyuncs.com/imagesimage-20241221231712133.png) ​ Yaml文件方式 ```yaml [root@xingdiancloud-native-master-a mysql]# cat 01-configmap.yaml apiVersion: v1 kind: ConfigMap metadata: namespace: mysql name: mysql labels: app: mysql data: master.cnf: | [mysqld] log-bin log_bin_trust_function_creators=1 lower_case_table_names=1 slave.cnf: | [mysqld] super-read-only log_bin_trust_function_creators=1 [root@xingdiancloud-native-master-a mysql]# kubectl apply -f 01-configmap.yaml ``` ##### 创建Service ​ Kuboard方式:略 ​ Yaml文件方式 ```yaml [root@xingdiancloud-native-master-a mysql]# cat 02-service.yaml # Headless service for stable DNS entries of StatefulSet members. apiVersion: v1 kind: Service metadata: namespace: mysql name: mysql labels: app: mysql spec: ports: - name: mysql port: 3306 clusterIP: None selector: app: mysql --- # Client service for connecting to any MySQL instance for reads. # For writes, you must instead connect to the master: mysql-0.mysql. apiVersion: v1 kind: Service metadata: namespace: mysql name: mysql-read labels: app: mysql spec: ports: - name: mysql port: 3306 selector: app: mysql [root@xingdiancloud-native-master-a mysql]# kubectl apply -f 02-service.yaml ``` ##### 创建StatefulSet控制器 ```yaml [root@xingdiancloud-native-master-a mysql]# cat 03-statefulset.yaml apiVersion: apps/v1 kind: StatefulSet metadata: name: mysql namespace: mysql spec: selector: matchLabels: app: mysql serviceName: mysql replicas: 3 template: metadata: labels: app: mysql spec: initContainers: - name: init-mysql image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/mysql:5.7.26 env: - name: TZ value: Asia/Shanghai command: - bash - "-c" - | set -ex # Generate mysql server-id from pod ordinal index. [[ `hostname` =~ -([0-9]+)$ ]] || exit 1 ordinal=${BASH_REMATCH[1]} echo [mysqld] > /mnt/conf.d/server-id.cnf # Add an offset to avoid reserved server-id=0 value. echo server-id=$((100 + $ordinal)) >> /mnt/conf.d/server-id.cnf # Copy appropriate conf.d files from config-map to emptyDir. if [[ $ordinal -eq 0 ]]; then cp /mnt/config-map/master.cnf /mnt/conf.d/ else cp /mnt/config-map/slave.cnf /mnt/conf.d/ fi volumeMounts: - name: conf mountPath: /mnt/conf.d - name: config-map mountPath: /mnt/config-map - name: clone-mysql image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/gcr.io/google-samples/xtrabackup:1.0 env: - name: TZ value: Asia/Shanghai command: - bash - "-c" - | set -ex # Skip the clone if data already exists. [[ -d /var/lib/mysql/mysql ]] && exit 0 # Skip the clone on master (ordinal index 0). [[ `hostname` =~ -([0-9]+)$ ]] || exit 1 ordinal=${BASH_REMATCH[1]} [[ $ordinal -eq 0 ]] && exit 0 # Clone data from previous peer. ncat --recv-only mysql-$(($ordinal-1)).mysql 3307 | xbstream -x -C /var/lib/mysql # Prepare the backup. xtrabackup --prepare --target-dir=/var/lib/mysql volumeMounts: - name: data mountPath: /var/lib/mysql subPath: mysql - name: conf mountPath: /etc/mysql/conf.d containers: - name: mysql image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/mysql:5.7.26 env: - name: MYSQL_ALLOW_EMPTY_PASSWORD value: "1" - name: TZ value: Asia/Shanghai ports: - name: mysql containerPort: 3306 volumeMounts: - name: data mountPath: /var/lib/mysql subPath: mysql - name: conf mountPath: /etc/mysql/conf.d resources: requests: cpu: 500m memory: 1Gi livenessProbe: exec: command: ["mysqladmin", "ping"] initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 5 readinessProbe: exec: # Check we can execute queries over TCP (skip-networking is off). command: ["mysql", "-h", "127.0.0.1", "-e", "SELECT 1"] initialDelaySeconds: 5 periodSeconds: 2 timeoutSeconds: 1 - name: xtrabackup image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/gcr.io/google-samples/xtrabackup:1.0 ports: - name: xtrabackup containerPort: 3307 command: - bash - "-c" - | set -ex cd /var/lib/mysql # Determine binlog position of cloned data, if any. if [[ -f xtrabackup_slave_info ]]; then # XtraBackup already generated a partial "CHANGE MASTER TO" query # because we're cloning from an existing slave. mv xtrabackup_slave_info change_master_to.sql.in # Ignore xtrabackup_binlog_info in this case (it's useless). rm -f xtrabackup_binlog_info elif [[ -f xtrabackup_binlog_info ]]; then # We're cloning directly from master. Parse binlog position. [[ `cat xtrabackup_binlog_info` =~ ^(.*?)[[:space:]]+(.*?)$ ]] || exit 1 rm xtrabackup_binlog_info echo "CHANGE MASTER TO MASTER_LOG_FILE='${BASH_REMATCH[1]}',\ MASTER_LOG_POS=${BASH_REMATCH[2]}" > change_master_to.sql.in fi # Check if we need to complete a clone by starting replication. if [[ -f change_master_to.sql.in ]]; then echo "Waiting for mysqld to be ready (accepting connections)" until mysql -h 127.0.0.1 -e "SELECT 1"; do sleep 1; done echo "Initializing replication from clone position" # In case of container restart, attempt this at-most-once. mv change_master_to.sql.in change_master_to.sql.orig mysql -h 127.0.0.1 <