17條避坑指南,獲贊5K+,這是一份來自谷歌工程師的數(shù)據(jù)庫經(jīng)驗(yàn)貼
「ACID 有很多含義」、「每個(gè)數(shù)據(jù)庫具有不同的一致性和隔離性」、「嵌套事務(wù)可能有害」…… 這些都是谷歌云工程師 Jaana Dogan 曾經(jīng)踩過的坑。在這篇文章中,她總結(jié)了 17 條這樣的經(jīng)驗(yàn)教訓(xùn),希望為剛接觸數(shù)據(jù)庫的小白提供一份避坑指南。目前,這一指南已在 medium 上收獲了 5k+ 贊。
絕大多數(shù)計(jì)算機(jī)系統(tǒng)都具有某種狀態(tài),而且很可能還依賴于一個(gè)存儲系統(tǒng)。我對數(shù)據(jù)庫的知識也是逐漸累積起來的,但在累積的過程中,我們的設(shè)計(jì)錯誤曾導(dǎo)致過數(shù)據(jù)丟失和中斷問題。在嚴(yán)重依賴數(shù)據(jù)的系統(tǒng)中,數(shù)據(jù)庫是系統(tǒng)設(shè)計(jì)的目標(biāo)和權(quán)衡的核心。盡管我們不可能忽略數(shù)據(jù)庫的工作方式,但應(yīng)用開發(fā)者可以預(yù)見或?qū)嶋H經(jīng)歷的問題往往都只是冰山一角。在本系列文章中,我將分享一些我專門找到的對不擅長數(shù)據(jù)庫領(lǐng)域的開發(fā)者很有用的見解:
- 如果 99.999% 的時(shí)間里網(wǎng)絡(luò)沒有問題,那你確實(shí)很幸運(yùn)。
- ACID 有很多含義。
- 每個(gè)數(shù)據(jù)庫具有不同的一致性和隔離性。
- 當(dāng)你無法搞定鎖時(shí),就使用樂觀鎖。
- 除了臟讀和數(shù)據(jù)丟失,還存在其它異常。
- 我的數(shù)據(jù)庫和我在排序方面并不總是一致的。
- 應(yīng)用層面的分片可以存在于該應(yīng)用之外。
- AUTOINCREMENT 可能有害。
- 過時(shí)的數(shù)據(jù)可能有用而且是無鎖的。
- 任何時(shí)鐘源之間都會發(fā)生時(shí)鐘偏移。
- 延遲(latency)有很多含義。
- 評估每個(gè)事務(wù)的性能需求。
- 嵌套事務(wù)可能有害。
- 事務(wù)不應(yīng)維持應(yīng)用狀態(tài)。
- 查詢計(jì)劃器能提供有關(guān)數(shù)據(jù)庫的一切信息。
- 在線遷移可能很復(fù)雜,但卻可以實(shí)現(xiàn)。
- 數(shù)據(jù)庫顯著增長時(shí)會引入不可預(yù)測性。
如果 99.999% 的時(shí)間里網(wǎng)絡(luò)沒有問題,那你確實(shí)很幸運(yùn)。
人們至今仍在論辯如今的網(wǎng)絡(luò)連接技術(shù)有多可靠以及由于網(wǎng)絡(luò)中斷而導(dǎo)致系統(tǒng)停機(jī)的情況有多頻繁。可行的研究很有限,而且這些研究往往由擁有使用定制硬件的專用網(wǎng)絡(luò)的大型組織以及特定人員所主導(dǎo)。
憑借 99.999% 的服務(wù)可用性,谷歌僅把 Spanner(谷歌散布在全球的數(shù)據(jù)庫)出現(xiàn)的問題中的 7.6% 歸因于網(wǎng)絡(luò)連接,盡管該公司稱其專用網(wǎng)絡(luò)是這種可用性背后的核心原因。Bailis 和 Kingsbury 2014 年的調(diào)查向 Peter Deutsch 于 1994 年提出的分布式計(jì)算的謬誤(Fallacies of Distributed Computing)之一發(fā)起了挑戰(zhàn)。網(wǎng)絡(luò)真的可靠嗎?
我們并沒有來自巨頭企業(yè)之外的調(diào)查結(jié)果或在公共互聯(lián)網(wǎng)上的調(diào)查結(jié)果。主要電信提供商也沒有足夠的數(shù)據(jù),讓人無法了解他們的客戶端遇到的問題有多少可追溯到網(wǎng)絡(luò)問題。我們常會遇到大型云提供商的網(wǎng)絡(luò)堆棧中斷的情況,這可能導(dǎo)致部分互聯(lián)網(wǎng)下線幾個(gè)小時(shí),但只有影響力很高的事件才會影響到大量可見客戶端。網(wǎng)絡(luò)中斷可能影響范圍很大,但不是每個(gè)案例都會產(chǎn)生嚴(yán)重影響。云客戶端也不一定需要詳細(xì)了解他們遇到的問題。當(dāng)出現(xiàn)中斷時(shí),不可能識別出這是否是由提供商導(dǎo)致的網(wǎng)絡(luò)錯誤。對他們而言,第三方服務(wù)都是黑箱。如果不是主要提供商,是不可能估計(jì)出影響有多大的。
對比一下主要玩家公布的系統(tǒng)報(bào)告,如果可能導(dǎo)致中斷的潛在問題中僅有一小部分是網(wǎng)絡(luò)問題,那么可以說你是相當(dāng)幸運(yùn)的。網(wǎng)絡(luò)連接仍面臨著許多常規(guī)問題,比如硬件故障、拓?fù)渥兓?、管理配置更改和電源故障。但我最近看到一個(gè)新聞,發(fā)現(xiàn)鯊魚撕咬也是一個(gè)現(xiàn)實(shí)存在的問題——已經(jīng)出現(xiàn)過鯊魚撕咬海底光纜的案例。
ACID 有很多含義
ACID 表示原子性(atomicity)、一致性(consistency)、隔離性(isolation)、持久性(durability)。ACID 是數(shù)據(jù)庫事務(wù)(database transaction)需要向用戶確保有效的屬性——即使在出現(xiàn)崩潰、錯誤、硬件故障等情況時(shí)也需要保證這些屬性。如果沒有 ACID 或類似的保證,應(yīng)用開發(fā)者將難以區(qū)分他們自己的職責(zé)與數(shù)據(jù)庫能夠提供的保證。大多數(shù)關(guān)系事務(wù)數(shù)據(jù)庫都會盡力符合 ACID 指標(biāo),但 NoSQL 運(yùn)動等新方法催生了許多沒有 ACID 事務(wù)的數(shù)據(jù)庫,這些這些事務(wù)的實(shí)現(xiàn)成本比較高。
在我剛進(jìn)入這一行業(yè)時(shí),我們的技術(shù)主管當(dāng)時(shí)討論過 ACID 是否已是一個(gè)過時(shí)的概念??梢院侠淼卣f,ACID 可視為一種定義寬松的描述,而不是嚴(yán)格的實(shí)現(xiàn)標(biāo)準(zhǔn)?,F(xiàn)如今,我發(fā)現(xiàn) ACID 最有用的地方是它提供了問題的類別(以及可能的解決方案的類別)。
并非每個(gè)數(shù)據(jù)庫都符合 ACID,而在符合 ACID 的數(shù)據(jù)庫中,ACID 的解讀方式也可能不同。為什么 ACID 會有不同的實(shí)現(xiàn)方式?一個(gè)原因是在實(shí)現(xiàn) ACID 時(shí),需要權(quán)衡的東西太多了。數(shù)據(jù)庫在做廣告宣傳時(shí)可能會說自己符合 ACID,但在許多邊緣案例上仍可能有不同的解釋或在處理不太可能發(fā)生的事件時(shí)的方法不同。為了適當(dāng)?shù)乩斫夤收夏J胶驮O(shè)計(jì)權(quán)衡,開發(fā)者至少可以在高層面上了解數(shù)據(jù)庫實(shí)現(xiàn)各項(xiàng)功能的方式。
一個(gè)眾所周知的爭議問題是 MongoDB 在第 4 版后有多符合 ACID。MongoDB 很長時(shí)間都不支持日志功能,盡管默認(rèn)情況下其也不會更頻繁地(每 60 秒)將數(shù)據(jù)文件提交到磁盤。考慮以下情況,一個(gè)應(yīng)用執(zhí)行兩次寫入(w1 和 w2)。MongoDB 能夠在第一次寫入時(shí)保留更改,但無法在寫入 w2 時(shí)保留這項(xiàng)更改,因?yàn)檫@會出現(xiàn)由硬件故障所致的崩潰。
MongoDB 在寫入物理磁盤前崩潰而導(dǎo)致數(shù)據(jù)丟失的示意圖
將數(shù)據(jù)提交到磁盤的過程具有較高的成本,而通過避免提交,它們可以宣稱在寫入方面表現(xiàn)出色,但這樣就犧牲了持久性。如今,MongoDB 已經(jīng)有了日志功能,但臟寫(dirty writes)仍然可能影響數(shù)據(jù)的持久性,因?yàn)樗鼈兡J(rèn)是每 100 ms 提交一次。對于日志及這些日志所表示的更改的持久性,也可能會出現(xiàn)同樣的情況,不過這種風(fēng)險(xiǎn)要小得多。
每個(gè)數(shù)據(jù)庫具有不同的一致性和隔離性
在 ACID 屬性中,一致性和隔離性的不同實(shí)現(xiàn)細(xì)節(jié)的范圍是最廣的,因?yàn)槠渖婕暗臋?quán)衡因素更多。一致性和隔離性都是實(shí)現(xiàn)成本較高的屬性。為了保持?jǐn)?shù)據(jù)一致,它們需要協(xié)調(diào)而且正得到越來越多的討論。當(dāng)必須以水平方式擴(kuò)展數(shù)據(jù)中心時(shí)(尤其是對于不同的地區(qū)),這些問題會變得更加困難。因?yàn)榇藭r(shí)可用性會下降且網(wǎng)絡(luò)分區(qū)會越來越普遍,這會導(dǎo)致很難實(shí)現(xiàn)高層面的一致性。CAP 定理為這一現(xiàn)象給出了更普適的解釋。需要指出的是,即使有一些不一致性,一般應(yīng)用也能處理,或者程序開發(fā)者對這一問題有足夠的認(rèn)知,讓他們能為該應(yīng)用添加用于處理這一情況的邏輯,從而無需過于依賴他們的數(shù)據(jù)庫。
數(shù)據(jù)庫往往會提供多種不同的隔離層,這樣應(yīng)用開發(fā)者就可以基于自己的權(quán)衡策略來選擇最具成本效益的。當(dāng)隔離更弱時(shí),速度可能更快,但也可能導(dǎo)致數(shù)據(jù)競爭(data race)。當(dāng)隔離更強(qiáng)時(shí),不會出現(xiàn)某些潛在的數(shù)據(jù)競爭,但速度會更慢,而且還可能出現(xiàn)爭用(contention)情況,這甚至可能將數(shù)據(jù)庫的速度拖慢到中斷的程度。
現(xiàn)有并發(fā)模型及它們之間的關(guān)系概況
SQL 標(biāo)準(zhǔn)僅定義了 4 種隔離層級,但理論上和實(shí)踐中的層級都更多。jepson.io 很好地總結(jié)了現(xiàn)有并發(fā)模型的情況:https://jepsen.io/consistency。舉個(gè)例子,谷歌的 Spanner 使用了時(shí)鐘同步來保證外部可串行化,即使這是一種更嚴(yán)格的隔離層,但標(biāo)準(zhǔn)隔離層中卻并沒有這樣的定義。
SQL 標(biāo)準(zhǔn)中提及的隔離層級包括:
- 可串行化(最嚴(yán)格,成本最高):可串行化執(zhí)行(serializable execution)得到的效果與這些事務(wù)的某些序列執(zhí)行的效果一樣。序列執(zhí)行(serial execution)是指在每個(gè)事務(wù)執(zhí)行完成之后再執(zhí)行下一個(gè)事務(wù)。關(guān)于可串行化執(zhí)行,需要注意的一點(diǎn)是:由于解釋的差異性,它往往被實(shí)現(xiàn)為快照隔離(snapshot isolation),比如 Oracle,而快照隔離并不在 SQL 標(biāo)準(zhǔn)中。
- 可重復(fù)的讀取:當(dāng)前事務(wù)中未提交的讀取對當(dāng)前事務(wù)來說是可見的,但其它事務(wù)做出的改變(比如新插入的行)不是可見的。
- 已提交的讀取:未提交的讀取對事務(wù)來說不可見。只有已提交的寫入是可見的,但可能出現(xiàn)幻象讀?。╬hantom read)。如果另一個(gè)事務(wù)插入和提交了新的行,則當(dāng)前事務(wù)在查詢時(shí)可以看到它們。
- 未提交的讀取(最不嚴(yán)格,成本最低):允許臟讀(dirty read),事務(wù)可以看到其它事務(wù)做出的尚未提交的更改。在實(shí)踐中,這個(gè)層級可用于返回近似聚合結(jié)果,比如對一個(gè)表格的 COUNT(*) 查詢。
可串行化層級出現(xiàn)數(shù)據(jù)競爭的情況最少,但成本也最高,而且會讓系統(tǒng)出現(xiàn)最多爭用。其它隔離層級的成本更低一些,但也更可能出現(xiàn)數(shù)據(jù)競爭問題。某些數(shù)據(jù)庫允許自行設(shè)置隔離層級,某些數(shù)據(jù)庫則在這方面更為固執(zhí)一點(diǎn),并不一定支持所有這些層級。
而就算數(shù)據(jù)庫宣稱自己支持這些隔離層級,但只要仔細(xì)檢查一下它們的行為,就可以了解這些數(shù)據(jù)庫實(shí)際究竟是怎么做的。
每個(gè)數(shù)據(jù)庫在不同隔離層級上的并發(fā)異常概況
Martin Kleppmann 的 hermitage 項(xiàng)目總結(jié)了不同的并發(fā)異常,并說明了一個(gè)數(shù)據(jù)庫在不同的隔離層級上能否處理這樣的異常:https://github.com/ept/hermitage 。Kleppmann 的研究表明數(shù)據(jù)庫設(shè)計(jì)者會以不同的方式解釋隔離層級。
當(dāng)你無法搞定鎖時(shí),就使用樂觀鎖
鎖的成本非常高,不僅是因?yàn)樗鼈儠閿?shù)據(jù)庫引入更多爭用,而且還需要你的應(yīng)用服務(wù)器與數(shù)據(jù)庫之間存在一致的連接。網(wǎng)絡(luò)分區(qū)可能會更顯著地影響排它鎖(exclusive lock),這會導(dǎo)致難以識別和解決的死鎖(deadlock)。如果有些案例無法很好地使用排它鎖,可以選擇樂觀鎖(optimistic locking)。
樂觀鎖這種方法是指當(dāng)讀取某行時(shí)會記錄版本號、上次修改的時(shí)間戳或其校驗(yàn)和(checksum)。然后你可以在更改記錄之前檢查原子方面并無修改的版本。
- UPDATE products
- SET name = 'Telegraph receiver', version = 2
- WHERE id = 1 AND version = 1
如果另一項(xiàng)更新之前已經(jīng)修改了這一行,那么對 products 表的更新將影響 0 行。如果沒有更早的更新,則它會影響 1 行,則我們可以說更新成功了。
除了臟讀和數(shù)據(jù)丟失,還存在其它異常
當(dāng)我們在探討數(shù)據(jù)一致性時(shí),我們主要關(guān)注的是可能導(dǎo)致臟讀和數(shù)據(jù)丟失的競爭問題。但數(shù)據(jù)方面的異常并不止這兩種。
舉個(gè)例子,還有一種異常是寫偏序(write skew)。寫偏序更難以識別認(rèn)定,因?yàn)槲覀儾粫鲃拥厝ゲ檎疫@個(gè)問題。導(dǎo)致寫偏序的原因不是發(fā)生在寫入上的臟讀或數(shù)據(jù)丟失,而是因?yàn)閿?shù)據(jù)上的邏輯約束損壞。
比如,假設(shè)一個(gè)監(jiān)控應(yīng)用需要一個(gè)人類操作員始終處于待命狀態(tài)。
- BEGIN tx1; BEGIN tx2;SELECT COUNT(*)
- FROM operators
- WHERE oncall = true;
- 0 SELECT COUNT(*)
- FROM operators
- WHERE oncall = TRUE;
- 0UPDATE operators UPDATE operators
- SET oncall = TRUE SET oncall = TRUE
- WHERE userId = 4; WHERE userId = 2;COMMIT tx1;
在上面的情況中,如果這些事務(wù)中有兩個(gè)成功提交,就會出現(xiàn)寫偏序。即使此時(shí)沒有出現(xiàn)臟讀或數(shù)據(jù)丟失,數(shù)據(jù)也失去了完整性,因?yàn)槠渲付藘蓚€(gè)待命的人。
可串行化隔離、模式設(shè)計(jì)或數(shù)據(jù)庫約束有助于消除寫偏序。開發(fā)者需要在開發(fā)過程中識別這樣的異常,以避免生產(chǎn)過程中出現(xiàn)數(shù)據(jù)異常。話雖如此,識別代碼庫中的寫偏序卻非常之難。尤其是在大型系統(tǒng)中,如果負(fù)責(zé)基于同一表格構(gòu)建功能的不同團(tuán)隊(duì)之間沒有溝通且沒有互相檢查他們存取數(shù)據(jù)的方式,那么就會出現(xiàn)這種問題。
我的數(shù)據(jù)庫和我在排序方面并不總是一致的
數(shù)據(jù)庫提供的一大核心能力是排序保證,但排序結(jié)果可能會出乎應(yīng)用開發(fā)者的預(yù)料。數(shù)據(jù)庫查閱事務(wù)的順序就是它們接收這些事務(wù)的順序,而不是開發(fā)者查看它們時(shí)的程序設(shè)計(jì)順序。事務(wù)執(zhí)行的順序難以預(yù)測,尤其是在高容量的并發(fā)系統(tǒng)中。
在開發(fā)時(shí),尤其是在使用非阻塞軟件庫進(jìn)行開發(fā)時(shí),較差的樣式和可讀性可能會導(dǎo)致用戶認(rèn)為事務(wù)是按順序執(zhí)行的,即使它們可能以任何順序抵達(dá)數(shù)據(jù)庫。下面的程序看起來像是 T1 和 T2 將按順序調(diào)用,但如果這些函數(shù)是非阻塞的,則它們將立即帶著 promise 返回,調(diào)用的順序?qū)⑷Q于它們在數(shù)據(jù)庫中接收到的時(shí)間。
- result1 = T1() // results are actually promises
- result2 = T2()
如果需要原子性(以便完全提交或放棄所有操作)且序列很重要,則 T1 和 T2 中的操作應(yīng)該運(yùn)行在單個(gè)數(shù)據(jù)庫事務(wù)中。
應(yīng)用層面的分片可以存在于該應(yīng)用之外
分片(Sharding)是一種水平劃分?jǐn)?shù)據(jù)庫的方法。有的數(shù)據(jù)庫可以自動地對數(shù)據(jù)進(jìn)行水平分區(qū),有的數(shù)據(jù)庫則不支持這種功能或做得不好。當(dāng)數(shù)據(jù)架構(gòu)師 / 開發(fā)者可以預(yù)測訪問數(shù)據(jù)的方式時(shí),他們可能會在用戶區(qū)域創(chuàng)建水平分區(qū),而不是將這項(xiàng)工作委托給他們的數(shù)據(jù)庫。這種方式稱為應(yīng)用級分片(application-level sharding)。
應(yīng)用級分片這個(gè)名稱往往會給人帶來一種錯誤印象,讓人以為這種分片應(yīng)該存在于應(yīng)用服務(wù)之中。分片功能可以實(shí)現(xiàn)為數(shù)據(jù)庫的前面一層。取決于數(shù)據(jù)增長和架構(gòu)迭代情況,分片的要求可能會變得非常復(fù)雜。如果能在無需重新部署應(yīng)用服務(wù)器的前提下對某些策略進(jìn)行迭代,則會大有裨益。
應(yīng)用服務(wù)器與分片服務(wù)分離的架構(gòu)示例
如果將分片作為一個(gè)單獨(dú)的服務(wù),你就能更好地在不重新部署應(yīng)用服務(wù)器的前提下迭代分片策略。Vitess 就是應(yīng)用級分片系統(tǒng)的一個(gè)例子。Vitess 為 MySQL 提供了水平分片,并允許客戶端通過 MySQL 協(xié)議連接它;Vitess 會將數(shù)據(jù)分片到多個(gè)互相之間無聯(lián)系的 MySQL 節(jié)點(diǎn)上。
AUTOINCREMENT 可能有害
AUTOINCREMENT(自動遞增)是生成主鍵(primary key)的一種常用方法。數(shù)據(jù)庫被用作 ID 生成器以及數(shù)據(jù)庫中有 ID 生成指定表格的情況其實(shí)并不少見。但使用自動遞增生成主鍵的方式其實(shí)并不理想,原因有幾點(diǎn):
- 在分布式數(shù)據(jù)庫系統(tǒng)中,自動遞增很困難。為了生成 ID,需要使用全局鎖才行。而如果你可以生成 UUID,那么就不需要數(shù)據(jù)庫節(jié)點(diǎn)之間有任何合作。使用鎖的自動遞增可能導(dǎo)致爭用,并可能導(dǎo)致分布式情況中插入性能顯著下降。MySQL 等一些數(shù)據(jù)庫可能需要特定的配置和更多的注意才能正確地完成 master-master 復(fù)制。這樣的配置容易混亂而且可能導(dǎo)致寫入中斷。
- 某些數(shù)據(jù)庫有基于主鍵的分區(qū)算法。按順序排布的 ID 可能導(dǎo)致無法預(yù)測的熱點(diǎn),從而使得某些分區(qū)過于繁忙,另一些則一直空閑。
- 訪問數(shù)據(jù)庫中某行的最快方式是通過主鍵。如果你有更好的標(biāo)識記錄的方式,那么順序 ID 可能會讓表中最顯著的列成為無意義的值。請盡可能地選擇全局獨(dú)一的自然主鍵(比如用戶名)。
請考慮自動遞增 ID 與 UUID 對索引、分區(qū)和分片的影響,然后再決定哪種方式對你而言最好。
過時(shí)的數(shù)據(jù)可能有用而且是無鎖的
多版本并發(fā)控制(MVCC)能實(shí)現(xiàn)我們上面簡要討論過的很多一致性。Postgres 和 Spanner 等一些數(shù)據(jù)庫使用 MVCC 以讓每個(gè)事務(wù)都能看到一個(gè)快照,即該數(shù)據(jù)庫的一個(gè)更舊版本。參照快照的事務(wù)仍然可以串行化以實(shí)現(xiàn)一致性。當(dāng)讀取一個(gè)舊快照時(shí),實(shí)際讀取的是過時(shí)的數(shù)據(jù)。
但即使讀取的是稍微過時(shí)的數(shù)據(jù),也會很有用處,比如當(dāng)在生成數(shù)據(jù)分析結(jié)果或計(jì)算近似聚合值時(shí)。
讀取過時(shí)數(shù)據(jù)的第一大優(yōu)勢是延遲(尤其是當(dāng)你的數(shù)據(jù)庫分布在不同的地區(qū)時(shí))。MVCC 數(shù)據(jù)庫的第二大優(yōu)勢是其允許只讀事務(wù)是無鎖的。在需要大量讀取的應(yīng)用中,一個(gè)優(yōu)勢是用過時(shí)的數(shù)據(jù)也是可行的。
即便太平洋另一端有某個(gè)數(shù)據(jù)的最新版本,但也可以從本地讀取 5 秒前的過時(shí)副本。
數(shù)據(jù)庫會自動清除舊版本,而在某些情況下,數(shù)據(jù)庫也支持按需清理。舉個(gè)例子,Postgres 允許用戶按需執(zhí)行 VACUUM 操作或每隔一段時(shí)間自動執(zhí)行 VACUUM,而 Spanner 則是通過運(yùn)行一個(gè)垃圾收集器來丟棄時(shí)間超過 1 小時(shí)的版本。
任何時(shí)鐘源之間都會發(fā)生時(shí)鐘偏移
在計(jì)算領(lǐng)域,隱藏得最好的秘密是所有時(shí)間 API 都在說謊。我們的機(jī)器并不能準(zhǔn)確地知道當(dāng)前的時(shí)間是多少。我們的計(jì)算機(jī)全都包含一個(gè)用以產(chǎn)生計(jì)時(shí)信號的石英晶體。但石英晶體并不能準(zhǔn)確計(jì)時(shí)和計(jì)算時(shí)間偏移量,要么比實(shí)際時(shí)鐘快,要么就更慢。一天的偏移量甚至可達(dá) 20 秒。為了準(zhǔn)確,我們的計(jì)算機(jī)時(shí)間必須不時(shí)地與實(shí)際時(shí)間保持同步。
NTP 服務(wù)器可用于同步,但同步本身卻可能由于網(wǎng)絡(luò)的原因而出現(xiàn)延遲。與同一數(shù)據(jù)中心的 NTP 服務(wù)器同步?jīng)r且需要時(shí)間,與公共 NTP 服務(wù)器同步更是可能產(chǎn)生更大的偏移。
原子鐘和 GPS 時(shí)鐘是更好的確定當(dāng)前時(shí)間的信息源,但它們的部署成本更高,而且需要復(fù)雜的設(shè)置,不可能在每臺機(jī)器上都安裝。由于存在這些限制條件,數(shù)據(jù)中心通常使用的是多層方法。即在使用原子鐘和 / 或 GPS 時(shí)鐘提供準(zhǔn)確計(jì)時(shí)的同時(shí),再通過輔助服務(wù)器將時(shí)間信息廣播給其它機(jī)器。這意味著所有機(jī)器都與實(shí)際的當(dāng)前時(shí)間存在一定程度的偏移。
不僅如此,應(yīng)用和數(shù)據(jù)庫往往搭建在不同的機(jī)器中,甚至還可能位于不同的數(shù)據(jù)中心。因此,不僅分散在不同機(jī)器上的不同數(shù)據(jù)庫節(jié)點(diǎn)之間無法統(tǒng)一時(shí)間,應(yīng)用服務(wù)器時(shí)鐘和數(shù)據(jù)庫節(jié)點(diǎn)時(shí)鐘也無法統(tǒng)一。
谷歌的 TrueTime 為此采用了一種不同的方法。大多數(shù)人認(rèn)為谷歌在時(shí)鐘上的成果可以歸功于他們使用了原子鐘和 GPS 時(shí)鐘,但那其實(shí)僅僅是部分原因。TrueTime 實(shí)際上是這樣工作的:
- TrueTime 使用了兩個(gè)不同的時(shí)間信號源:GPS 時(shí)鐘和原子鐘。這些時(shí)鐘存在不同的故障模式,因此同時(shí)使用兩者可以提升可靠性。
- TrueTime 的 API 并不是常規(guī)型的。它會以區(qū)間的形式返回時(shí)間。因此實(shí)際時(shí)間事實(shí)上處于這個(gè)時(shí)間區(qū)間的上界和下界之間。因此,谷歌的分布式數(shù)據(jù)庫 Spanner 就可以等到它確定了當(dāng)前時(shí)間超過了特定時(shí)間之后才執(zhí)行事務(wù)。這種方法會給系統(tǒng)帶來一些延遲,尤其是當(dāng)主機(jī)通告的不確定性很高時(shí);但這種方法能保證正確性,即使數(shù)據(jù)庫分布在全球也是如此。
使用 TrueTime 的 Spanner 組件,其中 TT.now() 會返回一個(gè)時(shí)間區(qū)間,這樣 Spanner 就可以插入睡眠時(shí)間以確保當(dāng)前時(shí)間已超過特定時(shí)間戳。
當(dāng)當(dāng)前時(shí)間的置信度下降時(shí),Spanner 執(zhí)行操作可能會耗費(fèi)更多時(shí)間。因此,即使不可能獲得精準(zhǔn)的時(shí)鐘,保證時(shí)鐘的置信度對性能而言也是非常重要的。
延遲有很多含義
如果房間里有 10 個(gè)人,你問他們「延遲(latency)」是什么意思,你可能會得到 10 個(gè)不同的答案。在數(shù)據(jù)庫中,延遲通常是指數(shù)據(jù)庫延遲,而非客戶端所感知到的延遲。客戶端感知到的延遲包含數(shù)據(jù)庫延遲和網(wǎng)絡(luò)延遲。在調(diào)試不斷惡化的問題時(shí),分辨客戶端延遲和數(shù)據(jù)庫延遲是非常重要的。在收集和展示指標(biāo)時(shí),往往需要同時(shí)包含這兩種延遲。
評估每個(gè)事務(wù)的性能需求
有時(shí)候,數(shù)據(jù)庫會將它們的讀寫吞吐量和延遲作為性能優(yōu)勢的賣點(diǎn)來進(jìn)行宣傳。盡管這能在評估數(shù)據(jù)庫的性能時(shí)從較高層面上展現(xiàn)主要的限制因素,但為了更全面地進(jìn)行評估,需要單獨(dú)分開評估各個(gè)關(guān)鍵操作的性能,比如每次查詢或每個(gè)事務(wù)的執(zhí)行性能。示例:
- 為具有給定約束條件的包含 5000 萬行的表格 X 插入新的一行并填充相關(guān)表格時(shí)的吞吐量和延遲。
- 當(dāng)平均好友數(shù)為 500 時(shí),查詢一個(gè)用戶的好友的好友時(shí)的延遲。
- 當(dāng)用戶訂閱了 500 個(gè)賬號且每個(gè)小時(shí)有 X 項(xiàng)新輸入時(shí),檢索用戶時(shí)間線前 100 條記錄時(shí)的延遲。
評估和實(shí)驗(yàn)可能包含這樣的關(guān)鍵性案例,直到你有信心你的數(shù)據(jù)庫能夠滿足你的性能需求。另一個(gè)類似的經(jīng)驗(yàn)法則是在收集延遲指標(biāo)和設(shè)置 SLO 時(shí)考慮這種故障情況。
在收集每個(gè)操作的指標(biāo)時(shí)要注意高基數(shù)。如果你需要高基數(shù)的調(diào)試數(shù)據(jù),請使用日志或分布式的跟蹤方法。如果你想了解延遲調(diào)試方法,請參閱《Want to Debug Latency?》(https://medium.com/observability/want-to-debug-latency-7aa48ecbe8f7)。
嵌套事務(wù)可能有害
并非每個(gè)數(shù)據(jù)庫都支持嵌套事務(wù)(nested transactions),但如果支持,那么嵌套事務(wù)可能導(dǎo)致出人意料的程序設(shè)計(jì)錯誤,而且這種錯誤往往不易識別,直到出現(xiàn)了明顯異常才能看清。
如果你想要避免嵌套事務(wù),則可以使用客戶端軟件庫來檢測和避免嵌套事務(wù)。如果你不能避免嵌套事務(wù),則必須注意不要出現(xiàn)意料之外的情況,即當(dāng)提交的事務(wù)因?yàn)樽邮聞?wù)而被意外拋棄時(shí)。
如果將事務(wù)封裝在不同的層中,可能會出現(xiàn)出人意料的嵌套事務(wù)案例,而從可讀性角度來看,其意圖可能將變得難以理解。看看下面的程序:
- with newTransaction():
- Accounts.create("609-543-222") with newTransaction():
- Accounts.create("775-988-322")
- throw Rollback();
以上代碼的結(jié)果是什么?是兩個(gè)事務(wù)都會回滾還是僅回滾內(nèi)部那個(gè)事務(wù)?如果我們當(dāng)時(shí)依賴的多層軟件庫將該事務(wù)的創(chuàng)建過程封裝起來不為我們所見,我們還能識別和改進(jìn)這樣的案例嗎?
假設(shè)一個(gè)具有多項(xiàng)操作(比如 newAccount)的數(shù)據(jù)層已經(jīng)在它們自己的事務(wù)中實(shí)現(xiàn)了。當(dāng)你用更高層的業(yè)務(wù)邏輯(它們運(yùn)行在自己的事務(wù)中)運(yùn)行它們時(shí),會發(fā)生什么?隔離性和一致性又會怎樣?
- function newAccount(id string) {
- with newTransaction():
- Accounts.create(id)
- }
與其耗費(fèi)資源去解決這些仍待解決的問題,還不如不使用嵌套事務(wù)。即使不創(chuàng)建它們自己的事務(wù),你的數(shù)據(jù)層仍可以實(shí)現(xiàn)高層操作。然后,業(yè)務(wù)邏輯會啟動事務(wù),在事務(wù)上運(yùn)行操作,提交或中止。
- function newAccount(id string) {
- Accounts.create(id)
- }// In main application:with newTransaction():
- // Read some data from database for configuration.
- // Generate an ID from the ID service.
- Accounts.create(id) Uploads.create(id) // create upload queue for the user.
事務(wù)不應(yīng)維持應(yīng)用狀態(tài)
應(yīng)用開發(fā)者可能會想在事務(wù)中使用應(yīng)用狀態(tài)來更新特定的值或調(diào)整查詢參數(shù)。這時(shí)所要考慮的一個(gè)關(guān)鍵事項(xiàng)是選擇合適的范圍??蛻舳嗽谟龅骄W(wǎng)絡(luò)問題時(shí)往往會重試事務(wù)。如果一個(gè)事務(wù)依賴于在其它地方會變化的狀態(tài),那么其可能根據(jù)該問題中數(shù)據(jù)競爭的可能性選擇錯誤的值。事務(wù)應(yīng)注意應(yīng)用中的數(shù)據(jù)競爭。
- var seq int64with newTransaction():
- newSeq := atomic.Increment(&seq)
- Entries.query(newSeq) // Other operations...
上面的事務(wù)不管最終結(jié)果究竟如何,在每次運(yùn)行時(shí)都會增加序列號。如果因?yàn)榫W(wǎng)絡(luò)問題而導(dǎo)致提交失敗,則在第二次重試時(shí)會使用不同的序列號進(jìn)行查詢。
查詢計(jì)劃器能提供有關(guān)數(shù)據(jù)庫的一切信息
查詢計(jì)劃器(query planner)決定了查詢在數(shù)據(jù)庫中的執(zhí)行方式。它們還會在運(yùn)行之前分析和優(yōu)化這些查詢。計(jì)劃器僅能基于其擁有的信號提供某些可能的估計(jì)。如何確定找到以下查詢的結(jié)果的方法:
- SELECT * FROM articles where author = "rakyll" order by title;
檢索結(jié)果的方法有兩種:
- 全表掃描:我們可以遍歷表中的每一項(xiàng),然后返回作者名匹配的文章,然后再執(zhí)行排序。
- 索引掃描:我們可以使用索引來查找匹配的 ID,檢索這些行,再執(zhí)行排序。
查詢計(jì)劃器的作用是確定哪種策略是最佳選擇。不過對于哪些可以預(yù)測,哪些可能導(dǎo)致糟糕的決策,查詢計(jì)劃器僅有有限的信號。數(shù)據(jù)庫管理員(DBA)或開發(fā)者可使用它們來診斷和優(yōu)化表現(xiàn)較差的查詢。當(dāng)數(shù)據(jù)庫升級時(shí),如果新版本的數(shù)據(jù)庫出現(xiàn)了性能問題,那么這個(gè)數(shù)據(jù)庫可以調(diào)節(jié)查詢計(jì)劃器并進(jìn)行自我診斷。慢查詢?nèi)罩?、延遲問題或關(guān)于執(zhí)行時(shí)間的統(tǒng)計(jì)信息等報(bào)告可用于確定需要優(yōu)化的查詢。
查詢計(jì)劃器提供的某些指標(biāo)可能具有較多噪聲,尤其是當(dāng)估計(jì)延遲或 CPU 時(shí)間時(shí)。作為對查詢計(jì)劃器的補(bǔ)充,跟蹤和執(zhí)行路徑工具對診斷這些問題而言可能會更加有用,不過并非每個(gè)數(shù)據(jù)庫都會提供這樣的工具。
在線遷移可能很復(fù)雜,但卻可以實(shí)現(xiàn)
在線或?qū)崟r(shí)遷移的意思是在不停機(jī)且不損害數(shù)據(jù)正確性的同時(shí)從一個(gè)數(shù)據(jù)庫遷移到另一個(gè)數(shù)據(jù)庫。如果是遷移到同樣的數(shù)據(jù)庫 / 引擎,在線遷移會更為簡單;但如果是遷移到性能特性和組織結(jié)構(gòu)要求不同的新數(shù)據(jù)庫,那情況會復(fù)雜得多。
在線遷移有多種模式,下面介紹其中一種:
- 開始向兩個(gè)數(shù)據(jù)庫執(zhí)行雙寫入(dual writes)。在這一階段,新數(shù)據(jù)庫還不包含所有數(shù)據(jù),但將開始看到新數(shù)據(jù)。一旦這一步得到了保證,你就可以進(jìn)入下一步了。
- 讓讀取路徑可同時(shí)使用這兩個(gè)數(shù)據(jù)庫。
- 主要使用新數(shù)據(jù)庫來進(jìn)行讀取和寫入。
- 停止向舊數(shù)據(jù)庫寫入,但繼續(xù)保持從舊數(shù)據(jù)庫讀取。此時(shí),新數(shù)據(jù)庫仍未包含所有新數(shù)據(jù),而在獲取舊記錄時(shí),可能還需要回退至舊數(shù)據(jù)庫。
- 這時(shí)候,舊數(shù)據(jù)庫處于只讀狀態(tài)。從舊數(shù)據(jù)庫取出新數(shù)據(jù)庫缺失的值對新數(shù)據(jù)庫進(jìn)行回填。遷移完成后,所有的讀取和寫入路徑都將使用新數(shù)據(jù)庫,舊數(shù)據(jù)庫則從系統(tǒng)中移除。
如果你需要更具體的案例,可以看看 Stripe 的遵循這一模式的遷移策略:https://stripe.com/blog/online-migrations
數(shù)據(jù)庫顯著增長時(shí)會引入不可預(yù)測性
數(shù)據(jù)庫增長會讓你遭遇不可預(yù)測的擴(kuò)展問題。我們對自己數(shù)據(jù)庫的內(nèi)部情況越了解,可能就越難預(yù)測它們的擴(kuò)展情況,還有些事情是我們無法預(yù)測的。
在數(shù)據(jù)庫增大時(shí),之前關(guān)于數(shù)據(jù)規(guī)模和網(wǎng)絡(luò)容量需求的假設(shè)和預(yù)期都將變得過時(shí)。這時(shí)候,為了避免中斷,需要大規(guī)模地重寫組織結(jié)構(gòu)、大規(guī)模地改進(jìn)運(yùn)營、解決容量問題、重新考慮部署方案或遷移到其它數(shù)據(jù)庫。
不要以為了解你當(dāng)前數(shù)據(jù)庫的內(nèi)部情況就萬無一失了,規(guī)模擴(kuò)大還會帶來新的未知。無法預(yù)測的熱點(diǎn)、數(shù)據(jù)不平衡的分布、意料之外的容量和硬件問題、不斷增長的流量和新的網(wǎng)絡(luò)分區(qū)都會讓你重新考慮你的數(shù)據(jù)庫、數(shù)據(jù)模型、部署模型和部署規(guī)模。
【本文是51CTO專欄機(jī)構(gòu)“機(jī)器之心”的原創(chuàng)譯文,微信公眾號“機(jī)器之心( id: almosthuman2014)”】