本文主要介绍如何使用StatefulSet搭建高可用的MySQL集群。
关键词:k8s
前言
为什么要用K8s搭建MySQL集群?
- 在业务全面上云的背景下,为了要使用现有的设备快速搭建高可用的MySQL集群,K8s无非是一种比较方便的方案。
StatefulSet
StatefulSet被用来管理有状态应用的工作负载API对象。MySQL服务就是一个典型的有状态应用。
和Deployment类似,StatefulSet也是用来管理基于相同容器Spec的一组Pod。但和Deployment不同的是,StatefulSet为他们的Pod维护了一个有粘性的ID。这些Pod是基于相同的Spec来创建的,但是不能替换:无论怎么调度,他们Pod都有一个永久不变的ID。
如果使用PV卷为Pod提供持久存储,可以使用StatefulSet作为解决方案的一部分,尽管StatefulSet中的单个Pod仍可能出现故障,但持久的
Pod 标识符使得将现有卷与替换已失败 Pod 的新 Pod 相匹配变得更加容易。
StatefulSet主要用于以下状态的应用更新:
- 需要稳定的、唯一的网络标识符;
- 需要稳定的、持久的存储;
- 需要有序的、优雅的部署和扩缩;
- 需要有序的、自动的滚动更新。
稳定意味着Pod的调度或者重新调度的整个过程是持久性的;
如果应用程序不需要任何稳定的标识符或者有序的部署、删除或扩缩,则应该使用无状态的副本控制提供的工作负载来部署应用程序,如Deployment或者ReplicaSet;
- StatefulSet的应用都是一个一个依次创建的;
限制
Pod存储必须要由PV驱动基于StrongeClass来提供,或者由管理员预先提供;
删除或者扩缩StatefulSet不会删除它关联的PVC;
StatefulSet需要无头服务(headless
service)来负责创建Pod的网络标识,管理员需要创建此服务;
当删除一个StatefulSet时,StatefulSet不提供任何终止Pod的保证。为了实现StatefulSet有序终止,可以在删除之前将StatefulSet缩容至0;
默认Pod管理策略是滚动更新,出现异常时可能需要人工干预才能恢复
组件
下面的示例演示了StatefulSet的组件
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
| apiVersion: v1 kind: Service metadata: name: nginx labels: app: nginx spec: ports: - port: 80 name: web clusterIP: None selector: app: nginx --- apiVersion: apps/v1 kind: StatefulSet metadata: name: web spec: selector: matchLabels: app: nginx serviceName: "nginx" replicas: 3 minReadySeconds: 10 template: metadata: labels: app: nginx spec: terminationGracePeriodSeconds: 10 containers: - name: nginx image: k8s.gcr.io/nginx-slim:0.8 ports: - containerPort: 80 name: web volumeMounts: - name: www mountPath: /usr/share/nginx/html volumeClaimTemplates: - metadata: name: www spec: accessModes: [ "ReadWriteOnce" ] storageClassName: "my-storage-class" resources: requests: storage: 1Gi
|
MySQL HA搭建
普通MySQL集群的搭建可以参考:https://www.modb.pro/db/29214
主要就是以下几个步骤:
- 安装数据库
- 主库开启binlog
- 授权
- 登陆主库查看此时日子状态
- 导出主库当前数据
- 从库和指定serverid
- 从库写入主库数据
- 指定开始同步位置
K8s集群上的MySQL集群搭建略有不同,主要通过StatefulSet+ConfigMap+initContainer的模式和xtrabackup+ncat软件来实现主从复制;
- 首先创建主库和从库的my.cnf文件,存入ConfigMap。这样可以持久化配置文件;
- 然后创建InitContainer。InitContainer容器是首先创建的容器,且该容器成功推出是Pod
Ready的比较条件;
- InitContainer:init-mysql
主要任务是根据hostname,选择拷贝主库的配置文件还是从库的文件到Volume中,这样就区分了Master和Slave;
- InitContainer:clone-mysql
任务是为级联复制做准备。如果存在数据,跳过克隆;跳过主库的克隆;从上一个Ready的Pod克隆数据来;准备备份为后面的节点服务;
- 创建Container。这里的Container就是正常业务的MySQL容器了;
- 创建Sicar
Container:backup-sql,主要任务是:调整当前节点的主从设置;准备为下一个节点提供复制的文件
资源清单文件
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 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299
| apiVersion: v1 kind: ConfigMap metadata: name: mysql-ha labels: app: mysql app.kubernetes.io/name: mysql data: init.sql: | use mysql; /* update user set host = '%' where user ='root'; flush privileges; */ grant all privileges on *.* to 'root'@'%'; flush privileges; primary.cnf: | # 仅在主服务器上应用此配置 [mysqld] lower_case_table_names=1 relay-log=mysql-relay log-bin=mysql-bin gtid_mode=on enforce_gtid_consistency
max_connections=1000 character_set_server=utf8mb4 collation_server=utf8mb4_general_ci default_authentication_plugin=mysql_native_password replica.cnf: | # 仅在副本服务器上应用此配置 [mysqld] lower_case_table_names=1 log_replica_updates=1 relay-log=mysql-relay log-bin=mysql-bin gtid_mode=on enforce_gtid_consistency
max_connections=1000 character_set_server=utf8mb4 collation_server=utf8mb4_general_ci default_authentication_plugin=mysql_native_password ---
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 ---
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 --- 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: securityContext: runAsUser: 999 runAsGroup: 999 fsGroup: 999 initContainers: - name: label-pod image: hub.deepsoft-tech.com/wf09/curl imagePullPolicy: IfNotPresent env: - name: PODNAME valueFrom: fieldRef: fieldPath: metadata.name command: - bash - "-c" - | set -ex APISERVER=https://kubernetes.default.svc # 服务账号令牌的路径 SERVICEACCOUNT=/var/run/secrets/kubernetes.io/serviceaccount # 读取 Pod 的名字空间 NAMESPACE=$(cat ${SERVICEACCOUNT}/namespace) # 读取服务账号的持有者令牌 TOKEN=$(cat ${SERVICEACCOUNT}/token) # 引用内部证书机构(CA) CACERT=${SERVICEACCOUNT}/ca.crt # 使用令牌访问 API # 基于 Pod 序号生成 MySQL 服务器的 ID。 [[ `hostname` =~ -([0-9]+)$ ]] || exit 1 ordinal=${BASH_REMATCH[1]} if [[ $ordinal -eq 0 ]]; then FLAG=false else FLAG=true fi curl -X PATCH \ --cacert ${CACERT} \ -H "Content-Type:application/json-patch+json" \ -H "Authorization: Bearer ${TOKEN}" ${APISERVER}/api \ -d \ '[ { "op": "add", "path": "/metadata/labels/readonly", "value": "'"$FLAG"'" } ]' \ ${APISERVER}/api/v1/namespaces/${NAMESPACE}/pods/${PODNAME} - name: init-mysql image: ubuntu imagePullPolicy: IfNotPresent command: - bash - "-c" - | set -ex # 基于 Pod 序号生成 MySQL 服务器的 ID。 [[ `hostname` =~ -([0-9]+)$ ]] || exit 1 ordinal=${BASH_REMATCH[1]} echo [mysqld] > /mnt/conf.d/server-id.cnf # 添加偏移量以避免使用 server-id=0 这一保留值。 echo server-id=$((100 + $ordinal)) >> /mnt/conf.d/server-id.cnf # 将合适的 conf.d 文件从 config-map 复制到 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 echo "Find init.sql..." cp /mnt/config-map/init.sql /docker-entrypoint-initdb.d/init.sql [[ $? -eq 0 ]] || exit 1 cat /docker-entrypoint-initdb.d/init.sql ls -l /var/lib/mysql [[ $? -eq 0 ]] || exit 1 volumeMounts: - name: data mountPath: /var/lib/mysql subPath: mysql - name: init-sql mountPath: /docker-entrypoint-initdb.d/ - name: conf mountPath: /mnt/conf.d - name: config-map mountPath: /mnt/config-map - name: clone-mysql image: hub.deepsoft-tech.com/wf09/backupsql command: - bash - "-c" - | set -ex # 如果已有数据,则跳过克隆。 [[ -d /var/lib/mysql/mysql ]] && exit 0 # 跳过主实例(序号索引 0)的克隆。 [[ `hostname` =~ -([0-9]+)$ ]] || exit 1 ordinal=${BASH_REMATCH[1]} [[ $ordinal -eq 0 ]] && exit 0 # 从原来的对等节点克隆数据。 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:8.0.29-debian imagePullPolicy: IfNotPresent env: - name: MYSQL_ROOT_PASSWORD value: "root" ports: - name: mysql containerPort: 3306 volumeMounts: - name: socket mountPath: /var/run/mysqld - name: init-sql mountPath: /docker-entrypoint-initdb.d/ - 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", "-proot"] initialDelaySeconds: 150 failureThreshold: 10 readinessProbe: exec: command: ["mysql", "-uroot", "-proot", "-e", "SELECT 1"] initialDelaySeconds: 60 failureThreshold: 5 - name: backup-sql image: hub.deepsoft-tech.com/wf09/backupsql imagePullPolicy: Always ports: - name: xtrabackup containerPort: 3307 command: - bash - "-c" - | set -ex cd /var/lib/mysql [[ `hostname` =~ -([0-9]+)$ ]] || exit 1 ordinal=${BASH_REMATCH[1]} # 如果是第0个说明是Master,跳过设置主从的部分 [[ $ordinal -eq 0 ]] && exec ncat --listen --keep-open --send-only --max-conns=1 3307 -c \ "xtrabackup --backup --slave-info --stream=xbstream --user=root --password=root"
echo "Waiting for mysqld to be ready (accepting connections)" until mysql -uroot -proot -e "SELECT 1" ; do sleep 1; done
echo "Initializing replication from clone position" mysql -uroot -proot \ -e "STOP SLAVE;" \ -e "RESET SLAVE;" \ -e "CHANGE MASTER TO \ MASTER_HOST='mysql-0.mysql', \ MASTER_USER='root', \ MASTER_PASSWORD='root', \ MASTER_AUTO_POSITION = 1;" \ -e "START SLAVE;" || exit 1 exec ncat --listen --keep-open --send-only --max-conns=1 3307 -c \ "xtrabackup --backup --slave-info --stream=xbstream --user=root --password=root" volumeMounts: - name: socket mountPath: /var/run/mysqld - name: data mountPath: /var/lib/mysql subPath: mysql - name: conf mountPath: /etc/mysql/conf.d resources: requests: cpu: 100m memory: 100Mi volumes: - name: socket emptyDir: {} - name: conf emptyDir: {} - name: init-sql emptyDir: {} - name: config-map configMap: name: mysql-ha volumeClaimTemplates: - metadata: name: data spec: storageClassName: rook-ceph-block accessModes: ["ReadWriteOnce"] resources: requests: storage: 1Gi
|
如何给Pod动态的增加Label
现在要求Service
A可以直接连接主库,即可以直接连接到StatefulSet创建的第一个Pod;Service
B可以直接连接所有的从库,即除去第一个Pod都可以通过Service负载均衡到后端Pod。
现在思路是通过 initContainerd 容器,在work
Pod起来之前中执行以下逻辑:
- 判断hostname,如果带0,说明是第一个主库,通过REST API请求K8s API
Server,根据每一个Pod都会自动挂载默认Service
Account(投射卷)的机制,可以获取到请求K8s API
Server的密钥。
部分资源清单:
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
| initContainers: - name: label-pod image: hub.deepsoft-tech.com/wf09/curl imagePullPolicy: IfNotPresent env: - name: PODNAME valueFrom: fieldRef: fieldPath: metadata.name command: - bash - "-c" - | set -ex APISERVER=https://kubernetes.default.svc # 服务账号令牌的路径 SERVICEACCOUNT=/var/run/secrets/kubernetes.io/serviceaccount # 读取 Pod 的名字空间 NAMESPACE=$(cat ${SERVICEACCOUNT}/namespace) # 读取服务账号的持有者令牌 TOKEN=$(cat ${SERVICEACCOUNT}/token) # 引用内部证书机构(CA) CACERT=${SERVICEACCOUNT}/ca.crt # 使用令牌访问 API # 基于 Pod 序号生成 MySQL 服务器的 ID。 [[ `hostname` =~ -([0-9]+)$ ]] || exit 1 ordinal=${BASH_REMATCH[1]} if [[ $ordinal -eq 0 ]]; then FLAG=false else FLAG=true fi curl -X PATCH \ --cacert ${CACERT} \ -H "Content-Type:application/json-patch+json" \ -H "Authorization: Bearer ${TOKEN}" ${APISERVER}/api \ -d \ '[ { "op": "add", "path": "/metadata/labels/readonly", "value": "'"$FLAG"'" } ]' \ ${APISERVER}/api/v1/namespaces/${NAMESPACE}/pods/${PODNAME}
|
上面
⚠️默认情况下默认服务账号是不能更改Pod
Label的,需要执行一下资源清单开放权限
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
| apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: patch-my-pods namespace: default labels: app: patch-my-pods rules: - apiGroups: [""] resources: ["pods"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: patch-my-pods namespace: default roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: patch-my-pods subjects: - kind: ServiceAccount name: default namespace: default
|
apply以后可以检查一下自己是否有权限
1
| kubectl auth can-i patch pods --as=system:serviceaccount:default:default -n default
|
执行完以上操作就可以动态的Pod添加标签啦~
问题
[ERROR]
[MY-010544] [Repl] Failed to open the relay log
当使用xtrabackup对从库进行冷备份时,并使用这个从库的备份创建一个新的从库实例时,会出现此问题。
通过relay
log介绍,很容易知道由于mysql.slave_relay_log_info表中保留了以前的复制信息,导致新从库启动时无法找到对应文件。
此时需要登陆到从库的MySQL实例,重置一下slave即可
1 2 3
| mysql> stop slave; mysql> reset slave; mysql> start slave;
|
The
slave I/O thread stops because master and slave have equal MySQL server
UUIDs
这个问题通常是因为备份数据库时,把数据库的UUID也复制了下来。而主从关系要求这个UUID必须唯一。这个uuid通常存在/var/log/mysql/auto.cnf文件夹下,通常删除该文件,重启一下数据库即可。
PS:在开启GTID复制时,貌似不会自动复制auto.cnf此文件。
引用