PostgreSQL如何保障數據的一致性


玩過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_onlysuper_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弄錯文件。

  1. 檢查WAL文件頭中記錄的數據庫實例的唯一標識是否和本數據庫一致
  2. 檢查WAL頁面頭中記錄的頁地址是否正確
  3. 其它檢查

上面第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;
	}
...
}
September 2, 2017


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM