自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Uber為什么放棄Postgres選擇遷移到MySQL?

開發(fā) 前端 新聞
Uber 的早期架構(gòu)包含了一個用 Python 開發(fā)的單體后端應(yīng)用程序,這個應(yīng)用程序使用 Postgres 作為數(shù)據(jù)存儲。

 Uber 的早期架構(gòu)包含了一個用 Python 開發(fā)的單體后端應(yīng)用程序,這個應(yīng)用程序使用 Postgres 作為數(shù)據(jù)存儲。從那個時候開始,Uber 的架構(gòu)已經(jīng)發(fā)生了巨大變化,變成了微服務(wù),并采用新的數(shù)據(jù)平臺模型。具體地說,之前使用 Postgres 的地方,現(xiàn)在改用 Schemaless,一種構(gòu)建在 MySQL 之上的新型數(shù)據(jù)庫分片層。在本文中,我們將探討 Postgres 的一些缺點,并解釋為什么我們要在 MySQL 之上構(gòu)建 Schemaless 和其他后端服務(wù)。

1. Postgres 架構(gòu)

我們遭遇了 Postgres 的諸多限制:

  • 低效的寫入操作;
  • 低效的數(shù)據(jù)復(fù)制;
  • 數(shù)據(jù)損壞問題;
  • 糟糕的副本 MVCC 支持;
  • 難以升級到新版本。

我們將通過分析 Postgres 的表和索引在磁盤上的表示方式來探究以上這些限制,并將其與 MySQL 的 InnoDB 存儲引擎進行比較。請注意,我們的分析主要是基于我們對較舊的 Postgres 9.2 版本系列的經(jīng)驗。據(jù)我們所知,在本文中討論的內(nèi)部架構(gòu)在較新的 Postgres 發(fā)行版中并未發(fā)生顯著變化,并且至少自 Postgres 8.3 發(fā)行版(現(xiàn)在已近 10 歲)以來,9.2 版本的基本設(shè)計都沒有發(fā)生顯著變化。

磁盤表示

一個關(guān)系型數(shù)據(jù)庫必須能夠執(zhí)行一些關(guān)鍵任務(wù):

  • 提供插入、更新和刪除能力;
  • 提供修改模式的能力;
  • 支持 MVCC,讓不同的數(shù)據(jù)庫連接具有各自的事務(wù)視圖。

這些功能如何協(xié)同工作是設(shè)計數(shù)據(jù)庫磁盤數(shù)據(jù)表示的重要部分。

Postgres 的一個核心設(shè)計是不可變數(shù)據(jù)行。這些不可變數(shù)據(jù)行在 Postgres 中被稱為“元組”。這些元組通過 ctid 來唯一標識。從概念上看,ctid 表示元組在磁盤上的位置(即物理磁盤偏移)??赡軙卸鄠€ ctid 描述單個行(例如,為了支持 MVCC,可能存在一個數(shù)據(jù)行的多個版本,或者一個數(shù)據(jù)行的舊版本還沒有被 autovacuum 進程回收掉)。元組集合構(gòu)成一張表。表本身是有索引的,這些索引被組織成某種數(shù)據(jù)結(jié)構(gòu)(通常是 B 樹),將索引字段映射到 ctid。

