如遇图片无法加载请使用代理访问

前言

本文主要讲述 2023 K3s Rancher 如何部署测试 集群HA MySQL

如果你还没有部署 K3s 和 Rancher ,你可以浏览上一篇文章:【K3S】01 - 异地集群初始化

如果你没部署过 单节点 的MySQL ,你可以浏览这篇文章:【K3S】02 - Rancher 中间件单节点部署

请注意,即便这是k8s默认的MySQL集群搭建例子,它依旧不适用于生产环境,不适用的原因请查看章节 反思与探索,如果你想部署生产环境适用的MySQL HA,请查看文章 Vitess


环境声明

hostname 系统 配置 节点 角色 部署
m1 Ubuntu-Server(20.04) 2c4g 192.168.0.67/32 control-plane,etcd,master k3s(v1.24.6+k3n1) server
nginx
rancher(2.7.1)
Helm(3.10.3)
n1 Ubuntu-Server(20.04) 1c2g 192.168.0.102/32 control-plane,etcd,master k3s(v1.24.6+k3n1) server
m2 Ubuntu-Server(20.04) 2c4g 172.25.4.244/32 control-plane,etcd,master k3s(v1.24.6+k3n1) server
harbor Ubuntu-Server(20.04) 2c4g 192.168.0.88 Docker-Hub
Jenkins CI/CD
Harbor(2.7.1)
Jenkins(2.3)
Docker-Compose

节点均用 WireGuard 打通内网,后续所有节点路由均用内网ip访问,具体详细的节点内容请访问上一篇文章


部署MySQL

概念剖析

在部署之前,我们先搞清楚几个概念

什么是MySQL主从

MySQL主从复制是一种在多个MySQL数据库服务器之间同步数据的方法。主服务器(Master)是一个用于写操作的服务器,而从服务器(Slave)是用于读操作和备份的服务器

其基本原理如下:

  1. 首先,从库连接主库 并请求复制数据。在连接时,从库向主库发送一个“同步请求”,主库则向从库发送“同步响应”,表示从库已经与主库成功连接,并且可以接收来自主库的变更日志

  2. 当主库中的数据发生变更时,主库会将这些变更记录在二进制日志(Binary Log)中,同时发送到所有的从库中。二进制日志是MySQL的一种日志格式,它记录了对MySQL数据库中数据的 增删改操作

  3. 当从库接收到来自主库的二进制日志时,它会将日志中的内容写入到自己的 中继日志Relay Log)中。中继日志是从库的一种日志格式,它记录了从主库接收到的二进制日志的内容

  4. 从库会将 中继日志 中的内容应用到自己的数据库中,以更新自己的数据,从而实现与主库的同步

  5. 当从库和主库之间的网络连接出现问题时,从库会 尝试重新连接 主库,以保证数据的同步性

需要注意的是,主从复制并 不是实时同步 ,而是存在一定的延迟。这是因为主库中的数据变更需要经过网络传输和从库的处理,才能最终同步到从库中。因此,在使用主从复制时,需要根据业务需要和数据量等因素来 合理设置延迟时间

主从复制有以下几个意义:

  1. 提高系统性能:主从复制可以 分担主服务器的读负载 ,从而提高整个系统的读性能。此外,从服务器还可以 用于处理备份 ,从而减轻主服务器的负担
  2. 提高数据安全性:主从复制可以保证数据的备份和恢复,从而提高数据的安全性。如果主服务器发生故障,可以 快速切换 到从服务器上,从而避免数据的丢失
  3. 分布式数据处理:主从复制可以实现 分布式 数据处理,将 数据分散在不同的服务器上,从而提高整个系统的处理能力
  4. 实现数据分离:主从复制可以将不同的数据分离到不同的服务器上,从而实现 数据分离和管理 ,使整个系统更加灵活和可控

什么是MySQL主备

MySQL主备是一种高可用性架构,它使用两个MySQL服务器:一个主服务器(Master)和一个备份服务器(Slave)。主服务器处理所有的写操作(INSERT,UPDATE,DELETE等),而备份服务器则作为主服务器的备份,它将主服务器上的所有数据变更复制到自己本地的数据副本中。

当主服务器发生故障时,备份服务器会 自动接管并成为新的主服务器 ,从而确保数据库服务的可用性。这种主备架构可以提高数据库的可用性和可靠性,并减少数据丢失的风险。

