Mysql主从同步原理

Rainbond部署Mysql主从集群应用详解-LMLPHP

1)在Slave 服务器上执行sart slave命令开启主从复制开关,开始进行主从复制。

2)此时,Slave服务器的IO线程会通过在master上已经授权的复制用户权限请求连接master服务器,并请求从执行binlog日志文件的指定位置(日志文件名和位置就是在配置主从复制服务时执行change master命令指定的)之后开始发送binlog日志内容。

3)Master服务器接收到来自Slave服务器的IO线程的请求后,其上负责复制的IO线程会根据Slave服务器的IO线程请求的信息分批读取指定binlog日志文件指定位置之后的binlog日志信息,然后返回给Slave端的IO线程。返回的信息中除了binlog日志内容外,还有在Master服务器端记录的IO线程。返回的信息中除了binlog中的下一个指定更新位置。

4)当Slave服务器的IO线程获取到Master服务器上IO线程发送的日志内容、日志文件及位置点后,会将binlog日志内容依次写到Slave端自身的Relay Log(即中继日志)文件(Mysql-relay-bin.xxx)的最末端,并将新的binlog文件名和位置记录到master-info文件中,以便下一次读取master端新binlog日志时能告诉Master服务器从新binlog日志的指定文件及位置开始读取新的binlog日志内容。

5)Slave服务器端的SQL线程会实时检测本地Relay Log 中IO线程新增的日志内容,然后及时把Relay LOG 文件中的内容解析成sql语句,并在自身Slave服务器上按解析SQL语句的位置顺序执行应用这样sql语句,并在relay-log.info中记录当前应用中继日志的文件名和位置点。

Mysql主从同步注意事项

  • master节点和slave节点的uuid不同
  • master节点和slave节点的server_id不同
  • slave节点需要自动执行向master节点注册的操作

制作Mysql容器镜像

同一镜像创建不同容器的uuid

用同一mysql镜像创建mysql主从集群时,发现每台mysql服务的uuid都是相同的,是因为在数据初始化时将uuid写在了/var/lib/mysql/auto.cnf文件中,造成每个容器的uuid都是相同的。

为了解决不同容器的uuid不同问题,需要在mysql启动生成配置文件后并在启动前 随机生成一个uuid写入到/var/lib/mysql/auto.cnf,这样就可以确保同一镜像生成的容器的uuid都不相同。

为了达成这一目标,我们修改了mysql镜像自带的启动脚本/usr/local/bin/docker-entrypoint.sh

if [ ! -d "$DATADIR/mysql" ]; then
		file_env 'MYSQL_ROOT_PASSWORD'
		if [ -z "$MYSQL_ROOT_PASSWORD" -a -z "$MYSQL_ALLOW_EMPTY_PASSWORD" -a -z "$MYSQL_RANDOM_ROOT_PASSWORD" ]; then
			echo >&2 'error: database is uninitialized and password option is not specified '
			echo >&2 '  You need to specify one of MYSQL_ROOT_PASSWORD, MYSQL_ALLOW_EMPTY_PASSWORD and MYSQL_RANDOM_ROOT_PASSWORD'
			exit 1
		fi

		mkdir -p "$DATADIR"

		echo 'Initializing database'
		"$@" --initialize-insecure
		echo 'Database initialized'
                # 位于mysql启动脚本90行,新增如下语句
                psd="/proc/sys/kernel/random/uuid"
                str=$(cat $psd)
                uuid="server-uuid="${str}
                echo "[auto]" > /var/lib/mysql/auto.cnf
                echo $uuid >> /var/lib/mysql/auto.cnf


同一服务不同实例的server_id处理

用同一MYSQL镜像创建MYSQL主从集群时,如何确保每个MYSQL服务的server_id不同?

k8s在创建容器时,会为每个容器创建创建一个主机名( 如:gr78648d-0),创建多个容器后面的数字会依次递增,所以可以利用这一特性生成不同的server_id(主机名数字部分 + 指定数字),然后在maser和slave使用不同的数字即可。

从库自动初始化

创建slave数据库时,我们希望salve应用下的每个实例,在扩容后,会自动向主库注册。

这需要salve应用中实例初始化时,自动执行指定的SQL脚本。这要借助于官方MYSQL镜像所提供的特定功能:数据库初始化时,会自动读取/docker-entrypoint-initdb.d/中*.sql 文件并执行。

为了实现上述两个目标,我们在镜像的自定义启动脚本 /run/docker-entrypoint.sh进行指定:

