DockerMysql云计算数据库集群高可用

使用基于Docker镜像快速部署MariaDB Galera Cluster集群 图文实战教程

MariaDB Galera Cluster(下文简称MGC集群),是一套在MySQL innodb存储引擎上面实现多主、数据实时同步以及强一致性的关系存储架构,业务层面无需做读写分离工作,数据库读写压力都能按照既定的规则分发到 各个节点上去,在数据方面完全兼容 MariaDB 和 MySQL。

现在是Docker容器化时代,纯手工编译、配置的部署方式我就不介绍了,感兴趣的可以自己去搜索相关教程。本文主要是分享一下自制的全自动部署MGC集群的Docker镜像(选用 MariaDB 10.3.12版本,基于Docker Host网络模式),最大程度简化了MGC集群的部署难度。

一、特性介绍

1、概述

MariaDB Galera Cluster(下文简称MGC集群),是一套在MySQL innodb存储引擎上面实现多主、数据实时同步以及强一致性的关系存储架构,业务层面无需做读写分离工作,数据库读写压力都能按照既定的规则分发到 各个节点上去,在数据方面完全兼容 MariaDB 和 MySQL。

使用基于Docker镜像快速部署MariaDB Galera Cluster集群 图文实战教程

2、功能特性

  • 同步复制 Synchronous replication
  • Active-active multi-master 拓扑逻辑
  • 可对集群中任一节点进行数据读写
  • 自动成员控制,故障节点自动从集群中移除
  • 自动节点加入
  • 真正并行的复制,基于行级
  • 直接客户端连接,原生的 MySQL 接口
  • 每个节点都包含完整的数据副本
  • 多台数据库中数据同步由 wsrep 接口实现

3、局限性

  • 目前的复制仅仅支持InnoDB存储引擎,任何写入其他引擎的表,包括mysql.*表将不会复制,但是DDL语句会被复制的,因此创建用户将会被复制,但是insert into mysql.user…将不会被复制的.
  • DELETE操作不支持没有主键的表,没有主键的表在不同的节点顺序将不同,如果执行SELECT…LIMIT… 将出现不同的结果集.
  • 在多主环境下LOCK/UNLOCK TABLES不支持,以及锁函数GET_LOCK(), RELEASE_LOCK()…
  • 查询日志不能保存在表中。如果开启查询日志,只能保存到文件中。
  • 允许最大的事务大小由wsrep_max_ws_rows和wsrep_max_ws_size定义。任何大型操作将被拒绝。如大型的LOAD DATA操作。
  • 由于集群是乐观的并发控制,事务commit可能在该阶段中止。如果有两个事务向在集群中不同的节点向同一行写入并提交,失败的节点将中止。对 于集群级别的中止,集群返回死锁错误代码(Error: 1213 SQLSTATE: 40001 (ER_LOCK_DEADLOCK)).
  • XA事务不支持,由于在提交上可能回滚。
  • 整个集群的写入吞吐量是由最弱的节点限制,如果有一个节点变得缓慢,那么整个集群将是缓慢的。为了稳定的高性能要求,所有的节点应使用统一的硬件。
  • 集群节点建议最少3个。
  • 如果DDL语句有问题将破坏集群。

4、技术原理

Galera集群的复制功能是基于认证的复制,其流程如下:

使用基于Docker镜像快速部署MariaDB Galera Cluster集群 图文实战教程

当客户端发出一个commit的指令,在事务被提交之前,所有对数据库的更改都会被write-set收集起来,并且将write-set 记录的内容发送给其他节点。

write-set 将在每个节点上使用搜索到的主键进行确认性认证测试,测试结果决定着节点是否应用write-set更改数据。如果认证测试失败,节点将丢弃 write-set ;如果认证测试成功,则事务提交,工作原理如下图:

使用基于Docker镜像快速部署MariaDB Galera Cluster集群 图文实战教程

关于新节点的加入,流程如下:

使用基于Docker镜像快速部署MariaDB Galera Cluster集群 图文实战教程

新加入的节点叫做Joiner,给Joiner提供复制的节点叫Donor。在该过程中首先会检查本地grastate.dat文件的seqno事务号是否在远端donor节点galera.cache文件里,如果存在,那么进行Incremental State Transfer(IST)增量同步复制,将剩余的事务发送过去;如果不存在那么进行State Snapshot Transfer(SST)全量同步复制。SST有三种全量拷贝方式:mysqldump、rsync和mariabackup(原xtrabackup)。SST的方法可以通过wsrep_sst_method这个参数来设置。