通常,這些 ctid 對用戶是透明的,但了解它們的工作原理有助于了解 Postgres 表的磁盤結(jié)構(gòu)。要查看當前行的 ctid,可以在語句中將“ctid”添加到列列表中:

  1. uber@[local] uber=> SELECT ctid, * FROM my_table LIMIT 1
  2.  
  3. -[ RECORD 1 ]--------+------------------------------ 
  4.  
  5. ctid | (0,1
  6.  
  7. ...其他字段... 

我們通過一個簡單的用戶表來解釋這個。對于每個用戶,我們都有一個自動遞增的用戶 ID 主鍵、用戶的名字和姓氏以及用戶的出生年份。我們還針對用戶全名(名字和姓氏)定義了復(fù)合二級索引,并針對用戶的出生年份定義了另一個二級索引。創(chuàng)建表的 DDL 可能是這樣的:

  1. CREATE TABLE users ( 
  2.  
  3. id SERIAL, 
  4.  
  5. first TEXT, 
  6.  
  7. last TEXT, 
  8.  
  9. birth_year INTEGER, 
  10.  
  11. PRIMARY KEY (id) 
  12.  
  13. ); 
  14.  
  15. CREATE INDEX ix_users_first_last ON users (first, last); 
  16.  
  17. CREATE INDEX ix_users_birth_year ON users (birth_year); 

這里定義了三個索引:一個主鍵索引和兩個二級索引。

我們往表中插入以下這些數(shù)據(jù),包括一些有影響力的歷史數(shù)學(xué)家:

如前所述,這里的每一行都有一個隱式、唯一的 ctid。因此,我們可以這樣考慮表的內(nèi)部表示形式:

主鍵索引(將 id 映射到 ctid)的定義如下:

B 樹索引是在 id 字段上定義的,并且 B 樹中的每個節(jié)點都存有 ctid 的值。請注意,在這種情況下,由于使用了自動遞增的 ID,B 樹中字段的順序恰好與表中的順序相同,但并不是一直都這樣。

二級索引看起來差不多,主要區(qū)別在于字段的存儲順序不同,因為 B 樹必須按字典順序來組織。(first,last) 索引從名字的字母表順序開始:

類似的,birth_year 索引按照升序排列,如下所示:

對于后兩種情況,二級索引中的 ctid 字段不是按照字典順序遞增的,這與自動遞增主鍵的情況不同。

假設(shè)我們需要更新該表中的一條記錄,比如我們要更新 al-Khwārizmī的出生年份。如前所述,行的元組是不可變的。因此,為了更新記錄,我們向表中添加了一個新的元組。這個新的元組有一個新的 ctid,我們將其稱為 I。Postgres 需要區(qū)分新元組 I 與舊元組 D。在內(nèi)部,Postgres 在每個元組中保存了一個版本字段和一個指向先前元組的指針(如果有的話)。因此,表的最新結(jié)構(gòu)如下所示:

只要存在 al-Khwārizmī行的兩個版本,索引中就必須同時包含兩個行的條目。為簡便起見,我們省略了主鍵索引,只顯示了二級索引,如下所示:

我們用紅色表示舊數(shù)據(jù)行,用綠色表示新數(shù)據(jù)行。Postgres 使用另一個版本字段來確定哪個元組是最新的。數(shù)據(jù)庫根據(jù)這個字段確定哪個元組對不允許查看新版本數(shù)據(jù)的事務(wù)可見。

在 Postgres 中,主索引和二級索引都直接指向磁盤上的元組偏移量。當元組位置發(fā)生變化時,必須更新所有索引。

復(fù)制

當我們在表中插入新行時,如果啟用了流式復(fù)制,Postgres 需要對其進行復(fù)制。為了能夠在發(fā)生崩潰后恢復(fù),數(shù)據(jù)庫維護了預(yù)寫日志(WAL),并用它來實現(xiàn)兩階段提交。即使未啟用流式復(fù)制,數(shù)據(jù)庫也必須維護 WAL,因為 WAL 可以保證 ACID 中的原子性和持久性。

為了更好地理解 WAL,我們可以想象一下如果數(shù)據(jù)庫意外發(fā)生崩潰(例如突然斷電)會發(fā)生什么。WAL 代表了一系列數(shù)據(jù)庫計劃對表和索引在磁盤上內(nèi)容做出的更改。Postgres 守護進程在啟動時會將 WAL 的數(shù)據(jù)與磁盤上的實際數(shù)據(jù)進行對比。如果 WAL 中包含未反映到磁盤上的數(shù)據(jù),數(shù)據(jù)庫就會更正元組或索引數(shù)據(jù),并回滾出現(xiàn)在 WAL 中但在事務(wù)中沒有被提交的數(shù)據(jù)。

Postgres 通過將主數(shù)據(jù)庫上的 WAL 發(fā)送給副本來實現(xiàn)流式復(fù)制。每個副本數(shù)據(jù)庫就像是在進行崩潰恢復(fù),不斷地應(yīng)用 WAL 更新。流式復(fù)制和實際發(fā)生崩潰恢復(fù)之間的唯一區(qū)別是,處于“熱備用”模式的副本在應(yīng)用 WAL 時可以提供查詢服務(wù),但真正處于崩潰恢復(fù)模式的 Postgres 數(shù)據(jù)庫通常會拒絕提供查詢服務(wù),直到數(shù)據(jù)庫實例完成崩潰恢復(fù)過程。

因為 WAL 實際上是為實現(xiàn)崩潰恢復(fù)而設(shè)計的,所以它包含了底層的磁盤更新信息。WAL 包含了元組及其磁盤偏移量(即行 ctid)在磁盤上的表示。如果副本完全與主數(shù)據(jù)庫同步,此時暫停 Postgres 的主數(shù)據(jù)庫和副本,那么副本的磁盤內(nèi)容與主數(shù)據(jù)庫的磁盤內(nèi)容將完全一致。因此,如果副本與主數(shù)據(jù)庫不同步,可以用 rsync 之類的工具來修復(fù)。

2. Postgres 的設(shè)計所帶來的后果

Postgres 的設(shè)計導(dǎo)致 Uber 的數(shù)據(jù)效率低下,還讓我們遇到了很多麻煩。

寫入放大

Postgres 的第一個問題是寫入放大。通常,寫入放大是指將數(shù)據(jù)寫入 SSD 磁盤時遇到的問題:小的邏輯更新(例如,寫入幾個字節(jié))在轉(zhuǎn)換到物理層時會放大,成本會變高。在之前的示例中,如果我們對 al-Khwārizmī的出生年份進行小的邏輯更新,必須進行至少四個物理更新:

  1. 將新的行元組寫入表空間;
  2. 更新主鍵索引;
  3. 更新 (first,last) 索引;
  4. 更新 birth_year 索引。

實際上,這四個更新也只反映了對主表空間的寫操作。除此之外,這些寫操作也需要反映在 WAL 中,因此磁盤上的寫操作總數(shù)會變得更多。

這里值得注意的是更新 2 和更新 3。在更新 al-Khwārizmī的出生年份時,實際上并沒有修改它的主鍵,也沒有修改名字和姓氏。但盡管如此,仍然必須在數(shù)據(jù)庫中創(chuàng)建新的行元組,以便更新這些索引。對于具有大量二級索引的表,這些多余的步驟可能會導(dǎo)致效率低下。例如,如果我們在一張表中定義了十二個索引,即使只更新了單個索引對應(yīng)的字段,也必須將該更新傳播給所有 12 個索引,以便反映新行的 ctid。

復(fù)制

這個寫入放大問題自然也轉(zhuǎn)化到了復(fù)制層,因為復(fù)制發(fā)生在磁盤級別。數(shù)據(jù)庫并不會復(fù)制小的邏輯記錄,例如“將 ctid D 的出生年份更改為 770”,而是將之前的 4 個 WAL 條目傳播到網(wǎng)絡(luò)上。因此,寫入放大問題也轉(zhuǎn)化為復(fù)制放大問題,Postgres 復(fù)制數(shù)據(jù)流很快變得非常冗長,可能占用大量帶寬。

如果 Postgres 復(fù)制僅發(fā)生在單個數(shù)據(jù)中心內(nèi),那么復(fù)制帶寬可能就不是問題。現(xiàn)代網(wǎng)絡(luò)設(shè)備和交換機可以處理大量帶寬,很多托管服務(wù)提供商還提供了免費或便宜的數(shù)據(jù)中心內(nèi)部帶寬。但是,如果要在數(shù)據(jù)中心之間進行復(fù)制,問題就會迅速升級。例如,Uber 最初使用了西海岸托管中心里的物理服務(wù)器。為了進行災(zāi)備,我們在東海岸托管中心添加了服務(wù)器。于是,我們在西部數(shù)據(jù)中心里有一個主 Postgres 實例(加上副本),在東部也有一個副本集。

級聯(lián)復(fù)制將數(shù)據(jù)中心間的帶寬限制為只能滿足主數(shù)據(jù)庫和單個副本之間的帶寬需求,雖然第二個數(shù)據(jù)中心里還有很多副本。因為 Postgres 復(fù)制協(xié)議的冗繁,使用了大量索引的數(shù)據(jù)庫會有很大的數(shù)據(jù)量。購買跨地域大帶寬成本非常高昂,即使錢不成問題,也不可能獲得與本地帶寬類似的效果。這個帶寬問題也給 WAL 歸檔帶來了麻煩。除了將所有 WAL 更新從西海岸發(fā)送到東海岸之外,我們還要將所有 WAL 都存檔到文件存儲服務(wù)中,這是為了確保在發(fā)生災(zāi)難時我們可以還原數(shù)據(jù)。在早期的流量高峰期間,我們寫入存儲服務(wù)的帶寬不夠快,無法跟上 WAL 的寫入速度。

數(shù)據(jù)損壞

在例行升級主數(shù)據(jù)庫以便增加數(shù)據(jù)庫容量的過程中,我們遭遇了 Postgres 9.2 個一個 bug。因為副本在切換時間方面出現(xiàn)了錯誤,導(dǎo)致其中一些副本錯誤地應(yīng)用了一小部分 WAL 記錄。由于這個問題,一些本應(yīng)由版本控制機制標記為無效的記錄實際上并未被標記為無效。

下面的查詢說明了這個錯誤將如何影響我們的用戶表:

  1. SELECT * FROM users WHERE id = 4

這個查詢將返回兩條記錄:初始的 al-Khwārizmī行(出生年份為 780 CE)和新的 al-Khwārizmī行(出生年份為 770 CE)。如果將 ctid 添加到 WHERE 中,對于這兩條返回的記錄,我們將看到不同的 ctid 值。

這個問題非常煩人。首先,我們無法得知這個問題究竟影響了多少行數(shù)據(jù)。數(shù)據(jù)庫返回的重復(fù)結(jié)果在很多情況下會導(dǎo)致應(yīng)用程序邏輯故障。我們最終添加了防御性編程語句,用來檢測會出現(xiàn)這個問題的表。這個錯誤影響到了所有服務(wù)器,而在不同的副本實例上損壞的數(shù)據(jù)行是不一樣的。也就是說,在其中一個副本實例上,行 X 可能是壞的,行 Y 是好的,但是在另一副本實例上,行 X 可能是好,行 Y 可能是壞的。我們無法確定數(shù)據(jù)損壞的副本數(shù)量以及問題是否影響了主數(shù)據(jù)庫。

據(jù)我們所知,每個數(shù)據(jù)庫只有幾行數(shù)據(jù)會出現(xiàn)這個問題,但我們擔心的是,由于復(fù)制發(fā)生在物理級別,最后可能會完全破壞數(shù)據(jù)庫索引。B 樹索引很重要的一點是必須定期進行重新平衡(rebalance),并且當子樹移動到新的磁盤位置時,這些重新平衡操作可能會完全改變樹的結(jié)構(gòu)。如果移動了錯誤的數(shù)據(jù),則可能導(dǎo)致樹的大部分完全無效。

最后,我們找到了問題所在,并確定新的主數(shù)據(jù)庫沒有損壞的數(shù)據(jù)行。我們通過從主數(shù)據(jù)庫的最新快照重新同步所有副本(這是一個費力的過程)來修復(fù)副本的數(shù)據(jù)損壞問題。

我們遇到的錯誤只出現(xiàn)在 Postgres 9.2 的某些版本中,并且已經(jīng)修復(fù)了很長時間了。但是,我們?nèi)匀粨拇祟愬e誤會再次發(fā)生。新版本的 Postgres 可能還會出現(xiàn)此類錯誤,并且由于數(shù)據(jù)復(fù)制的方式,這類問題有可能被傳播到所有的數(shù)據(jù)庫中。

副本 MVCC

Postgres 沒有提供真正的副本 MVCC 支持。副本只應(yīng)用 WAL 更新,導(dǎo)致它們在任何時候都具有與主數(shù)據(jù)庫相同的磁盤數(shù)據(jù)副本。這種設(shè)計給 Uber 帶來了麻煩。

Postgres 需要為 MVCC 維護舊數(shù)據(jù)的一個副本。如果流式復(fù)制遇到一個正在執(zhí)行的事務(wù),而數(shù)據(jù)庫更新影響到了事務(wù)范圍內(nèi)的行,那么更新操作就會被阻塞。在這種情況下,Postgres 會暫停 WAL 線程,直到事務(wù)結(jié)束。如果事務(wù)處理要花費很長時間,這就會是個問題,因為副本可能嚴重滯后于主數(shù)據(jù)庫。因此,Postgres 在這種情況下應(yīng)用超時策略:如果一個事務(wù)導(dǎo)致 WAL 發(fā)生阻塞一定的時間,Postgres 將會終止這個事務(wù)。

這種設(shè)計意味著副本通常會比主數(shù)據(jù)庫落后幾秒鐘,很容易出現(xiàn)事務(wù)被終止的情況。例如,假設(shè)開發(fā)人員寫了一些代碼,需要通過電子郵件將收據(jù)發(fā)送給用戶。根據(jù)編寫方式的不同,代碼可能會隱式地讓數(shù)據(jù)庫事務(wù)處于打開狀態(tài),直到電子郵件完成發(fā)送為止。盡管在執(zhí)行不相關(guān)的阻塞 IO 時一直打開數(shù)據(jù)庫事務(wù)是很糟糕的做法,但大多數(shù)工程師并不是數(shù)據(jù)庫專家,他們可能也不知道有這個問題,特別是在使用隱藏了底層細節(jié)的 ORM 框架時。

