使用Saltstack安装Mariadb Galera Cluster

在过去几年里,我一直没有仔细学习过数据库运维,一方面是因为是组内有专门的DBA负责,我应该把精力花在没人管的地方;另一方面也是数据库是运维里非常重要的一个分支,基本自成体系,沉没成本较大。最近机缘巧合,准备接管数据库,借着这个机会系统的学习一下如何进行数据库运维。

由于MySQL被收购后,存在闭源的风险,加之CentOS7等Linux发行版默认都是用MariaDB替换MySQL,所以我也没太犹豫,准备后续都是用MariaDB。为了更方便的进行数据库运维,选定了MariaDB Galera Cluster进行安装测试。(有了Saltstack之后,我发现写博客的必要性都降低了,之前很大程度上是为了做个记录,后续参考使用,但是通过Saltstack安装后,看看SLS就行了^_^)

SLS目录结构

首先看一下SLS的目录结构吧。这参考了上篇文章所介绍的MySQL范例的一些内容,不过还差不少,比如没有完全的模块化,只是把简单的拆成了几个小文件,串行Include,不过第一个项目能写成这样我也知足啦。

mariadb_galera
├── config.sls      # 生成my.cnf、数据目录和设定iptables
├── defaults.yaml   # 定义不同环境下的变量
├── files
│   └── centos6
│       └── my.cnf  # my.cnf模板
├── init.sls        # 调用入口
├── server.sls      # MariaDB Galera Cluster及相关依赖包安装
└── service.sls     # 服务启动及数据库、账号初始化

通过Pillar定义好集群节点和账号信息

  • top.sls:

    base:
      'db0[1-3]\.opjasee\.com':               # Pillar中包含初始root密码等非常敏感的信息
        - match: pcre                         # 因此要做好防护,只有集群节点能访问
        - mariadb_galera.cluster01            # 另外Gitlab权限也需要多加注意
    
  • mariadb_galera/cluster01.sls:

    mariadb:                                  # 这部分定义了集群基本信息
      server:                                 # 主要用于生成配置
        root_password: hardpasswd             # 初始化账号
      galera:                                 # 以及设定iptables
        name: galera01
        sst_user: sst
        sst_password: hardpasswd
        nodes:
          db01.opjasee.com: 172.16.100.101
          db02.opjasee.com: 172.16.100.102
          db03.opjasee.com: 172.16.100.103
    

定义不同环境的变量

  • defaults.yaml:

    {% load_yaml as rawmap %}
    '6':                              # 我们目前不太可能会使用CentOS之外的发行版
      server: MariaDB-Galera-server   # 但会一直跟进CentOS的更新
      client: MariaDB-client          # 因此以版本号作为不同环境的key
      galera: galera                  # 目前尚未使用CentOS7,因此只有'6'一个配置
      old:                            # 根据版本号变化的可能有老版本MySQL包名
        mysql: mysql                  # 和MariaDB仓库配置、MariaDB模板等
        server: mysql-server
        mysql_libs: mysql-libs
      repo:
        filename: MariaDB
        baseurl: http://yum.mariadb.org/10.0.17/centos6-amd64/
        gpgkey: https://yum.mariadb.org/RPM-GPG-KEY-MariaDB
      source_config: salt://mariadb_galera/files/centos6/my.cnf
      mariadb_config: /etc/my.cnf
    {% endload %}
    

