玩過MySQL的人應該都知道,由於MySQL是邏輯復制,從根子上是難以保證數據一致性的。玩MySQL玩得好的專家們知道有哪些坑,應該怎么回避。為了保障MySQL數據的一致性,甚至會動用paxos,raft之類的終極武器建立嚴密的防護網。如果不會折騰,真不建議用MySQL存放一致性要求高的數據。
PostgreSQL由於是物理復制,天生就很容易保障數據一致性,而且回放日志的效率很高。 我們實測的結果,MySQL5.6的寫qps超過4000備機就跟不上主機了;PG 8核虛機的寫qps壓到2.3w備機依然毫無壓力,之所以只壓到2.3w是因為主節點的CPU已經跑滿壓不上去了。
那么,和MySQL相比,PG有哪些措施用於保障數據的一致性呢?
1. 嚴格單寫
PG的備庫處於恢復狀態,不斷的回放主庫的WAL,不具備寫能力。
而MySQL的單寫是通過在備機上設置read_only
或super_read_only
實現的,DBA在維護數據庫的時候可能需要解除只讀狀態,在解除期間發生點什么,或自動化腳本出個BUG都可能引起主備數據不一致。甚至備庫在和主庫建立復制關系之前數據就不是一致的,MySQL的邏輯復制並不阻止兩個不一致的庫建立復制關系。
2. 串行化的WAL回放
PG的備庫以和主庫完全相同順序串行化的回放WAL日志。
MySQL中由於存在組提交,以及為了解決單線程復制回放慢而采取的並行復制,不得不在復制延遲和數據一致性之前做取舍。 並且這里牽扯到的邏輯很復雜,已經檢出了很多的BUG;因為邏輯太復雜了,未來出現新BUG的概率應該相對也不會低。
3. 同步復制
PG通過synchronous_commit
參數設置復制的持久性級別。
下面這些級別越往下越嚴格,從remote_write
開始就可以保證單機故障不丟數據了。
- off
- local
- remote_write
- on
- remote_apply
每個級別的含義參考手冊:19.5. 預寫式日志
MySQL通過半同步復制在很大程度上降低了failover丟失數據的概率。MySQL的主庫在等待備庫的應答超時時半同步復制會自動降級成異步,此時發生failover會丟失數據。
4. 全局唯一的WAL標識
WAL文件頭中保存了數據庫實例的唯一標識(Database system identifier),可以確保不同數據庫實例產生的WAL可以區別開,同一集群的主備庫擁有相同唯一標識。
PG提升備機的時候會同時提升備機的時間線,時間線是WAL文件名的一部分,通過時間線就可以把新主和舊主產生的WAL區別開。 (如果同時提升2個以上的備機,就無法這樣區分WAL了,當然這種情況正常不應該發生。)
WAL記錄在整個WAL邏輯數據流中的偏移(lsn)作為WAL的標識。
以上3者的聯合可唯一標識WAL記錄
MySQL5.6開始支持GTID了,這對保障數據一致性是個極大的進步。對於邏輯復制來說,GITD確實做得很棒,但是和PG物理復制的時間線+lsn相比起來就顯得太復雜了。時間線+lsn只是2個數字而已;GTID卻是一個復雜的集合,而且需要定期清理。
MySQL的GTID是長這樣的:
e6954592-8dba-11e6-af0e-fa163e1cf111:1-5:11-18,
e6954592-8dba-11e6-af0e-fa163e1cf3f2:1-27
5. 數據文件的checksum
在初始化數據庫時,使用-k
選項可以打開數據文件的checksum功能。(建議打開,造成的性能損失很小) 如果底層存儲出現問題,可通過checksum及時發現。
initdb -k $datadir
MySQL也只支持數據文件的checksum,沒什么區別。
6. WAL記錄的checksum
每條WAL記錄里都保存了checksum信息,如果WAL的傳輸存儲過程中出現錯誤可及時發現。
MySQL的binlog記錄里也包含checksum,沒什么區別。
7. WAL文件的驗證
WAL可能來自歸檔的拷貝或人為拷貝,PG在讀取WAL文件時會進行驗證,可防止DBA弄錯文件。
- 檢查WAL文件頭中記錄的數據庫實例的唯一標識是否和本數據庫一致
- 檢查WAL頁面頭中記錄的頁地址是否正確
- 其它檢查
上面第2項檢查的作用主要是應付WAL再利用。
PG在清理不需要的WAL文件時,有2種方式,1是刪除,2是改名為未來的WAL文件名防止頻繁創建文件。
看下面的例子,000000030000000000000015
及以后的WAL文件的修改日期比前面的WAL還要老,這些WAL文件就是被重命名了的。
[postgres@node1 ~]$ ll data1/pg_wal/
total 213000
-rw-------. 1 postgres postgres 41 Aug 27 00:53 00000002.history
-rw-------. 1 postgres postgres 16777216 Sep 1 23:56 000000030000000000000012
-rw-------. 1 postgres postgres 16777216 Sep 2 11:05 000000030000000000000013
-rw-------. 1 postgres postgres 16777216 Sep 2 11:05 000000030000000000000014
-rw-------. 1 postgres postgres 16777216 Aug 27 00:57 000000030000000000000015
-rw-------. 1 postgres postgres 16777216 Aug 27 00:58 000000030000000000000016
-rw-------. 1 postgres postgres 16777216 Aug 27 00:59 000000030000000000000017
-rw-------. 1 postgres postgres 16777216 Aug 27 00:59 000000030000000000000018
-rw-------. 1 postgres postgres 16777216 Aug 27 00:59 000000030000000000000019
-rw-------. 1 postgres postgres 16777216 Aug 27 00:59 00000003000000000000001A
-rw-------. 1 postgres postgres 16777216 Aug 27 00:59 00000003000000000000001B
-rw-------. 1 postgres postgres 16777216 Aug 27 00:59 00000003000000000000001C
-rw-------. 1 postgres postgres 16777216 Aug 27 00:59 00000003000000000000001D
-rw-------. 1 postgres postgres 16777216 Sep 1 23:56 00000003000000000000001E
-rw-------. 1 postgres postgres 84 Aug 27 01:02 00000003.history
drwx------. 2 postgres postgres 34 Sep 1 23:56 archive_status
由於有上面的第2項檢查,如果讀到了這些WAL文件,可以立即識別出來。
[postgres@node1 ~]$ pg_waldump data1/pg_wal/000000030000000000000015
pg_waldump: FATAL: could not find a valid record after 0/15000000
MySQL的binlog文件名一般是長下面這樣的,從binlog文件名上看不出任何和GTID的映射關系。
mysql_bin.000001
不同機器上產生的binlog文件可能同名,如果要管理多套MySQL,千萬別拿錯文件。因為MySQL是邏輯復制,這些binlog文件就像SQL語句一樣,拿到哪里都可以執行。
## 參考
src/backend/access/transam/xlogreader.c:
static bool
ValidXLogRecord(XLogReaderState *state, XLogRecord *record, XLogRecPtr recptr)
{
pg_crc32c crc;
/* Calculate the CRC */
INIT_CRC32C(crc);
COMP_CRC32C(crc, ((char *) record) + SizeOfXLogRecord, record->xl_tot_len - SizeOfXLogRecord);
/* include the record header last */
COMP_CRC32C(crc, (char *) record, offsetof(XLogRecord, xl_crc));
FIN_CRC32C(crc);
if (!EQ_CRC32C(record->xl_crc, crc))
{
report_invalid_record(state,
"incorrect resource manager data checksum in record at %X/%X",
(uint32) (recptr >> 32), (uint32) recptr);
return false;
}
return true;
}
...
static bool
ValidXLogPageHeader(XLogReaderState *state, XLogRecPtr recptr,
XLogPageHeader hdr)
{
...
if (state->system_identifier &&
longhdr->xlp_sysid != state->system_identifier)
{
char fhdrident_str[32];
char sysident_str[32];
/*
* Format sysids separately to keep platform-dependent format code
* out of the translatable message string.
*/
snprintf(fhdrident_str, sizeof(fhdrident_str), UINT64_FORMAT,
longhdr->xlp_sysid);
snprintf(sysident_str, sizeof(sysident_str), UINT64_FORMAT,
state->system_identifier);
report_invalid_record(state,
"WAL file is from different database system: WAL file database system identifier is %s, pg_control database system identifier is %s",
fhdrident_str, sysident_str);
return false;
}
...
if (hdr->xlp_pageaddr != recaddr)
{
char fname[MAXFNAMELEN];
XLogFileName(fname, state->readPageTLI, segno);
report_invalid_record(state,
"unexpected pageaddr %X/%X in log segment %s, offset %u",
(uint32) (hdr->xlp_pageaddr >> 32), (uint32) hdr->xlp_pageaddr,
fname,
offset);
return false;
}
...
}