升級 Postgres

由于復(fù)制發(fā)生在物理層面,所以我們無法在 Postgres 的不同版本之間復(fù)制數(shù)據(jù)。Postgres 9.3 的主數(shù)據(jù)庫不能被復(fù)制到 Postgres 9.2 的副本,而 Postgres 9.2 的主數(shù)據(jù)庫也不能被復(fù)制到 Postgres 9.3 的副本。

我們按照以下這些步驟從一個 Postgres GA 版本升級到另一個版本:

  • 關(guān)閉主數(shù)據(jù)庫。
  • 在主數(shù)據(jù)庫上運行 pg_upgrade 命令,這個命令會就地更新主數(shù)據(jù)庫數(shù)據(jù)。對于大型數(shù)據(jù)庫,通常需要花費數(shù)小時,并且在這個過程過程中無法從主數(shù)據(jù)庫讀取數(shù)據(jù)。
  • 再次啟動主數(shù)據(jù)庫。
  • 創(chuàng)建主數(shù)據(jù)庫的最新快照。這一步驟完全復(fù)制了主數(shù)據(jù)庫的所有數(shù)據(jù),因此大型數(shù)據(jù)庫也需要花費數(shù)小時。
  • 擦除所有副本,并將最新的快照從主數(shù)據(jù)庫還原到副本上。
  • 將副本帶回到復(fù)制層次結(jié)構(gòu)中。等待副本完全跟上主數(shù)據(jù)庫的所有更新。

