17條避坑指南,獲贊5K+,這是一份來自谷歌工程師的數(shù)據(jù)庫經(jīng)驗(yàn)貼
點(diǎn)擊關(guān)注上方“杰哥的IT之旅”,
設(shè)為“置頂或星標(biāo)”,第一時間送達(dá)干貨。
作者:Jaana Dogan
機(jī)器之心編譯
參與:Panda、張倩
「ACID 有很多含義」、「每個數(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),而且很可能還依賴于一個存儲系統(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% 的時間里網(wǎng)絡(luò)沒有問題,那你確實(shí)很幸運(yùn)。
ACID 有很多含義。
每個數(shù)據(jù)庫具有不同的一致性和隔離性。
當(dāng)你無法搞定鎖時,就使用樂觀鎖。
除了臟讀和數(shù)據(jù)丟失,還存在其它異常。
我的數(shù)據(jù)庫和我在排序方面并不總是一致的。
應(yīng)用層面的分片可以存在于該應(yīng)用之外。
AUTOINCREMENT 可能有害。
過時的數(shù)據(jù)可能有用而且是無鎖的。
任何時鐘源之間都會發(fā)生時鐘偏移。
延遲(latency)有很多含義。
評估每個事務(wù)的性能需求。
嵌套事務(wù)可能有害。
事務(wù)不應(yīng)維持應(yīng)用狀態(tài)。
查詢計(jì)劃器能提供有關(guān)數(shù)據(jù)庫的一切信息。
在線遷移可能很復(fù)雜,但卻可以實(shí)現(xiàn)。
數(shù)據(jù)庫顯著增長時會引入不可預(yù)測性。
如果 99.999% 的時間里網(wǎng)絡(luò)沒有問題,那你確實(shí)很幸運(yùn)。
人們至今仍在論辯如今的網(wǎng)絡(luò)連接技術(shù)有多可靠以及由于網(wǎng)絡(luò)中斷而導(dǎo)致系統(tǒng)停機(jī)的情況有多頻繁??尚械难芯亢苡邢?,而且這些研究往往由擁有使用定制硬件的專用網(wǎng)絡(luò)的大型組織以及特定人員所主導(dǎo)。
我們并沒有來自巨頭企業(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)下線幾個小時,但只有影響力很高的事件才會影響到大量可見客戶端。網(wǎng)絡(luò)中斷可能影響范圍很大,但不是每個案例都會產(chǎn)生嚴(yán)重影響。云客戶端也不一定需要詳細(xì)了解他們遇到的問題。當(dāng)出現(xiàn)中斷時,不可能識別出這是否是由提供商導(dǎo)致的網(wǎng)絡(luò)錯誤。對他們而言,第三方服務(wù)都是黑箱。如果不是主要提供商,是不可能估計(jì)出影響有多大的。
對比一下主要玩家公布的系統(tǒng)報告,如果可能導(dǎo)致中斷的潛在問題中僅有一小部分是網(wǎng)絡(luò)問題,那么可以說你是相當(dāng)幸運(yùn)的。網(wǎng)絡(luò)連接仍面臨著許多常規(guī)問題,比如硬件故障、拓?fù)渥兓?、管理配置更改和電源故障。但我最近看到一個新聞,發(fā)現(xiàn)鯊魚撕咬也是一個現(xiàn)實(shí)存在的問題——已經(jīng)出現(xiàn)過鯊魚撕咬海底光纜的案例。
ACID 有很多含義
ACID 表示原子性(atomicity)、一致性(consistency)、隔離性(isolation)、持久性(durability)。ACID 是數(shù)據(jù)庫事務(wù)(database transaction)需要向用戶確保有效的屬性——即使在出現(xiàn)崩潰、錯誤、硬件故障等情況時也需要保證這些屬性。如果沒有 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ù)主管當(dāng)時討論過 ACID 是否已是一個過時的概念??梢院侠淼卣f,ACID 可視為一種定義寬松的描述,而不是嚴(yán)格的實(shí)現(xiàn)標(biāo)準(zhǔn)?,F(xiàn)如今,我發(fā)現(xiàn) ACID 最有用的地方是它提供了問題的類別(以及可能的解決方案的類別)。
并非每個數(shù)據(jù)庫都符合 ACID,而在符合 ACID 的數(shù)據(jù)庫中,ACID 的解讀方式也可能不同。為什么 ACID 會有不同的實(shí)現(xiàn)方式?一個原因是在實(shí)現(xiàn) ACID 時,需要權(quán)衡的東西太多了。數(shù)據(jù)庫在做廣告宣傳時可能會說自己符合 ACID,但在許多邊緣案例上仍可能有不同的解釋或在處理不太可能發(fā)生的事件時的方法不同。為了適當(dāng)?shù)乩斫夤收夏J胶驮O(shè)計(jì)權(quán)衡,開發(fā)者至少可以在高層面上了解數(shù)據(jù)庫實(shí)現(xiàn)各項(xiàng)功能的方式。
一個眾所周知的爭議問題是 MongoDB 在第 4 版后有多符合 ACID。MongoDB 很長時間都不支持日志功能,盡管默認(rèn)情況下其也不會更頻繁地(每 60 秒)將數(shù)據(jù)文件提交到磁盤??紤]以下情況,一個應(yīng)用執(zhí)行兩次寫入(w1 和 w2)。MongoDB 能夠在第一次寫入時保留更改,但無法在寫入 w2 時保留這項(xiàng)更改,因?yàn)檫@會出現(xiàn)由硬件故障所致的崩潰。

