深入剖析Kubernetes学习笔记:StatefulSet-MySQL集群(20)
一、需求描述
1、自然语言来描述
-
是一个“主从复制”(Maser-Slave Replication)的 MySQL 集群;
-
有 1 个主节点(Master);
-
有多个从节点(Slave);
-
从节点需要能水平扩展;
-
所有的写操作,只能在主节点上执行;
-
读操作可以在所有节点上执行。
2、图形描述
二、需求分析
1、通过 XtraBackup 将 Master 节点的数据备份到指定目录。
1 2 | $ cat xtrabackup_binlog_info TheMaster-bin.000001 481 |
2、配置 Slave 节点
Slave 节点在第一次启动前,需要先把 Master 节点的备份数据,连同备份信息文件,一起拷贝到自己的数据目录(/var/lib/mysql)下。然后,我们执行这样一句 SQL:
1 2 3 4 5 6 | TheSlave|mysql> CHANGE MASTER TO MASTER_HOST= '$masterip' , MASTER_USER= 'xxx' , MASTER_PASSWORD= 'xxx' , MASTER_LOG_FILE= 'TheMaster-bin.000001' , MASTER_LOG_POS=481; |
3、启动 Slave 节点
1 | TheSlave|mysql> START SLAVE; |
这样,Slave 节点就启动了。它会使用备份信息文件中的二进制日志文件和偏移量,与主节点进行数据同步。
4、在这个集群中添加更多的 Slave 节点
需要注意的是,新添加的 Slave 节点的备份数据,来自于已经存在的 Slave 节点
通过上面的叙述,我们不难看到,将部署 MySQL 集群的流程迁移到 Kubernetes 项目上,需要能够“容器化”地解决下面的“三座大山”:
-
Master 节点和 Slave 节点需要有不同的配置文件(即:不同的 my.cnf);
-
Master 节点和 Salve 节点需要能够传输备份信息文件;
-
在 Slave 节点第一次启动之前,需要执行一些初始化 SQL 操作;
三、第一座大山:Master 节点和 Slave 节点需要有不同的配置文件
1、思路
2、MySQL 的配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | apiVersion: v1 kind: ConfigMap metadata: name: mysql labels: app: mysql data: master.cnf: | # 主节点MySQL的配置文件 [mysqld] log-bin slave.cnf: | # 从节点MySQL的配置文件 [mysqld] super- read -only |
在这里,我们定义了 master.cnf 和 slave.cnf 两个 MySQL 的配置文件。
3、ConfigMap
4、两个 Service 定义
接下来,我们需要创建两个 Service 来供 StatefulSet 以及用户使用。这两个 Service 的定义如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | apiVersion: v1 kind: Service metadata: name: mysql labels: app: mysql spec: ports: - name: mysql port: 3306 clusterIP: None selector: app: mysql --- apiVersion: v1 kind: Service metadata: name: mysql- read labels: app: mysql spec: ports: - name: mysql port: 3306 selector: app: mysql |
1、可以看到
2、不同点
3、读写分离
四、第二座大山:Master 节点和 Salve 节点需要能够传输备份信息文件(大致框架)
思路
大致的框架
所以首先,我们先为 StatefulSet 对象规划一个大致的框架,如下图所示:
selector
replicas
有状态应用
管理存储状态
五、第二座大山:设计template 字段。
1、人格分裂
2、从 ConfigMap 中,获取 MySQL 的 Pod 对应的配置文件
为此,我们需要进行一个初始化操作,根据节点的角色是 Master 还是 Slave 节点,为 Pod 分配对应的配置文件。此外,MySQL 还要求集群里的每个节点都有一个唯一的 ID 文件,名叫 server-id.cnf。
而根据我们已经掌握的 Pod 知识,这些初始化操作显然适合通过 InitContainer 来完成。所以,我们首先定义了一个 InitContainer,如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | ... # template.spec initContainers: - name: init-mysql image: mysql:5.7 command : - bash - "-c" - | set -ex # 从Pod的序号,生成server-id [[ ` hostname ` =~ -([0-9]+)$ ]] || exit 1 ordinal=${BASH_REMATCH[1]} echo [mysqld] > /mnt/conf .d /server-id .cnf # 由于server-id=0有特殊含义,我们给ID加一个100来避开它 echo server- id =$((100 + $ordinal)) >> /mnt/conf .d /server-id .cnf # 如果Pod序号是0,说明它是Master节点,从ConfigMap里把Master的配置文件拷贝到/mnt/conf.d/目录; # 否则,拷贝Slave的配置文件 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 |
InitContainer
其中,文件拷贝的源目录 /mnt/config-map,正是 ConfigMap 在这个 Pod 的 Volume,如下所示:
1 2 3 4 5 6 7 8 | ... # template.spec volumes: - name: conf emptyDir: {} - name: config-map configMap: name: mysql |
通过这个定义,init-mysql 在声明了挂载 config-map 这个 Volume 之后,ConfigMap 里保存的内容,就会以文件的方式出现在它的 /mnt/config-map 目录当中。
3、在 Slave Pod 启动前,从 Master 或者其他 Slave Pod 里拷贝数据库数据到自己的目录下。
为了实现这个操作,我们就需要再定义第二个 InitContainer,如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | ... # template.spec.initContainers - name: clone-mysql image: gcr.io /google-samples/xtrabackup :1.0 command : - bash - "-c" - | set -ex # 拷贝操作只需要在第一次启动时进行,所以如果数据已经存在,跳过 [[ -d /var/lib/mysql/mysql ]] && exit 0 # Master节点(序号为0)不需要做这个操作 [[ ` hostname ` =~ -([0-9]+)$ ]] || exit 1 ordinal=${BASH_REMATCH[1]} [[ $ordinal - eq 0 ]] && exit 0 # 使用ncat指令,远程地从前一个节点拷贝数据到本地 ncat --recv-only mysql-$(($ordinal-1)).mysql 3307 | xbstream -x -C /var/lib/mysql # 执行--prepare,这样拷贝来的数据就可以用作恢复了 xtrabackup --prepare --target- dir = /var/lib/mysql volumeMounts: - name: data mountPath: /var/lib/mysql subPath: mysql - name: conf mountPath: /etc/mysql/conf .d |
在这个名叫 clone-mysql 的 InitContainer 里,我们使用的是 xtrabackup 镜像(它里面安装了 xtrabackup 工具)。
做判断
传输数据
/var/lib/mysql 目录,实际上正是一个名为 data 的 PVC,
一致性状态
六、第三座大山:在 Slave 节点第一次启动之前,需要执行一些初始化 SQL 操作
容器是一个单进程模型。
你可能已经想到了,我们可以为这个 MySQL 容器额外定义一个 sidecar 容器,来完成这个操作,它的定义如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | ... # template.spec.containers - name: xtrabackup image: gcr.io /google-samples/xtrabackup :1.0 ports: - name: xtrabackup containerPort: 3307 command : - bash - "-c" - | set -ex cd /var/lib/mysql # 从备份信息文件里读取MASTER_LOG_FILEM和MASTER_LOG_POS这两个字段的值,用来拼装集群初始化SQL if [[ -f xtrabackup_slave_info ]]; then # 如果xtrabackup_slave_info文件存在,说明这个备份数据来自于另一个Slave节点。这种情况下,XtraBackup工具在备份的时候,就已经在这个文件里自动生成了"CHANGE MASTER TO" SQL语句。所以,我们只需要把这个文件重命名为change_master_to.sql.in,后面直接使用即可 mv xtrabackup_slave_info change_master_to.sql. in # 所以,也就用不着xtrabackup_binlog_info了 rm -f xtrabackup_binlog_info elif [[ -f xtrabackup_binlog_info ]]; then # 如果只存在xtrabackup_binlog_inf文件,那说明备份来自于Master节点,我们就需要解析这个备份信息文件,读取所需的两个字段的值 [[ ` cat xtrabackup_binlog_info` =~ ^(.*?)[[:space:]]+(.*?)$ ]] || exit 1 rm xtrabackup_binlog_info # 把两个字段的值拼装成SQL,写入change_master_to.sql.in文件 echo "CHANGE MASTER TO MASTER_LOG_FILE= '${BASH_REMATCH[1]}' ,\ MASTER_LOG_POS=${BASH_REMATCH[2]}" > change_master_to.sql. in fi # 如果change_master_to.sql.in,就意味着需要做集群初始化工作 if [[ -f change_master_to.sql. in ]]; then # 但一定要先等MySQL容器启动之后才能进行下一步连接MySQL的操作 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" # 将文件change_master_to.sql.in改个名字,防止这个Container重启的时候,因为又找到了change_master_to.sql.in,从而重复执行一遍这个初始化流程 mv change_master_to.sql. in change_master_to.sql.orig # 使用change_master_to.sql.orig的内容,也是就是前面拼装的SQL,组成一个完整的初始化和启动Slave的SQL语句 mysql -h 127.0.0.1 <<EOF $(<change_master_to.sql.orig), MASTER_HOST= 'mysql-0.mysql' , MASTER_USER= 'root' , MASTER_PASSWORD= '' , MASTER_CONNECT_RETRY=10; START SLAVE; EOF fi # 使用ncat监听3307端口。它的作用是,在收到传输请求的时候,直接执行"xtrabackup --backup"命令,备份MySQL的数据并发送给请求者 exec ncat --listen --keep- open --send-only --max-conns=1 3307 -c \ "xtrabackup --backup --slave-info --stream=xbstream --host=127.0.0.1 --user=root" volumeMounts: - name: data mountPath: /var/lib/mysql subPath: mysql - name: conf mountPath: /etc/mysql/conf .d |
可以看到,在这个名叫 xtrabackup 的 sidecar 容器的启动命令里,其实实现了两部分工作。
第一部分工作,当然是 MySQL 节点的初始化工作
这个初始化需要使用的 SQL,是 sidecar 容器拼装出来、保存在一个名为 change_master_to.sql.in 的文件里的,具体过程如下所示:
sidecar 容器首先会判断当前 Pod 的 /var/lib/mysql 目录下,是否有 xtrabackup_slave_info 这个备份信息文件。
MySQL 节点的初始化流程
接下来,sidecar 容器就可以执行初始化了。从上面的叙述中可以看到,只要这个 change_master_to.sql.in 文件存在
所以,这时候,sidecar 容器只需要读取并执行 change_master_to.sql.in 里面的“CHANGE MASTER TO”指令,再执行一句 START SLAVE 命令,一个 Slave 节点就被成功启动了。
初始化操作完成后
在完成 MySQL 节点的初始化后,这个 sidecar 容器的第二个工作,则是启动一个数据传输服务。
1、具体做法
2、值得一提
至此,我们也就翻越了“第三座大山”,完成了 Slave 节点第一次启动前的初始化工作。
七、定义MySQL容器
扳倒了这“三座大山”后,我们终于可以定义 Pod 里的主角,MySQL 容器了。有了前面这些定义和初始化工作,MySQL 容器本身的定义就非常简单了,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | ... # template.spec containers: - name: mysql image: mysql:5.7 env : - name: MYSQL_ALLOW_EMPTY_PASSWORD value: "1" 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 : # 通过TCP连接的方式进行健康检查 command : [ "mysql" , "-h" , "127.0.0.1" , "-e" , "SELECT 1" ] initialDelaySeconds: 5 periodSeconds: 2 timeoutSeconds: 1 |
镜像
如果 MySQL 容器是 Slave 节点的话
livenessProbe
readinessProbe
至此,一个完整的主从复制模式的 MySQL 集群就定义完了。
八、运行 StatefulSet
首先,我们需要在 Kubernetes 集群里创建满足条件的 PV
如果你使用的是我们在第 11 篇文章《从 0 到 1:搭建一个完整的 Kubernetes 集群》里部署的 Kubernetes 集群的话,你可以按照如下方式使用存储插件 Rook:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | $ kubectl create -f rook-storage.yaml $ cat rook-storage.yaml apiVersion: ceph.rook.io /v1beta1 kind: Pool metadata: name: replicapool namespace: rook-ceph spec: replicated: size: 3 --- apiVersion: storage.k8s.io /v1 kind: StorageClass metadata: name: rook-ceph-block provisioner: ceph.rook.io /block parameters: pool: replicapool clusterNamespace: rook-ceph |
在这里,我用到了 StorageClass 来完成这个操作。它的作用,是自动地为集群里存在的每一个 PVC,调用存储插件(Rook)创建对应的 PV,从而省去了我们手动创建 PV 的机械劳动。我在后续讲解容器存储的时候,会再详细介绍这个机制。
1 | 备注:在使用 Rook 的情况下,mysql-statefulset.yaml 里的 volumeClaimTemplates 字段需要加上声明 storageClassName=rook-ceph-block,才能使用到这个 Rook 提供的持久化存储 |
然后,我们就可以创建这个 StatefulSet 了,如下所示:
1 2 3 4 5 6 | $ kubectl create -f mysql-statefulset.yaml $ kubectl get pod -l app=mysql NAME READY STATUS RESTARTS AGE mysql-0 2 /2 Running 0 2m mysql-1 2 /2 Running 0 1m mysql-2 2 /2 Running 0 1m |
可以看到,StatefulSet 启动成功后,会有三个 Pod 运行。
接下来,我们可以尝试向这个 MySQL 集群发起请求,执行一些 SQL 操作来验证它是否正常:
1 2 3 4 5 6 | $ kubectl run mysql-client --image=mysql:5.7 -i -- rm --restart=Never --\ mysql -h mysql-0.mysql <<EOF CREATE DATABASE test ; CREATE TABLE test .messages (message VARCHAR(250)); INSERT INTO test .messages VALUES ( 'hello' ); EOF |
如上所示,我们通过启动一个容器,使用 MySQL client 执行了创建数据库和表、以及插入数据的操作。需要注意的是,我们连接的 MySQL 的地址必须是 mysql-0.mysql(即:Master 节点的 DNS 记录)。因为,只有 Master 节点才能处理写操作。
而通过连接 mysql-read 这个 Service,我们就可以用 SQL 进行读操作,如下所示:
1 2 3 4 5 6 7 8 9 | $ kubectl run mysql-client --image=mysql:5.7 -i -t -- rm --restart=Never --\ mysql -h mysql- read -e "SELECT * FROM test.messages" Waiting for pod default /mysql-client to be running, status is Pending, pod ready: false +---------+ | message | +---------+ | hello | +---------+ pod "mysql-client" deleted |
在有了 StatefulSet 以后,你就可以像 Deployment 那样,非常方便地扩展这个 MySQL 集群,比如:
1 | $ kubectl scale statefulset mysql --replicas=5 |
这时候,你就会发现新的 Slave Pod mysql-3 和 mysql-4 被自动创建了出来。
而如果你像如下所示的这样,直接连接 mysql-3.mysql,即 mysql-3 这个 Pod 的 DNS 名字来进行查询操作:
1 2 3 4 5 6 7 8 9 | $ kubectl run mysql-client --image=mysql:5.7 -i -t -- rm --restart=Never --\ mysql -h mysql-3.mysql -e "SELECT * FROM test.messages" Waiting for pod default /mysql-client to be running, status is Pending, pod ready: false +---------+ | message | +---------+ | hello | +---------+ pod "mysql-client" deleted |
就会看到,从 StatefulSet 为我们新创建的 mysql-3 上,同样可以读取到之前插入的记录。也就是说,我们的数据备份和恢复,都是有效的。
九、总结
1、用一句话总结
2、关键点(坑)
人格分裂
阅后即焚
容器之间平等无序
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
2019-05-10 Linux性能优化实战学习笔记:第十七讲
2019-05-10 Linux性能优化实战学习笔记:第二十一讲