我們從 Postgres 9.1 開始,并成功完成了升級過程,遷移到了 Postgres 9.2。但是,這個過程花費了數(shù)小時,我們無力承擔再次執(zhí)行這種升級過程的費用。到 Postgres 9.3 發(fā)布時,Uber 的規(guī)模增長極大增加了我們的數(shù)據(jù)集,因此升級時間就變得更長了。因此,即使 Postgres 9.5 已經(jīng)發(fā)布了,我們的 Postgres 實例仍然是 9.2 版本。

如果你的 Postgres 是 9.4 或更高版本,可以使用 pgologic 之類的東西,它為 Postgres 實現(xiàn)了一個邏輯復(fù)制層。你可以用它在不同的 Postgres 版本之間復(fù)制數(shù)據(jù),這意味著可以從 9.4 升級到 9.5,而不會造成大面積停機。不過,這個功能仍然是有問題的,因為它尚未被集成到 Postgres 主線中。而對于那些使用較舊版本的 Postgres 的人來說,pgologic 并不適用。

3. MySQL 架構(gòu)

上文解釋了 Postgres 的一些局限性,接下來,我們將解釋為什么 MySQL 會成為 Uber 工程團隊存儲項目(例如 Schemaless)的新工具。在很多情況下,我們發(fā)現(xiàn) MySQL 更適合我們的使用場景。為了理解這些差異,我們研究了 MySQL 的架構(gòu),并將其與 Postgres 進行了對比。我們專門分析了 MySQL 的 InnoDB 存儲引擎。