將數(shù)據(jù)提交到磁盤的過程具有較高的成本,而通過避免提交,它們可以宣稱在寫入方面表現(xiàn)出色,但這樣就犧牲了持久性。如今,MongoDB 已經(jīng)有了日志功能,但臟寫(dirty writes)仍然可能影響數(shù)據(jù)的持久性,因?yàn)樗鼈兡J(rèn)是每 100 ms 提交一次。對于日志及這些日志所表示的更改的持久性,也可能會出現(xiàn)同樣的情況,不過這種風(fēng)險要小得多。
每個數(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ù)中心時(尤其是對于不同的地區(qū)),這些問題會變得更加困難。因?yàn)榇藭r可用性會下降且網(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)隔離更弱時,速度可能更快,但也可能導(dǎo)致數(shù)據(jù)競爭(data race)。當(dāng)隔離更強(qiáng)時,不會出現(xiàn)某些潛在的數(shù)據(jù)競爭,但速度會更慢,而且還可能出現(xiàn)爭用(contention)情況,這甚至可能將數(shù)據(jù)庫的速度拖慢到中斷的程度。

SQL 標(biāo)準(zhǔn)僅定義了 4 種隔離層級,但理論上和實(shí)踐中的層級都更多。jepson.io 很好地總結(jié)了現(xiàn)有并發(fā)模型的情況:https://jepsen.io/consistency。舉個例子,谷歌的 Spanner 使用了時鐘同步來保證外部可串行化,即使這是一種更嚴(yán)格的隔離層,但標(biāo)準(zhǔn)隔離層中卻并沒有這樣的定義。
SQL 標(biāo)準(zhǔn)中提及的隔離層級包括:
可串行化(最嚴(yán)格,成本最高):可串行化執(zhí)行(serializable execution)得到的效果與這些事務(wù)的某些序列執(zhí)行的效果一樣。序列執(zhí)行(serial execution)是指在每個事務(wù)執(zhí)行完成之后再執(zhí)行下一個事務(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ù)做出的改變(比如新插入的行)不是可見的。
已提交的讀?。何刺峤坏淖x取對事務(wù)來說不可見。只有已提交的寫入是可見的,但可能出現(xiàn)幻象讀取(phantom read)。如果另一個事務(wù)插入和提交了新的行,則當(dāng)前事務(wù)在查詢時可以看到它們。
未提交的讀?。ㄗ畈粐?yán)格,成本最低):允許臟讀(dirty read),事務(wù)可以看到其它事務(wù)做出的尚未提交的更改。在實(shí)踐中,這個層級可用于返回近似聚合結(jié)果,比如對一個表格的 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í)際究竟是怎么做的。

