技术博客 > 正文

如何在AWS EKS上实现Mysql8.0读写分离

2022-04-28

阅读本文时,我认为你已经具备kubernets基础,已经创建好了AWS EKS集群,并且能够使用kubectl管理集群。

1、概述
一般我们采用mysql作为数据库时,常用的是mysql5.x或mysql8.x版本。
数据库实例类型有单机版、高可用版、读写分离集群版。 前两种都比较容易实现。
今天我们来实现mysql8.0.26版本的读写分离,并且同步复制时采用密码方式。
为了更好地适应生产环境,以及后续的便利性,我们需要用到namespace(ns)、Configmap(cm),Secret,Service(svc),StatefulSet(sts)。

首先,描述一下我们想要部署的“有状态应用”。
1.是一个“主从复制”(Maser-Slave Replication)的 MySQL 集群;
2.有 1 个主节点(Master);有多个从节点(Slave);从节点需要能水平扩展;
3.所有的写操作,只能在主节点上执行;读操作可以在所有节点上执行。
这个主从模式的 MySQL 集群非常典型。我们通过一张图来表示。

@网宿科技:“非常”时期,非常感谢

2、存储
如果是自建kubernetes集群,你不懂golang开发的话,很难编写出CSI存储插件,存储类型可能会是你非常头疼的事情,但我们采用AWS EKS,这会让我们非常省心,因为AWS EKS已经帮我们做好了storageClass。采用storageClass,能够自动创建相应的pv和pvc,非常方便。
运行有状态应用Mysql,我们需要用到块存储,即AWS EBS。
AWS EBS存储类型有:io1,gp2,gp3,sc1,st1。存储类型区别详见https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volume-types.html。默认值:gp2。
通过如下命令,可以查看AWS EKS帮我们做好的storageClass。默认只有gp2类型的,我们可以直接使用。

$ kubectl get storageclass #或 $kubectl get sc
#会输出如下内容
NAME PROVISIONER AGE
gp2 (default) kubernetes.io/aws-ebs 37m

这个storageClass非常容易实现,它是由gp2-storage-class.yaml这个文件生成的,内容如下:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: gp2
annotations: storageclass.kubernetes.io/is-default-class: “true”
provisioner: kubernetes.io/aws-ebs
parameters:
type: gp2
fsType: ext4

如果,你想用其它类型的StorageClass,可以将配置文件中的type,更改为其它,如gp3。通过如下命令,即可创建。

$ kubectl create -f gp2-storage-class.yaml

3、命名空间namespace
配置文件中,如果没有指定namespace,默认会自动使用default,在生产环境中,最好是单独创建个命名空间,创建方法
mysql-ns.yaml

apiVersion: v1
kind: Namespace
metadata:
name: mysql

接下来使用如下命令创建namespace

$kubectl create -f mysql-ns.yaml

记得在secret、statufulset文件中,加上如下一行。

metadata
namespace: mysql

4、密钥Secret
我们将数据库的帐号密码存在Secret中。
Secret 对象要求这些数据必须是经过 Base64 转码的,以免出现明文密码的安全隐患。这个转码操作也很简单,比如:

$ echo -n ‘root’ |base64
cm9vdA==
$ echo -n ‘123456’ |base64
MTIzNDU2

然后使用如下文件如 mysql-secret.yaml

apiVersion: v1
kind: Secret
metadata:
name: prod-db-secret
namespace: mysql
type: Opaque
data:
password: MTIzNDU2
username: cm9vdA==

接下来,我们尝试一下创建这个 Secret:

$ kubectl create -f mysql-secret.yaml

5、配置ConfigMap
可能以前我们习惯在Deployment或Statufulset中写配置文件,我建议生产环境中,配置文件采用configmap。
做mysql读写分离时,Master 节点和 Slave 节点需要有不同的配置文件。
内容如下mysql-config.yaml

apiVersion: v1
kind: ConfigMap
metadata:
name: mysql
namespace: mysql
labels:
app: mysql
data:
master.cnf: |
[mysqld]
log-bin

slave.cnf: |
[mysqld]
super-read-only
relay_log_info_repository=table
master_info_repository=table
relay_log_recovery = 1

接下来,我们尝试一下创建这个 configmap:

$ kubectl create -f mysql-configmap.yaml

