额。。。你没看错,就是在 Kubernetes 中部署 MySQL 主从,虽然我感觉 MySQL 丢 Kubernetes 有点蛋疼吧,但没办法,公司要求我只能照做了。。
思路
就直接说一下编写下面 YAML 的思路了:
- 首先,创建了一个
ConfigMap
,其中包含了 MySQL 配置文件的模板,用于动态为各个 MySQLPod
生成配置文件,这个功能由StatefulSet
中的initContainers
中的mysql-sidecar-init
容器提供,在模板中可以看到已经开启了binlog
和relaylog
了; - 然后创建了一个
StatefulSet
,就是用它来管理 MySQLPod
,它有一个特点就是Pod
名称是有编号的,比如下面的StatefulSet
名称为mysql-server
,那么它管理的Pod
名称就为mysql-server-0
、mysql-server-1
、mysql-server-2
...; - 所以这里我就约定第一个创建的
Pod
为主库,后续所有创建的Pod
都是第一个Pod
的从库,所以这里就可以确定主库的名字就一直是mysql-server-0
了; - 现在唯一的一个问题就是如何构建主从关系了,这里就要用到 Kubernetes 内部的 DNS 了,在 Kubernetes 中可以通过
<pod_name>.<service_name>.<namespace>.svc.cluster.local
的方式来访问名为<pod_name>
的Pod
,所以在下面的示例中,主库的访问地址就固定为mysql-server-0.mysql-server.test.svc.cluster.local
了,<server_name>
通过环境变量SERVICE_NAME
传入,<pod_name>
和namespace
直接通过 DownwardAPI 获取; - 主库的地址已经确定了,最后就要考虑的是如何去执行 SQL 了,因为构建 MySQL 主从关系首先需要在主库创建复制用户,然后在从库执行
change master ...
,这里就用到了Pod
的生命周期钩子了,如下通过lifecycle.postStart
指定了 Pod 处于running
状态之前执行的命令,这个命令指定了运行一个/opt/run
二进制文件,它是我用python
的并打包好的二进制文件(需要源码的可以留言邮箱我发,绝对无毒。),可以自动根据上述逻辑完成主从关系的维护。当然,在这里你完全可以在钩子这写一大片 Shell 来完成我上述所说的功能,但是实在是太不优雅了? - 最后就可以运行这个 YAML 了,当然前提是你也准备好了
StorageClassName
提供 PV 的动态创建供给,我这里底层存储用的 Ceph,当然你也可以用 NFS 或其它的;
效果
$ kubectl apply -f mysql-cluster.yml
configmap/mysql-config-template created
statefulset.apps/mysql-server created
service/mysql-server created
# 因为 replicas 指定为 2,所以只会创建两个 Pod
$ kubectl get pod -n test
NAME READY STATUS RESTARTS AGE
mysql-server-0 1/1 Running 0 45m
mysql-server-1 1/1 Running 0 42m
# 检查主库的状态,可以看到 binlog 成功开启了
kubectl -n test exec mysql-server-0 -- mysql -uroot -p123456 -e 'show master status;'
mysql: [Warning] Using a password on the command line interface can be insecure.
File Position Binlog_Do_DB Binlog_Ignore_DB Executed_Gtid_Set
mysql-bin.000001 441 62347151-11e9-11eb-8bb2-0615ac65ca6c:1-10
# 检查从库的状态,可以看到 IO 线程和 SQL 线程也正常了
$ kubectl -n test exec mysql-server-1 -- mysql -uroot -pspeakin_root -e 'show slave status\G;' | grep -i 'running'
mysql: [Warning] Using a password on the command line interface can be insecure.
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates
# 测试一下扩容
$ kubectl -n test scale sts mysql-server --replicas=3
statefulset.apps/mysql-server scaled
# 查看 Pod 个数
$ kubectl get pod -n test
NAME READY STATUS RESTARTS AGE
mysql-server-0 1/1 Running 0 52m
mysql-server-1 1/1 Running 1 49m
mysql-server-2 1/1 Running 0 45s
# 检查新扩容的从库 Pod 状态
$ kubectl -n test exec mysql-server-2 -- mysql -uroot -pspeakin_root -e 'show slave status\G;' | grep -i 'running'
mysql: [Warning] Using a password on the command line interface can be insecure.
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates
YAML
镜像我已经提交到了公开的阿里云仓库,所以下面 YAML 的镜像可以直接使用。
apiVersion: v1
kind: ConfigMap
metadata:
name: mysql-config-template
namespace: test
data:
# MySQL 配置模板,my.cnf 中不可包含中文
my.cnf: |
[mysqld]
max_connections=1000
innodb_flush_log_at_trx_commit=2
innodb_buffer_pool_size=1G
# general_log_file = /var/log/mysql/mysql.log
# general_log = 1
# log_error = /var/log/mysql/error.log
server-id={{server_id}} # slave set 2 or other number
log_bin = /var/log/mysql/mysql-bin.log
expire_logs_days = 60
max_binlog_size = 100M
#binlog_do_db = include_database_name
#binlog_ignore_db = include_database_name
gtid_mode=on
enforce_gtid_consistency=on
#log-slave-updates=1
#binlog_format=row
#skip_slave_start=1
#log-slave-updates=true
#auto_increment_offset=1
#auto_increment_increment=2
character-set-server=utf8
[client]
default-character-set=utf8
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql-server
namespace: test
spec:
selector:
matchLabels:
app: mysql-server
serviceName: mysql-server
replicas: 2
template:
metadata:
labels:
app: mysql-server
spec:
containers:
- name: mysql-server
image: registry.cn-shenzhen.aliyuncs.com/zze/mysql-for-cluster:5.7.26
imagePullPolicy: IfNotPresent
livenessProbe:
tcpSocket:
port: 3306
initialDelaySeconds: 15
periodSeconds: 5
ports:
- containerPort: 3306
name: mysql-server
env:
# MySQL ROOT 用户密码
- name: MYSQL_ROOT_PASSWORD
value: "123456"
# 要创建的用于主从复制的用户
- name: REPL_USER
value: "rep"
# 主从复制用户的密码
- name: REPL_USER_PWD
value: "rep123"
# 暴露这个 StatefulSet 的 Service 名称,因为是根据域名访问主库的,所以需要 Service 名称拼接 FQDN,格式:<pod_name>.<service.name>.<namespace>.svc.cluster.local
- name: SERVICE_NAME
value: "mysql-server"
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
# 从库第一次启动时尝试连接主库的次数,超过则认为连接失败,放弃创建主从关系,默认为 100 次
- name: INIT_TRY_CONNECT_MASTER_COUNT
value: "200"
# 从库重启时尝试连接主库的次数,超过则认为连接失败,放弃维护主从关系,默认为 10 次
- name: POST_TRY_CONNECT_MASTER_COUNT
value: "5"
args:
- "--ignore-db-dir=lost+found"
volumeMounts:
- name: test-mysql-data
mountPath: /var/lib/mysql
- name: config
mountPath: /etc/mysql/conf.d
lifecycle:
postStart:
exec:
command:
- /opt/run
- create_cluster
initContainers:
# 初始化容器,作用是根据 ConfigMap 中的 my.cnf 模板动态创建 MySQL 配置文件
- name: mysql-sidecar-init
image: registry.cn-shenzhen.aliyuncs.com/zze/mysql-sidecar:1.0
imagePullPolicy: IfNotPresent
volumeMounts:
- name: mysql-config-template
mountPath: /template
- name: config
mountPath: /config
command:
- /opt/run
- init_conf
volumes:
- name: mysql-config-template
configMap:
name: mysql-config-template
- name: config
emptyDir: {}
volumeClaimTemplates:
- metadata:
name: test-mysql-data
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "ceph-sc-mysql"
resources:
requests:
storage: 512Mi
---
apiVersion: v1
kind: Service
metadata:
name: mysql-server
namespace: test
spec:
ports:
- port: 3306
protocol: TCP
targetPort: 3306
selector:
app: mysql-server
type: NodePort
评论区