InnoDB 的磁盤表示

與 Postgres 一樣,InnoDB 支持 MVCC 和可變數(shù)據(jù)等高級功能。關(guān)于 InnoDB 磁盤表示的詳盡細節(jié)不在本文的討論范圍之內(nèi),我們將把重點放在它與 Postgres 的主要區(qū)別上。

最主要的架構(gòu)差異是:Postgres 直接將索引記錄映射到磁盤上的位置,而 InnoDB 使用了二級結(jié)構(gòu)。InnoDB 的二級索引有一個指向主鍵值的指針,而不是指向磁盤位置的指針(如 Postgres 中的 ctid)。因此,MySQL 會將二級索引將索引鍵與主鍵相關(guān)聯(lián):

要基于 (first, last) 索引 執(zhí)行查詢,需要進行兩次查找。第一次先搜索表,找到記錄的主鍵。在找到主鍵之后,搜索主鍵索引,找到數(shù)據(jù)行對應(yīng)的磁盤位置。

所以,在執(zhí)行二級查找時,InnoDB 相比 Postgres 略有不利,因為 InnoDB 必須搜索兩個索引,而 Postgres 只需要搜索一個。但是,由于數(shù)據(jù)已經(jīng)規(guī)范化,在更新行數(shù)據(jù)時只需要更新實際發(fā)生變化的索引記錄。此外,InnoDB 通常會在原地進行行數(shù)據(jù)更新。為了支持 MVCC,如果舊事務(wù)需要引用一行數(shù)據(jù),MySQL 會將舊行復(fù)制到一個叫作回滾段的特殊區(qū)域中。

我們來看看更新 al-Khwārizmī的出生年份會發(fā)生什么。如果空間足夠,id 為 4 的那一行數(shù)據(jù)中的出生年份字段會進行原地更新(實際上,這個更新總是發(fā)生在原地,因為出生年份是一個占用固定空間量的整數(shù))。出生年份索引也進行原地更新。舊數(shù)據(jù)行將被復(fù)制到回滾段。主鍵索引不需要更新,(first, last) 索引也不需要更新。即使這張表有大量索引,也只需要更新包含 birth_year 字段的索引。假設(shè)我們基于 signup_date、last_login_time 等字段建立了索引,我們不需要更新這些索引,但在 Postgres 中需要更新。

這種設(shè)計還讓數(shù)據(jù)清理和壓縮變得更加高效?;貪L段中的數(shù)據(jù)可以直接清除,相比之下,Postgres 的 autovacuum 進程必須進行全表掃描來識別哪些行可以清除。

MySQL 使用了額外的中間層:二級索引記錄指向主索引記錄,主索引保存了數(shù)據(jù)行在磁盤上的位置。如果數(shù)據(jù)行偏移量發(fā)生變化,只需要更新主索引。

復(fù)制

MySQL 支持多種不同的復(fù)制模式:

  • 基于語句的復(fù)制將會復(fù)制邏輯 SQL 語句(它將按字面意義復(fù)制 SQL 語句,例如:UPDATE users SET birth_year = 770 WHERE id = 4);
  • 基于行的復(fù)制將會復(fù)制發(fā)生變化的行記錄;
  • 混合復(fù)制將這兩種模式混合在一起。

這幾種模式各有優(yōu)缺點。基于語句的復(fù)制通常是最緊湊的,但可能需要副本應(yīng)用大量語句來更新少量數(shù)據(jù)。另一方面,基于行的復(fù)制(與 Postgres WAL 復(fù)制類似)雖然更為冗繁,但更具可預(yù)測性和在副本上的更新效率。

在 MySQL 中,只有主索引有指向行的磁盤偏移量的指針。在進行復(fù)制時,這具有重要的意義。MySQL 復(fù)制流只需要包含有關(guān)行的邏輯更新信息。對于類似“將行 X 的時間戳從 T_1 更改為 T_2”這樣的更新,副本會自動推斷需要修改哪些索引。