Martin Kleppmann 的 hermitage 項(xiàng)目總結(jié)了不同的并發(fā)異常,并說明了一個數(shù)據(jù)庫在不同的隔離層級上能否處理這樣的異常:https://github.com/ept/hermitage 。Kleppmann 的研究表明數(shù)據(jù)庫設(shè)計(jì)者會以不同的方式解釋隔離層級。
當(dāng)你無法搞定鎖時,就使用樂觀鎖
鎖的成本非常高,不僅是因?yàn)樗鼈儠閿?shù)據(jù)庫引入更多爭用,而且還需要你的應(yīng)用服務(wù)器與數(shù)據(jù)庫之間存在一致的連接。網(wǎng)絡(luò)分區(qū)可能會更顯著地影響排它鎖(exclusive lock),這會導(dǎo)致難以識別和解決的死鎖(deadlock)。如果有些案例無法很好地使用排它鎖,可以選擇樂觀鎖(optimistic locking)。
樂觀鎖這種方法是指當(dāng)讀取某行時會記錄版本號、上次修改的時間戳或其校驗(yàn)和(checksum)。然后你可以在更改記錄之前檢查原子方面并無修改的版本。
UPDATE productsSET name = 'Telegraph receiver', version = 2WHERE id = 1 AND version = 1
如果另一項(xiàng)更新之前已經(jīng)修改了這一行,那么對 products 表的更新將影響 0 行。如果沒有更早的更新,則它會影響 1 行,則我們可以說更新成功了。
除了臟讀和數(shù)據(jù)丟失,還存在其它異常
當(dāng)我們在探討數(shù)據(jù)一致性時,我們主要關(guān)注的是可能導(dǎo)致臟讀和數(shù)據(jù)丟失的競爭問題。但數(shù)據(jù)方面的異常并不止這兩種。
舉個例子,還有一種異常是寫偏序(write skew)。寫偏序更難以識別認(rèn)定,因?yàn)槲覀儾粫鲃拥厝ゲ檎疫@個問題。導(dǎo)致寫偏序的原因不是發(fā)生在寫入上的臟讀或數(shù)據(jù)丟失,而是因?yàn)閿?shù)據(jù)上的邏輯約束損壞。
比如,假設(shè)一個監(jiān)控應(yīng)用需要一個人類操作員始終處于待命狀態(tài)。
BEGIN tx1; BEGIN tx2;SELECT COUNT(*)FROM operatorsWHERE oncall = true;0 SELECT COUNT(*)FROM operatorsWHERE oncall = TRUE;0UPDATE operators UPDATE operatorsSET oncall = TRUE SET oncall = TRUEWHERE userId = 4; WHERE userId = 2;COMMIT tx1; COMMIT tx2;
在上面的情況中,如果這些事務(wù)中有兩個成功提交,就會出現(xiàn)寫偏序。即使此時沒有出現(xiàn)臟讀或數(shù)據(jù)丟失,數(shù)據(jù)也失去了完整性,因?yàn)槠渲付藘蓚€待命的人。
可串行化隔離、模式設(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è)計(jì)順序。事務(wù)執(zhí)行的順序難以預(yù)測,尤其是在高容量的并發(fā)系統(tǒng)中。
在開發(fā)時,尤其是在使用非阻塞軟件庫進(jìn)行開發(fā)時,較差的樣式和可讀性可能會導(dǎo)致用戶認(rèn)為事務(wù)是按順序執(zhí)行的,即使它們可能以任何順序抵達(dá)數(shù)據(jù)庫。下面的程序看起來像是 T1 和 T2 將按順序調(diào)用,但如果這些函數(shù)是非阻塞的,則它們將立即帶著 promise 返回,調(diào)用的順序?qū)⑷Q于它們在數(shù)據(jù)庫中接收到的時間。
result1 = T1() // results are actually promisesresult2 = T2()
如果需要原子性(以便完全提交或放棄所有操作)且序列很重要,則 T1 和 T2 中的操作應(yīng)該運(yùn)行在單個數(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ù)的方式時,他們可能會在用戶區(qū)域創(chuàng)建水平分區(qū),而不是將這項(xiàng)工作委托給他們的數(shù)據(jù)庫。這種方式稱為應(yīng)用級分片(application-level sharding)。
應(yīng)用級分片這個名稱往往會給人帶來一種錯誤印象,讓人以為這種分片應(yīng)該存在于應(yīng)用服務(wù)之中。分片功能可以實(shí)現(xiàn)為數(shù)據(jù)庫的前面一層。取決于數(shù)據(jù)增長和架構(gòu)迭代情況,分片的要求可能會變得非常復(fù)雜。如果能在無需重新部署應(yīng)用服務(wù)器的前提下對某些策略進(jìn)行迭代,則會大有裨益。

