Uber為什么從Postgres遷移到MySQL
導(dǎo)論
Uber的早期架構(gòu)由一個單體后端應(yīng)用程序構(gòu)成,該應(yīng)用由Python編寫,Python使用Postgres以實現(xiàn)數(shù)據(jù)持久化。自那時起,Uber架構(gòu)已發(fā)生巨變,逐步轉(zhuǎn)化為微服務(wù)模式和新的數(shù)據(jù)平臺。特別是在之前一些使用Postgres的案例中,現(xiàn)在則改用Schemaless(一個基于MySQL的全新數(shù)據(jù)庫分片)。本文將探索Postgres的缺陷,解釋遷移到MySQL的基礎(chǔ)上構(gòu)建Schemaless和其它后端服務(wù)的原因。
Postgres的架構(gòu)
Postgres有很多局限性:
寫入架構(gòu)低效數(shù)據(jù)復(fù)制低效表損壞的問題糟糕的從庫MVCC支持新版本更新難度升級
下文將分析Postgres的表表示法和磁盤上的索引數(shù)據(jù),重點對比MySQL通過其InnoDB存儲引擎呈現(xiàn)相同數(shù)據(jù)的方法,以探索上述缺陷。注意:本文涉及的分析主要基于舊版Postgres 9.2系列。 眾所周知,本文論述的內(nèi)部架構(gòu)在新發(fā)布的Postgres中沒有太大變更。事實上,至少自Postgres 8.3的發(fā)布開始(距今近十年),Postgres 9.2中磁盤上表示法的基礎(chǔ)設(shè)計就一直沒有做出顯著調(diào)整。
磁盤上的數(shù)據(jù)格式
關(guān)系數(shù)據(jù)庫必須執(zhí)行下列關(guān)鍵任務(wù):
支持插入/更新/刪除功能支持schema變更功能實現(xiàn)一個多版本并發(fā)控制(MVCC)機制,促使不同的連接對其所處理的數(shù)據(jù)生成一致性的事務(wù)視圖
思考其所有特性如何協(xié)同運作是設(shè)計數(shù)據(jù)庫在磁盤上呈現(xiàn)數(shù)據(jù)的基礎(chǔ)。
Postgres的一項核心設(shè)計是行數(shù)據(jù)固定。該固定行在Postgres用語中又名“元組(tuple)”。在Postgres中,元組又通過ctid獲得唯一標(biāo)識。從概念上講,ctid代表元組在磁盤上的位置(例如物理磁盤偏移)。多個ctid可能能夠描述一個單行(例如多個行版本為了MVCC的目的而存在時,或是舊版本行未經(jīng)autovacuum進(jìn)程回收時)。一組元組的組織集合構(gòu)成表,表本身包含索引,索引經(jīng)組合構(gòu)成數(shù)據(jù)結(jié)構(gòu)(通常是B-tree結(jié)構(gòu)),從而將索引字段映射到ctid的有效載荷。
通常情況下,這些ctid是面向用戶透明的,但了解其運行方式有助于理解Postgres表在磁盤上的表架構(gòu)。若要查看行的當(dāng)前ctid,則可向WHERE子句中的欄目列表中添加“ctid”:
uber@[local] uber=》 SELECTctid, * FROM my_table LIMIT 1; -[ RECORD1]--------+------------------------------ctid | (0,1) 。。.other fields here.。。
為求布局細(xì)節(jié),先以一個簡單的用戶表為例。Uber針對每個用戶設(shè)置了自動遞增的用戶ID主鍵、用戶姓名和出生年份。同時Uber還設(shè)置了一個基于用戶全名(包括名和姓)的復(fù)合二級索引,和另一個基于用戶出生年份的二級索引。用以創(chuàng)建該表的DDL如下:
CREATETABLEusers ( id SERIAL, firstTEXT, lastTEXT, birth_year INTEGER, PRIMARYKEY(id) );CREATEINDEX ix_users_first_last ONusers (first, last);CREATEINDEX ix_users_birth_year ONusers (birth_year);
注意該定義中的三個索引:主鍵索引和兩個二級索引。
為求例證,將以下面的表格數(shù)據(jù)展開論述,表中數(shù)據(jù)均由歷史上頗具影響力的數(shù)學(xué)家構(gòu)成:
id
first
last
birth_year
1 Blaise Pascal 1623
2 Gottfried Leibniz 1646
3 Emmy Noether 1882
4 Muhammad al-Khwārizmī 780
5 Alan Turing 1912
6 Srinivasa Ramanujan 1887
7 Ada Lovelace 1815
8 Henri Poincaré 1854
如前所述,表中每一行隱含一個唯一且不公開的ctid。因此,表的內(nèi)部表示如下:
ctid
id
first
last
birth_year
A 1 Blaise Pascal 1623
B 2 Gottfried Leibniz 1646
C 3 Emmy Noether 1882
D 4 Muhammad al-Khwārizmī 780
E 5 Alan Turing 1912
F 6 Srinivasa Ramanujan 1887
G 7 Ada Lovelace 1815
H 8 Henri Poincaré 1854
設(shè)置主鍵索引(映射ID到ctid):
id
cti
1 A
2 B
3 C
4 D
5 E
6 F
7 G
8 H
B-tree結(jié)構(gòu)的設(shè)置基于id字段,且其每個節(jié)點都保存ctid值。在這個案例中需要注意的是,由于使用自動遞增id,B-tree中的字段順序有時會和表中順序相同,但是也不一定如此。
二級索引彼此都很相似;主要差異在于字段存儲順序,而字段在B-tree中必須以字典順序排布。(first,last)索引從名開始按字母表順序自上而下排列。
first
last
ctid
Ada Lovelace G
Alan Turing E
Blaise Pascal A
Emmy Noether C
Gottfried Leibniz B
Henri Poincaré H
Muhammad al-Khwārizmī D
Srinivasa Ramanujan F
同樣,birth_year(出生年份)聚集索引以升序排列:
birth_year
ctid
780 D
1623 A
1646 B
1815 G
1854 H
1887 F
1882 C
1912 E
綜上所述,不同于自動遞增主鍵的案例,在上面的情境下,各個二級索引中的ctid字段都不是按字母表順序升序排布的。
假設(shè)需要更新一條表記錄,比如將al-Khwārizmī的出生年份字段更新為另一個預(yù)估值770CE。如前所述,行元組是固定的,因此,為了更新記錄,需要向表中添加一個新元組。該新元組有一個新的非公開ctid,稱之為I。Postgres需要能夠區(qū)分I上的新元組和D上的舊元組。在內(nèi)部,Postgres將一個版本字段和指向前一個元組(如果有的話)的指針存于各個元組。據(jù)此,表的新結(jié)構(gòu)如下:
ctid
prev
id
first
last
birth_year
A null 1 Blaise Pascal 1623
B null 2 Gottfried Leibniz 1646
C null 3 Emmy Noether 1882
D null 4 Muhammad al-Khwārizmī 780
E null 5 Alan Turing 1912
F null 6 Srinivasa Ramanujan 1887
G null 7 Ada Lovelace 1815
H null 8 Henri Poincaré 1854
I D 4 Muhammad al-Khwārizmī 770
只要al-Khwārizmī存在兩個行版本,則索引必須包含這兩個行的條目。為求簡潔,Uber此處刪除了主鍵索引,只顯示二級索引:
first
last
ctid
Ada Lovelace G
Alan Turing E
Blaise Pascal A
Emmy Noether C
Gottfried Leibniz B
Henri Poincaré H
Muhammad al-Khwārizmī D
Muhammad al-Khwārizmī I
Srinivasa Ramanujan F
birth_year
ctid
770 I
780 D
1623 A
1646 B
1815 G
1854 H
1887 F
1882 C
1912 E
此處將舊版顯示為紅色,新版為綠色。在內(nèi)部,Postgre通過另一個字段保存行版本,以判定哪一個是最新元組。該新增字段幫助數(shù)據(jù)庫決定讓哪一個行元組服務(wù)于一個事務(wù),該事務(wù)可能不被允許查看最新行版本。
Postgres下,主索引和二級索引都直指磁盤上的元組偏移。若一個元組的位置發(fā)生改變,則必須更新全部索引。
非常好我支持^.^
(1) 100%
不好我反對
(0) 0%