除了数据备份之外,备份服务器还可以用于负载均衡,提高数据库的性能和可扩展性。通过将读操作分配给备份服务器处理,主服务器可以专注于处理写操作,从而提高整个系统的吞吐量和响应速度。

总的来说,MySQL主备架构是一种可靠的数据库解决方案,可以提供高可用性、高性能和数据备份等多种优势。


主备和主从的区别

他们都可以用作单节点读写的特性,但主备和主从的区别在于,它可以将 备用节点升级为主节点 ,而主从是不会有自主升级主节点的概念,而且主节点是支持读写的,而从节点一般只读不写。所以说,主从架构适合于 读多写少 的场景,而MySQL主备架构更适合于 读写操作均衡 或者 写操作更多 的场景。

所以一般小型的HA集群会部署一主两从的主从模式,到半夜的时候,让从节点开始进行备份、导出工作,中大型的集群会部署三主三从,当然,并不是越多越好,因为节点越多,延时越长,所以如果你想快速写入,可能Cassandra等列式数据库更能符合你的需求


xtrabackup 是什么

xtrabackup 是一个用于 MySQL 数据库备份和恢复的开源工具,由 Percona 公司开发。它通过采用基于流的备份和增量备份技术,可以在 不停止 MySQL 数据库的情况下对数据库进行备份和还原,从而避免了生产环境中的数据库停机时间。

xtrabackup 的主要作用包括:

  1. 备份 MySQL 数据库 :xtrabackup 可以创建 MySQL 数据库的完整备份,包括数据文件、日志文件、表结构等,并且备份过程中不会锁定表,从而避免了生产环境中的数据库停机时间。
  2. 增量备份 :xtrabackup 支持增量备份,可以只备份自上次完整备份或增量备份以来的更改,从而减小备份的体积和时间。
  3. 快速恢复 :xtrabackup 提供了快速恢复备份的功能,可以将备份文件还原到 MySQL 数据库,并且在还原过程中可以自动应用日志,从而保证数据库的一致性。
  4. 高性能 :xtrabackup 通过采用多线程和异步 I/O 等技术,可以在备份和还原过程中实现较高的性能,从而减小备份和恢复的时间窗口。
  5. 验证备份 :xtrabackup 提供了验证备份文件的功能,可以验证备份文件的完整性和一致性,确保备份文件可用于恢复数据库。
  6. 支持多种 MySQL 存储引擎 :xtrabackup 不仅支持 Percona Server 和 MySQL,还支持其他常见的 MySQL 存储引擎,如 InnoDB、MyISAM、Archive 等。
  7. 高度灵活 :xtrabackup 提供了丰富的备份和还原选项,可以根据需求进行配置,如备份类型、备份内容、压缩方式、加密等,从而满足不同场景下的备份和还原需求。

总的来说,xtrabackup 是一个强大的 MySQL 数据库备份和恢复工具,可以在生产环境中实现高效、高性能的数据库备份和恢复操作,降低了数据库备份和恢复对生产环境的影响。


部署MySQL5.7主从

在本小节中,我们将尝试用k8s官方的例子来部署一个MySQL5.7主从集群,这里就不像上一章节的Rancher那样,而是用YAML的方式代替可视化操作

原理

img

不难发现,主从节点的一致性是通过日志来驱动的,而这个日志的由来必须是主节点的Binary Log,所以主节点必须先启动,并且具有存储状态,从节点通过Relay Log 保持数据一致性,因此,用 Deployment 作为控制器来进行 mysql 集群的搭建是无法实现的,而这恰恰是 StatefulSet 擅长处理的场景

从节点需要配置只读操作,才可分散主节点读压力,才能更好的水平扩展


配置ConfigMap

本配置仅为测试使用,请勿部署在生产模式中

kubectl apply -f https://k8s.io/examples/application/mysql/mysql-configmap.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: ConfigMap
metadata:
name: mysql
labels:
app: mysql
app.kubernetes.io/name: mysql
data:
primary.cnf: |
# Apply this config only on the primary.
[mysqld]
log-bin
replica.cnf: |
# Apply this config only on replicas.
[mysqld]
super-read-only

