运行一个有状态的应用程序

该页面显示如何使用StatefulSet 控制器去运行一个有状态的应用程序。此例是一主多从的 MySQL 集群。

请注意 这不是生产配置。 重点是, MySQL 设置保留在不安全的默认值上,使重点放在 Kubernetes 中运行有状态应用程序的常规模式。

教程目标

  • 使用 StatefulSet 控制器部署复制的 MySQL 拓扑。
  • 发送 MySQL 客户端流量。
  • 观察对宕机的抵抗力。
  • 缩放 StatefulSet 的大小。

准备开始

要获知版本信息,请输入 kubectl version.

  • This tutorial assumes you are familiar with PersistentVolumes and StatefulSets, as well as other core concepts like Pods, Services, and ConfigMaps.
  • Some familiarity with MySQL helps, but this tutorial aims to present general patterns that should be useful for other systems. —>
  • 你必须拥有一个 Kubernetes 的集群,同时你的 Kubernetes 集群必须带有 kubectl 命令行工具。 如果你还没有集群,你可以通过 Minikube 构建一 个你自己的集群,或者你可以使用下面任意一个 Kubernetes 工具构建:

要获知版本信息,请输入 kubectl version.

部署 MySQL

部署 MySQL 示例,包含一个 ConfigMap,两个 Services,与一个 StatefulSet。

ConfigMap

从以下的 YAML 配置文件创建 ConfigMap :

application/mysql/mysql-configmap.yaml 运行一个有状态的应用程序 - 图1
  1. apiVersion: v1
  2. kind: ConfigMap
  3. metadata:
  4. name: mysql
  5. labels:
  6. app: mysql
  7. data:
  8. master.cnf: |
  9. # Apply this config only on the master.
  10. [mysqld]
  11. log-bin
  12. slave.cnf: |
  13. # Apply this config only on slaves.
  14. [mysqld]
  15. super-read-only
  1. kubectl apply -f https://k8s.io/examples/application/mysql/mysql-configmap.yaml

这个 ConfigMap 提供 my.cnf 覆盖,使您可以独立控制 MySQL 主服务器和从服务器的配置。 在这种情况下,您希望主服务器能够将复制日志提供给从服务器,并且希望从服务器拒绝任何不是通过复制进行的写操作。

ConfigMap 本身没有什么特别之处,它可以使不同部分应用于不同的 Pod。 每个 Pod 都会决定在初始化时要看基于 StatefulSet 控制器提供的信息。

Services

从以下 YAML 配置文件创建服务:

application/mysql/mysql-services.yaml 运行一个有状态的应用程序 - 图2
  1. # Headless service for stable DNS entries of StatefulSet members.
  2. apiVersion: v1
  3. kind: Service
  4. metadata:
  5. name: mysql
  6. labels:
  7. app: mysql
  8. spec:
  9. ports:
  10. - name: mysql
  11. port: 3306
  12. clusterIP: None
  13. selector:
  14. app: mysql
  15. —-
  16. # Client service for connecting to any MySQL instance for reads.
  17. # For writes, you must instead connect to the master: mysql-0.mysql.
  18. apiVersion: v1
  19. kind: Service
  20. metadata:
  21. name: mysql-read
  22. labels:
  23. app: mysql
  24. spec:
  25. ports:
  26. - name: mysql
  27. port: 3306
  28. selector:
  29. app: mysql
  1. kubectl apply -f https://k8s.io/examples/application/mysql/mysql-services.yaml

Headless Service 给 StatefulSet 控制器为集合中每个 Pod 创建的 DNS 条目提供了一个宿主。因为 Headless Service 名为 mysql,所以可以通过在同一 Kubernetes 集群和 namespace 中的任何其他 Pod 内解析 <pod-name>.mysql 来访问 Pod。

客户端 Service 称为 mysql-read,是一种常规 Service,具有其自己的群集 IP,该群集 IP 在报告为就绪的所有MySQL Pod 中分配连接。可能端点的集合包括 MySQL 主节点和所有从节点。

请注意,只有读取查询才能使用负载平衡的客户端 Service。因为只有一个 MySQL 主服务器,所以客户端应直接连接到 MySQL 主服务器 Pod (通过其在 Headless Service 中的 DNS 条目)以执行写入操作。