相比之下,Postgres 復(fù)制流包含了物理變更,例如“在磁盤偏移量 8,382,491 處寫入字節(jié) XYZ”。在使用 Postgres 時,對磁盤進行的每一個物理變更都需要包含在 WAL 流中。較小的邏輯修改(例如更新時間戳)也需要執(zhí)行很多磁盤變更:Postgres 必須插入新的元組,并更新所有索引,讓它們指向這個元組,所以會有很多變更被放入 WAL 流中。這種設(shè)計差異意味著 MySQL 復(fù)制二進制日志比 PostgreSQL WAL 流更緊湊。

復(fù)制方式也對副本的 MVCC 產(chǎn)生重要影響。由于 MySQL 復(fù)制流具有邏輯更新,副本可以具有真正的 MVCC 語義,所以對副本的讀取查詢不會阻塞復(fù)制流。相比之下,Postgres WAL 流包含了磁盤上的物理更改,Postgres 副本無法應(yīng)用與讀取查詢相沖突的復(fù)制更新,因此無法實現(xiàn) MVCC。

MySQL 的復(fù)制架構(gòu)意味著即使有 bug 導(dǎo)致表損壞,也不太可能會發(fā)生災(zāi)難性故障。因為復(fù)制發(fā)生在邏輯層,所以像重新平衡 B 樹之類的操作永遠不會導(dǎo)致索引損壞。一個典型的 MySQL 復(fù)制問題是語句被跳過(或者被應(yīng)用兩次),這可能導(dǎo)致數(shù)據(jù)丟失或無效,但不會導(dǎo)致數(shù)據(jù)庫中斷。

最后,MySQL 的復(fù)制架構(gòu)可以很容易在不同的 MySQL 版本之間進行復(fù)制。MySQL 的邏輯復(fù)制格式還意味著存儲引擎層中的磁盤變更不會影響復(fù)制格式。在進行 MySQL 升級時,典型的做法是一次將更新應(yīng)用于一個副本,在更新完所有副本后,將其中一個提升為新的主副本。這幾乎可以實現(xiàn)零停機升級,很容易就可以讓 MySQL 保持最新狀態(tài)。

4. MySQL 的其他優(yōu)勢

到目前為止,我們介紹了 Postgres 和 MySQL 的磁盤架構(gòu)。MySQL 還有其他一些重要方面也讓它的性能明顯優(yōu)于 Postgres。

緩沖池

首先,兩個數(shù)據(jù)庫的緩存方式不同。Postgres 為內(nèi)部緩存分配了一些內(nèi)存,但是與計算機上的內(nèi)存總量相比,這些緩存通常很小。為了提高性能,Postgres 允許內(nèi)核通過頁面緩存自動緩存最近訪問的磁盤數(shù)據(jù)。例如,我們最大的 Postgres 副本有 768 GB 的可用內(nèi)存,但實際上只有 25 GB 被用作 Postgres 的進程 RSS 內(nèi)存,這樣就為 Linux 頁面緩存留出了 700 GB 以上的可用內(nèi)存。

這種設(shè)計的問題在于,與訪問 RSS 內(nèi)存相比,通過頁面緩存訪問數(shù)據(jù)實際上開銷更大。為了從磁盤上查找數(shù)據(jù),Postgres 進程發(fā)出 lseek 和 read 系統(tǒng)調(diào)用來定位數(shù)據(jù)。這些系統(tǒng)調(diào)用中的每一個都會引起上下文切換,這比從主存儲器訪問數(shù)據(jù)的開銷更大。實際上,Postgres 在這方面甚至還沒有完全進行優(yōu)化:Postgres 并未利用 pread 系統(tǒng)調(diào)用,這個系統(tǒng)調(diào)用會將 seek 和 read 操作合并為一個系統(tǒng)調(diào)用。