如果你的程序是之前一直是在mysql5.7上运行,你可能会用到如下configMap

apiVersion: v1
kind: ConfigMap
metadata:
name: mysql
namespace: mysql
labels:
app: mysql
data:
master.cnf: |
[client]
default-character-set=utf8mb4
[mysql]
default-character-set=utf8mb4
[mysqld]
log-bin
binlog_expire_logs_seconds=2592000
max_connections=10000
binlog_format=MIXED
explicit_defaults_for_timestamp=true
lower_case_table_names=1
max_allowed_packet=128M
innodb-log-file-size=64M
default-time-zone=’+8:00’
default-authentication-plugin=mysql_native_password
character-set-client-handshake=FALSE
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
init_connect=‘SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci’
secure_file_priv=/var/lib/mysql
sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
slave.cnf: |
[client]
default-character-set=utf8mb4
[mysql]
default-character-set=utf8mb4
[mysqld]
super-read-only
max_connections=10000
binlog_format=MIXED
relay_log_info_repository=table
master_info_repository=table
relay_log_recovery = 1
explicit_defaults_for_timestamp=true
lower_case_table_names=1
max_allowed_packet=128M
innodb-log-file-size=64M
default-time-zone=’+8:00’
default-authentication-plugin=mysql_native_password
character-set-client-handshake=FALSE
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
init_connect=‘SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci’
secure_file_priv=/var/lib/mysql
sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
#如果CM配置文件slave.cnf部分没有relay_log_info_repository=table、master_info_repository=table和relay_log_recovery=1那么最后一个从节点,始终不会正常。

6、服务Service
6.1集群内部访问
我们通过Headless Service来实现集群内部的访问,见配置文件。
mysql-service.yaml

apiVersion: v1
kind: Service
metadata:
name: mysql
namespace: mysql
labels:
app: mysql
spec:
ports:

  • name: mysql
    port: 3306
    clusterIP: None
    selector:
    app: mysql
    apiVersion: v1
    kind: Service
    metadata:
    name: mysql-read
    namespace: mysql
    labels:
    app: mysql
    spec:
    ports:
  • name: mysql
    port: 3306
    selector:
    app: mysql

6.2集群外部访问
如果你想通过navicat等工具,远程连接数据库,可以通过如下两种方法。
方法一:通过nodePort,访问方式: nodeIP:30006
mysql-service-nodeport.yaml

#可读写
apiVersion: v1
kind: Service
metadata:
name: mysql-node-rw
namespace: mysql
labels:
app: mysql
spec:
ports:

  • port: 3306
    nodePort: 30306
    targetPort: 3306
    name: mysql
    type: NodePort
    selector:
    statefulset.kubernetes.io/pod-name: mysql-0

方法二:通过LB,推荐选用此方式,AWS会自动帮我们创建好LB,并做好配置。访问方式: lb公网IP:23306
mysql-service-lb.yaml

kind: Service
apiVersion: v1
metadata:
name: mysql-lb-rw
namespace: mysql
spec:
ports:

  • port: 23306 #这个端口就是公网IP的端口
    targetPort: 3306
    #nodePort: 31258 #如果你想指定work节点的端口,可以加上这行。
    selector:
    statefulset.kubernetes.io/pod-name: mysql-0
    type: LoadBalancer

若是访问只读实例,将serivce文件中selector改为如下内容,即可

selector:
app: mysql

7、使用StatefulSet运行有状态应用
最关键的来了。
在此配置文件中,能看到有initContainers,它的优先级最高。它包含了mysql容器和sidecar容器,它的作用主要用来主从节点初始化,及拷贝配置文件。 containers中也包含了mysql容器和sidecar容器,后续常态化运行这个containers。
其中sidecar容器的镜像 yuanzl/xtrabackup:2.0支持mysql8.0.26,mysql8.0.26以上版本不支持,如果mysql版本更高,需要重新做Docker镜像。
非常关键的,在配置文件中,能够看到volumeClaimTemplates的配置。只要在这部分配置文件中指定,我们需要的存储类型,容量等参数,EKS集群,会自动帮我们创建并绑定好pv和pvc。
本配置文件中,我声明了,我需要gp2类型的存储,容量为10G,访问方式为ReadWriteOnce(即只能挂在1个worker节点)。 另外我声明了 resources,即容器运行时的cpu和内存,可以根据需求更改。
mysql-statefulset.yaml

apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
namespace: mysql
spec:
selector:
matchLabels:
app: mysql
serviceName: mysql
replicas: 3
template:
metadata:
labels:
app: mysql
spec:
initContainers:
- name: init-mysql
image: mysql:8.0.26
imagePullPolicy: IfNotPresent
env:
- name: TZ
value: Asia/Shanghai
command:
- bash
- “-c”
- |
set -ex
# 使用pod ordinal index生成 mysql server-id.
[[ 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
# 从configmap拷贝配置 到emptyDir.
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: yuanzl/xtrabackup:2.0
imagePullPolicy: IfNotPresent
command:
- bash
- “-c”
- |
set -ex
# 如果data存在则跳过.
[[ -d /var/lib/mysql/mysql ]] && exit 0
# Skip the clone on master (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:8.0.26
imagePullPolicy: IfNotPresent
env:
- name: TZ
value: Asia/Shanghai
# 使用secret
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: prod-db-secret
key: password
ports:
- name: mysql
containerPort: 3306
volumeMounts:
- name: data
mountPath: /var/lib/mysql
subPath: mysql
- name: conf
mountPath: /etc/mysql/conf.d
livenessProbe:
exec:
command:
- /bin/sh
- “-c”
- MYSQL_PWD="${MYSQL_ROOT_PASSWORD}"
- mysqladmin ping"
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
readinessProbe:
exec:
command:
- /bin/sh
- “-c”
- MYSQL_PWD="${MYSQL_ROOT_PASSWORD}"
- mysql -h 127.0.0.1 -u root -e “SELECT 1”
initialDelaySeconds: 5
periodSeconds: 2
timeoutSeconds: 1
- name: xtrabackup
image: yuanzl/xtrabackup:2.0
imagePullPolicy: IfNotPresent
ports:
- name: xtrabackup
containerPort: 3307
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: prod-db-secret
key: password
command:
- bash
- “-c”
- |
set -ex
cd /var/lib/mysql
#cat /etc/hosts

      # 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 slave. (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 master. 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 -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
        mysql -h 127.0.0.1 -uroot -p${MYSQL_ROOT_PASSWORD} <<EOF
        $(<change_master_to.sql.orig),
        MASTER_HOST='mysql-0.mysql',
        MASTER_USER='root',
        MASTER_PASSWORD='${MYSQL_ROOT_PASSWORD}',
        MASTER_CONNECT_RETRY=10;
        START SLAVE;
      EOF
      echo "master slave config ok"
      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 --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: “1G”
    storageClassName: gp2

然后,我们就可以创建这个 StatefulSet 了,如下所示:

$ kubectl create -f mysql-statefulset.yaml
$ kubectl get pod -l app=mysql -n 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

8、验证
现在,mysql集群已经正常运行,我们可以尝试向这个 MySQL 集群发起写请求,执行一些 SQL 操作来验证它是否正常。
注意,写请求用的是mysql-0.mysql,并且指定了namespace

$ kubectl run mysql-client --image=mysql:8.0.26 -i --rm --restart=Never -n mysql –
mysql -h mysql-0.mysql -uroot -p123456 <<EOF
CREATE DATABASE test;
CREATE TABLE test.messages (message VARCHAR(250));
INSERT INTO test.messages VALUES (‘hello’);
EOF

通过连接 mysql-read 这个 Service,我们就可以用 SQL 进行读操作,如下所示:

$ kubectl run mysql-client --image=mysql:8.0.26 -i -t --rm --restart=Never -n mysql –
mysql -h mysql-read -uroot -p123456 -e “SELECT * FROM test.messages”
mysql: [Warning] Using a password on the command line interface can be insecure.
±--------+
| message |
±--------+
| hello |
±--------+
pod “mysql-client” deleted
在互联网上,使用Navicat,通过AWS EKS,自动创建的lb的公网IP,也可以正常访问。

@网宿科技:“非常”时期,非常感谢

另外,我们可以通过如下命令扩展集群

$ kubectl scale statefulset mysql --replicas=5

本文内容的版权持有者为网宿科技股份有限公司(“网宿科技”),未经许可,不得转载。