StatefulSet

最后,从以下 YAML 配置文件创建 StatefulSet:

application/mysql/mysql-statefulset.yaml 运行一个有状态的应用程序 - 图3
  1. apiVersion: apps/v1
  2. kind: StatefulSet
  3. metadata:
  4. name: mysql
  5. spec:
  6. selector:
  7. matchLabels:
  8. app: mysql
  9. serviceName: mysql
  10. replicas: 3
  11. template:
  12. metadata:
  13. labels:
  14. app: mysql
  15. spec:
  16. initContainers:
  17. - name: init-mysql
  18. image: mysql:5.7
  19. command:
  20. - bash
  21. - “-c
  22. - |
  23. set -ex
  24. # Generate mysql server-id from pod ordinal index.
  25. [[ hostname =~ -([0-9]+)$ ]] || exit 1
  26. ordinal=${BASH_REMATCH[1]}
  27. echo [mysqld] > /mnt/conf.d/server-id.cnf
  28. # Add an offset to avoid reserved server-id=0 value.
  29. echo server-id=$((100 + $ordinal)) >> /mnt/conf.d/server-id.cnf
  30. # Copy appropriate conf.d files from config-map to emptyDir.
  31. if [[ $ordinal -eq 0 ]]; then
  32. cp /mnt/config-map/master.cnf /mnt/conf.d/
  33. else
  34. cp /mnt/config-map/slave.cnf /mnt/conf.d/
  35. fi
  36. volumeMounts:
  37. - name: conf
  38. mountPath: /mnt/conf.d
  39. - name: config-map
  40. mountPath: /mnt/config-map
  41. - name: clone-mysql
  42. image: gcr.io/google-samples/xtrabackup:1.0
  43. command:
  44. - bash
  45. - “-c
  46. - |
  47. set -ex
  48. # Skip the clone if data already exists.
  49. [[ -d /var/lib/mysql/mysql ]] && exit 0
  50. # Skip the clone on master (ordinal index 0).
  51. [[ hostname =~ -([0-9]+)$ ]] || exit 1
  52. ordinal=${BASH_REMATCH[1]}
  53. [[ $ordinal -eq 0 ]] && exit 0
  54. # Clone data from previous peer.
  55. ncat recv-only mysql-$(($ordinal-1)).mysql 3307 | xbstream -x -C /var/lib/mysql
  56. # Prepare the backup.
  57. xtrabackup prepare target-dir=/var/lib/mysql
  58. volumeMounts:
  59. - name: data
  60. mountPath: /var/lib/mysql
  61. subPath: mysql
  62. - name: conf
  63. mountPath: /etc/mysql/conf.d
  64. containers:
  65. - name: mysql
  66. image: mysql:5.7
  67. env:
  68. - name: MYSQL_ALLOW_EMPTY_PASSWORD
  69. value: 1
  70. ports:
  71. - name: mysql
  72. containerPort: 3306
  73. volumeMounts:
  74. - name: data
  75. mountPath: /var/lib/mysql
  76. subPath: mysql
  77. - name: conf
  78. mountPath: /etc/mysql/conf.d
  79. resources:
  80. requests:
  81. cpu: 500m
  82. memory: 1Gi
  83. livenessProbe:
  84. exec:
  85. command: [“mysqladmin”, ping”]
  86. initialDelaySeconds: 30
  87. periodSeconds: 10
  88. timeoutSeconds: 5
  89. readinessProbe:
  90. exec:
  91. # Check we can execute queries over TCP (skip-networking is off).
  92. command: [“mysql”, “-h”, 127.0.0.1”, “-e”, SELECT 1”]
  93. initialDelaySeconds: 5
  94. periodSeconds: 2
  95. timeoutSeconds: 1
  96. - name: xtrabackup
  97. image: gcr.io/google-samples/xtrabackup:1.0
  98. ports:
  99. - name: xtrabackup
  100. containerPort: 3307
  101. command:
  102. - bash
  103. - “-c
  104. - |
  105. set -ex
  106. cd /var/lib/mysql
  107. # Determine binlog position of cloned data, if any.
  108. if [[ -f xtrabackup_slave_info && x$(<xtrabackup_slave_info)” != x ]]; then
  109. # XtraBackup already generated a partial “CHANGE MASTER TO” query
  110. # because we’re cloning from an existing slave. (Need to remove the tailing semicolon!)
  111. cat xtrabackup_slave_info | sed -E s/;$//g’ > change_master_to.sql.in
  112. # Ignore xtrabackup_binlog_info in this case (it’s useless).
  113. rm -f xtrabackup_slave_info xtrabackup_binlog_info
  114. elif [[ -f xtrabackup_binlog_info ]]; then
  115. # We’re cloning directly from master. Parse binlog position.
  116. [[ cat xtrabackup_binlog_info =~ ^(.?)[[:space:]]+(.?)$ ]] || exit 1
  117. rm -f xtrabackup_binlog_info xtrabackup_slave_info
  118. echo CHANGE MASTER TO MASTER_LOG_FILE=’${BASH_REMATCH[1]}’,\
  119. MASTER_LOG_POS=${BASH_REMATCH[2]}” > change_master_to.sql.in
  120. fi
  121. # Check if we need to complete a clone by starting replication.
  122. if [[ -f change_master_to.sql.in ]]; then
  123. echo Waiting for mysqld to be ready (accepting connections)”
  124. until mysql -h 127.0.0.1 -e SELECT 1”; do sleep 1; done
  125. echo Initializing replication from clone position
  126. mysql -h 127.0.0.1 \
  127. -e $(<change_master_to.sql.in), \
  128. MASTER_HOST=’mysql-0.mysql’, \
  129. MASTER_USER=’root’, \
  130. MASTER_PASSWORD=’’, \
  131. MASTER_CONNECT_RETRY=10; \
  132. START SLAVE;” || exit 1
  133. # In case of container restart, attempt this at-most-once.
  134. mv change_master_to.sql.in change_master_to.sql.orig
  135. fi
  136. # Start a server to send backups when requested by peers.
  137. exec ncat listen keep-open send-only max-conns=1 3307 -c \
  138. xtrabackup backup slave-info stream=xbstream host=127.0.0.1 user=root
  139. volumeMounts:
  140. - name: data
  141. mountPath: /var/lib/mysql
  142. subPath: mysql
  143. - name: conf
  144. mountPath: /etc/mysql/conf.d
  145. resources:
  146. requests:
  147. cpu: 100m
  148. memory: 100Mi
  149. volumes:
  150. - name: conf
  151. emptyDir: {}
  152. - name: config-map
  153. configMap:
  154. name: mysql
  155. volumeClaimTemplates:
  156. - metadata:
  157. name: data
  158. spec:
  159. accessModes: [“ReadWriteOnce”]
  160. resources:
  161. requests:
  162. storage: 10Gi
  1. kubectl apply -f https://k8s.io/examples/application/mysql/mysql-statefulset.yaml