安装相关服务

  • server.sls:

    {% from "mariadb_galera/defaults.yaml" import rawmap with context %}
    {%- set mariadb = salt['grains.filter_by'](rawmap, grain='osmajorrelease') %}
    
    mariadb_repo:
      pkgrepo.managed:
        - comments:
          - '# Managed by Saltstack'
        - humanname: {{ mariadb.repo.filename }}
        - name: MariaDB
        - baseurl: {{ mariadb.repo.baseurl }}
        - gpgkey: {{ mariadb.repo.gpgkey }}
        - gpgcheck: 1
    
    percona_repo:
      cmd.run:
        - name: yum install -y http://www.percona.com/downloads/percona-release/redhat/0.1-3/percona-release-0.1-3.noarch.rpm
        - unless: ls /etc/yum.repos.d/percona-release.repo
    
    epel:                                  # 使用xtrabackup进行SST需要socat进行流传输,socat在EPEL仓库里
      pkg.installed:
        - name: epel-release
    
    mysql_old:                             # 删除老的MySQL,否则安装MariaDB时会冲突
      pkg.removed:
        - pkgs:
          - {{ mariadb.old.mysql }}
          - {{ mariadb.old.server }}
          - {{ mariadb.old.mysql_libs }}
    
    mariadb_server:
      pkg.installed:
        - pkgs:
          - {{ mariadb.server }}
          - {{ mariadb.client }}
          - {{ mariadb.galera }}
        - require:
          - pkg: mysql_old
          - pkgrepo: mariadb_repo
    
    percona_xtrabackup:                    # 使用xtrabackup进行SST需要安装percona-xtrabackup
      pkg.installed:                       # 这种SST方法的流程如下:
        - name: percona-xtrabackup         # Donor使用innobackupex备份数据库文件并通过socat传输到joiner
        - require:                         # joiner收到数据后,使用innobackupex进行文件恢复
          - cmd: percona_repo              # 之后启动MariaDB
          - pkg: mariadb_server
    
    socat:
      pkg.installed:
        - name: socat
        - require:
          - pkg: epel
          - pkg: mariadb_server
    
    restore_pkgs:                          # 卸载mysql-libs时会同时卸载sysstat和crontabs
      pkg.installed:                       # 安装完MariaDB后需要恢复
        - pkgs:
          - crontabs
          - sysstat
        - require:
          - pkg: mariadb_server
      service.running:
        - name: crond
        - enable: True
    