如果將分片作為一個單獨(dú)的服務(wù),你就能更好地在不重新部署應(yīng)用服務(wù)器的前提下迭代分片策略。Vitess 就是應(yīng)用級分片系統(tǒng)的一個例子。Vitess 為 MySQL 提供了水平分片,并允許客戶端通過 MySQL 協(xié)議連接它;Vitess 會將數(shù)據(jù)分片到多個互相之間無聯(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ù)據(jù)可能有用而且是無鎖的
多版本并發(fā)控制(MVCC)能實(shí)現(xiàn)我們上面簡要討論過的很多一致性。Postgres 和 Spanner 等一些數(shù)據(jù)庫使用 MVCC 以讓每個事務(wù)都能看到一個快照,即該數(shù)據(jù)庫的一個更舊版本。參照快照的事務(wù)仍然可以串行化以實(shí)現(xiàn)一致性。當(dāng)讀取一個舊快照時,實(shí)際讀取的是過時的數(shù)據(jù)。
但即使讀取的是稍微過時的數(shù)據(jù),也會很有用處,比如當(dāng)在生成數(shù)據(jù)分析結(jié)果或計(jì)算近似聚合值時。
讀取過時數(shù)據(jù)的第一大優(yōu)勢是延遲(尤其是當(dāng)你的數(shù)據(jù)庫分布在不同的地區(qū)時)。MVCC 數(shù)據(jù)庫的第二大優(yōu)勢是其允許只讀事務(wù)是無鎖的。在需要大量讀取的應(yīng)用中,一個優(yōu)勢是用過時的數(shù)據(jù)也是可行的。

即便太平洋另一端有某個數(shù)據(jù)的最新版本,但也可以從本地讀取 5 秒前的過時副本。
數(shù)據(jù)庫會自動清除舊版本,而在某些情況下,數(shù)據(jù)庫也支持按需清理。舉個例子,Postgres 允許用戶按需執(zhí)行 VACUUM 操作或每隔一段時間自動執(zhí)行 VACUUM,而 Spanner 則是通過運(yùn)行一個垃圾收集器來丟棄時間超過 1 小時的版本。
任何時鐘源之間都會發(fā)生時鐘偏移
在計(jì)算領(lǐng)域,隱藏得最好的秘密是所有時間 API 都在說謊。我們的機(jī)器并不能準(zhǔn)確地知道當(dāng)前的時間是多少。我們的計(jì)算機(jī)全都包含一個用以產(chǎn)生計(jì)時信號的石英晶體。但石英晶體并不能準(zhǔn)確計(jì)時和計(jì)算時間偏移量,要么比實(shí)際時鐘快,要么就更慢。一天的偏移量甚至可達(dá) 20 秒。為了準(zhǔn)確,我們的計(jì)算機(jī)時間必須不時地與實(shí)際時間保持同步。
NTP 服務(wù)器可用于同步,但同步本身卻可能由于網(wǎng)絡(luò)的原因而出現(xiàn)延遲。與同一數(shù)據(jù)中心的 NTP 服務(wù)器同步?jīng)r且需要時間,與公共 NTP 服務(wù)器同步更是可能產(chǎn)生更大的偏移。
原子鐘和 GPS 時鐘是更好的確定當(dāng)前時間的信息源,但它們的部署成本更高,而且需要復(fù)雜的設(shè)置,不可能在每臺機(jī)器上都安裝。由于存在這些限制條件,數(shù)據(jù)中心通常使用的是多層方法。即在使用原子鐘和 / 或 GPS 時鐘提供準(zhǔn)確計(jì)時的同時,再通過輔助服務(wù)器將時間信息廣播給其它機(jī)器。這意味著所有機(jī)器都與實(shí)際的當(dāng)前時間存在一定程度的偏移。
不僅如此,應(yīng)用和數(shù)據(jù)庫往往搭建在不同的機(jī)器中,甚至還可能位于不同的數(shù)據(jù)中心。因此,不僅分散在不同機(jī)器上的不同數(shù)據(jù)庫節(jié)點(diǎn)之間無法統(tǒng)一時間,應(yīng)用服務(wù)器時鐘和數(shù)據(jù)庫節(jié)點(diǎn)時鐘也無法統(tǒng)一。
谷歌的 TrueTime 為此采用了一種不同的方法。大多數(shù)人認(rèn)為谷歌在時鐘上的成果可以歸功于他們使用了原子鐘和 GPS 時鐘,但那其實(shí)僅僅是部分原因。TrueTime 實(shí)際上是這樣工作的:
TrueTime 使用了兩個不同的時間信號源:GPS 時鐘和原子鐘。這些時鐘存在不同的故障模式,因此同時使用兩者可以提升可靠性。
TrueTime 的 API 并不是常規(guī)型的。它會以區(qū)間的形式返回時間。因此實(shí)際時間事實(shí)上處于這個時間區(qū)間的上界和下界之間。因此,谷歌的分布式數(shù)據(jù)庫 Spanner 就可以等到它確定了當(dāng)前時間超過了特定時間之后才執(zhí)行事務(wù)。這種方法會給系統(tǒng)帶來一些延遲,尤其是當(dāng)主機(jī)通告的不確定性很高時;但這種方法能保證正確性,即使數(shù)據(jù)庫分布在全球也是如此。

