xtrabackup是一個MySQL備份還原的經常使用工具,實際使用過程應該都是shell或者python封裝的自動化腳本,尤爲是備份。
對還原來講,對於基於完整和增量備份的還原,還原差別備份須要指定增量備份等等一系列容易出錯的手工操做,以及binlog的還原等,若是純手工操做的話很是麻煩。
即使是你記性很是好,對xtrabackup很是熟悉,純手工操做的話,很是容易出錯,其實也上網找過,還原沒有發現太好用的自動化還原腳本。
因而就本身用Python封裝了xtrabackup備份和還原的過程,能夠作到自動化備份,基於時間點的自動化還原等等。python
須要對xtrabackup有必定的瞭解,包括流式備份,壓縮備份,Xtrabackup還原,mysqlbinlog還原等等。mysql
1,基於xtrabackup的流式壓縮備份。
2,週六/或者任意時間的第一次備份爲完整備份,其餘時間爲基於上一次備份的增量備份。
3,將備份開始時間,結束時間,備份路徑等信息寫入一個日誌文件,方便後續自動化還原的時候解析。sql
效果以下:無論是何時,第一次必須爲完整備份,而後根據上述規則,繼續執行備份的話爲基於最新一次備份的增量備份,每備份完成後生成修改備份日誌列表信息。shell
實現:數據庫
1 # -*- coding: utf-8 -*- 2 import os 3 import time 4 import datetime 5 import sys 6 import socket 7 import shutil 8 import logging 9 10 logging.basicConfig(level=logging.INFO 11 #handlers={logging.FileHandler(filename='backup_log_info.log', mode='a', encoding='utf-8')} 12 ) 13 14 15 host = "127.0.0.1" 16 port = "7000" 17 user = "root" 18 password = "root" 19 cnf_file = "/usr/local/mysql57_data/mysql7000/etc/my.cnf" 20 backup_dir = "/usr/local/backupdata" 21 backupfilelist = os.path.join(backup_dir,"backupfilelist.log") 22 backup_keep_days = 15 23 24 #獲取備份類型,週六進行完備,平時增量備份,若是沒有全備,執行完整備份 25 def get_backup_type(): 26 backup_type = None 27 if os.path.exists(backupfilelist): 28 with open(backupfilelist, 'r') as f: 29 lines = f.readlines() 30 if(lines): 31 last_line = lines[-1] #get last backup name 32 if(last_line): 33 if(time.localtime().tm_wday==6): 34 backup_type = "full" 35 else: 36 backup_type = "incr" 37 else: 38 backup_type = "full" 39 else: 40 backup_type = "full" 41 else: 42 #full backup when first backup 43 open(backupfilelist, "a").close() 44 backup_type = "full" 45 return backup_type 46 47 #獲取最後一次備份信息 48 def get_last_backup(): 49 last_backup = None 50 if os.path.exists(backupfilelist): 51 with open(backupfilelist, 'r') as f: 52 lines = f.readlines() 53 last_line = lines[-1] # get last backup name 54 if (last_line): 55 last_backup = os.path.join(backup_dir, last_line.split("|")[-1]) 56 return last_backup.replace("\n","") 57 58 59 #探測實例端口號 60 def get_mysqlservice_status(): 61 mysql_stat = 0 62 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 63 result = s.connect_ex((host, int(port))) 64 #port os open 65 if (result == 0): 66 mysql_stat = 1 67 return mysql_stat 68 69 #清理過時的歷史備份信息 70 def clean_expired_file(): 71 for backup_name in os.listdir(backup_dir): 72 if os.path.isdir(backup_name): 73 bak_datetime = datetime.datetime.strptime(backup_name.replace("_full","").replace("_incr",""), '%Y%m%d%H%M%S') 74 if(bak_datetime<datetime.datetime.now() - datetime.timedelta(days=backup_keep_days)): 75 shutil.rmtree(os.path.join(backup_dir, backup_name)) 76 77 #完整備份 78 def full_backup(backup_file_name): 79 os.system("[ ! -d {0}/{1} ] && mkdir -p {0}/{1}".format(backup_dir,backup_file_name)) 80 logfile = os.path.join(backup_dir, "{0}/{1}/backuplog.log".format(backup_dir,backup_file_name)) 81 backup_commond = ''' innobackupex --defaults-file={0} --no-lock {1}/{6} --user={2} --password={3} --host="{4}" --port={5} --tmpdir={1}/{6} --stream=xbstream --compress --compress-threads=8 --parallel=4 --extra-lsndir={1}/{6} > {1}/{6}/{6}.xbstream 2>{7} '''.\ 82 format(cnf_file,backup_dir,user,password,host,port,backup_file_name,logfile) 83 execute_result = os.system(backup_commond) 84 return execute_result 85 86 #增量備份 87 def incr_backup(backup_file_name): 88 os.system("[ ! -d {0}/{1} ] && mkdir -p {0}/{1}".format(backup_dir, backup_file_name)) 89 current_backup_dir = "{0}/{1}".format(backup_dir, backup_file_name) 90 logfile = os.path.join(backup_dir, "{0}/{1}/backuplog.log".format(backup_dir, backup_file_name)) 91 #增量備份基於上一個增量/完整備份 92 incremental_basedir = get_last_backup() 93 backup_commond = '''innobackupex --defaults-file={0} --no-lock {6} --user={2} --password={3} --host={4} --port={5} --stream=xbstream --tmpdir={6} --compress --compress-threads=8 --parallel=4 --extra-lsndir={6} --incremental --incremental-basedir={7} 2> {8} > {6}/{9}.xbstream '''\ 94 .format(cnf_file,backup_dir,user,password,host,port,current_backup_dir,incremental_basedir,logfile,backup_file_name) 95 # print(backup_commond) 96 execute_result = os.system(backup_commond) 97 return execute_result 98 99 #刷新binlog,意義不大,本來計劃在完整備份以後執行一個binlog的切換,暫時棄用 100 def flush_log(): 101 flush_log_commond = ''' mysql -h${0} -u${1} - p${2} -P${1} mysql - e"flush logs" '''.format(user,password,host,port) 102 os.system(flush_log_commond) 103 104 105 if __name__ == '__main__': 106 mysql_stat = get_mysqlservice_status() 107 backup_type = get_backup_type() 108 if mysql_stat <= 0 : 109 logging.info("mysql instance is inactive,backup exit") 110 sys.exit(1) 111 try: 112 start_time = datetime.datetime.now().strftime('%Y%m%d%_H%M%S') 113 logging.info(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')+"--------start backup") 114 #flush_log() 115 backup_file_name = start_time 116 execute_result = None 117 if(backup_type == "full"): 118 backup_file_name = backup_file_name+"_full" 119 logging.info("execute full backup......") 120 execute_result = full_backup(backup_file_name) 121 if (execute_result == 0): 122 logging.info(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + "--------begin cleanup history backup") 123 logging.info("execute cleanup backup history......") 124 clean_expired_file() 125 logging.info(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + "--------finsh cleanup history backup") 126 else: 127 backup_file_name = backup_file_name + "_incr" 128 logging.info("execute incr backup......") 129 execute_result = incr_backup(backup_file_name) 130 if(execute_result==0): 131 finish_time = datetime.datetime.now().strftime('%Y%m%d%_H%M%S') 132 backup_info = start_time+"|"+finish_time+"|"+start_time+ "_" + backup_type 133 with open(backupfilelist, 'a+') as f: 134 f.write(backup_info + '\n') 135 logging.info(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')+"--------finish backup") 136 else: 137 logging.info(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + "--------xtrabackup failed.please check log") 138 except: 139 raise 140 sys.exit(1)
說直白一點,以這裏的基於時間點或者是position的還原,就是一個不斷找文件的過程,
1,首先任何還原,都須要一個建立於還原點前的完整備份。
2,基於上述完整備份,利用還原的時間點與xtrbackup的備份日誌去作對比來獲取所需的增量備份(0個或者1個或者多個)。
3,基於上面兩步找到的(完整+增量)備份,利用最後一個備份的position,用於第一個binlog還原時指定start-position,
同時利用binlog的最後修改時間與還原的時間點對比,決定使用那些binlog,同時最後一個binlog要指定stop-datime= 還原的時間點app
1,如何還原時間點的最新的一個完整備份
備份的時候維護一個備份信息,以下,這裏是backfilelist.log,包括備份開始時間,結束時間,備份類型,備份路徑等。
能夠根據備份開始時間,找到第一個早於還原時間點的完整備份 socket
2,若是找到恢復所須要的差別備份
同1,從完整備份開始,依次向後找各個增量備份,直到最後一個早於還原時間點的差別備份,可能有一個或者多個ide
3,如何找到差別備份以後,須要哪些binlog
基於binlog文件自身的最後修改時間屬性信息,從2中找到的最後一個差別備份的時間,開始向後依次找binlog,可能有一個或者多個 工具
以下是一個基於時間點來還原數據庫的demo,沒寫入兩條數據,執行一次備份(上述備份會自動區分完整備份或者差別備份)
三次備份以後,繼續寫兩條數據,flush logs,而後繼續分兩次分別寫兩條數據,目的是將數據分散到不一樣的binlog中,最後刪除所有數據
而後基於刪除數據以前的時間點來自動生成還原數據庫的shell,執行shell便可達到還原數據庫的目的。優化
以下執行基於時間點的rextrabackup.py文件以後,時間點爲"2019-08-01 18:50:59",也就是發生刪除操做的前一個時間點,來生成的還原信息。
其實只須要重定向到一個shell文件中,執行shell文件便可自動化還原,或者直接在python腳本中執行這些命令,便可自動化完成還原操做。
這裏爲了顯示,打印了出來。
能夠發現,基於時間點的還原,找到的文件是預期的:
1個完整備份,2個增量備份,2個binlog日誌中的一部分數據,
其中binlog日誌還原的start-position成功地銜接到最後一個增量備份的position,同時最後一個binlog日誌的還原停留在指定的時間點。
自動生成的shell還原代碼
################uncompress backup file###################
innobackupex --apply-log --redo-only /temp/restoretmp/20190801184134_full innobackupex --apply-log --redo-only /temp/restoretmp/20190801184134_full --incremental-dir=/temp/restoretmp/20190801184335_inc innobackupex --apply-log --redo-only /temp/restoretmp/20190801184134_full --incremental-dir=/temp/restoretmp/20190801184518_inc innobackupex --apply-log /temp/restoretmp/20190801184134_full ################stop mysql service################### systemctl stop mysqld_7000 ####################backup current database file########################### mv /usr/local/mysql57_data/mysql7000/data /usr/local/mysql57_data/mysql7000/data_20190801185855 mkdir /usr/local/mysql57_data/mysql7000/data chown -R mysql.mysql /usr/local/mysql57_data/mysql7000/data ################restore backup data################### innobackupex --defaults-file=/usr/local/mysql57_data/mysql7000/etc/my.cnf --copy-back --rsync /temp/restoretmp/20190801184134_full chown -R mysql.mysql /usr/local/mysql57_data/mysql7000/data ################stop mysql service################### systemctl start mysqld_7000 ################restore data from binlog################### cd /usr/local/mysql57_data/mysql7000/log/bin_log mysqlbinlog mysql_bin_1300.000001 --skip-gtids=true --start-position=982 | mysql mysql -h127.0.0.1 -uroot -proot -P7000 mysqlbinlog mysql_bin_1300.000002 --skip-gtids=true --stop-datetime="2019-08-01 18:50:59" | mysql -h127.0.0.1 -uroot -proot -P7000
日誌信息
實現
# -*- coding: utf-8 -*- import os import time import datetime import sys import socket import logging logging.basicConfig(level=logging.INFO #handlers={logging.FileHandler(filename='restore_log_info.log', mode='a', encoding='utf-8')} ) host = "127.0.0.1" port = "7000" user = "root" password = "root" instance_name = "mysqld_7000" stop_at = "2019-08-01 18:50:59" cnf_file = "/usr/local/mysql57_data/mysql7000/etc/my.cnf" backup_dir = "/usr/local/backupdata/" dest_dir = "/temp/restoretmp/" xtrabackuplog_name = "backuplog.log" backupfilelist = os.path.join(backup_dir,"backupfilelist.log") #根據key值,獲取MySQL配置文件中的value def get_config_value(key): value = None if not key: return value if os.path.exists(cnf_file): with open(cnf_file, 'r') as f: for line in f: if (line.split("=")[0]): if(line[0:1]!="#" and line[0:1]!="["): if (key==line.split("=")[0].strip()): value =line.split("=")[1].strip() return value def stop_mysql_service(): print("################stop mysql service###################") print("systemctl stop {}".format(instance_name)) def start_mysql_service(): print("################stop mysql service###################") print("systemctl start {0}".format(instance_name)) #返回備份日誌中的最新的一個早於stop_at時間的完整備份,以及其後面的增量備份 def get_restorefile_list(): list_backup = [] list_restore_file = [] if os.path.exists(backupfilelist): with open(backupfilelist, 'r') as f: lines = f.readlines() for line in lines: list_backup.append(line.replace("\n","")) if (list_backup): for i in range(len(list_backup) - 1, -1, -1): list_restore_file.append(list_backup[i]) backup_name = list_backup[i].split("|")[2] if "full" in backup_name: full_backup_time = list_backup[i].split("|")[1] if(stop_at<full_backup_time): break else: list_restore_file = None #restore file in the list_restore_log list_restore_file.reverse() return list_restore_file #解壓縮須要還原的備份文件,包括一個完整備份以及N個增量備份(N>=0) def uncompress_backup_file(): print("################uncompress backup file###################") list_restore_backup = get_restorefile_list() #若是沒有生成時間早於stop_at的完整備份,沒法恢復,退出 if not list_restore_backup: raise("There is no backup that can be restored") exit(1) for restore_log in list_restore_backup: #解壓備份文件 backup_name = restore_log.split("|")[2] backup_path = restore_log.split("|")[2] backup_full_name = os.path.join(backup_dir,backup_path,backup_name) backup_path = os.path.join(backup_dir,restore_log.split("|")[-1]) #print('''[ ! -d {0} ] && mkdir -p {0}'''.format(os.path.join(dest_dir,backup_name))) os.system('''[ ! -d {0} ] && mkdir -p {0}'''.format(os.path.join(dest_dir,backup_name))) #print("xbstream -x < {0}.xbstream -C {1}".format(backup_full_name,os.path.join(dest_dir,backup_name))) os.system("xbstream -x < {0}.xbstream -C {1}".format(backup_full_name,os.path.join(dest_dir,backup_name))) #print("cd {0}".format(os.path.join(dest_dir,backup_name))) os.system("cd {0}".format(os.path.join(dest_dir,backup_name))) #print('''for f in `find {0}/ -iname "*\.qp"`; do qpress -dT4 $f $(dirname $f) && rm -f $f; done '''.format(os.path.join(dest_dir,backup_name))) os.system('''for f in `find {0}/ -iname "*\.qp"`; do qpress -dT4 $f $(dirname $f) && rm -f $f; done'''.format(os.path.join(dest_dir,backup_name))) current_backup_begin_time = None current_backup_end_time = None #比較當前備份的結束時間和stop_at,若是當前備份開始時間小於stop_at而且結束時間大於stop_at,解壓縮備份結束 with open(os.path.join(dest_dir,backup_name,"xtrabackup_info"), 'r') as f: for line in f: if line and line.split("=")[0].strip()=="start_time": current_backup_begin_time = line.split("=")[1].strip() if line and line.split("=")[0].strip()=="end_time": current_backup_end_time = line.split("=")[1].strip() #按照stop_at時間點還原的最後一個數據庫備份,結束從第一個完整備份開始的解壓過程 if current_backup_begin_time<=stop_at<=current_backup_end_time: break #返回最後一個備份文件,須要備份文件中的xtrabackup_info,解析出當前備份的end_time,從而確認須要哪些binlog return backup_name #根據返回最後一個備份文件,須要備份文件中的xtrabackup_info,結合stop_at,確認須要還原的binlog文件,以及binlog的position信息 def restore_database_binlog(last_backup_file): print("################restore data from binlog###################") binlog_dir = get_config_value("log-bin") if not (backup_dir): binlog_dir = get_config_value("log_bin") print("cd {0}".format(os.path.dirname(binlog_dir))) last_backup_file =os.path.join(dest_dir,last_backup_file,"xtrabackup_info") #parse backuplog.log and get binlog name and position backup_position_binlog_file = None backup_position = None with open(last_backup_file, 'r') as f: lines = f.readlines() for line in lines: if "binlog_pos = filename " in line: backup_position_binlog_file = line.replace("binlog_pos = filename ", "").split(",")[0] backup_position_binlog_file = backup_position_binlog_file.replace("'", "") backup_position = line.replace("binlog_pos = filename ", "").split(",")[1].strip() backup_position = backup_position.split(" ")[1].replace("'", "") pass else: continue # /usr/local/mysql57_data/mysql8000/log/bin_log/mysql_bin_1300 binlog_config = get_config_value("log-bin") binlog_path = os.path.dirname(binlog_config) binlog_files = os.listdir(binlog_path) #若是沒有找到binlog,忽略binlog的還原 if not binlog_files: exit(1) #對binlog文件排序,按順序遍歷binlog,獲取binlog的最後的修改時間,與stop_at作對比,判斷還原的過程是否須要某個binlogfile binlog_files.sort() binlog_files_for_restore = [] # 恢復數據庫的指定時間點 stop_at_time = datetime.datetime.strptime(stop_at, '%Y-%m-%d %H:%M:%S') for binlog in binlog_files: if (".index" in binlog or "relay" in binlog): continue #保留最後一個備份中的binlog,以及其後面的binlog,這部分binlog會在還原的時候用到 if (int(binlog.split(".")[-1]) >= int(backup_position_binlog_file.split(".")[-1])): binlog_files_for_restore.append(binlog) binlog_file_count = 0 #第一個文件,從上最後一個差別備份的position位置開始,最後一個文件,須要stop_at到指定的時間 for binlog in binlog_files_for_restore: if not os.path.isdir(binlog): #binlog物理文件的最後修改時間 binlog_file_updatetime = datetime.datetime.strptime(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(os.stat(binlog_path+"/"+binlog).st_mtime)),'%Y-%m-%d %H:%M:%S') #判斷binlog的生成時間,是否大於stop_at,對於修改時間大於stop_at的日誌,須要所有還原,不須要stop_at指定截止點 if stop_at_time > binlog_file_updatetime : if (binlog_file_count < 1): if (len(binlog_files_for_restore) == 1): # 找到差別備份以後的第一個binlog,須要根據差別備份的position,來過來第一個binlog文件 restore_commond = '''mysqlbinlog {0} --skip-gtids=true --start-position={1} --stop-datetime="{2}" | mysql mysql -h{3} -u{4} -p{5} -P{6}''' \ .format(binlog, backup_position, stop_at, host, user, password, port) print(restore_commond) binlog_file_count = binlog_file_count + 1 else: # 找到差別備份以後的第一個binlog,須要根據差別備份的position,來過來第一個binlog文件 restore_commond = '''mysqlbinlog {0} --skip-gtids=true --start-position={1} | mysql mysql -h{2} -u{3} -p{4} -P{5}''' \ .format(binlog, backup_position, host, user, password, port) print(restore_commond) binlog_file_count = binlog_file_count + 1 else: # 從第二個文件開始,binlog須要所有還原 restore_commond = '''mysqlbinlog {0} --skip-gtids=true | mysql mysql -h{1} -u{2} -p{3} -P{4}''' \ .format(binlog, host, user, password, port) print(restore_commond) binlog_file_count = binlog_file_count + 1 else: if (binlog_file_count < 1): restore_commond = '''mysqlbinlog {0} --skip-gtids=true --start-position={1} --stop-datetime={2} | mysql -h{3} -u{4} -p{5} -P{6}'''.format(binlog, backup_position,stop_at,host,user,password,port) print(restore_commond) binlog_file_count = binlog_file_count + 1 else: if (binlog_file_count >= 1): restore_commond = '''mysqlbinlog {0} --skip-gtids=true --stop-datetime="{1}" | mysql -h{2} -u{3} -p{4} -P{5}'''.format(binlog, stop_at,host,user,password,port) print(restore_commond) binlog_file_count = binlog_file_count + 1 break def apply_log_for_backup(): list_restore_backup = get_restorefile_list() start_flag = 1 full_backup_path = None for current_backup_file in list_restore_backup: #解壓備份文件 current_backup_name = current_backup_file.split("|")[2] current_backup_fullname = os.path.join(dest_dir, current_backup_name) if(start_flag==1): full_backup_path = current_backup_fullname start_flag = 0 print("innobackupex --apply-log --redo-only {0}".format(full_backup_path)) else: print("innobackupex --apply-log --redo-only {0} --incremental-dir={1}".format(full_backup_path,current_backup_fullname)) #apply_log for full backup at last(remove --read-only parameter) print("innobackupex --apply-log {0}".format(full_backup_path)) def restore_backup_data(): print("####################backup current database file###########################") datadir_path = get_config_value("datadir") print("mv {0} {1}".format(datadir_path,datadir_path+"_"+ datetime.datetime.now().strftime('%Y%m%d%H%M%S'))) print("mkdir {0}".format(datadir_path)) print("chown -R mysql.mysql {0}".format(datadir_path)) print("################restore backup data###################") list_restore_backup = get_restorefile_list() full_restore_path= dest_dir + list_restore_backup[0].split("|")[-1].replace(".xbstream","") print("innobackupex --defaults-file={0} --copy-back --rsync {1}".format(cnf_file,full_restore_path)) print("chown -R mysql.mysql {0}".format(datadir_path)) def restore_database(): #解壓縮須要還原的備份文件 last_backup_file_path = uncompress_backup_file() #對備份文件apply-log apply_log_for_backup() #中止mysql服務 stop_mysql_service() #恢復備份 restore_backup_data() #啓動MySQL服務 start_mysql_service() #從binlog中恢復數據 restore_database_binlog(last_backup_file_path) if __name__ == '__main__': restore_database()
最後不要忘了清理戰場:
1,解壓縮的備份文件還留在指定的路徑中,
2,還原以前備份的data文件,以data_日期命名的文件,也沒有清理
擠時間寫出來的,粗略測了一下沒有問題,以實現功能爲主,沒有進一步封裝,後續會以此爲基礎進行優化。