您可以通过运行以下命令查看启动进度:

  1. kubectl get pods -l app=mysql --watch

一段时间后,您应该看到所有3个 Pod 都开始运行:

  1. NAME READY STATUS RESTARTS AGE
  2. mysql-0 2/2 Running 0 2m
  3. mysql-1 2/2 Running 0 1m
  4. mysql-2 2/2 Running 0 1m

输入Ctrl+C取消观察。 如果您看不到任何进度,确保已启用前提条件中提到的动态 PersistentVolume 预配器。

该清单使用多种技术来管理作为 StatefulSet 一部分的有状态 Pod。下一节重点介绍其中一些技巧,以解释 StatefulSet 创建 Pod 时发生的状况。

了解有状态的 Pod 初始化

StatefulSet 控制器一次按顺序启动 Pod 序数索引。它一直等到每个 Pod 报告就绪为止,然后再开始下一个 Pod。

此外,控制器为每个 Pod 分配一个唯一,稳定的表单名称 <statefulset-name>-<ordinal-index> 其结果是 Pods 名为 mysql-0,mysql-1 和 mysql-2。

上述 StatefulSet 清单中的 Pod 模板利用这些属性来执行 MySQL 复制的有序启动。

生成配置

在启动 Pod 规范中的任何容器之前, Pod 首先运行任何初始容器按照定义的顺序。

第一个名为 init-mysql 的初始化容器,根据序号索引生成特殊的 MySQL 配置文件。

