一、需求描述
1、自然语言来描述
-
是一个“主从复制”(Maser-Slave Replication)的 MySQL 集群;
-
有 1 个主节点(Master);
-
有多个从节点(Slave);
-
从节点需要能水平扩展;
-
所有的写操作,只能在主节点上执行;
-
读操作可以在所有节点上执行。
2、图形描述
二、需求分析
1、通过 XtraBackup 将 Master 节点的数据备份到指定目录。
$ cat xtrabackup_binlog_info TheMaster-bin.000001 481
2、配置 Slave 节点
Slave 节点在第一次启动前,需要先把 Master 节点的备份数据,连同备份信息文件,一起拷贝到自己的数据目录(/var/lib/mysql)下。然后,我们执行这样一句 SQL:
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 节点
TheSlave|mysql> START SLAVE;
这样,Slave 节点就启动了。它会使用备份信息文件中的二进制日志文件和偏移量,与主节点进行数据同步。
4、在这个集群中添加更多的 Slave 节点
需要注意的是,新添加的 Slave 节点的备份数据,来自于已经存在的 Slave 节点
通过上面的叙述,我们不难看到,将部署 MySQL 集群的流程迁移到 Kubernetes 项目上,需要能够“容器化”地解决下面的“三座大山”:
-
Master 节点和 Slave 节点需要有不同的配置文件(即:不同的 my.cnf);
-
Master 节点和 Salve 节点需要能够传输备份信息文件;
-
在 Slave 节点第一次启动之前,需要执行一些初始化 SQL 操作;
三、第一座大山:Master 节点和 Slave 节点需要有不同的配置文件
1、思路
2、MySQL 的配置文件
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 的定义如下所示:
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,如下所示
... # 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,如下所示:
... # 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,如下所示
... # 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 容器,来完成这个操作,它的定义如下所示:
... # 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 容器本身的定义就非常简单了,如下所示:
... # 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:
$ 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 的机械劳动。我在后续讲解容器存储的时候,会再详细介绍这个机制。
备注:在使用 Rook 的情况下,mysql-statefulset.yaml 里的 volumeClaimTemplates 字段需要加上声明 storageClassName=rook-ceph-block,才能使用到这个 Rook 提供的持久化存储
然后,我们就可以创建这个 StatefulSet 了,如下所示:
$ 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 操作来验证它是否正常:
$ 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 进行读操作,如下所示:
$ 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 集群,比如:
$ kubectl scale statefulset mysql --replicas=5
这时候,你就会发现新的 Slave Pod mysql-3 和 mysql-4 被自动创建了出来。
而如果你像如下所示的这样,直接连接 mysql-3.mysql,即 mysql-3 这个 Pod 的 DNS 名字来进行查询操作:
$ 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、关键点(坑)
人格分裂
阅后即焚
容器之间平等无序