日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問(wèn)題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營(yíng)銷解決方案
Uber為什么放棄Postgres選擇遷移到MySQL?

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

創(chuàng)新互聯(lián)建站長(zhǎng)期為1000+客戶提供的網(wǎng)站建設(shè)服務(wù),團(tuán)隊(duì)從業(yè)經(jīng)驗(yàn)10年,關(guān)注不同地域、不同群體,并針對(duì)不同對(duì)象提供差異化的產(chǎn)品和服務(wù);打造開(kāi)放共贏平臺(tái),與合作伙伴共同營(yíng)造健康的互聯(lián)網(wǎng)生態(tài)環(huán)境。為瑯琊企業(yè)提供專業(yè)的成都網(wǎng)站制作、網(wǎng)站建設(shè)、外貿(mào)網(wǎng)站建設(shè)瑯琊網(wǎng)站改版等技術(shù)服務(wù)。擁有10余年豐富建站經(jīng)驗(yàn)和眾多成功案例,為您定制開(kāi)發(fā)。

1. Postgres 架構(gòu)

我們?cè)庥隽?Postgres 的諸多限制:

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

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

磁盤(pán)表示

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

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

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

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

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

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

我們通過(guò)一個(gè)簡(jiǎn)單的用戶表來(lái)解釋這個(gè)。對(duì)于每個(gè)用戶,我們都有一個(gè)自動(dòng)遞增的用戶 ID 主鍵、用戶的名字和姓氏以及用戶的出生年份。我們還針對(duì)用戶全名(名字和姓氏)定義了復(fù)合二級(jí)索引,并針對(duì)用戶的出生年份定義了另一個(gè)二級(jí)索引。創(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); 

這里定義了三個(gè)索引:一個(gè)主鍵索引和兩個(gè)二級(jí)索引。

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

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

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

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

二級(jí)索引看起來(lái)差不多,主要區(qū)別在于字段的存儲(chǔ)順序不同,因?yàn)?B 樹(shù)必須按字典順序來(lái)組織。(first,last) 索引從名字的字母表順序開(kāi)始:

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

對(duì)于后兩種情況,二級(jí)索引中的 ctid 字段不是按照字典順序遞增的,這與自動(dòng)遞增主鍵的情況不同。

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

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

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

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

復(fù)制

當(dāng)我們?cè)诒碇胁迦胄滦袝r(shí),如果啟用了流式復(fù)制,Postgres 需要對(duì)其進(jìn)行復(fù)制。為了能夠在發(fā)生崩潰后恢復(fù),數(shù)據(jù)庫(kù)維護(hù)了預(yù)寫(xiě)日志(WAL),并用它來(lái)實(shí)現(xiàn)兩階段提交。即使未啟用流式復(fù)制,數(shù)據(jù)庫(kù)也必須維護(hù) WAL,因?yàn)?WAL 可以保證 ACID 中的原子性和持久性。

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

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

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

2. Postgres 的設(shè)計(jì)所帶來(lái)的后果

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

寫(xiě)入放大

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

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

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

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

復(fù)制

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

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

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

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

在例行升級(jí)主數(shù)據(jù)庫(kù)以便增加數(shù)據(jù)庫(kù)容量的過(guò)程中,我們?cè)庥隽?Postgres 9.2 個(gè)一個(gè) bug。因?yàn)楦北驹谇袚Q時(shí)間方面出現(xiàn)了錯(cuò)誤,導(dǎo)致其中一些副本錯(cuò)誤地應(yīng)用了一小部分 WAL 記錄。由于這個(gè)問(wèn)題,一些本應(yīng)由版本控制機(jī)制標(biāo)記為無(wú)效的記錄實(shí)際上并未被標(biāo)記為無(wú)效。

下面的查詢說(shuō)明了這個(gè)錯(cuò)誤將如何影響我們的用戶表:

 
 
 
 
  1. SELECT * FROM users WHERE id = 4; 

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

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

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

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

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

副本 MVCC

Postgres 沒(méi)有提供真正的副本 MVCC 支持。副本只應(yīng)用 WAL 更新,導(dǎo)致它們?cè)谌魏螘r(shí)候都具有與主數(shù)據(jù)庫(kù)相同的磁盤(pán)數(shù)據(jù)副本。這種設(shè)計(jì)給 Uber 帶來(lái)了麻煩。

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

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

升級(jí) Postgres

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

我們按照以下這些步驟從一個(gè) Postgres GA 版本升級(jí)到另一個(gè)版本:

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

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

如果你的 Postgres 是 9.4 或更高版本,可以使用 pgologic 之類的東西,它為 Postgres 實(shí)現(xiàn)了一個(gè)邏輯復(fù)制層。你可以用它在不同的 Postgres 版本之間復(fù)制數(shù)據(jù),這意味著可以從 9.4 升級(jí)到 9.5,而不會(huì)造成大面積停機(jī)。不過(guò),這個(gè)功能仍然是有問(wèn)題的,因?yàn)樗形幢患傻?Postgres 主線中。而對(duì)于那些使用較舊版本的 Postgres 的人來(lái)說(shuō),pgologic 并不適用。

3. MySQL 架構(gòu)

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

InnoDB 的磁盤(pán)表示

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

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

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

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

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

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

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

復(fù)制

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

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

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

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

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

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

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

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

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

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

緩沖池

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

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

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

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

連接處理

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

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

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

5. 結(jié)論

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


新聞名稱:Uber為什么放棄Postgres選擇遷移到MySQL?
文章網(wǎng)址:http://m.5511xx.com/article/ccodjsh.html