1 galera mariadb
首先MariaDB是一個數據庫,能夠當作是MySQL的一個分支,因爲MySQL被SUN收購,因此MySQL面臨着閉源的風險,當時MySQL之父Widenius並無加入SUN,而是基於MySQL的代碼開發新的分支,命名爲MariaDB,並所有開源。html
Galera是Galera Cluster,是一種爲數據庫設計的新型的、數據不共享的、高度冗餘的高可用方案,galera mariadb就是集成了Galera插件的MariaDB集羣,Galera自己是具備多主特性的,因此galera mariadb不是傳統的主備模式的集羣,而是多主節點架構。node
2 galera mariadb的配置方式
個人一篇OpenStack高可用模塊博客中其中有一段是描述搭建galera mariadb的(2.2.1數據庫服務高可用配置):OpenStack高可用方案及配置python
3 galera mariadb的一些基本概念
(1)當前節點數據庫狀態mysql
MariaDB [(none)]> show status like 'wsrep_local_state_comment'; +---------------------------+--------+ | Variable_name | Value | +---------------------------+--------+ | wsrep_local_state_comment | Synced | +---------------------------+--------+
狀態查詢表:sql
狀態 | 狀態說明 |
Open | 節點啓動成功,嘗試鏈接到集羣 |
Primary | 節點已處於集羣中,在新節點加入時,選取donor進行數據庫同步時會產生的狀態 |
Joiner | 節點處於等待接收或正在接收同步文件的狀態 |
Joined | 節點完成數據同步,但還有部分數據不是最新的,在追趕與集羣數據一致的狀態 |
Synced | 節點正常提供服務的狀態,表示當前節點數據狀態與集羣數據狀態是一致的 |
Donor | 表示該節點被選爲Donor節點,正在爲新加進來的節點進行全量數據同步,此時該節點對客戶端不提供服務 |
(2)Primary Component
在網絡發生故障時,因爲網絡鏈接緣由,集羣可能被分紅好幾個小集羣,但只能有一個集羣能夠繼續進行數據修改,集羣的這部分稱爲Primary Component數據庫
(3)GTID
英文全稱爲Global Transaction ID,由UUID和sequence number偏移量組成,wsrep api中定義的集羣內部全局事務id,一個順序id,用來集羣集羣中狀態改變的惟一標誌及隊列中的偏移量json
(4)SST
英文全稱爲State Snapshot Transfer,即狀態快照遷移:經過從一個節點到另外一個節點遷移完整的數據拷貝(全量拷貝)。當一個新的節點加入到集羣中,新的節點從集羣中已有節點進行數據同步,開始進行狀態快照遷移。
Galera中有兩種不一樣的狀態遷移方法:
<1>邏輯數據遷移:採用mysqldump命令,這是一個阻塞式的方法。
<2>物理數據遷移:該方法採用rsync、rsync_wan、xtrabackup等方法直接在服務器之間拷貝數據,接收的服務器在拷貝完數據後啓動服務。
能夠經過配置文件中修改SST的方式:
wsrep_sst_method=rsyncbootstrap
(5)IST
英文全稱爲Increamental State Transfer,即增量狀態遷移:集羣一個節點經過識別新加入的節點缺失的事務操做,將該操做發送,而並不像SST那樣的全量數據拷貝。最多見狀況就是該節點以前已經存在於該集羣,只是關機重啓了,從新加入該集羣會使用IST進行同步。api
(6)grastate.dat
能夠經過該文件查看到該節點記錄的uuid和seqno,也就是上面說的GTID,當節點正常退出Galera集羣時,會將GTID的值更新到該文件中,以下:服務器
[root@abc3 ~]# cat /var/lib/mysql/grastate.dat # GALERA saved state version: 2.1 uuid: 30ae87da-8e8e-11e8-810c-6a8da854119b seqno: 33557 safe_to_bootstrap: 0
若是該節點數據庫服務正在運行,則seqno的值是-1的
(7)gvwstate.dat
當節點造成或改變Primary Component時,節點會建立或更新該文件,確保節點保留最新Primary Component的狀態,若是節點正常關閉,該文件會被刪除。
4 一些故障場景的恢復
(1)場景1
其中1個節點掛了,通常只須要重啓A節點的服務便可
(2)場景2
全部節點都掛了,重啓服務時不能單純的所有重啓,須要找狀態最新的那個節點啓動,且啓動時須要加上--wsrep-new-cluster參數,該節點啓動後其它節點再正常啓動服務便可。
這裏就涉及到一個關鍵點,那就是怎麼找哪一個是狀態最新的那個節點,第5點介紹查找最新節點的策略。
5 恢復策略和自動恢復腳本
(1)恢復策略
<1>首先判斷當前數據庫集羣中是否有服務在啓動着,若是有則直接啓動服務便可
<2>若是當前全部節點的數據庫服務都掛了,則須要找狀態最新的那個節點讓它攜帶--wsrep-new-cluster參數啓動,啓動起來以後其它節點直接啓動服務便可。
查找最新節點策略:
首先獲取各節點的grastate.dat文件中的seqno值,值最大的那個就是最新的節點;若是全部節點的seqno都是-1,則去比較全部節點的gvwstate.dat文件中的my_uuid和view_id是否相等,相等的那個則做爲第一個啓動節點,第一個啓動節點啓動後,其它節點正常啓動便可;若是依然未找到則須要人工干預來恢復了。
如下是我本身寫的自動恢復腳本:
#!/usr/bin/python2 # -*- coding: utf-8 -*- import os import time import traceback import logging import sys # 初始化日誌對象 logger = logging.getLogger("check-or-recover-galera") log_file='/var/log/check-or-recover-galera/check-or-recover-galera.log' if not os.path.exists(log_file): os.system('mkdir -p /var/log/check-or-recover-galera/') os.system('touch ' + log_file) formatter = logging.Formatter('%(asctime)s (filename)s[line:%(lineno)d] %(levelname)s %(message)s') file_handler = logging.FileHandler(log_file) file_handler.setFormatter(formatter) logger.addHandler(file_handler) logger.setLevel(logging.DEBUG) import socket PORT = 10000 BUFF_SIZE = 10240 def test_connect_ok(ip): client_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client_sock.settimeout(3) client_sock.connect((ip, PORT)) client_sock.close() # 這個方法要求在要遠程的節點上須要有個進程在監聽PORT端口等待處理命令 def send_request(ip, data, timeout=60): test_connect_ok(ip) client_sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) client_sock.settimeout(timeout) client_sock.connect((ip, PORT)) client_sock.send(data) ret_data = client_sock.recv(BUFF_SIZE) client_sock.close() return ret_data def remote_send_request(ip, data, timeout=60): res_remote = send_request(ip, json.dumps(data), timeout=timeout) if res_remote is None or res_remote == '': raise Exception('res_remote is null') res_remote = json.loads(res_remote) if res_remote['ret_state'] != 'success': raise Exception('ret_state is not success') return res_remote # 默認vmbr0是本地ip def get_local_ip(): cmd_out = os.popen('cat /etc/sysconfig/network-scripts/ifcfg-vmbr0 2>/dev/null |grep IPADDR').read() if cmd_out and cmd_out != '': cmd_out = cmd_out.strip() cmd_out = cmd_out.replace('"', '').replace(' ', '') tmp = cmd_out.split('=') if len(tmp) >= 2: ip = tmp[1] return ip return None # 獲取各節點的seqno值 def get_all_nodes_seqno(node_ips_arr): seqno_dict = {} data = {'req_type': 'get_seqno'} for node_ip in node_ips_arr: try: res_remote = remote_send_request(node_ip, data) seqno_dict[node_ip] = res_remote['seqno'] except Exception,e: seqno_dict[node_ip] = -1 logger.error(traceback.format_exc()) return seqno_dict # 獲取各節點的gvwstate.dat文件的my_uuid和view_id的比對值結果 def get_all_nodes_uv_is_equal(node_ips_arr): uv_equal_dict = {} data = {'req_type': 'get_uv_equal_value'} for node_ip in node_ips_arr: try: res_remote = remote_send_request(node_ip, data) uv_equal_dict[node_ip] = res_remote['equal'] except Exception,e: uv_equal_dict[node_ip] = 0 logger.error(traceback.format_exc()) return uv_equal_dict # 檢查自身mariadb服務是否已經啓動 def check_is_active_now(): is_active = os.popen('systemctl is-active mysqld_safe 2>/dev/null').read() is_active = is_active.strip() if is_active and is_active == 'active': logger.info('the mariadb is already up') return True return False # 第一個啓動的節點 def start_mariadb_with_wsrep(): os.system("sed -i 's/--wsrep-new-cluster//' /usr/lib/systemd/system/mysqld_safe.service") os.system("sed -i 's/user=mysql/user=mysql --wsrep-new-cluster/' /usr/lib/systemd/system/mysqld_safe.service") os.system("sed -i 's/safe_to_bootstrap:.*/safe_to_bootstrap: 1/' /var/lib/mysql/grastate.dat") os.system('systemctl daemon-reload') os.system('systemctl start mysqld_safe') # 將配置文件恢復回去 os.system("sed -i 's/--wsrep-new-cluster//' /usr/lib/systemd/system/mysqld_safe.service") os.system('systemctl daemon-reload') time.sleep(10) if check_is_active_now() is True: return True else: logger.error('use option wsrep-new-cluster start mariadb failed') return False def main(): while True: try: time.sleep(10) # 先檢測本身的mariadb是否已經本身啓動 if check_is_active_now() is True: time.sleep(60) continue # 這裏應該先檢測下thintaskd服務是否已經啓動,若是還沒啓動則需等待 is_thintaskd_active = os.popen('/etc/init.d/thintaskd status 2>/dev/null |grep active |grep running').read() if not is_thintaskd_active or is_thintaskd_active == '': logger.info('wait thintaskd service start') time.sleep(5) # 獲取當前galera的集羣的各節點的ip node_ips_info = os.popen("cat /etc/my.cnf.d/mariadb-server.cnf |grep '^wsrep_cluster_address'").read() node_ips_str = node_ips_info.split('gcomm://')[1] node_ips_str = node_ips_str.strip() node_ips_arr = node_ips_str.split(',') # 檢測其它節點是否已經有在運行着的 data = {'req_type': 'check_mariadb_service'} has_mariadb_service_on = False for node_ip in node_ips_arr: try: res_remote = remote_send_request(node_ip, data) state = res_remote['state'] if state == 'active': has_mariadb_service_on = True # 找到在運行着的節點 logger.info('find the running mariadb service node:' + node_ip) # 直接啓動本身服務 os.system('systemctl start mysqld_safe') time.sleep(10) if check_is_active_now() is True: time.sleep(60) else: logger.info('start mariadb service error') break except Exception,e: logger.error(traceback.format_exc()) logger.error('check_mariadb_service for ' + node_ip + ' failed, error:' + e.message) if has_mariadb_service_on is True: continue # 若是全部節點的mariadb都沒在運行,則須要尋找一個節點進行啓動 seqno_dict = get_all_nodes_seqno(node_ips_arr) logger.info('get seqno_dict:%s', seqno_dict) # 根據seqno值判斷哪一個節點爲啓動節點 first_boot_node = None max_seqno = -2 for key in seqno_dict: if seqno_dict[key] > max_seqno: max_seqno = seqno_dict[key] first_boot_node = key if first_boot_node is not None: logger.info('find the first_boot_node by seqno, first_boot_node:' + first_boot_node) # 判斷這個啓動節點是否是本身,若是是就啓動,不然等待其它節點啓動起來 if first_boot_node == get_local_ip(): if start_mariadb_with_wsrep() is True: time.sleep(60) else: logger.info('wait node ' + first_boot_node + ' start mariadb service') time.sleep(5) continue else: logger.info("all node's seqno is -1") # 若是全部節點的seqno都是-1則說明多是所有主機非正常中止的,好比斷電等 # 這時則經過比對gvwstate.dat文件的my_uuid和view_id是否相等來決定從這個節點啓動 # 當集羣時乾淨狀態中止的時候該文件是被刪除的 uv_equal_dict = get_all_nodes_uv_is_equal(node_ips_arr) # 根據返回的值判斷哪一個是啓動節點,1表示是,0表示否 for key in uv_equal_dict: if uv_equal_dict[key] == 1: first_boot_node = key logger.info('find the first_boot_node by uv_equal_dict, first_boot_node:' + first_boot_node) break if first_boot_node is not None: # 判斷這個啓動節點是否是本身,若是是就啓動,不然等待其它節點啓動起來 if first_boot_node == get_local_ip(): if start_mariadb_with_wsrep() is True: time.sleep(60) else: logger.info('wait node ' + first_boot_node + ' start mariadb service') time.sleep(5) continue else: logger.info("can not find first_boot_node by gvwstate.dat file") # 若是通過上述步驟依然找不到啓動節點,須要人工進行干預了,或者能夠隨機挑選個節點進行啓動 logger.error('can not find first_boot_node, maybe you should ask admin to deal with this problem') time.sleep(5) except Exception,e: logger.error(traceback.format_exc()) logger.error('error:' + e.message) if __name__ == "__main__": sys.exit(main())
如下是自定義的mysqld_safe.service服務的文件,你能夠將它放在/usr/lib/systemd/system/mysqld_safe.service
[Unit] Description=Thinputer API Server After=syslog.target network.target [Service] Type=notify NotifyAccess=all TimeoutStartSec=0 User=root ExecStartPre=/usr/libexec/mysql-check-socket ExecStartPre=/usr/libexec/mysql-prepare-db-dir %n ExecStart=/bin/mysqld_safe --defaults-file=/etc/my.cnf.d/mariadb-server.cnf --user=mysql [Install] WantedBy=multi-user.target