目前,mysql在互聯網行業使用地如火如荼,很多大型網站都在使用MySQL數據庫,通過搭建mysql主備集群,實現高性能,高可用的存儲方案。mysql集群的共同特性是通過復制來實現主備間的同步,保證主備數據的一致性。這樣才能保證讀寫分離,備庫為主庫分擔壓力,提高整個集群的可用性和性能。
為什么需要數據一致性校驗?由於大部分搭建mysql服務的都是PC集群,尤其是在集群達到一定規模后,硬件出故障幾乎是必然的。mysql復制是異步復制,當主機出現故障時,就會出現丟數據的可能,造成主備數據不一致,無法正常對外提供服務。另外,當現有的PC集群容量不足時,需要對集群擴容,擴容就涉及到數據遷移。遷移一般都包括全量和增量,在不停服務的情況下,當遷移完數據后,需要校驗數據的一致性,保證遷移后不對業務造成影響。
什么是數據一致性?這里僅僅針對mysql,或是關系型數據庫,一致性主要包括兩方面,表結構一致和數據內容一致。一般情況下,表結構變更相對是少的,而且不一致的概率也很小,即使檢查,也相對容易;而導致數據內容不一致的情況很多,所以我們更關心的數據內容的一致性。
如何實現數據一致性校驗?一種思路就是逐行逐字段比較主庫和備庫的表;另外一種思路是,不逐行逐字段比較,取而代之的是分別對主庫和備庫計算校驗和,通過判斷校驗和是否相同,確定主備庫數據是否一致。兩種思路都很簡單,第一種思路正確性高,但性能比較差,因為返回大量的結果集導致大量的網絡IO和磁盤IO;而第二種思路則恰好相反,性能會更好,少了IO,多消耗了一些CPU資源(計算校驗和),正確性不如第一種思路。但是考慮到生產環境下,數據時時刻刻都是動態變化的,就沒那么簡單了。通過對表加鎖,可以保證我們在校驗時,數據是靜態的,待我們順利完成校驗后,再解鎖。mysql自帶命令CHECKSUM TABLE,就是通過鎖表方式來保證數據是靜態的。這種方式對於小表,訪問量小的表還好,若表非常大,校驗需要很長時間,生產環境是不能容忍的。既然要保持靜態就需要鎖表,可不可以縮短鎖表時間呢?pt-table-checksum通過將表分片,每次只對一部分行上鎖,這樣在校驗過程中,一時刻只有部分行被鎖住,減少對業務的影響。
目前業界使用比較廣泛的是percona公司的pt-table-checksum,下文我將詳細介紹該工具的使用和原理,並分析其不足以及可以改進的地方。
pt-table-checksum工具通過在主庫上執行一個校驗和的sql語句,然后通過復制,相同語句會在從庫執行(pt-table-checksum要求復制工作在語句級復制模式下)。通過replace...select語句將校驗和結果存儲在結果表,然后對比主庫和從庫的相同塊的記錄數目和校驗和,判斷主備庫數據是否一致。這里要注意的是, pt-table-checksum 不是對一個表僅作一個校驗和,因為如果表特別大,將會對DB造成很大的負載,影響正常業務。一個表一個校驗和就退化到mysql自帶命令CHECKSUM TABLE了,不僅需要鎖表,而且不准確。pt-table-checksum將表按用戶設置的塊大小,將表分成若干份,然后對每個塊計算一個校驗和。這樣即使表特別大,分塊后也只會鎖住部分記錄,對DB的負載壓力也大大降低。由於多個表校驗可以並發,可以大大提高校驗效率,通過參數-max-load可以防止load過大。
pt-table-checksum基本能滿足我們的日常需求,但是它還有一些需要完善的地方,首先,僅僅支持表粒度的並發,當檢查一個大表時,需要耗費大量的時間,另外多表並行執行時,並行度也不能通過參數的設置,而是通過--max-load間接設置。其次,通過分塊生成校驗和雖然加快了校驗速度,但1000行算一個4字節的校驗值(默認是一個塊1000行),產生沖突的可能性很大,即使pt-table-checksum設計的校驗和算法很復雜。最后,僅僅塊粒度的不一致還不夠,我們需要精確的知道到底是哪一條記錄,甚至哪一列不一致,並且給出訂正語句,所以哪位同學有興趣,還可以對其進行進一步優化。
最后,我簡單介紹下pt-table-checksum的使用,關於里面的參數的配置我就不一一列舉了,感興趣的同學可以參考http://www.percona.com/doc/percona-toolkit/2.2/pt-table-checksum.html
1.創建用於校驗的用戶,並授權
grant all privileges on *.* to ptcheck@'%' identified by 'ptcheck';
2.測試table_pt_check表結構
Create Table: CREATE TABLE `table_pt_check` (
`c1` int(11) NOT NULL AUTO_INCREMENT,
`c2` int(11) DEFAULT NULL,
PRIMARY KEY (`c1`)
) ENGINE=InnoDB AUTO_INCREMENT=26672747 DEFAULT CHARSET=utf8
3.校驗chuck庫中 table_pt_check表
pt-table-checksum --host='127.0.0.1' --user='ptcheck' --password='ptcheck' --port=3306 --databases='chuck' --tables='table_pt_check' --replicate=test.checksums
--replicate=test.checksums,指定校驗結果存儲在test庫中的checksums中。通過上述3個步驟就能檢查主備庫的數據是否一致了。
校驗結果存儲表結構如下:
Create Table: CREATE TABLE `checksums` (
`db` char(64) NOT NULL, //庫名
`tbl` char(64) NOT NULL, //表名
`chunk` int(11) NOT NULL, //分塊號
`chunk_time` float DEFAULT NULL, //分塊執行時間
`chunk_index` varchar(200) DEFAULT NULL, //分塊使用的索引,主鍵索引或唯一索引
`lower_boundary` text, //分塊的下界值
`upper_boundary` text, //分塊的上界值
`this_crc` char(40) NOT NULL, //分塊的哈希值
`this_cnt` int(11) NOT NULL, //分塊的記錄數目
`master_crc` char(40) DEFAULT NULL, //master上分塊的哈希值
`master_cnt` int(11) DEFAULT NULL, //master上分塊的記錄數目
`ts` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`db`,`tbl`,`chunk`),
KEY `ts_db_tbl` (`ts`,`db`,`tbl`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
校驗主備是否一致的SQL如下:
SELECT db,
tbl,
Sum(this_cnt) AS total_rows,
Count(*) AS chunks
FROM test.checksums
WHERE ( master_cnt <> this_cnt
OR master_crc <> this_crc
OR Isnull(master_crc) <> Isnull(this_crc) )
GROUP BY db, tbl;
通過--explain參數可以展示pt-table-checksum在執行過程的SQL:
replace INTO `test`.`checksums` (db, tbl, chunk, chunk_index, lower_boundary, upper_boundary, this_cnt, this_crc)
select 'chuck', 'table_pt_check', '7', 'PRIMARY', '21685456', '26100570', COUNT(*) AS cnt, COALESCE(LOWER(CONV(BIT_XOR(CAST(CRC32(CONCAT_WS('#', `c1`, `c2`, CONCAT(ISNULL(`c2`)))) AS UNSIGNED)), 10, 16)), 0) AS crc FROM `chuck`.`table_pt_check` FORCE INDEX(`PRIMARY`) WHERE ((`c1` >= '21685456')) AND ((`c1` <= '26100570'))
注意:計算校驗和的關鍵函數BIT_XOR,通過這個聚合函數,將分塊中每一行每一列的納入計算對象,理論上保證了通過一個校驗和可以判斷主備分塊數據是否一致。