# define server_id and anyother cluster configuration
# 通过环境变量来区分当前镜像创建主库或从库
if [ ${MYSQL_ROLE} == "master" ];then
   # 借助有状态应用主机名特点,截取其中的数字
   server_id=${HOSTNAME#*-}
   # 主库ID设置为1
   MYSQLC_MYSQLD_SERVER_ID=`expr $server_id + 1`
   export MYSQLC_MYSQLD_SERVER_ID
   # 指定生成主库特定的配置
   export MYSQLC_MYSQLD_binlog_ignore_db=mysql
   export MYSQLC_MYSQLD_log_bin=mysql-bin
else
   # 借助有状态应用主机名特点,截取其中的数字
   server_id=${HOSTNAME#*-}
   # 从库各实例ID,从2开始排序
   MYSQLC_MYSQLD_SERVER_ID=`expr $server_id + 2`
   export MYSQLC_MYSQLD_SERVER_ID
   # 指定生成从库特定配置
   export MYSQLC_MYSQLD_replicate_ignore_db=mysql
   export MYSQLC_MYSQLD_log_bin=mysql-bin
   # 将从库所需要的初始化脚本模版拷贝到指定目录
   cp -a /tmp/init-slave.sql /docker-entrypoint-initdb.d/
   # 根据实例特定的环境变量,对初始化脚本模版进行更改
   sed -i -r -e "s/MYSQL_ROOT_PASSWORD/${MYSQL_ROOT_PASSWORD}/g" \
             -e "s/MYSQL_USER/${MYSQL_USER}/g" /docker-entrypoint-initdb.d/init-slave.sql
fi

制作镜像的Dockerfile解析

FROM percona:5.7.23-stretch
LABEL creater="barnett"
ENV MYSQL_VERSION=5.7.23
ENV TZ=Asia/Shanghai

RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list; \
    rm -rf /etc/apt/sources.list.d/percona.list && apt-get update; \
    apt-get install -y --no-install-recommends wget net-tools vim; \
    rm -rf /var/lib/apt/lists/*; \
    wget -O /usr/local/bin/env2file -q https://github.com/barnettZQG/env2file/releases/download/0.1.1/env2file-linux; \
    chmod +x /usr/local/bin/env2file;
# 自定义启动脚本
ADD docker-entrypoint.sh /run/docker-entrypoint.sh
# mysql官方启动脚本
ADD ./run/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
ADD ./run/mysqld.cnf /etc/mysql/percona-server.conf.d/mysqld.cnf
# 拷贝slave初始化脚本模版到镜像中,以备调用
ADD ./sql /tmp/
EXPOSE 3306
VOLUME ["/var/lib/mysql", "/var/log/mysql"]
ENV MYSQL_ROOT_PASSWORD=changeme
ENTRYPOINT [ "/run/docker-entrypoint.sh" ]
CMD [ "mysqld" ]

创建并配置mysql-master服务

创建mysql-master服务组件

代码地址:https://github.com/goodrain-apps/percona-mysql.git?dir=5.7

代码分支:cluster

通过Dockerfile创建服务 参考文档 基于Dockerfile源码创建服务

Rainbond部署Mysql主从集群应用详解-LMLPHP

mysql-master服务 相关配置

Rainbond部署Mysql主从集群应用详解-LMLPHP

  • 开启3306端口对内服务,并更改使用别名为MYSQL
  • 配置关键环境变量

其中 除MYSQL_ROLE外,其他环境变量要在服务创建完成后,转移到连接信息中去。

  • 部署属性中,修改应用类型为 有状态应用

创建Slave服务

创建mysql-slave服务组件

创建方式同mysql-master服务组件一致。

mysql-slave服务 相关配置

区别于mysql-master服务组件,mysql-slave服务组件配置如下:

  • 开启3307端口对内服务,并更改使用别名为 MYSQL_SLAVE
  • 配置环境变量
  • mysql-slave服务依赖于mysql-master服务

至此,一个基本的 MYSQL主从集群就已经搭建完成,如需要发布到应用市场供随时下载使用,请参考应用分享与发布

读写分离

机制

MYSQL主从集群的一个好处就是,可以配置master库负责写入,slave库负责查询,slave自动从master同步数据的读写分离结构。

如果设置得当,这样的结构可以大幅度提高数据库性能的同时,降低主库的压力。

使用方法

如果用户的业务程序已经支持读写分离,那么只需要设置:

  • 数据库写入地址为mysql-master服务地址,如果使用Rainbond服务依赖,则可以用 ${MYSQL_HOST}:${MYSQL_PORT}的方式指定连接地址。
  • 数据库查询地址为mysql-slave服务地址,如果使用Rainbond服务依赖,则可以用${MYSQL_SLAVE_HOST}:${MYSQL_SLAVE_PORT}的方式指定连接地址。

如果用户的业务程序不支持读写分离,那么就要靠支持读写分离的中间件实现。

Atlas中间件

Atlas是由奇虎360开源的数据库中间件,基于mysql官方提供的mysql-proxy改良而来。通过将mysql-proxy的LUA脚本,用C语言重新实现,Atlas提供了比mysql-proxy更强大的性能。经由中间件的代理,用户只需要配置数据库连接地址为 Atlas 服务地址,对于数据库的写入和查询,则由 Atlas 来管理。

详细了解Atlas

我们提供的docker化的Atlas组件,用户可以直接基于Dockerfile源码构建这个项目:

Atlas-docker项目地址

Rainbond已经发布的 Mysql主从集群 应用,已经集成了该中间件。

高阶实现

当前架构缺点

目前搭建的 MYSQL主从集群,是一个master节点,对接多个slave节点。这样的架构在小规模集群下没有问题。但是如果集群规模很大、slave节点过多的时候,由master向所有slave节点同步数据这一过程将变成性能的瓶颈。

架构的优化

当用户完全掌握了如何基于Rainbond搭建MYSQL主从集群后,可以自己尝试,专门创建一个slave节点,作为数据同步节点使用。

该节点向上对接 master节点,来同步数据;向下对接slave集群,分发由master节点同步来的数据。

这样做的好处是,master节点只需要对接一个数据同步节点来同步数据,可以更加专注于数据的写入。其他slave节点从数据同步节点来同步数据。

Rainbond部署Mysql主从集群应用详解-LMLPHP

如果有用户实现了这种优化,欢迎将其分享到应用市场中,供更多的人来使用。

08-06 19:30