进行相关配置

  • 首先看一下files/centos6/my.cnf模板:

    # DO NOT CHANGE THIS FILE!
    # This config is generated by SALTSTACK
    # and all change will be overrided on next salt call
    {%- set galera_name = salt['pillar.get']('mariadb:galera:name', 'galera01') -%}
    {%- set ipaddresses = salt['pillar.get']('mariadb:galera:nodes', {}).values() -%}
    {%- set ipaddresses = ",".join(ipaddresses) -%}
    {%- set node_ip = salt['network.ipaddrs']('bond0')[0] -%}
    {%- set server_id = node_ip.split('.')[3]|int -%}
    {%- set sst_user = salt['pillar.get']('mariadb:galera:sst_user', 'sst') -%}
    {%- set sst_password = salt['pillar.get']('mariadb:galera:sst_password', 'notset') %}
    
    [client]
    port= 3306
    socket= /work/mysql/mysql.sock
    
    # 此处省略mysqldump等配置
    
    [mysqld]
    port        = 3306
    socket      = /work/mysql/mysql.sock
    datadir = /work/mysql/data                        # 使用xtrabackup进行SST时必须配置datadir
    log_bin = /work/mysql/data/mysql-bin              # 并且binlog必须也存在datadir下
    log-bin-index = /work/mysql/data/mysql-bin.index  # 因为xtrabackup不会处理datadir以外的目录
    server_id = {{ server_id }}                       # server_id直接用ip尾段定义
    
    # 此处省略各种参数及见仁见智的配置
    
    [galera]
    wsrep_provider=/usr/lib64/galera/libgalera_smm.so
    wsrep_provider_options="gcache.size=4G"           # 为了增大IST窗口,这个值最好设置大一点,write set的增长速度可参考binlog
    wsrep_cluster_address="gcomm://{{ ipaddresses }}" # 把所有节点的ip都配上,不过本机ip会自动被忽略(blacklisted)
    wsrep_cluster_name={{ galera_name }}
    wsrep_sst_method=xtrabackup-v2                    # 大部分是使用rsync,不过xtrabackup不会block donor,两者速度都很快
    wsrep_sst_auth={{ sst_user }}:{{ sst_password }}  # 使用rsync时不需要配置,使用mysqldump和xtrabackup时需要
    wsrep_node_address={{ node_ip }}                  #   不过这个只是donor备份使用,并不能用于joiner验证,说明见下面iptables配置部分
    wsrep_node_name="{{ grains['fqdn'] }}"
    binlog_format=row                                 # Galera Cluster里binglog各式只能是row
    default_storage_engine=InnoDB                     # Galera不支持非事务型引擎
    innodb_autoinc_lock_mode=2                        # Galera Cluster里该参数只能是2
    wsrep_slave_threads=4
    
  • config.sls:

    {% from "mariadb_galera/defaults.yaml" import rawmap with context %}
    {%- set mariadb = salt['grains.filter_by'](rawmap, grain='osmajorrelease') %}
    {% set cluster_ips = salt['pillar.get']('mariadb:galera:nodes', {}).values() %}
    
    include:
      - mariadb_galera.server
    
    mariadb_config:
      file.managed:
        - source: {{ mariadb.source_config }}
        - name: {{ mariadb.mariadb_config }}
        - user: root
        - group: root
        - mode: 644
        - template: jinja
        - require:
          - pkg: mariadb_server
    
    mariadb_dir:
      file.directory:
        - name: /work/mysql
        - makedirs: True
        - user: mysql
        - group: mysql
        - dir_mode: 700
        - require_in:
          - file: mariadb_config
    
    {% for subdir in ['data', 'log', 'tmp'] %} # MySQL为什么不自动创建缺失的目录呢
    mariadb_subdirs_{{ subdir }}:
      file.directory:
        - name: /work/mysql/{{ subdir }}
        - makedirs: True
        - user: mysql
        - group: mysql
        - dir_mode: 755
        - require_in:
          - file: mariadb_config
    {% endfor %}
    
    {% for ip in cluster_ips %}                # 不知道为什么,好像很少人关心SST的安全问题
    iptables_allow_{{ loop.index }}:           # 可能有些人误以为wsrep_sst_auth是新节点请求SST的"钥匙"
      iptables.insert:                         # 实际上,rsync方法根本不需要该配置
        - position: 1                          # 在xtrabackup中,这也只是donor自身用于连接数据库导出数据的
        - table: filter                        # 虽然在mysqldump中号称这个密码同时用于donor和joiner
        - chain: INPUT                         # 但实际上joiner可能是别人完全控制的
        - dport: 4567                          # 目前没有合理的认证方法
        - source: {{ ip }}                     # 任何能连通数据库主机的节点都可以通过SST获取数据库所有数据
        - proto: tcp                           # https://mariadb.atlassian.net/browse/MDEV-6385
        - jump: ACCEPT                         # 暂时只能通过防火墙解决,只允许集群内节点连通4567端口
        - save: True                           # 其他节点无法通过该端口发起SST请求
    {% endfor %}                               # SST数据传输默认通过4444端口,禁用该端口应该也可以
                                               # 但是禁用4567会更彻底,虽然可能有很低的性能损耗
    iptables_deny:                             # 不过Saltstack目前的iptables模块也有问题
      iptables.append:                         # 会重复添加相同的规则
        - table: filter                        # 目前应该已有修复方案但尚未更新到rpm中
        - chain: INPUT                         # https://raw.githubusercontent.com/saltstack/salt/develop/salt/modules/iptables.py
        - dport: 4567
        - proto: tcp
        - jump: DROP
        - save: True
    