这个ConfigMap用于配置 主从的配置文件 ,其中:

  • primary.cnf 是主实例的配置文件,replica.cnf 是副本(从节点)的配置文件
  • log-bin 用于启动二进制日志,记录了数据库所有的增删改操作,并传输给从节点的中继日志,实现数据同步的效果
  • super-read-only 用于标识从节点仅支持读操作,用于分散主节点压力,加强集群读能力

配置Service

kubectl apply -f https://k8s.io/examples/application/mysql/mysql-services.yaml

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
# Headless service for stable DNS entries of StatefulSet members.
apiVersion: v1
kind: Service
metadata:
name: mysql
labels:
app: mysql
app.kubernetes.io/name: mysql
spec:
ports:
- name: mysql
port: 3306
clusterIP: None
selector:
app: mysql
---
# Client service for connecting to any MySQL instance for reads.
# For writes, you must instead connect to the primary: mysql-0.mysql.
apiVersion: v1
kind: Service
metadata:
name: mysql-read
labels:
app: mysql
app.kubernetes.io/name: mysql
readonly: "true"
spec:
ports:
- name: mysql
port: 3306
selector:
app: mysql

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

而第二个 Service,由于在 selector 中指定了 app: mysql,所以它会代理所有具有这个 label 的节点,也就是集群中的所有节点。readonly: "true"标签标识从节点只读。请注意,只有读查询才能使用负载均衡的客户端 Service

在同一 Kubernetes 集群和命名空间中的任何其他 Pod 内解析 <Pod 名称>.mysql 来访问 Pod (等部署完我们再测试下)


配置StatefulSet

在配置StatefulSet前,我们首先思考容器启动的准备工作:

  • 所有节点需要读取指定位置下的配置文件 primaty.cnfreplica.cnf,因此我们首先需要 在容器内映射这两个文件
  • 从节点需要拷贝主节点的binlog
  • 从节点需要还原binlog的内容

这里官方模板是使用的本地存储,我们需要改为NFS(此小节末尾处有详细的解释说明)

wget https://k8s.io/examples/application/mysql/mysql-statefulset.yaml

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
selector:
matchLabels:
app: mysql
app.kubernetes.io/name: mysql
serviceName: mysql
replicas: 3
template:
metadata:
labels:
app: mysql
app.kubernetes.io/name: mysql
spec:
initContainers:
- name: init-mysql
image: mysql:5.7
command:
- bash
- "-c"
- |
set -ex
# Generate mysql server-id from pod ordinal index.
[[ $HOSTNAME =~ -([0-9]+)$ ]] || exit 1
ordinal=${BASH_REMATCH[1]}
echo [mysqld] > /mnt/conf.d/server-id.cnf
# Add an offset to avoid reserved server-id=0 value.
echo server-id=$((100 + $ordinal)) >> /mnt/conf.d/server-id.cnf
# Copy appropriate conf.d files from config-map to emptyDir.
if [[ $ordinal -eq 0 ]]; then
cp /mnt/config-map/primary.cnf /mnt/conf.d/
else
cp /mnt/config-map/replica.cnf /mnt/conf.d/
fi
volumeMounts:
- name: conf
mountPath: /mnt/conf.d
- name: config-map
mountPath: /mnt/config-map
- name: clone-mysql
image: gcr.io/google-samples/xtrabackup:1.0
command:
- bash
- "-c"
- |
set -ex
# Skip the clone if data already exists.
[[ -d /var/lib/mysql/mysql ]] && exit 0
# Skip the clone on primary (ordinal index 0).
[[ `hostname` =~ -([0-9]+)$ ]] || exit 1
ordinal=${BASH_REMATCH[1]}
[[ $ordinal -eq 0 ]] && exit 0
# Clone data from previous peer.
ncat --recv-only mysql-$(($ordinal-1)).mysql 3307 | xbstream -x -C /var/lib/mysql
# Prepare the backup.
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_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:
# Check we can execute queries over TCP (skip-networking is off).
command: ["mysql", "-h", "127.0.0.1", "-e", "SELECT 1"]
initialDelaySeconds: 5
periodSeconds: 2
timeoutSeconds: 1
- name: xtrabackup
image: gcr.io/google-samples/xtrabackup:1.0
ports:
- name: xtrabackup
containerPort: 3307
command:
- bash
- "-c"
- |
set -ex
cd /var/lib/mysql