三种同步方式的特性对比如下:

使用基于Docker镜像快速部署MariaDB Galera Cluster集群 图文实战教程

所以,在生产环境强烈推荐使用mariabackup的同步方式。

——以上内容均整理自网络。

二、Docker镜像

现在是Docker容器化时代,纯手工编译、配置的部署方式我就不介绍了,感兴趣的可以自己去搜索相关教程。本文主要是分享一下自制的全自动部署MGC集群的Docker镜像(选用 MariaDB 10.3.12版本,基于Docker Host网络模式),最大程度简化了MGC集群的部署难度。

1、改进说明

相比于官方以及其他第三方Docker镜像,张戈封装的镜像有如下改进:

  • 一键式下发,无人值守创建MGC集群;
  • 支持MySQL(包含galera)全局任意参数设置;
  • 支持镜像之外的任何自定义脚本,支持sh、sql、gz、env等;
  • 成员节点故障恢复后自动探测,找到可用点后重新加入集群;
  • 支持集群、单点以及普通三种模式,可以覆盖各种关系存储场景;
  • 支持数据库初始化自定义设置,包含root、自定义账号、数据库、密码、主机等相关信息;
  • 支持节点恢复时的传输限速(单位KB/s),解决集群节点恢复顶满带宽问题,支持rsync / mariadbbackup 2种同步方式限速(独有改进)。

2、部署说明

①、环境准备

和上文提到的MGC特性一样,集群部署需要3台服务器,比如:

  • 节点1:192.168.1.100
  • 节点2:192.168.1.101
  • 节点3:192.168.1.102

在3台服务器上初始化安装好Docker,完成环境准备工作。

相关docker安装教程请参考:

 

②、快速启动

在3台服务器上执行如下命令(不要求先后顺序):

 run -d \
    --net=host \
    --name=demo \
    -e cluster_name=demo-3310 \
    -e my_port=3310 \ # 由于使用Host网络模式,所以需要给每个集群分配一个独有端口,避免冲突
    -e node1=192.168.1.100 \
    -e node2=192.168.1.101 \
    -e node3=192.168.1.102 \
    -e mysql_user=demo \
    -e mysql_user_password=123456 \
    -v /data/mariadb-galera:/data/mariadb-galera \
   jagerzhang/mariadb-galera

执行后,可以执行 docker logs -f demo-3310 查看启动日志,也可以执行 tail -f /data/mariadb-galera/logs/error.log 查看运行日志。

节点1的Docker启动日志如下:

[ xxxx:~]$ docker logs -f demo-3330
==================================== Auto-join Galera Cluster  =======================================
>> Can't found any activated node, It's maybe the first one, just start without join_address.
==================================== Initialization Infomation =======================================
cluster_name: demo
current_node: 192.168.1.100:3310
cluster_members: 192.168.1.100:3310,192.168.1.101:3310,192.168.1.102:3310

>> Start initialize configuration (Touch the file /data/mariadb-galera/lock/global.lock can skiped).
set pid_file=/data/mariadb-galera/lock/mysqld.pid
set max_connections=500
set log_error=/data/mariadb-galera/logs/error.log
set port=3310
set innodb_buffer_pool_size=2867M
set slow_query_log_file=/data/mariadb-galera/logs/slow.log
set datadir=/data/mariadb-galera/data
set innodb_log_file_size=573M
set report_host=192.168.1.100:3310
set server_id=1100
=================================== MySQL Daemon Runing Infomation ===================================
>> initializing database


PLEASE REMEMBER TO SET A PASSWORD FOR THE MariaDB root USER !
To do so, start the server, then issue the following commands:

'/usr/bin/mysqladmin' -u root password 'new-password'
'/usr/bin/mysqladmin' -u root -h 192.168.1.100 password 'new-password'

Alternatively you can run:
'/usr/bin/mysql_secure_installation'

which will also give you the option of removing the test
databases and anonymous user created by default.  This is
strongly recommended for production servers.

See the MariaDB Knowledgebase at http://mariadb.com/kb or the
MySQL manual for more instructions.