启动服务并初始化账号

  • service.sls:

    {% from "mariadb_galera/defaults.yaml" import rawmap with context %}
    {%- set mariadb = salt['grains.filter_by'](rawmap, grain='osmajorrelease') %}
    {% set galera_status = salt['grains.get']('galera_status', None) %}
    {% set root_password = salt['pillar.get']('mariadb:server:root_password', 'notset') %}
    {% set sst_user = salt['pillar.get']('mariadb:galera:sst_user', 'sst') %}
    {% set sst_password = salt['pillar.get']('mariadb:galera:sst_password', 'notset') %}
    
    include:
      - mariadb_galera.config
    
    {% if galera_status == 'new' %}             # Galera Cluster启动的主要问题是,第一个节点需要使用bootstrap启动
    del_galera_status:                          # 并且需要执行mysql_install_db来生成系统库
      grains.absent:                            # 剩下的所有节点直接start即可
        - name: galera_status                   # 因此使用一个grains标志位来区分这两种角色
        - destructive: True                     # 第一个节点启动时删除该标志
    
    install_db:
      cmd.run:
        - name: mysql_install_db --user=mysql
        - unless: ls /work/mysql/data/mysql     # 虽然不应该出现执行两次mysql_install_db的情况
        - require:                              # 但多增加点约束肯定没坏处
          - pkg: mariadb_server
          - file: mariadb_config
          - grains: del_galera_status
    
    start_wsrep:
      cmd.run:
        - name: service mysql bootstrap
        - require:
          - pkg: mariadb_server
          - pkg: percona_xtrabackup
          - pkg: socat
          - file: mariadb_config
          - cmd: install_db
    
    mariadb_root_password:                      # 设定root初始化密码,服务器启动后此密码最好修改一下
      cmd.run:
        - name: mysqladmin -u root password '{{ root_password|replace("'", "'\"'\"'") }}'
        - onlyif: mysql -u root -e "SELECT 1;"
        - require:
          - cmd: start_wsrep
    
    {% for host in ['127.0.0.1', '::1', salt['grains.get']('host')] %}
    mariadb_root_password_{{ loop.index }}:
      cmd.run:
        - name: mysql -u root --password={{ root_password|replace("'", "'\"'\"'") }} -e "SET PASSWORD FOR 'root'@'{{ host }}' = PASSWORD('{{ root_password|replace("'", "'\"'\"'") }}');"
        - require:
          - cmd: mariadb_root_password
    {% endfor %}
    
    {% for host in ['localhost', salt['grains.get']('host')] %}
    mariadb_delete_anonymous_user_{{ loop.index }}:
      cmd.run:                                  # 听说删除匿名用户比较安全
        - name: mysql -u root --password={{ root_password|replace("'", "'\"'\"'") }} -e "DROP USER ''@'{{ host }}' ;"
        - require:
          - cmd: mariadb_root_password
    {% endfor %}
    
    user_sst:                                   # 启动后修改sst密码还没测试过,不过预计作为donor执行innobackupex会异常
      cmd.run:                                  # 这些权限应该够了
        - name: mysql -u root --password={{ root_password|replace("'", "'\"'\"'") }} -e "GRANT RELOAD, LOCK TABLES, REPLICATION CLIENT, CREATE TABLESPACE, SUPER ON *.* TO '{{ sst_user }}'@'localhost' IDENTIFIED BY '{{ sst_password|replace("'", "'\"'\"'") }}';"
        - require:
          - cmd: mariadb_root_password
    {% else %}
    mariadb_service:
      service.running:
        - name: mysql
        - require:
          - pkg: mariadb_server
          - pkg: percona_xtrabackup
          - pkg: socat
          - file: mariadb_config
    {% endif %}
    

使用及一些问题

  • 目前可通过三条命令完成MariaDB Galera Cluster的搭建及初始化:

    $ salt 'db01.opjasee.com' grains.setval galera_status new
    $ salt 'db01.opjasee.com' state.sls mariadb_galera
    $ salt -E 'db0[23].opjasee.com' state.sls mariadb_galera
    
  • 不过如果整个Galera Cluster重启,是不能直接highstate的,因为第一个启动的节点依然需要使用bootstrap启动。

  • 如果新增一个节点,需要在整个集群中增加相应的iptables规则,不过由于目前Saltstack的iptables模块有问题,会造成重复添加,也需要注意下。

另外,Severalnines提供了CLUSTERCONTROL,可以图形化的完成Galera Cluster的部署,有兴趣的可以试试。

Loading Disqus comments...
Table of Contents