當(dāng)當(dāng)前時間的置信度下降時,Spanner 執(zhí)行操作可能會耗費(fèi)更多時間。因此,即使不可能獲得精準(zhǔn)的時鐘,保證時鐘的置信度對性能而言也是非常重要的。
延遲有很多含義
如果房間里有 10 個人,你問他們「延遲(latency)」是什么意思,你可能會得到 10 個不同的答案。在數(shù)據(jù)庫中,延遲通常是指數(shù)據(jù)庫延遲,而非客戶端所感知到的延遲??蛻舳烁兄降难舆t包含數(shù)據(jù)庫延遲和網(wǎng)絡(luò)延遲。在調(diào)試不斷惡化的問題時,分辨客戶端延遲和數(shù)據(jù)庫延遲是非常重要的。在收集和展示指標(biāo)時,往往需要同時包含這兩種延遲。
評估每個事務(wù)的性能需求
有時候,數(shù)據(jù)庫會將它們的讀寫吞吐量和延遲作為性能優(yōu)勢的賣點(diǎn)來進(jìn)行宣傳。盡管這能在評估數(shù)據(jù)庫的性能時從較高層面上展現(xiàn)主要的限制因素,但為了更全面地進(jìn)行評估,需要單獨(dú)分開評估各個關(guān)鍵操作的性能,比如每次查詢或每個事務(wù)的執(zhí)行性能。示例:
為具有給定約束條件的包含 5000 萬行的表格 X 插入新的一行并填充相關(guān)表格時的吞吐量和延遲。
當(dāng)平均好友數(shù)為 500 時,查詢一個用戶的好友的好友時的延遲。
當(dāng)用戶訂閱了 500 個賬號且每個小時有 X 項(xiàng)新輸入時,檢索用戶時間線前 100 條記錄時的延遲。
評估和實(shí)驗(yàn)可能包含這樣的關(guān)鍵性案例,直到你有信心你的數(shù)據(jù)庫能夠滿足你的性能需求。另一個類似的經(jīng)驗(yàn)法則是在收集延遲指標(biāo)和設(shè)置 SLO 時考慮這種故障情況。
在收集每個操作的指標(biāo)時要注意高基數(shù)。如果你需要高基數(shù)的調(diào)試數(shù)據(jù),請使用日志或分布式的跟蹤方法。如果你想了解延遲調(diào)試方法,請參閱《Want to Debug Latency?》(https://medium.com/observability/want-to-debug-latency-7aa48ecbe8f7)。
嵌套事務(wù)可能有害
并非每個數(shù)據(jù)庫都支持嵌套事務(wù)(nested transactions),但如果支持,那么嵌套事務(wù)可能導(dǎo)致出人意料的程序設(shè)計(jì)錯誤,而且這種錯誤往往不易識別,直到出現(xiàn)了明顯異常才能看清。
如果你想要避免嵌套事務(wù),則可以使用客戶端軟件庫來檢測和避免嵌套事務(wù)。如果你不能避免嵌套事務(wù),則必須注意不要出現(xiàn)意料之外的情況,即當(dāng)提交的事務(wù)因?yàn)樽邮聞?wù)而被意外拋棄時。
如果將事務(wù)封裝在不同的層中,可能會出現(xiàn)出人意料的嵌套事務(wù)案例,而從可讀性角度來看,其意圖可能將變得難以理解。看看下面的程序:
with newTransaction():Accounts.create("609-543-222") with newTransaction():Accounts.create("775-988-322")throw Rollback();
以上代碼的結(jié)果是什么?是兩個事務(wù)都會回滾還是僅回滾內(nèi)部那個事務(wù)?如果我們當(dāng)時依賴的多層軟件庫將該事務(wù)的創(chuàng)建過程封裝起來不為我們所見,我們還能識別和改進(jìn)這樣的案例嗎?
假設(shè)一個具有多項(xiàng)操作(比如 newAccount)的數(shù)據(jù)層已經(jīng)在它們自己的事務(wù)中實(shí)現(xiàn)了。當(dāng)你用更高層的業(yè)務(wù)邏輯(它們運(yùn)行在自己的事務(wù)中)運(yùn)行它們時,會發(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ù)。這時所要考慮的一個關(guān)鍵事項(xiàng)是選擇合適的范圍??蛻舳嗽谟龅骄W(wǎng)絡(luò)問題時往往會重試事務(wù)。如果一個事務(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)行時都會增加序列號。如果因?yàn)榫W(wǎng)絡(luò)問題而導(dǎo)致提交失敗,則在第二次重試時會使用不同的序列號進(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ù)據(jù)庫出現(xiàn)了性能問題,那么這個數(shù)據(jù)庫可以調(diào)節(jié)查詢計(jì)劃器并進(jìn)行自我診斷。慢查詢?nèi)罩?、延遲問題或關(guān)于執(zhí)行時間的統(tǒng)計(jì)信息等報告可用于確定需要優(yōu)化的查詢。
查詢計(jì)劃器提供的某些指標(biāo)可能具有較多噪聲,尤其是當(dāng)估計(jì)延遲或 CPU 時間時。作為對查詢計(jì)劃器的補(bǔ)充,跟蹤和執(zhí)行路徑工具對診斷這些問題而言可能會更加有用,不過并非每個數(shù)據(jù)庫都會提供這樣的工具。
在線遷移可能很復(fù)雜,但卻可以實(shí)現(xiàn)
在線或?qū)崟r遷移的意思是在不停機(jī)且不損害數(shù)據(jù)正確性的同時從一個數(shù)據(jù)庫遷移到另一個數(shù)據(jù)庫。如果是遷移到同樣的數(shù)據(jù)庫 / 引擎,在線遷移會更為簡單;但如果是遷移到性能特性和組織結(jié)構(gòu)要求不同的新數(shù)據(jù)庫,那情況會復(fù)雜得多。
在線遷移有多種模式,下面介紹其中一種:
開始向兩個數(shù)據(jù)庫執(zhí)行雙寫入(dual writes)。在這一階段,新數(shù)據(jù)庫還不包含所有數(shù)據(jù),但將開始看到新數(shù)據(jù)。一旦這一步得到了保證,你就可以進(jìn)入下一步了。
讓讀取路徑可同時使用這兩個數(shù)據(jù)庫。
主要使用新數(shù)據(jù)庫來進(jìn)行讀取和寫入。
停止向舊數(shù)據(jù)庫寫入,但繼續(xù)保持從舊數(shù)據(jù)庫讀取。此時,新數(shù)據(jù)庫仍未包含所有新數(shù)據(jù),而在獲取舊記錄時,可能還需要回退至舊數(shù)據(jù)庫。
這時候,舊數(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ù)庫顯著增長時會引入不可預(yù)測性
數(shù)據(jù)庫增長會讓你遭遇不可預(yù)測的擴(kuò)展問題。我們對自己數(shù)據(jù)庫的內(nèi)部情況越了解,可能就越難預(yù)測它們的擴(kuò)展情況,還有些事情是我們無法預(yù)測的。
在數(shù)據(jù)庫增大時,之前關(guān)于數(shù)據(jù)規(guī)模和網(wǎng)絡(luò)容量需求的假設(shè)和預(yù)期都將變得過時。這時候,為了避免中斷,需要大規(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ī)模。
原文鏈接:https://medium.com/@rakyll/things-i-wished-more-developers-knew-about-databases-2d0178464f78
end
本公眾號全部博文已整理成一個目錄,請?jiān)诠娞柡笈_回復(fù)「m」獲??!
2、還不會用 Git 的程序員,這個項(xiàng)目讓你邊玩邊學(xué)!
3、百度開源的 71 個項(xiàng)目,你知道幾個?
4、PyCharm 詳細(xì)使用指南!
5、2019 年 Python 開發(fā)者調(diào)查結(jié)果發(fā)布:Linux 最受歡迎
6、2019 最受歡迎的密碼排行榜
關(guān)注微信公眾號『杰哥的IT之旅』,后臺回復(fù)“1024”查看更多內(nèi)容,回復(fù)“微信”添加我微信。
好文和朋友一起看~