该脚本通过从 Pod 名称的末尾提取索引来确定自己的序号索引,该名称由 hostname 命令返回。 然后将序数(带有数字偏移量以避免保留值)保存到 MySQL conf.d 目录中的文件 server-id.cnf 中。 这将转换 StatefulSet 提供的唯一,稳定的身份控制器进入需要相同属性的 MySQL 服务器 ID 的范围。

通过将内容复制到 conf.d 中,init-mysql 容器中的脚本也可以应用 ConfigMap 中的 master.cnf 或 slave.cnf。由于示例拓扑由单个 MySQL 主节点和任意数量的从节点组成,因此脚本仅将序数 0 指定为主节点,而将其他所有人指定为从节点。与 StatefulSet 控制器的部署顺序保证,这样可以确保 MySQL 主服务器在创建从服务器之前已准备就绪,以便它们可以开始复制。

克隆现有数据

通常,当新的 Pod 作为从节点加入集合时,必须假定 MySQL 主节点可能已经有数据。还必须假设复制日志可能不会一直追溯到时间的开始。 这些保守假设的关键是允许正在运行的 StatefulSet 随时间扩大和缩小而不是固定在其初始大小。

第二个名为 clone-mysql 的初始化容器,第一次在从属 Pod 上以空 PersistentVolume 启动时,会对从属 Pod 执行克隆操作。这意味着它将从另一个运行的 Pod 复制所有现有数据,因此其本地状态足够一致,可以开始主从服务器复制。

MySQL 本身不提供执行此操作的机制,因此该示例使用了一种流行的开源工具 Percona XtraBackup。 在克隆期间,源 MySQL 服务器可能会降低性能。 为了最大程度地减少对 MySQL 主机的影响,该脚本指示每个 Pod 从序号较低的 Pod 中克隆。 可以这样做的原因是 StatefulSet 控制器始终确保在启动 Pod N + 1 之前 Pod N 已准备就绪。

开始复制

初始化容器成功完成后,常规容器将运行。 MySQL Pods 由运行实际 mysqld 服务器的 mysql 容器和充当辅助工具的 xtrabackup 容器组成。

xtrabackup 辅助工具查看克隆的数据文件,并确定是否有必要在从属服务器上初始化 MySQL 复制。 如果是这样,它将等待 mysqld 准备就绪,然后执行带有从 XtraBackup 克隆文件中提取的复制参数 CHANGE MASTER TOSTART SLAVE 命令。

一旦从服务器开始复制后,它会记住其 MySQL 主服务器。并且如果服务器重新启动或连接中断,则会自动重新连接。 另外,因为从服务器会以其稳定的 DNS 名称查找主服务器(mysql-0.mysql),即使由于重新安排而获得新的 Pod IP,他们也会自动找到主服务器。

最后,开始复制后,xtrabackup 容器监听来自其他 Pod 的连接数据克隆请求。 如果 StatefulSet 扩大规模,或者下一个 Pod 失去其 PersistentVolumeClaim 并需要重新克隆,则此服务器将无限期保持运行。

发送客户端流量

您可以通过运行带有 mysql:5.7 镜像的临时容器并运行 mysql 客户端二进制文件,将测试查询发送到 MySQL 主服务器(主机名 mysql-0.mysql )。

  1. kubectl run mysql-client --image=mysql:5.7 -i --rm --restart=Never --\
  2. mysql -h mysql-0.mysql <<EOF
  3. CREATE DATABASE test;
  4. CREATE TABLE test.messages (message VARCHAR(250));
  5. INSERT INTO test.messages VALUES ('hello');
  6. EOF

使用主机名 mysql-read 将测试查询发送到任何报告为就绪的服务器:

  1. kubectl run mysql-client --image=mysql:5.7 -i -t --rm --restart=Never --\
  2. mysql -h mysql-read -e "SELECT * FROM test.messages"

您应该获得如下输出:

  1. Waiting for pod default/mysql-client to be running, status is Pending, pod ready: false
  2. +---------+
  3. | message |
  4. +---------+
  5. | hello |
  6. +---------+
  7. pod "mysql-client" deleted