Please report any problems at http://mariadb.org/jira

The latest information about MariaDB is available at http://mariadb.org/.
You can find additional information about the MySQL part at:
http://dev.mysql.com
Consider joining MariaDB's strong and vibrant community:
https://mariadb.org/

>> database initialized
>> Found the galera configuration, waiting for mysql really start
>> mysql init process in progress...
2019-06-17 11:42:53 0 [Note] mysqld (mysqld 10.3.12-MariaDB-log) starting as process 224 ...
>> WARNNING: password for root is Empty, not recommended!

/docker-entrypoint.sh: ignoring /data/mariadb-galera/initdb.d/*


>> mysql init process done. ready for start up.

190617 11:42:57 mysqld_safe Logging to '/data/mariadb-galera/logs/error.log'.
190617 11:42:57 mysqld_safe Starting mysqld daemon with databases from /data/mariadb-galera/data

其他节点的启动将会自动探测其他节点端口是否就绪,尝试加入集群,比如节点2的启动日志如下:

[xxx:~]$ docker logs -f demo-3330
==================================== Auto-join Galera Cluster  =======================================
>> 192.168.1.102 is not ready, retry check...
>> 192.168.1.100 is not ready, retry check...
>> 192.168.1.102 is not ready, retry check...
>> 192.168.1.100 is not ready, retry check...
>> 192.168.1.102 is not ready, retry check...
>> 192.168.1.100 is not ready, retry check...
>> 192.168.1.102 is not ready, retry check...
>> 192.168.1.100 is ready, start join cluster 
==================================== Initialization Infomation =======================================
cluster_name: demo
current_node: 192.168.1.101:3310
cluster_members: 192.168.1.100:3310,192.168.1.101:3310,192.168.1.102:3310

>> Start initialize configuration (Touch the file /data/mariadb-galera/lock/global.lock can skiped).
set pid_file=/data/mariadb-galera/lock/mysqld.pid
set max_connections=500
set log_error=/data/mariadb-galera/logs/error.log
set port=3310
set innodb_buffer_pool_size=2867M
set slow_query_log_file=/data/mariadb-galera/logs/slow.log
set datadir=/data/mariadb-galera/data
set innodb_log_file_size=573M
set report_host=192.168.1.101:3310
set server_id=1101
=================================== MySQL Daemon Runing Infomation ===================================
>> initializing database

PLEASE REMEMBER TO SET A PASSWORD FOR THE MariaDB root USER !
To do so, start the server, then issue the following commands:

'/usr/bin/mysqladmin' -u root password 'new-password'
'/usr/bin/mysqladmin' -u root -h 192.168.1.101 password 'new-password'

Alternatively you can run:
'/usr/bin/mysql_secure_installation'

which will also give you the option of removing the test
databases and anonymous user created by default.  This is
strongly recommended for production servers.

See the MariaDB Knowledgebase at http://mariadb.com/kb or the
MySQL manual for more instructions.

Please report any problems at http://mariadb.org/jira

The latest information about MariaDB is available at http://mariadb.org/.
You can find additional information about the MySQL part at:
http://dev.mysql.com
Consider joining MariaDB's strong and vibrant community:
https://mariadb.org/

>> database initialized
190617 11:43:04 mysqld_safe Logging to '/data/mariadb-galera/logs/error.log'.
190617 11:43:04 mysqld_safe Starting mysqld daemon with databases from /data/mariadb-galera/data

三个节点都启动成功后,可以执行如下命令查看集群状态(用户名、密码与启动参数一致):

mysql -h192.168.1.100 -P3310 -udemo -p123456 -e "show status like '%wsrep%'"

正常结果如下:

+------------------------------+--------------------------------------------------------------+
| Variable_name                | Value                                                        |
+------------------------------+--------------------------------------------------------------+
| wsrep_apply_oooe             | 0.000000                                                     |
| wsrep_apply_oool             | 0.000000                                                     |
| wsrep_apply_window           | 1.000000                                                     |
| wsrep_causal_reads           | 0                                                            |
| wsrep_cert_deps_distance     | 1.000000                                                     |
| wsrep_cert_index_size        | 5                                                            |
| wsrep_cert_interval          | 0.000000                                                     |
| wsrep_cluster_conf_id        | 3                                                            |
| wsrep_cluster_size           | 3                                                            |
| wsrep_cluster_state_uuid     | fc7361db-90b1-11e9-92f5-b2c06a7dace0                         |
| wsrep_cluster_status         | Primary                                                      |
| wsrep_cluster_weight         | 3                                                            |
| wsrep_commit_oooe            | 0.000000                                                     |
| wsrep_commit_oool            | 0.000000                                                     |
| wsrep_commit_window          | 1.000000                                                     |
| wsrep_connected              | ON                                                           |
| wsrep_desync_count           | 0                                                            |
| wsrep_evs_delayed            |                                                              |
| wsrep_evs_evict_list         |                                                              |
| wsrep_evs_repl_latency       | 0/0/0/0/0                                                    |
| wsrep_evs_state              | OPERATIONAL                                                  |
| wsrep_flow_control_paused    | 0.000000                                                     |
| wsrep_flow_control_paused_ns | 0                                                            |
| wsrep_flow_control_recv      | 0                                                            |
| wsrep_flow_control_sent      | 0                                                            |
| wsrep_gcomm_uuid             | 049f60a7-90b2-11e9-8363-03f090c3c22c                         |
| wsrep_incoming_addresses     | 192.168.1.100:3310,192.168.1.101:3310,192.168.1.102:3310     |
| wsrep_last_committed         | 15                                                           |
| wsrep_local_bf_aborts        | 0                                                            |
| wsrep_local_cached_downto    | 9                                                            |
| wsrep_local_cert_failures    | 0                                                            |
| wsrep_local_commits          | 0                                                            |
| wsrep_local_index            | 2                                                            |
| wsrep_local_recv_queue       | 0                                                            |
| wsrep_local_recv_queue_avg   | 0.000000                                                     |
| wsrep_local_recv_queue_max   | 1                                                            |
| wsrep_local_recv_queue_min   | 0                                                            |
| wsrep_local_replays          | 0                                                            |
| wsrep_local_send_queue       | 0                                                            |
| wsrep_local_send_queue_avg   | 0.000000                                                     |
| wsrep_local_send_queue_max   | 1                                                            |
| wsrep_local_send_queue_min   | 0                                                            |
| wsrep_local_state            | 4                                                            |
| wsrep_local_state_comment    | Synced                                                       |
| wsrep_local_state_uuid       | fc7361db-90b1-11e9-92f5-b2c06a7dace0                         |
| wsrep_open_connections       | 0                                                            |
| wsrep_open_transactions      | 0                                                            |
| wsrep_protocol_version       | 9                                                            |
| wsrep_provider_name          | Galera                                                       |
| wsrep_provider_vendor        | Codership Oy <

 

>                            |
| wsrep_provider_version       | 25.3.25(r3836)                                               |
| wsrep_ready                  | ON                                                           |
| wsrep_received               | 10                                                           |
| wsrep_received_bytes         | 5834                                                         |
| wsrep_repl_data_bytes        | 0                                                            |
| wsrep_repl_keys              | 0                                                            |
| wsrep_repl_keys_bytes        | 0                                                            |
| wsrep_repl_other_bytes       | 0                                                            |
| wsrep_replicated             | 0                                                            |
| wsrep_replicated_bytes       | 0                                                            |
| wsrep_thread_count           | 2                                                            |
+------------------------------+--------------------------------------------------------------+
61 rows in set (0.001 sec)

3、自定义参数

自定义参数均使用Docker环境变量的方式传入,使用方法为启动容器时,在命令中加入 -e 参数名=参数值即可,比如想预创建一个 demo 数据库,则启动命令如下:

 run -d \
    --net=host \
    --name=demo \
    -e mysql_database=demo \ # 预创建 demo 数据库
    -e cluster_name=demo \
    -e my_port=3310 \
    -e node1=192.168.1.100 \
    -e node2=192.168.1.101 \
    -e node3=192.168.1.102 \
    -e mysql_user=demo \
    -e mysql_user_password=123456 \
    -v /data/mariadb-galera/demo-3310:/data/mariadb-galera \
   jagerzhang/mariadb-galera

①、当前版本支持的自定义参数

  • mysql_user 预创建数据库用户
  • mysql_user_password 用户密码,指定用户密码(由于脚本用@符号作为分隔符,所以注意密码里面不要含有@符号)
  • mysql_user_database 用户权限DB,指定用户具备哪个数据库的权限
  • mysql_user_grand 是否赋予grand权限,可选值为1/0,值为1则表示改用户具有创建用户的权限
  • mysql_database 创建数据库,预创建数据库,比如 -e mysql_database=demo,将会创建一个demo数据库
  • mysql_root_host root可远程的主机,指定root可以登录的主机
  • mysql_root_password root密码,指定root密码(由于脚本用@符号作为分隔符,所以注意密码里面不要含有@符号)
  • mysql_random_root_password 随机密码,让root密码随机生成,-e mysql_random_root_password=1,与mysql_root_password 参数互斥
  • transfer_limit 传输限速,单位 kb/s ,限制集群恢复时带宽,防止顶满内网带宽上限
  • cluster_name 集群名字
  • cluster_mode 是否使用集群模式
  • join_address 指定已存在的成员IP,主要用于集群扩容或节点重建
  • interface 指定绑定的网卡,用于集群自动探测时确认容器节点(nodeX),缺省优先级为:br0 > eth1 > eth0,若不在缺省顺序内,请务必指定实际网卡名

②、支持所有MySQL和Galera参数

使用my_${参数名} 形式来自定义MySQL运行参数,比如:-e my_tmp_table_size=512M,可以改变MySQL的tmp_table_size为512MB;

使用wsrep_${参数名} 形式来自定义Galera参数,比如:-e wsrep_sst_method=rsync,可以修改Galera集群的同步方式为rsync模式。

4、高级玩法

①、改变参数

用过Docker的朋友应该知道,容器一旦运行,那启动时通过 -e 指定的环境变量都不能修改,除非重建容器。但是,本次封装镜像,考虑到MySQL在后续运维过程中难免会需要修改一些参数的情况,比如上文提到的 tmp_table_size参数,启动时指定了,那容器不销毁直接重启都会使用启动时指定的值。为了处理这种特殊情况,镜像还留了一个类似『后门』的自定义参数配置。

这个文件位于:/data/mariadb-galera/demo-3310/conf/custom.cfg,集群首次启动自动生成,默认内容如下:

# put some custom configuration in this file can change zhe container by yourself.
# Usage:
#      export cluster_name=new_cluster_name
#      unset node2
# 

当我们确实需要修改容器的某个环境变量,又不想重建容器的情况下,可以将这个变量添加到custom.cfg文件,比如需要修改tmp_table_size这个值,如下修改即可:

# put some custom configuration in this file can change zhe container by yourself.
# Usage:
#      export cluster_name=new_cluster_name
#      unset node2
# 
export my_tmp_table_size=256M

Ps:当然,变量名称依然需要遵循镜像约定形式,说白了就是启动时 -e 参数名是什么,这里就export 参数名=参数值,简单吧!

②、初始化锁

我们知道,MySQL安装后首次启动都需要初始化db,本镜像基于Dockerfile过程透明,因此初始化db的过程也在容器首次启动当中实现。为了防止容器在重启时出现重复初始化db,这里设计了2个锁文件:

  • /data/mariadb-galera/demo-3310/initdb.lock 防止MySQL db重复初始化,如果后面要再次初始化,请在重启之前删除即可。
  • /data/mariadb-galera/demo-3310/global.lock 阻止Galera、MySQL参数的初始化,说白了就是让 -e 指定MySQL和Galera环境变量失效,不再修改配置文件,这个文件默认不存在,如果需要阻止相关初始化,则手工创建这个空文件即可。

③、使用Docker API

Docker支持API远程控制,因为在生产环境中,我们通过远程调用Docker API来快速拉起MGC集群,实现一键快速创建集群。

这里简单的分享一个用于快速拉起集群的Python测试Demo,仅供参考:

#-*- coding:utf-8 -*-
# author:jagerzhang
import sys,re,os,time
import ConfigParser
import requests
import json
reload(sys)
sys.setdefaultencoding('utf8')
config  = ConfigParser.ConfigParser()

config.read('%s/config.cfg' % os.path.dirname(os.path.realpath(__file__)))

def gen_create_request(member_address=None):
    env_list=[]
    conf_list={}
    for conf in config.sections():
        for conf_name in config.options(conf):
            conf_value = config.get(conf,conf_name)
            env_list.append("%s=%s" % (conf_name,conf_value))
            conf_list[conf_name] = conf_value
    if member_address is not None:
        env_list.append("WSREP_MEMBER_ADDRESS=%s" % member_address)
    create_json = {
    "Env": env_list,
    "Image": "jagerzhang/mariadb-galera:10.3.12",
    "HostConfig": {
       "Binds": [
           "%s/%s-%s:/data/mariadb-galera" % (conf_list["mount_dir"],conf_list["cluster_name"],conf_list["my_port"]),
            "/etc/localtime:/etc/localtime"
        ],
        "Memory": int(conf_list["memory"])*1024*1024*1024,
        "MemorySwap": -1,
        "CpusetCpus": "",
        "Privileged": True,
        "RestartPolicy": {
            "restart": "always"
        },
        "NetworkMode": "host"
        }
    }
    print create_json
    return conf_list,create_json

def create_container(host,cluster_name,my_port,create_json):
    headers = {'Content-type': 'application/json'}
    print "pulling image..."
    url          =  "http://%s:2375/v1.24/images/create?fromImage=jagerzhang/mariadb-galera&tag=10.3.12" % host
    result       =  requests.post(url)
    print result.text
    if result.status_code == 200:
        print "%s pull image success: %s" % (host,result.status_code)
        url          =  "http://%s:2375/containers/create?name=%s-%s"%(host,cluster_name,my_port)
        print "%s create container..." % host
        result       =  requests.post(url,data=json.dumps(create_json),headers=headers).json()
        try:
            url          =  "http://%s:2375/containers/%s/start" % (host,result['Id'])
            print "%s start container..." % host
            result       =  requests.post(url)
            if result.status_code == 204:
                print "%s start container success: %s" % (host,result.status_code)
                return 0
            else:
                print "%s start container failed: %s" % (host,result.status_code)
        except:
            print "%s create container failed: %s" % (host,result)
    else:
        print "%s pull image failed: %s" % (host,result.status_code)
    return result

def main():
    headers = {'Content-type': 'application/json'}
    conf_list,create_json = gen_create_request()
    node1        =  conf_list["node1"]
    node2        =  conf_list["node2"]
    node3        =  conf_list["node3"]
    cluster_name =  conf_list["cluster_name"]
    my_port         =  conf_list["my_port"]
    print create_container(node1,cluster_name,my_port,create_json)
    print create_container(node2,cluster_name,my_port,create_json)
    print create_container(node3,cluster_name,my_port,create_json)

main()

保存上述代码为 deploy.py,然后在相同的目录新增配置文件 config.cfg,内容如下:

[global]
cluster_name=demo
node1=192.168.1.100
node2=192.168.1.101
node3=192.168.1.102

[custom]
mysql_user=demo
mysql_user_host=%
mysql_user_grant=1
mysql_database=demo
mysql_user_password=123456
mysql_root_password="demo.2019"
transfer_limit=6000
memory=8
my_port=3310
mount_dir=/data/mysql_data

最后,只需要执行 python deploy.py 就可以快速拉起名为demo的MGC集群,是不是非常简单?当然,实际生产环境可以将这个功能集成到自动化运维平台,工具脚本仅仅是为了测试。

三、小结

本次分享的全自动MGC镜像目前已在我们的生产环境推广使用近半年,目前表现稳定。通过本镜像,使MariaDB Galera Cluster集群的可运维性得到了大幅度提升。在镜像的封装设计上,张戈也是投入了非常多的心血,由于文章篇幅有限以及时间关系,很多细节或实现原理都没能一一介绍到位,比如同步限速、全局参数可自定义、全自动创建集群等特性的实现原理,每一个拿来都能简单写一篇文章了。

目前,本镜像已完全开源,包括Dockerfile和相关自动化脚本均托管在DockerHub和GitHub上,感兴趣的同学可以自行前往查看:

最后提一句,MariaDB Galera Cluster集群可以配合使用数据库代理中间件ProxySQL或maxscale来实现DB负载均衡、读写分离。目前,我们在生产环境已全面切换到ProxySQL代理,表现也非常稳定,且可运维性良好,后续有时间再来整理分享,敬请期待!

(END)

文章来自“张戈的博客”,作者“Jager”:https://zhang.ge/5150.html
若需要帮助,请点击以上原文链接联系原作者。