# Determine binlog position of cloned data, if any.
if [[ -f xtrabackup_slave_info && "x$(<xtrabackup_slave_info)" != "x" ]]; then
# XtraBackup already generated a partial "CHANGE MASTER TO" query
# because we're cloning from an existing replica. (Need to remove the tailing semicolon!)
cat xtrabackup_slave_info | sed -E 's/;$//g' > change_master_to.sql.in
# Ignore xtrabackup_binlog_info in this case (it's useless).
rm -f xtrabackup_slave_info xtrabackup_binlog_info
elif [[ -f xtrabackup_binlog_info ]]; then
# We're cloning directly from primary. Parse binlog position.
[[ `cat xtrabackup_binlog_info` =~ ^(.*?)[[:space:]]+(.*?)$ ]] || exit 1
rm -f xtrabackup_binlog_info xtrabackup_slave_info
echo "CHANGE MASTER TO MASTER_LOG_FILE='${BASH_REMATCH[1]}',\
MASTER_LOG_POS=${BASH_REMATCH[2]}" > change_master_to.sql.in
fi

# Check if we need to complete a clone by starting replication.
if [[ -f change_master_to.sql.in ]]; then
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"
mysql -h 127.0.0.1 \
-e "$(<change_master_to.sql.in), \
MASTER_HOST='mysql-0.mysql', \
MASTER_USER='root', \
MASTER_PASSWORD='', \
MASTER_CONNECT_RETRY=10; \
START SLAVE;" || exit 1
# In case of container restart, attempt this at-most-once.
mv change_master_to.sql.in change_master_to.sql.orig
fi

# Start a server to send backups when requested by peers.
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
resources:
requests:
cpu: 100m
memory: 100Mi
volumes:
- name: conf
emptyDir: {}
- name: config-map
configMap:
name: mysql
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi

更改其中的 volumeClaimTemplates,这里的 nfs-client 请更换为你NFS配置的存储类名称 :

1
2
3
4
5
6
7
8
9
10
volumeClaimTemplates:
- metadata:
name: data
annotations:
volume.beta.kubernetes.io/storage-class: nfs-client
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 5Gi

image-20230407140144830

如果你是国内的服务器,可能无法拉取镜像 gcr.io/google-samples/xtrabackup:1.0,此处我们拉取国内镜像registry.cn-hangzhou.aliyuncs.com/hxpdocker/xtrabackup:1.0。如果你想使用私有仓库,请手动拉取此镜像,并上传至私有镜像仓库即可

kubectl apply -f mysql-statefulset.yaml

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
selector:
matchLabels:
app: mysql
app.kubernetes.io/name: mysql
serviceName: mysql
replicas: 3
template:
metadata:
labels:
app: mysql
app.kubernetes.io/name: mysql
spec:
initContainers:
- name: init-mysql
image: mysql:5.7
command:
- bash
- "-c"
- |
set -ex
# Generate mysql server-id from pod ordinal index.
[[ $HOSTNAME =~ -([0-9]+)$ ]] || exit 1
ordinal=${BASH_REMATCH[1]}
echo [mysqld] > /mnt/conf.d/server-id.cnf
# Add an offset to avoid reserved server-id=0 value.
echo server-id=$((100 + $ordinal)) >> /mnt/conf.d/server-id.cnf
# Copy appropriate conf.d files from config-map to emptyDir.
if [[ $ordinal -eq 0 ]]; then
cp /mnt/config-map/primary.cnf /mnt/conf.d/
else
cp /mnt/config-map/replica.cnf /mnt/conf.d/
fi
volumeMounts:
- name: conf
mountPath: /mnt/conf.d
- name: config-map
mountPath: /mnt/config-map
- name: clone-mysql
image: registry.cn-hangzhou.aliyuncs.com/hxpdocker/xtrabackup:1.0
command:
- bash
- "-c"
- |
set -ex
# Skip the clone if data already exists.
[[ -d /var/lib/mysql/mysql ]] && exit 0
# Skip the clone on primary (ordinal index 0).
[[ `hostname` =~ -([0-9]+)$ ]] || exit 1
ordinal=${BASH_REMATCH[1]}
[[ $ordinal -eq 0 ]] && exit 0
# Clone data from previous peer.
ncat --recv-only mysql-$(($ordinal-1)).mysql 3307 | xbstream -x -C /var/lib/mysql
# Prepare the backup.
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_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:
# Check we can execute queries over TCP (skip-networking is off).
command: ["mysql", "-h", "127.0.0.1", "-e", "SELECT 1"]
initialDelaySeconds: 5
periodSeconds: 2
timeoutSeconds: 1
- name: xtrabackup
image: registry.cn-hangzhou.aliyuncs.com/hxpdocker/xtrabackup:1.0
ports:
- name: xtrabackup
containerPort: 3307
command:
- bash
- "-c"
- |
set -ex
cd /var/lib/mysql