为了演示 mysql-read 服务在服务器之间分配连接,您可以在循环中运行 SELECT @@server_id

  1. kubectl run mysql-client-loop --image=mysql:5.7 -i -t --rm --restart=Never --\
  2. bash -ic "while sleep 1; do mysql -h mysql-read -e 'SELECT @@server_id,NOW()'; done"

您应该看到报告的 @@server_id 发生随机变化,因为每次尝试连接时都可能选择了不同的端点:

  1. +-------------+---------------------+
  2. | @@server_id | NOW() |
  3. +-------------+---------------------+
  4. | 100 | 2006-01-02 15:04:05 |
  5. +-------------+---------------------+
  6. +-------------+---------------------+
  7. | @@server_id | NOW() |
  8. +-------------+---------------------+
  9. | 102 | 2006-01-02 15:04:06 |
  10. +-------------+---------------------+
  11. +-------------+---------------------+
  12. | @@server_id | NOW() |
  13. +-------------+---------------------+
  14. | 101 | 2006-01-02 15:04:07 |
  15. +-------------+---------------------+

要停止循环时可以按 Ctrl+C ,但是让它在另一个窗口中运行非常有用,这样您就可以看到以下步骤的效果。

模拟 Pod 和 Node 的宕机时间

为了证明从从节点缓存而不是单个服务器读取数据的可用性提高,请在使 Pod 退出 Ready 状态时,保持 SELECT @@server_id 循环从上面运行。

退出 Readiness Probe

mysql 容器的readiness probe运行命令 mysql -h 127.0.0.1 -e 'SELECT 1',以确保服务器已启动并能够执行查询。

迫使 readiness probe 失败的一种方法就是执行该命令:

  1. kubectl exec mysql-2 -c mysql -- mv /usr/bin/mysql /usr/bin/mysql.off

这进入 Pod mysql-2 的实际容器文件系统,并重命名 mysql 命令,以便 readiness probe 无法找到它。 几秒钟后, Pod 会将其中一个容器报告为未就绪,您可以通过运行以下命令进行检查:

  1. kubectl get pod mysql-2

就绪 列中查找 1/2

  1. NAME READY STATUS RESTARTS AGE
  2. mysql-2 1/2 Running 0 3m

此时,您应该会看到 SELECT @@server_id 循环继续运行,尽管它不再报告 102 。 回想一下,init-mysql 脚本将 server-id 定义为 100 + $ordinal ,因此服务器 ID 102 对应于 Pod mysql-2

现在修复 Pod,几秒钟后它应该重新出现在循环输出中:

  1. kubectl exec mysql-2 -c mysql -- mv /usr/bin/mysql.off /usr/bin/mysql

删除 Pods

如果删除了 Pod,则 StatefulSet 还会重新创建 Pod,类似于 ReplicaSet 对无状态 Pod 所做的操作。

  1. kubectl delete pod mysql-2

StatefulSet 控制器注意到不再存在 mysql-2 Pod,并创建了一个具有相同名称并链接到相同 PersistentVolumeClaim 的新 Pod。 您应该看到服务器 ID 102 从循环输出中消失了一段时间,然后自行返回。

排除 Node

如果您的 Kubernetes 集群具有多个节点,则可以通过发出以下命令drain来模拟节点停机时间(例如升级节点时)。

首先确定 MySQL Pods 之一在哪个节点上:

  1. kubectl get pod mysql-2 -o wide

节点名称应显示在最后一列中:

  1. NAME READY STATUS RESTARTS AGE IP NODE
  2. mysql-2 2/2 Running 0 15m 10.244.5.27 kubernetes-node-9l2t

然后通过运行以下命令耗尽节点,该命令将其封锁,以使新的 Pod 不能在那里调度,然后驱逐任何现有的 Pod。 将 <node-name> 替换为在上一步中找到的 Node 的名称。

这可能会影响节点上的其他应用程序,因此最好 仅在测试集群中执行此操作

  1. kubectl drain <node-name> --force --delete-local-data --ignore-daemonsets

现在,您可以观察 Pod 在其他节点上的重新安排:

  1. kubectl get pod mysql-2 -o wide --watch

