实战 Kubernetes StatefulSet -- MySQL 主从集群搭建

2022-05-14 12:25:01   最后更新: 2022-05-14 12:25:01   访问数量:48




 

前面两篇文章,我们详细介绍了 Kubernetes 中 StatefulSet 的网络状态和存储状态:

 

有状态的节点控制器 -- StatefulSet 及其网络状态

 

有状态的节点控制器 StatefulSet 的存储状态

 

那么,StatefulSet 究竟要怎么将这两者结合起来呢?实践中有哪些经典的案例可以通过 StatefulSet 方便快捷地来实现呢?本文我们就来详细介绍一下。

 

 

mysql 集群是一个非常典型的有状态应用,和 elasticsearch、kafka 等自选举的集群不同,mysql 的集群组建显得有些“原始”。

 

 

 

此前我们介绍过如何来构建一个 mysql 主从集群:

 

MySQL 分布式主从读写分离架构及实战

 

对于 mysql 集群来说,我们首先要选取主节点,并且启动它,如果这是一个已有数据 mysql 节点,还需要考虑如何备份 mysql 主节点上的数据。

 

此后,我们需要用另一套配置来启动若干从节点,并且在这些从节点上恢复上一步中主节点上的备份数据。

 

完成上述配置之后,我们还必须考虑如何保证只让主节点处理写请求,而读请求则可以在任意节点上执行。

 

除此以外,从节点的水平扩展也是必须考虑另一个问题。

 

由此可见,mysql 主从集群的构建具有网络状态 -- 主节点必须先行启动,并且具有存储状态 -- 每个节点需要有自己独立的存储,很显然,用 Deployment 作为控制器来进行 mysql 集群的搭建是无法实现的,而这恰恰是 StatefulSet 擅长处理的场景。

 

 

3.1 主从节点不同的配置文件

 

mysql 主节点与从节点拥有完全不同的配置,主节点需要开启 log-bin 通过二进制的方式导出 bin-log 来实现主从复制,从节点需要配置主节点的信息外,还需要配置 super-read-only 来实现从节点的只读。

 

这在 Kubernetes 中是很容易实现的,我们只需要在 ConfigMap 中定义两套配置,然后在 pod 描述中依据不同的 pod 序号选择挂载不同的配置即可。

 

下面是一个 ConfigMap 的示例:

 

apiVersion: v1 kind: ConfigMap metadata: name: mysql labels: app: mysql data: master.cnf: | # 主节点MySQL的配置文件 [mysqld] log-bin slave.cnf: | # 从节点MySQL的配置文件 [mysqld] super-read-only

 

 

3.2 用 Service 实现主从的读写分离

 

接下来,我们创建两个 Service,来实现对主从的读写分离:

 

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

 

 

由于第一个 Service 配置了 clusterIP: None,所以它是一个 Headless Service,也就是它会代理编号为 0 的节点,也就是主节点。

 

而第二个 Service,由于在 selector 中指定了 app: mysql,所以它会代理所有具有这个 label 的节点,也就是集群中的所有节点。

 

 

有了上述两部分准备工作,我们就要开始着手构建我们的 MySQL 集群了,那么,最为首要的当然就是如何去初始化整个集群的各个节点了。

 

集群启动前,所需的初始化步骤有:

 

  1. 各个节点正确获取对应的 ConfigMap 中的配置文件,并且放置在 mysql 配置文件所在的路径。
  2. 如果节点是从节点,那么需要先将数据拷贝到对应路径下。
  3. 在从节点上执行数据初始化命令。

 

这些操作我们可以通过定义一系列 InitContainers 来实现。

 

4.1 正确获取节点对应的配置文件

 

对于 StatefulSet 而言,每个 pod 各自的 hostname 中所具有的序号就是它们的唯一 id,因此我们可以通过正则表达式来获取这个 id,并且规定 id 为 0 表示主节点,于是,通过判断 server 的 id,就可以对 ConfigMap 中不同的配置进行获取了:

 

... # 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 来避开 0 echo server-id=$((100 + $ordinal)) >> /mnt/conf.d/server-id.cnf # 如果Pod序号是0,说明它是Master节点,拷贝 master 配置 # 否则,拷贝 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

 

 

4.2 在从节点中实现数据拷贝

 

按照上一小节中的例子,我们已经知道如何去判断当前节点是否是 Master 节点,于是,我们很容易实现只在 Slave 节点中并且数据不存在的情况下进行数据拷贝操作:

 

... # 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

 

 

在这个示例中,我们使用了 ncat 命令实现从上一个已经启动的节点拷贝数据到当前节点,并且使用了第三方的备份还原工具 xtrabackup 来实现数据的恢复。

 

 

5.1 从节点启动前的数据初始化与恢复

 

在 initContainers 中,我们实现了在从节点中,将上一个节点的备份数据拷贝到当前节点的工作,那么,接下来我们就要去恢复这个数据了。

 

与此同时,我们还需要在 mysql 的实际运行中实时执行数据的同步、恢复与备份工作。上文提到的 xtrabackup 很方便的实现了这一系列功能。我们可以将这个集成工具作为一个 sidecar 启动,完成上述这些操作:

 

... # 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

 

 

5.2 MySQL 容器的启动

 

接下来,我们就要启动我们的 MySQL 容器了:

 

... # 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 5.7 版本镜像,并且挂载了在 initContainers 中已经完成数据与配置初始化的路径作为数据路径与配置路径。

 

同时我们配置了健康监测:

 

  1. livenessProbe:存活探针,定时执行命令,来检测节点是否存活,如果检测失败,则自动重启节点;
  2. readinessProbe:就绪探针,在启动后周期执行指令,只有当指令执行成功后,才允许 Service 将请求转发给节点。

 

 

有了上述所有的描述,我们已经完整构建出了一个支持横向扩展的 MySQL 主从集群的搭建,他的配置如下:

 

 

 

 

执行 kubectl create 命令我们就可以让这个集群运行起来了:

 

\$ kubectl create -f mysql-statefulset.yaml

 

接下来我们就可以通过调用对应的 Service 来实现 SQL 的执行,例如我们希望在主节点上执行建表与插入操作:

 

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-read 这个 service 来实现读操作:

 

$ 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 | +---------+

 

 

 

欢迎关注微信公众号,以技术为主,涉及历史、人文等多领域的学习与感悟,每周三到七篇推文,只有全部原创,只有干货没有鸡汤

 

linux 使用及配置相关






kubernetes      pod      容器化      云原生      statefulset      网络状态      perstentvolume     


京ICP备2021035038号