# Determine binlog position of cloned data, if any.
if [[ -f xtrabackup_slave_info && "x$(<xtrabackup_slave_info)" != "x" ]]; then
# XtraBackup already generated a partial "CHANGE MASTER TO" query
# because we're cloning from an existing replica. (Need to remove the tailing semicolon!)
cat xtrabackup_slave_info | sed -E 's/;$//g' > change_master_to.sql.in
# Ignore xtrabackup_binlog_info in this case (it's useless).
rm -f xtrabackup_slave_info xtrabackup_binlog_info
elif [[ -f xtrabackup_binlog_info ]]; then
# We're cloning directly from primary. Parse binlog position.
[[ `cat xtrabackup_binlog_info` =~ ^(.*?)[[:space:]]+(.*?)$ ]] || exit 1
rm -f xtrabackup_binlog_info xtrabackup_slave_info
echo "CHANGE MASTER TO MASTER_LOG_FILE='${BASH_REMATCH[1]}',\
MASTER_LOG_POS=${BASH_REMATCH[2]}" > change_master_to.sql.in
fi

# Check if we need to complete a clone by starting replication.
if [[ -f change_master_to.sql.in ]]; then
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"
mysql -h 127.0.0.1 \
-e "$(<change_master_to.sql.in), \
MASTER_HOST='mysql-0.mysql', \
MASTER_USER='root', \
MASTER_PASSWORD='', \
MASTER_CONNECT_RETRY=10; \
START SLAVE;" || exit 1
# In case of container restart, attempt this at-most-once.
mv change_master_to.sql.in change_master_to.sql.orig
fi

# Start a server to send backups when requested by peers.
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
resources:
requests:
cpu: 100m
memory: 100Mi
volumes:
- name: conf
emptyDir: {}
- name: config-map
configMap:
name: mysql
volumeClaimTemplates:
- metadata:
name: data
annotations:
volume.beta.kubernetes.io/storage-class: nfs-client
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 5Gi

启动成功:

image-20230407140930391