它看起来应该像这样:

  1. NAME READY STATUS RESTARTS AGE IP NODE
  2. mysql-2 2/2 Terminating 0 15m 10.244.1.56 kubernetes-node-9l2t
  3. [...]
  4. mysql-2 0/2 Pending 0 0s <none> kubernetes-node-fjlm
  5. mysql-2 0/2 Init:0/2 0 0s <none> kubernetes-node-fjlm
  6. mysql-2 0/2 Init:1/2 0 20s 10.244.5.32 kubernetes-node-fjlm
  7. mysql-2 0/2 PodInitializing 0 21s 10.244.5.32 kubernetes-node-fjlm
  8. mysql-2 1/2 Running 0 22s 10.244.5.32 kubernetes-node-fjlm
  9. mysql-2 2/2 Running 0 30s 10.244.5.32 kubernetes-node-fjlm

再次,您应该看到服务器 ID 102SELECT @@server_id 循环输出一段时间,然后返回。

现在 uncordon 节点,使其恢复为正常模式:

  1. kubectl uncordon <node-name>

扩展从节点数量

使用 MySQL 复制,您可以通过添加从节点来扩展读取查询的能力。 使用 StatefulSet,您可以使用单个命令执行此操作:

  1. kubectl scale statefulset mysql --replicas=5

查看新的 Pod 的运行情况:

  1. kubectl get pods -l app=mysql --watch

一旦 Pod 启动,您应该看到服务器 IDs 103104 开始出现在 SELECT @@server_id 循环输出中。

您还可以验证这些新服务器在存在之前已添加了数据:

  1. kubectl run mysql-client --image=mysql:5.7 -i -t --rm --restart=Never --\
  2. mysql -h mysql-3.mysql -e "SELECT * FROM test.messages"
  1. Waiting for pod default/mysql-client to be running, status is Pending, pod ready: false
  2. +---------+
  3. | message |
  4. +---------+
  5. | hello |
  6. +---------+
  7. pod "mysql-client" deleted

向下缩放也是不停顿的:

  1. kubectl scale statefulset mysql --replicas=3

但是请注意,按比例放大会自动创建新的 PersistentVolumeClaims,而按比例缩小不会自动删除这些 PVC。 这使您可以选择保留那些初始化的 PVC,以更快地进行缩放,或者在删除它们之前提取数据。

您可以通过运行以下命令查看此信息:

  1. kubectl get pvc -l app=mysql

这表明,尽管将 StatefulSet 缩小为3,所有5个 PVC 仍然存在:

  1. NAME STATUS VOLUME CAPACITY ACCESSMODES AGE
  2. data-mysql-0 Bound pvc-8acbf5dc-b103-11e6-93fa-42010a800002 10Gi RWO 20m
  3. data-mysql-1 Bound pvc-8ad39820-b103-11e6-93fa-42010a800002 10Gi RWO 20m
  4. data-mysql-2 Bound pvc-8ad69a6d-b103-11e6-93fa-42010a800002 10Gi RWO 20m
  5. data-mysql-3 Bound pvc-50043c45-b1c5-11e6-93fa-42010a800002 10Gi RWO 2m
  6. data-mysql-4 Bound pvc-500a9957-b1c5-11e6-93fa-42010a800002 10Gi RWO 2m

如果您不打算重复使用多余的 PVC,则可以删除它们:

  1. kubectl delete pvc data-mysql-3
  2. kubectl delete pvc data-mysql-4

清理现场

  1. 通过在终端上按 Ctrl+C 取消 SELECT @@server_id 循环,或从另一个终端运行以下命令:

    1. kubectl delete pod mysql-client-loop --now
  1. 删除 StatefulSet。这也开始终止 Pod。

    1. kubectl delete statefulset mysql
  1. 验证 Pod 消失。 他们可能需要一些时间才能完成终止。

    1. kubectl get pods -l app=mysql

当以上内容返回时,您将知道 Pod 已终止:

  1. No resources found.
  1. 删除 ConfigMap,Services 和 PersistentVolumeClaims。

    1. kubectl delete configmap,service,pvc -l app=mysql
  1. 如果您手动设置 PersistentVolume,则还需要手动删除它们,并释放基础资源。 如果您使用了动态预配器,当得知您删除 PersistentVolumeClaims 时,它将自动删除 PersistentVolumes。 一些动态预配器(例如用于 EBS 和 PD 的预配器)也会在删除 PersistentVolumes 时释放基础资源。

接下来