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

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

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

    准备开始

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

    • 您需要有一个带有默认的动态持续卷供应程序,或者自己静态的提供持久卷来满足这里使用的。
    • This tutorial assumes you are familiar with PersistentVolumes and , as well as other core concepts like Pods, , 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 命令行工具。 如果你还没有集群,你可以通过 构建一 个你自己的集群,或者你可以使用下面任意一个 Kubernetes 工具构建:

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

    • 本教程假定您熟悉 与 StatefulSets, 以及其他核心概念,例如, Services, 与 .
    • 熟悉 MySQL 会有所帮助,但是本教程旨在介绍对其他系统应该有用的常规模式。

    部署 MySQL

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

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

    1. kubectl apply -f https://k8s.io/examples/application/mysql/mysql-configmap.yaml

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

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

    Services

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

    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:

    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-2 2/2 Running 0 1m

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

    该清单使用多种技术来管理作为 StatefulSet 一部分的有状态 Pod。下一节重点介绍其中一些技巧,以解释 StatefulSet 创建 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 的范围。

    克隆现有数据

    通常,当新的 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 发生随机变化,因为每次尝试连接时都可能选择了不同的端点:

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

    模拟 Pod 和 Node 的宕机时间

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

    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

    首先确定 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,您可以使用单个命令执行此操作:

    查看新的 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. 如果您手动设置 PersistentVolume,则还需要手动删除它们,并释放基础资源。 如果您使用了动态预配器,当得知您删除 PersistentVolumeClaims 时,它将自动删除 PersistentVolumes。 一些动态预配器(例如用于 EBS 和 PD 的预配器)也会在删除 PersistentVolumes 时释放基础资源。

    接下来

    • 在中查找其他有状态的应用程序示例。