下面我们详细剖析此配置:

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
# 选择器用于匹配Service中的配置,以便服务发现
selector:
matchLabels:
app: mysql
app.kubernetes.io/name: mysql
serviceName: mysql
# 一主两从,所以副本为3
replicas: 3
template:
metadata:
labels:
app: mysql
app.kubernetes.io/name: mysql
spec:
# 第一步:初始化容器
initContainers:
# 第一步、第一节:复制配置文件
- name: init-mysql
image: mysql:5.7
command:
- bash
- "-c"
- |
set -ex
# 对于 StatefulSet 而言,每个 pod 各自的 hostname 中所具有的序号就是它们的唯一 id
# 因此我们可以通过正则表达式来获取这个 id,并且规定 id 为 0 表示主节点
# 于是,通过判断 server 的 id,就可以对 ConfigMap 中不同的配置进行获取了
[[ $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节点,拷贝 master 配置 primary.cnf
if [[ $ordinal -eq 0 ]]; then
cp /mnt/config-map/primary.cnf /mnt/conf.d/
# 否则,拷贝 Slave 的配置 replica.cnf
else
cp /mnt/config-map/replica.cnf /mnt/conf.d/
fi
# 卷内文件映射
volumeMounts:
- name: conf
mountPath: /mnt/conf.d
- name: config-map
mountPath: /mnt/config-map
# 第一步、第二节:从节点复制binlog、从节点还原binlog
- name: clone-mysql
# 使用国内镜像
image: registry.cn-hangzhou.aliyuncs.com/hxpdocker/xtrabackup:1.0
command:
- bash
- "-c"
- |
set -ex
# 数据如果存在从节点复制 binlog,因为后续从节点会用 mysql 的 relay log
# 来真正的主从一致,此处只是我们手动初始化同步
# 如果已经同步了,就不用再初始化了
[[ -d /var/lib/mysql/mysql ]] && exit 0
# 主节点不用复制 binlog
[[ `hostname` =~ -([0-9]+)$ ]] || exit 1
ordinal=${BASH_REMATCH[1]}
[[ $ordinal -eq 0 ]] && exit 0
# 使用 ncat 指令,远程地从前一个节点拷贝数据到本地
# 注意,这里是从 前一个节点 拷贝的
# 你可以在每个节点的网络流量中观测到,它并非全是由主节点拷贝数据的
# 也就是对应 mysql-$(($ordinal-1)).mysql
ncat --recv-only mysql-$(($ordinal-1)).mysql 3307 | xbstream -x -C /var/lib/mysql
# 使用第三方应用 xtrabackup 还原数据
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_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
# 容器就绪探针,在启动后周期执行指令,只有当指令执行成功后,才允许 Service 将请求转发给节点
readinessProbe:
exec:
# Check we can execute queries over TCP (skip-networking is off).
command: ["mysql", "-h", "127.0.0.1", "-e", "SELECT 1"]
initialDelaySeconds: 5
periodSeconds: 2
timeoutSeconds: 1
# 第三步:从节点连接主节点
- name: xtrabackup
image: registry.cn-hangzhou.aliyuncs.com/hxpdocker/xtrabackup:1.0
ports:
- name: xtrabackup
containerPort: 3307
command:
- bash
- "-c"
- |
set -ex
cd /var/lib/mysql

# 首先我们要明白从节点是如何注册到主节点的
# 是通过如下语句来执行的
# CHANGE MASTER TO
# MASTER_HOST='192.168.232.146',
# MASTER_PORT=3310,
# MASTER_USER='root',
# MASTER_PASSWORD='123456',
# MASTER_LOG_FILE='mysql-bin.00003',
# MASTER_LOG_POS=993;
# -----------------------------

# 从备份信息文件里读取 MASTER_LOG_FILEM 和 MASTER_LOG_POS 这两个字段的值
# 用来拼装集群初始化SQL
if [[ -f xtrabackup_slave_info && "x$(<xtrabackup_slave_info)" != "x" ]]; then

# 如果 xtrabackup_slave_info 文件存在,说明这个备份数据来自于另一个 Slave 节点
# XtraBackup 在备份的时候,就已经在这个文件里自动生成了"CHANGE MASTER TO" SQL语句
# 所以,我们只需要把这个文件重命名为 change_master_to.sql.in ,后面直接使用即可
cat xtrabackup_slave_info | sed -E 's/;$//g' > change_master_to.sql.in

# 所以,也就用不着 xtrabackup_binlog_info 了
rm -f xtrabackup_slave_info xtrabackup_binlog_info

# 如果只存在 xtrabackup_binlog_inf 文件,那说明备份来自于 Master 节点
elif [[ -f xtrabackup_binlog_info ]]; then

# 我们就需要解析这个备份信息文件,读取所需的两个字段的值
[[ `cat xtrabackup_binlog_info` =~ ^(.*?)[[:space:]]+(.*?)$ ]] || exit 1
rm -f xtrabackup_binlog_info xtrabackup_slave_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

# 使用 change_master_to.sql.orig 的内容,组成一个完整的初始化和启动Slave的SQL语句
# 执行注册命令,开始数据同步
echo "Initializing replication from clone position"
mysql -h 127.0.0.1 \
-e "$(<change_master_to.sql.in), \
MASTER_HOST='mysql-0.mysql', \
MASTER_USER='root', \
MASTER_PASSWORD='', \
MASTER_CONNECT_RETRY=10; \
START SLAVE;" || exit 1
# 将文件 change_master_to.sql.in 改个名字
# 防止这个 Container 重启的时候,因为又找到了 change_master_to.sql.in
# 从而重复执行一遍这个初始化流程
mv change_master_to.sql.in change_master_to.sql.orig
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
resources:
requests:
cpu: 100m
memory: 100Mi
volumes:
- name: conf
emptyDir: {}
- name: config-map
configMap:
name: mysql
# 指定存储
volumeClaimTemplates:
- metadata:
name: data
annotations:
volume.beta.kubernetes.io/storage-class: nfs-client
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 5Gi

验证

模拟写操作

我们通过 Service 中 name 为 mysql 的配置来连接数据库,通过 mysql-0.mysql ,来指定连接主库

在MySQL所在的任意节点内执行

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

模拟读操作

我们通过 Service 中 name 为 mysql-read 的配置来连接数据库,此配置会直接连接所有节点,因为他们都能做到读操作,并且此配置可以做到负载均衡

在MySQL所在的任意节点内执行

1
2
3
4
5
6
7
8
kubectl run mysql-client --image=mysql:5.7 -i -t --rm --restart=Never -- \
mysql -h mysql-read -e "SELECT * FROM test.messages"
# 结果如下
+---------+
| message |
+---------+
| hello |
+---------+

模拟从节点宕机

这里我们模拟其中一个从节点宕机,来测试 mysql-read 服务发现是否做到了负载均衡

在MySQL所在的任意节点内执行

1
2
3
4
5
6
# 模拟 mysql-1 宕机
kubectl exec mysql-1 -c mysql -- mv /usr/bin/mysql /usr/bin/mysql.off

# 执行三次
kubectl run mysql-client --image=mysql:5.7 -i -t --rm --restart=Never -- \
mysql -h mysql-read -e "SELECT * FROM test.messages"

image-20230410110314649

image-20230410110415508

可以看到,当存活探针已发现服务不健康后,会自动不转发流量至该节点,满足了读的高可用


模拟主节点宕机

这里我们模拟主节点宕机,来测试 mysql 服务发现是否还能支持写入,测试 mysql-read 服务发现是否做到了负载均衡

在MySQL所在的任意节点内执行

1
2
3
4
5
6
7
8
# 模拟主节点宕机
kubectl exec mysql-0 -c mysql -- mv /usr/bin/mysql /usr/bin/mysql.off

# 测试写入
kubectl run mysql-client --image=mysql:5.7 -i --rm --restart=Never --\
mysql -h mysql-0.mysql <<EOF
INSERT INTO test.messages VALUES ('hello2');
EOF

image-20230410141610752

image-20230410141638832

这里已经无法识别主机名 mysql-0.mysql 了,因为这个容器已经处于不健康的状态

我们尝试使用 mysql-read 查询操作,看从节点是否还能继续读操作

1
2
3
4
5
6
7
8
kubectl run mysql-client --image=mysql:5.7 -i -t --rm --restart=Never -- \
mysql -h mysql-read -e "SELECT * FROM test.messages"
# 结果如下
+---------+
| message |
+---------+
| hello |
+---------+

可以看到,Service 负载均衡成功将流量转发至健康的从节点

不难发现,单主多从适用于面对大量读操作,但如果写操作同样频繁,导致主库宕机,整个服务是无法写入,依然无法做到高可用


反思与探索

水平扩展

我们每次的水平扩展都是按照 xtrabackup 底层扩展,但这种水平扩展的方式显然很“拖沓“

NFS存储

NFS 存储是通过网络进行数据传输的,因此网络带宽和延迟对其性能有重要影响。如果网络带宽不足或延迟过高,则会导致 NFS 存储性能下降。如果 NFS 服务器负载过高,如同时有大量的读写请求,则会影响其性能

所以很显然,NFS并不适用于MySQL做数据库的存储后端,数据库的可用性基本为零,读写性能太差

有必要部署在Kubernetes上吗

Kubernetes更加适合Deployment无状态应用的部署,强行使用StatefulSet来部署,显然丧失了Kubertnetes的许多特性,如果出现了问题,运维难度增大,很难定位到问题


因此,我们需要一个部署简单,水平扩展轻松,高可用且支持故障转移的工具来部署MySQL,所以衍生出了Documentation (vitess.io)这个项目

在下一篇文章中,我们将通过Vitess来部署生产环境MySQL


参考文章

[1] 实战 Kubernetes StatefulSet – MySQL 主从集群搭建 (techlog.cn)

[2] The Vitess Docs | Vitess Operator for Kubernetes