1 概述
在K8S环境中,配置单个MySQL是非常方便的,但如果需要配置MySQL主从,则比较麻烦。
配置MySQL主从有下面三个特殊的地方:
- 主库的配置文件和从库的配置文件,也就是需要根据主从的不同使用不同的配置文件。
- 从库开始的时候,主库可能已经有数据了,需要备份这些数据到从库作为初始数据。
- 从库启动前,需要连接主库并指定同步文件的名称和从哪个位置开始同步数据,然后启动从库。
如果是手工搭建MySQL主从,上面这些特殊地方手工一个个完成即可。但在K8S环境中,MySQL主从属于有状态的,需要由statefulset来编排,也就是上面那些特殊地方,需要用Pod中的容器来协助完成,才能够纳入到statefulset中进行编排,否则就无法做到自动多一个或者少一个从库。
2 配置
2.1 yaml配置
1、设定namespace
采用了自定义namespace,好处是pod、statefulset、service等的名称不怕重复,这也是推荐的一种实践。下面除了PV对象之外,其它对象都加上此namespace,方便管理,缺点是后面查询对象的命令都需要加上-n mysql-demo03参数指定namespace来查询。所有对象都加上相同的label,方便需要全部查询对象的场景。
XML
# mysql-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: mysql-demo03
labels:
app: mysql
2、主从库的配置文件
主库主要是开启binlog,使得从库可以同步binlog文件来保证数据与主库一致。从库则需要设置只读,不支持写入数据。
XML
# mysql-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mysql
namespace: mysql-demo03
labels:
app: mysql
data:
master.cnf: |
[mysqld]
log-bin
slave.cnf: |
[mysqld]
super-read-only
3、设置MySQL数据库的访问密码
使用Secret类型时,数据需要用Base64编码,可以在Linux命令行中执行命令进行Base64编码:echo -n "123456" | base64
XML
# mysql-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: mysql-secret
namespace: mysql-demo03
labels:
app: mysql
type: Opaque
data:
password: MTIzNDU2
4、开放访问端口
使用NodePort类型的Service,支持从外部访问MySQL,注意在Windows系统需要使用WSL的IP,Linux则可以使用宿主机的IP。
XML
# mysql-service.yaml
apiVersion: v1
kind: Service
metadata:
name: mysql
namespace: mysql-demo03
labels:
app: mysql
spec:
type: NodePort
ports:
- name: mysql
port: 3306
nodePort: 31306
selector:
app: mysql
---
apiVersion: v1
kind: Service
metadata:
name: mysql-read
namespace: mysql-demo03
labels:
app: mysql
spec:
ports:
- name: mysql
port: 3306
selector:
app: mysql
5、定义文件目录类型的PV
作为例子,使用宿主机上的目录作为PV最简单。注意Windows系统中需要用WSL里面的的目录,Linux上可以直接使用宿主机上的目录。也可以配置NFS之类的来提供PV。注意storageClassName,需要在下面是statefulset中映射上,否则PVC无法正确绑定PV。volumeClaimTemplates中补充storageClassName的配置,用于匹配PV。
XML
# mysql-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: demo03-mysql-pv01
labels:
app: mysql
spec:
storageClassName: demo03-mysql-pv
accessModes:
- ReadWriteOnce
capacity:
storage: 1Gi
hostPath:
path: /mnt/host/d/mysql_demo03/volume-pv01
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: demo03-mysql-pv02
labels:
app: mysql
spec:
storageClassName: demo03-mysql-pv
accessModes:
- ReadWriteOnce
capacity:
storage: 1Gi
hostPath:
path: /mnt/host/d/mysql_demo03/volume-pv02
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: demo03-mysql-pv03
labels:
app: mysql
spec:
storageClassName: demo03-mysql-pv
accessModes:
- ReadWriteOnce
capacity:
storage: 1Gi
hostPath:
path: /mnt/host/d/mysql_demo03/volume-pv03
6、编排容器
使用statefulset编排容器,使用两个initContainers,一个通过提取容器的编号来区分主从,从而获取不同的配置;第二个组装启动从库的同步SQL。还提供了一个常规容器xtrabackup来备份数据,备份数据用来初始化从库数据和提供同步初始信息。
XML
# mysql-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
namespace: mysql-demo03
labels:
app: mysql
spec:
selector:
matchLabels:
app: mysql
serviceName: mysql
replicas: 3
template:
metadata:
labels:
app: mysql
spec:
initContainers:
- name: init-mysql
image: ubuntu:20.04
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: password
command:
- bash
- "-c"
- |
set -ex
# statefulset为pod生成hostname,里面包含序号,提取序号来作为server-id,并区分主从(第0个为主、其它为从)
[[ $(hostname) =~ -([0-9]+)$ ]] || exit 1
ordinal=${BASH_REMATCH[1]}
echo [mysqld] > /mnt/conf.d/server-id.cnf
# 由于server-id不能为 0,因此给ID加个数来避开它
echo server-id=$((100 + $ordinal)) >> /mnt/conf.d/server-id.cnf
# 如果pod序号为0则为主,其它为从,区分主从获取不同的配置
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
- name: clone-mysql
image: ist0ne/xtrabackup:1.0
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: password
command:
- bash
- "-c"
- |
set -ex
# 一个新的MySQL需要拷贝已有的数据,如果数据已经存在,就不需要再次拷贝了
[[ -d /var/lib/mysql/mysql ]] && exit 0
# 从hostname中提取pod序号
[[ $(hostname) =~ -([0-9]+)$ ]] || exit 1
ordinal=${BASH_REMATCH[1]}
# 主MySQL不用拷贝数据
[[ $ordinal == 0 ]] && exit 0
# 从前一个序号的MySQL中拷贝数据
ncat --recv-only mysql-$(($ordinal-1)).mysql 3307 | xbstream -x -C /var/lib/mysql
xtrabackup --prepare --target-dir=/var/lib/mysql
volumeMounts:
- name: data
mountPath: /var/lib/mysql
subPath: mysql
- name: conf
mountPath: /etc/mysql/conf.d
containers:
- name: mysql
image: mysql:5.7
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: password
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", "-uroot", "-p${MYSQL_ROOT_PASSWORD}"]
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
readinessProbe:
exec:
command: ["mysqladmin", "ping", "-uroot", "-p${MYSQL_ROOT_PASSWORD}"]
initialDelaySeconds: 5
periodSeconds: 2
timeoutSeconds: 1
- name: xtrabackup
image: ist0ne/xtrabackup:1.0
ports:
- name: xtrabackup
containerPort: 3307
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: password
command:
- bash
- "-c"
- |
set -ex
cd /var/lib/mysql
# 启动从库前,需要执行"CHANGE MASTER TO" SQL,里面需要提供MASTER_LOG_FILE(binlog文件名)和MASTER_LOG_POS(同步数据位置)
# 如果通过xtrabackup备份了数据文件,则可以从备份文件里获取到这两个信息
if [[ -f xtrabackup_slave_info ]]; then
# xtrabackup_slave_info文件来自于从库,文件里已经包含"CHANGE MASTER TO" SQL,改名字后直接使用即可
mv xtrabackup_slave_info change_master_to.sql.in
rm -f xtrabackup_binlog_info
elif [[ -f xtrabackup_binlog_info ]]; then
# xtrabackup_binlog_info文件来自于主库,需要从中提取MASTER_LOG_FILE和MASTER_LOG_POS并组装"CHANGE MASTER TO" SQL
[[ $(cat xtrabackup_binlog_info) =~ ^(.*?)[[:space:]]+(.*?)$ ]] || exit 1
rm xtrabackup_binlog_info
echo "CHANGE MASTER TO MASTER_LOG_FILE='${BASH_REMATCH[1]}',\
MASTER_LOG_POS=${BASH_REMATCH[2]}" > change_master_to.sql.in
fi
if [[ -f change_master_to.sql.in ]]; then
# 等待MySQL容器ready之后才能执行上面组装的SQL
echo "Waiting for mysqld to be ready(accepting connections)"
until mysql -uroot -p${MYSQL_ROOT_PASSWORD} -e "SELECT 1"; do sleep 1; done
echo "Initializing replication from clone position"
mv change_master_to.sql.in change_master_to.sql.orig
# 执行"CHANGE MASTER TO" SQL,并启动Slave库同步
mysql -uroot -p${MYSQL_ROOT_PASSWORD} << EOF
$(< change_master_to.sql.orig),
MASTER_HOST='mysql-0.mysql.weixnie',
MASTER_USER='root',
MASTER_PASSWORD='${MYSQL_ROOT_PASSWORD}',
MASTER_CONNECT_RETRY=10;
START SLAVE;
EOF
fi
# ncat监听3307端口,有新数据则备份
exec ncat --listen --keep-open --send-only --max-conns=1 3307 -c \
"xtrabackup --backup --slave-info --stream=xbstream --user=root --password=${MYSQL_ROOT_PASSWORD}"
volumeMounts:
- name: data
mountPath: /var/lib/mysql
subPath: mysql
- name: conf
mountPath: /etc/mysql/conf.d
volumes:
- name: conf
emptyDir: {}
- name: config-map
configMap:
name: mysql
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi
storageClassName: demo03-mysql-pv
volumeMode: Filesystem
通过kubectl apply -f xx.yaml,按顺序执行上面yaml文件即可。
2.2 一些问题
1、容器init-mysql需要使用正确的镜像。
这个容器中用到了bash脚本,需要使用提供bash命令的镜像,比如上例换成了ubuntu:20.04镜像,提供完整的Linux命令,方便测试时可以多一些命令可以使用,缺点是镜像比较大。正式使用时可以选用小一点的镜像。
2、Service需要在Statefulset之前执行
Statefulset中从库需要连接主库,由于Pod本身的IP是不确定的,所以需要Service来提供稳定的连接服务。如果Service后执行,需要重启Statefulset。
3、不能使用127.0.0.1 IP
Statefulset里面有bash脚本中访问MySQL库的时候,如果用了-h 127.0.0.1,带着这个参数实际是访问不了的,在Pod中并不认这个IP,应该使用Service名称访问。
4、配置好PV
没有PV的时候,Pod也启动不起来。需要先配置好PV,最简单的配置PV方式就是使用宿主机的目录,如果是Windows系统,需要注意要把宿主机的目录转为WSL里的目录,上例使用的就是Windows的目录。
2.3 检验
1、使用kubectl get pod -n mysql-demo03查看Pod的状态,观察STATUS字段需要为Running,READY字段里的两个数字要相等,这样Pod才正常运行。
2、使用kubectl exec -it mysql-0 -n mysql-demo03 -- /bin/bash 进入Pod,用mysql -uroot -p命令登录MySQL,在里面能够正常执行SQL命令。主库可查可写,从库只能查不能写。
3、修改statefulset里的replicas字段值,增减从库数量,通过kubectl rollout status sts/mysql 命令查看从库的增减过程。
4、如果遇到问题,则使用kubectl logs pod_name -c container_name -n mysql-demo03来查看具体的日志,根据日志解决问题。
3 小结
使用K8S来部署主从,有点在yaml里写shell脚本的嫌疑,如果不使用Go语言来自定义Operator,就只能先用这种方式了。