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