相比之下,InnoDB 存儲引擎通過緩沖池實現(xiàn)了自己的 LRU。從邏輯上講,這與 Linux 頁面緩存相似,但它是在用戶空間中實現(xiàn)的。盡管 InnoDB 緩沖池的設(shè)計比 Postgres 的設(shè)計要復(fù)雜得多,但它具備一些優(yōu)勢:

  1. 可以實現(xiàn)自定義 LRU。例如,可以檢測出可能會破壞 LRU 的訪問模式,并防止其造成更大問題。
  2. 較少的上下文切換。通過 InnoDB 緩沖池訪問的數(shù)據(jù)不需要進行用戶 / 內(nèi)核上下文切換。最壞的情況是發(fā)生 TLB 未命中,這些開銷相對較小,可以通過使用大頁面來緩解。

連接處理

MySQL 通過一個連接一個線程的方式來實現(xiàn)并發(fā)連接。這種開銷相對較低,每個線程都有自己的棧內(nèi)存和分配給特定連接的緩沖堆內(nèi)存。在 MySQL 中使用 10000 個左右的并發(fā)連接,這種情況并不少見,實際上,在我們現(xiàn)有的某些 MySQL 實例上,連接數(shù)已經(jīng)接近這個數(shù)字。

但是,Postgres 采用的是一個連接一個進程的設(shè)計,這比一個連接一個線程的設(shè)計要昂貴得多。派生新進程比生成新線程占用更多的內(nèi)存。此外,進程之間的 IPC 比線程之間的 IPC 也昂貴得多。Postgres 9.2 通過 System V IPC 原語實現(xiàn) IPC,而不是使用輕量級的 futex。futex 比 System V IPC 更快,因為通常情況下,futex 不存在竟態(tài)條件,因此無需進行上下文切換。

除了內(nèi)存和 IPC 開銷,Postgres 似乎也無法很好地支持大量連接,即使有足夠的可用內(nèi)存。我們在 Postgres 中使用數(shù)百個活動連接時遇到了大問題。Postgres 文檔建議采用進程外連接池機制來處理大量連接,但沒有詳細說明是為什么。因此,我們使用 pgbouncer 來處理 Postgres 的連接池。但是,我們的后端服務(wù)偶爾會出現(xiàn) bug,導(dǎo)致它們打開的活動連接過多,從而延長了宕機時間。

5. 結(jié)論

在 Uber 早期,Postgres 為我們提供了很好的服務(wù),但是隨著公司規(guī)模的增長,我們遇到了伸縮性問題。現(xiàn)在,我們?nèi)匀槐A袅艘恍┡f的 Postgres 實例,但大部分數(shù)據(jù)庫都建立在 MySQL 之上(通常使用 Schemaless 層),或者在某些特殊情況下會使用像 Cassandra 這樣的 NoSQL 數(shù)據(jù)庫。

 

責(zé)任編輯:張燕妮 來源: 架構(gòu)頭條
相關(guān)推薦

2020-01-18 09:35:03

微服務(wù)團隊架構(gòu)

2020-03-12 08:00:34

MySQL遷移TiDB

2018-12-21 11:26:49

MySQLMongoDB數(shù)據(jù)庫

2020-04-20 08:08:23

MongoDBElasticsear數(shù)據(jù)庫

2021-07-07 10:48:00

DigGoWire

2021-01-25 07:40:37

Druid數(shù)據(jù)eBay

2021-02-01 07:20:51

KafkaPulsar搜索

2020-09-09 09:38:47

GoLangNodeJS編程語言

2020-10-13 09:25:27

ESClickHouse搜索引擎

2018-09-28 10:06:21

移動開發(fā)App

2023-09-22 10:48:42

MySQLPostgreSQL

2023-09-14 23:08:54

PostgreSQLMySQL

2020-02-24 09:38:05

PythonGo語言Linux

2020-08-14 10:40:35

RestTemplatRetrofitJava

2017-08-31 17:43:06

云端遷移云計算

2021-04-22 15:55:56

UCaaS統(tǒng)一通信企業(yè)通信

2013-06-21 13:49:08

MariaDB

2019-08-20 09:24:54

Python編程語言Java

2010-11-17 09:18:47

私有云遷移

2011-06-08 10:30:08

MongoDB
點贊
收藏

51CTO技術(shù)棧公眾號