作者 | Jaana Dogan,谷歌軟件工程師
譯者 | 彎月,責編 | 夕顏
出品 | CSDN(ID:CSDNnews)
大多數計算機系統都有某種需要保存到存儲系統的狀態。多年來我積累了許多數據庫知識,大多都是從導致數據丟失和網站下線的設計錯誤中吸取的教訓。在以數據為主的系統中,數據庫是整個系統設計的核心,也是需要權衡的重點。盡管數據庫的工作原理不可忽視,但許多應用程序開發者看到的、經歷過的問題依然僅僅是冰山一角。在這篇文章中,我想分享一些我認為特別有用的知識。
1 99.999%的情況下網絡耗時不是問題,只不過是運氣好
人們經常說如今的網絡很穩定,也有人爭論許多系統宕機都是因為網絡故障。現在這方面的研究很有限,而且多數研究都集中在擁有獨立網絡、專用硬件和配備了專人負責的大型組織中。
google的Spanner(Google的分布式數據庫)實現了99.999%的服務在線,他們的研究表明,僅有7.6%的問題是由網絡導致的,盡管他們將高可用性歸功于專用的網絡。2014年Bailis和Kingsbury的調查(https://cacm.acm.org/magazines/2014/9/177925-the-network-is-reliable/fulltext)挑戰了1994年Peter Deutsh發表的“分布式計算的陷阱”(https://en.wikipedia.org/wiki/Fallacies_of_distributed_computing)。網絡真的可靠嗎?
除了這些巨頭之外,互聯網上并沒有太完整的調查。大型供應商也沒有提供足夠的有關客戶網絡故障的數據。云提供商的網絡導致的故障同樣會導致部分互聯網宕機數個小時,但這也僅僅是一小部分許多人能注意到的故障而已。有時候,許多我們不得而知的網絡宕機也會造成巨大影響。云服務的客戶也不一定能夠注意到自己遇到的問題。當網絡宕機時,客戶并不能確定故障是由供應商的網絡故障導致的。對于他們而言,第三方服務就是黑盒子。只有那些供應商自己才能估計出影響的范圍。
與那些大企業的系統相比,如果你的系統中網絡問題導致的宕機只占很小比例,那么只能說你的運氣好。網絡依然會受到各種傳統問題的影響,如硬件故障、拓撲變化、管理配置改變,以及斷電等。但我最近聽說,鯊魚也會導致網絡故障(https://twitter.com/rakyll/status/1249891472993693696)。
ACID有很多含義
ACID指原子性、一致性、孤立性和持久性。這些是數據庫事務需要保證的幾項特性,只有保障這幾項特性才能在崩潰、錯誤、硬件故障等極端情況下保證數據的有效性。沒有ACID或類似的保障,應用程序開發者就不能判斷哪些應該由數據庫負責,哪些應該由開發者自己負責。絕大多數關系型事務數據庫都試圖遵循ACID,但新的標準(如NoSQL)催生了一大批不保證ACID的數據庫,因為ACID的實現代價相當大。
我剛剛入行時,曾經和技術領導爭論過ACID是否已經過時。可以說,ACID只是一個松散的描述,而不是嚴格的實現標準。如今,我認為ACID非常有用,因為它提供了一大類的問題(以及一大類可能的解決方案)。
并非每個數據庫都支持ACID,即使是實現了ACID的數據庫,對于ACID的解讀也不盡相同。其原因之一就是實現ACID需要做出的妥協非常多。數據庫通常聲稱自己符合ACID,但在某些極端情況下依然會有不同的解釋,或者它們對于那些“不太可能發生”的情況的處理方法也不一樣。開發者至少可以在較高層次上理解數據庫的實現方式,理解它們何時會出現問題,以及設計上的取舍。
最有爭議的莫過于MongoDB的ACID實現(即使在第四版發行后該爭論依然存在。)MongoDB很長時間都不支持日志,盡管它每隔60秒(甚至更長)才會向磁盤保存一次數據。考慮如下情況:應用程序發出了兩個寫請求(w1和w2)。Mongo能將第一個寫請求持久化,但w2的持久化會因為硬件故障而失敗。
該圖演示了MongoDB在寫入磁盤之前失敗時的數據情況。
將數據提交到磁盤是一項昂貴的操作,避免提交能夠以犧牲持久性的代價換來性能的提升。今天,MongoDB已經支持日志,但臟寫操作依然可能會影響數據的持久性,因為默認情況下MongoDB每隔100毫秒提交一次數據。因此,即使有日志支持,類似的情況依然可能發生,盡管發生故障時導致的變化丟失會少很多。
3每個數據庫的一致性和隔離性的能力都不同
在ACID屬性中,一致性和隔離性在不同實現之間最大的區別,因為實現上的妥協更多。一致性和隔離性都是非常昂貴的功能。兩者都要求協調,為了保證數據一致性,這兩者都會導致競爭的發生。當需要在多個數據中心水平擴展時(特別是跨越不同地理區域的多個數據中心時),該問題就愈發嚴重。關于這個現象更一般的解釋參見CAP理論。值得指出的是,應用程序能夠處理一部分不一致性的問題,有經驗的程序員也可以添加額外的邏輯,因此不必完全依賴數據庫。
數據庫通常提供不同的隔離層,這樣應用程序開發者可以選取性價比最佳的隔離層使用。較弱的隔離速度較快,但可能會引發數據競爭。強隔離會消除可能的數據競爭,但速度比較慢,而且可能會引入數據沖突,從而拖慢數據庫速度,甚至會導致宕機。
現有并發模型及其關系概覽
SQL標準只定義了四層隔離,盡管無論從理論還是從實踐來看,更多的隔離層也是可能的。jepson.io(https://jepsen.io/consistency)提供了對于以后并發模型的另一種觀點,你可以閱讀一下。例如,Google的Spanner能夠保證帶有時鐘同步的外部序列化,而且這是一個標準中沒有定義的、更強的隔離層。
SQL標準中提到的隔離級別如下:
-
可序列化(最嚴格,最昂貴):在同一個事務中,按順序執行與并行執行能夠產生相同的效果。串行執行是指每個事務在下一個事務開始之前執行完成。需要指出,序列化通常實現為“快照隔離”(如Oracle),而快照隔離并非SQL標準的內容。
-
可重復的讀操作:當前事務中未提交的讀操作僅對當前事務可見,而其他事務造成的讀操作是不可見的。
-
讀提交:未提交的讀操作對于其他事務不可見。只有提交的寫操作才可見,但可能會出現影子讀取的問題。如果另一個事務插入并提交了新行,那么當前事務可能會看到新的數據。
-
未提交的讀操作(不太嚴格,但比較廉價):允許臟讀取,事務可以看到其他事務尚未提交的改變。在實踐中,該級別可以用來返回大致的聚合結果,如在表上執行COUNT(*)操作。
可序列化的級別允許發生的數據競爭最少,但實現起來最昂貴,會給系統引入最多的數據競爭。其他隔離級別比較廉價,但會增加數據競爭的可能性。一些數據庫允許自己設置隔離級別,另一些數據庫則不支持某些隔離級別。
即使自稱支持某些隔離級別的數據庫,也需要仔細檢查其行為,理解其實際的操作:
不同數據庫的不同隔離級別的實現。
在無法保持鎖的情況下可以采用樂觀鎖
鎖的實現可能非常昂貴,不僅因為它會引入數據競爭,還要求應用程序與數據庫服務器之間存在穩定的連接。排他鎖可能會更嚴重地受到網絡分區的影響,并導致難以識別和解決的死鎖。如果無法持有排他鎖,則可以選擇樂觀鎖。
樂觀鎖指的是,在讀取行時,記錄版本號以及最近修改的時間戳或其校驗和。然后,您可以在更改記錄之前檢查版本是否沒有原子更改。
UPDATE products
SET name = 'Telegraph receiver', version = 2
WHERE id = 1 AND version = 1
如果另一個更新之前更改了該行,則對產品表的更新將影響0行。如果沒有更早的更新,它將影響1行,并且我們可以判斷我們的更新已成功。
5 除了臟讀取和數據丟失之外,還有其他的異常情況
在討論數據一致性時,通常我們會將注意力放在可能會導致臟讀取和數據丟失的數據競爭上。然而數據的異常情況并不止這兩種。
這類異常的一個例子是寫偏斜。寫偏斜很難識別,因為我們沒有特意尋找它們。寫偏斜的原因不是發生臟讀取或數據丟失,而是數據的邏輯約束被破壞。
例如,假設某個監控應用程序要求隨時必須有一個操作員在值守。
在上述情況中,如果兩個事務都成功提交,則會發生寫偏斜。盡管沒有發生任何臟讀取,也沒有發生數據丟失,數據的一致性也會被破壞,因為兩個操作員都被分配了值守任務。
可序列化的隔離、結構設計或數據庫約束可以幫助消滅寫偏斜問題。開發者應該能夠在開發過程中識別出這種異常,并在生產環境中避免這種數據異常。話雖如此,識別代碼中可能出現的寫偏斜非常困難。即使在大型系統中,如果不同的團隊負責同一個表上的不同功能,而缺乏互相交流,也會出現這種問題。
6 數據庫和我對于順序的理解不一致
數據庫的核心功能之一就是保證順序,但數據庫理解的順序可能與應用程序開發者看到的順序不一致。數據庫看到的事務順序是按照接收的時間排序的,而不是開發者認為的順序。在高并發系統中,事務的執行順序很難預測。
在開發期間,尤其是在使用非阻塞庫時,較差的代碼風格和可讀性可能會導致以下問題:用戶認為事務可以順序執行,即使它們可以以任何順序到達數據庫。下面的程序使T1和T2看起來將被順序調用,但是如果這些函數是非阻塞的并且立即以承諾返回,則調用的順序將取決于它們在數據庫中收到的時間。
如果要求原子性(完整提交或放棄所有操作),而且順序很重要,那么操作T1和T2應該在同一個數據庫事務中運行。
7應用程序層的分片可能需要在應用程序之外實現
分片是數據庫水平擴展的一種方式。盡管一些數據庫能夠自動實現水平擴展,但也有一些數據庫不能,或者不擅長該功能。如果數據架構師和開發者能夠預測數據點訪問方式,那么可以在用戶的層次實現水平分片,而不是將該任務交給數據庫。這稱為應用程序層的分片。
“應用程序層分片”這個名字通常會帶來錯誤的印象——分片存在于應用程序服務之間。然而,分片功能可以實現為數據庫之上的一層。根據數據增長和數據結構的迭代方式,分片需求可能會變得非常復雜。如果能迭代實現某些策略,而不需要重新部署應用程序服務器,是最理想的。
應用程序服務器與分片服務解耦合的例子
將分片作為單獨的服務可以提高迭代分片策略的能力,而不必重新部署應用程序。應用程序級分片系統的此類示例之一是Vitess。Vitess為MySQL提供了水平分片,并允許客戶端通過MySQL協議連接到它,并且它在彼此不認識的各個MySQL節點上分片了數據。
8 AUTOINCREAMENT可能有害
AUTOINCREMENT是生成主鍵的常用方式。利用它生成ID的情況數不勝數,但有些數據庫也會使用專門的ID生成方式。下面是使用自增和使用專用主鍵生成方式的一些優缺點:
-
在分布式數據庫系統中,自增可能會很困難。需要全局鎖才能生成ID。如果生成UUID,則數據庫的各個節點之間不需要任何合作。帶有鎖的自增可能會導致數據沖突,可能會導致分布式下的插入操作產生嚴重的性能降級。一些數據庫如MySQL可能會要求特殊的配置,才能正確地在雙主架構中實現自增。這種配置很容易搞錯,從而導致寫操作失敗。
-
一些數據庫具有基于主鍵的分區算法。順序ID可能會導致無法預測的熱點,并且可能會使某些分區不堪重負,而另一些分區則保持空閑狀態。
-
訪問數據庫中行的最快方法是通過其主鍵。如果有更好的方法來標識記錄,則順序ID可能會使表中最重要的列成為無意義的值。請盡可能選擇一個全局唯一的自然主鍵(例如用戶名)。
在采用自增ID或UUID作為索引、分區或分片時需要認真考慮哪種方式最合適。
過時的數據可能有用且無鎖
多版本并發控制(MVCC)引入了許多我們上面談過的一致性功能。一些數據庫(如Postgres,Spanner)使用了MVCC來保證每個事務都看到一個快照(數據庫過去的某個版本)。事務和快照依然可以序列化,以保證一致性。在從舊的快照中讀取時,你讀到的是過時的數據。
讀取稍稍過時的數據可能很有用,例如從數據中生成分析報告,或者計算大致的聚合值。
讀取過時數據的第一個好處就是延遲(特別是當數據庫分布在不同物理地區時)。MVCC數據庫的第二個好處就是,它允許只讀事務無鎖執行。如果能夠接受過時數據, 那么在存在大量讀取的應用程序中,無鎖是最大的優勢。
即使太平洋另一側的數據庫中存在最新版本,應用程序服務器依然從本地副本中讀取5秒之前的舊數據。
數據庫會自動清理舊版本,在某些情況下,你可以按需進行清理。例如,Postgres允許用戶按需使用VACUUM命令,同時也會每隔一段時間自動執行清理。Spanner會定時執行垃圾回收來清理超過一個小時的舊版本。
只要存在時鐘,就會發生時鐘偏斜
計算中最隱秘的秘密就是所有時間的API都是謊言。我們的機器無法準確知道當前時間。我們的計算機都包含一個石英晶體,該石英晶體會產生一個計時信號。但是石英晶體無法準確地計算時間,總會比實際的時鐘快或慢。每天的漂移可能長達20秒。為了準確起見,我們的計算機上的時間需要不時地與實際時間同步。
我們使用NTP服務器進行同步,但是同步本身可能出現網絡延遲。與同一數據中心中的NTP服務器同步可能需要一些時間,與公用NTP服務器同步可能會導致更多偏差。
原子鐘和GPS時鐘是確定當前時間的更好來源,但是它們太昂貴,并且需要復雜的設置,因此無法安裝在每臺機器上。考慮到這些限制,數據中心通常會使用多層方法。原子鐘或GPS時鐘能夠提供準確的計時,它們的時間會通過另一臺服務器廣播到其他機器。這意味著每臺機器都會在一定程度上偏離實際當前時間。
還有復雜的情況。應用程序和數據庫通常位于不同的計算機中(甚至可能位于不同的數據中心)。不僅分布在幾臺計算機上的數據庫節點無法在時間上達成共識,應用服務器時鐘和數據庫節點時鐘也無法達成共識。
Google的TrueTime在這里采用了不同的方法。大多數人認為Google在時鐘方面的進步可以歸因于對原子鐘和GPS時鐘的使用,但這只是一部分原因。TrueTime的實際工作原理如下:
-
TrueTime使用兩個不同的源:GPS和原子鐘。這些鐘表的失敗模式不同,因此同時使用兩種可以提高可靠性。
-
TrueTime使用了非傳統的API。它返回一個時間范圍,實際的時間位于下界和上界之間的任意地方。Google的分布式數據庫Spanner會一直等待,直到它能確信當前的時間超過了某個特定時間。該方法給系統引入了一些延遲,特別是當主服務器廣播的不確定較高時,但即使在全球分布式的情況下依然能夠提供正確性。
Spanner的組件使用TrueTime,其中TT.now返回一個時間范圍,這樣Spanner可以插入sleep來確保當前時間超過某個特定時間。
對當前時間的信心下降,意味著Spanner操作可能需要更多時間。因此,盡管不可能擁有精確的時鐘,但高可信度對于性能依然很重要。
延遲有很多含義
對于“延遲”一詞每個人的理解都不同。在數據庫中,延遲通常稱為“數據庫延遲”,而不是客戶端感知的延遲。客戶端看到的延遲是數據庫延遲和網絡延遲之和。在調試不斷升級的問題時,能夠識別客戶端和數據庫延遲至關重要。收集和顯示指標時,請始終考慮同時使用兩者。
評測性能需要針對每個事務進行
有時,數據庫在宣傳其性能和限制時會使用讀寫吞吐量和延遲作為指標。而實際上,在評估新數據庫的性能時,更全面的方法是分別評估關鍵操作(每個查詢和/或每個事務)。例如:
-
在表X(包含五千萬行數據,并帶有約束)中插入一行并填充關聯數據的寫吞吐量和延遲。
-
當每個用戶的平均朋友數為500時,查詢給定用戶的朋友的朋友的延遲。
-
當每個用戶訂閱了500個賬號,每個賬號每小時有X條消息時,獲取時間線的最新100條記錄的延遲。
評測和實驗應當包含這種極端用例,才能確信數據庫是否能夠滿足你的性能要求。類似的經驗法則是,在收集延遲數據和設定服務水平目標時也要用這種細致的用例。
嵌套事務的弊大于利
并非每個數據庫都支持嵌套事務,但是當嵌套數據庫支持嵌套事務時,嵌套事務可能會導致出人意料的編程錯誤,這些錯誤通常很難被發現,直到你發現數據異常。
如果想避免嵌套事務,客戶端庫通常可以幫你檢測并避免嵌套事務。如果無法避免,則必須注意避免出現子事務會導致已提交的事務意外中止的意外情況。
將事務封裝在不同的層中可能會導致令人驚訝的嵌套事務案例,并且從可讀性的角度來看,其意圖會很難理解。看一看以下程序:
上述代碼的結果是什么?它會回滾兩個事務,還是只會回滾內層事務?如果這段代碼依賴于庫中的多個層,每個層都封裝了事務處理?我們怎樣才能識別并改善這種情況?
想像一個包含多個操作(如newAccount)的數據層,該數據層已經實現了自己的事務處理。數據層無需自己創建事務就能實現高層操作。于是,業務邏輯可以啟動事務、在事務中執行操作,然后提交或放棄。
14 事務不應該維持應用程序狀態
應用程序開發者也許想在事務中使用應用程序狀態,以更新特定的值,或修改查詢參數。需要考慮的一個關鍵因素就是要使用正確的作用域。客戶端通常會在發生網絡問題時重試整個事務。如果事務依賴于某個狀態,而該狀態可能在其他地方被修改,那么事務就可能會在數據競爭發生時讀取錯誤的值。事務應當注意應用程序內的數據競爭。
上述事務每次運行時會增加序列數字,而不管實際執行結果如何。如果網絡故障導致提交失敗,那么第二次嘗試時就會使用不同的序列數字進行查詢。
15 查詢計劃器可以報告數據庫的信息
查詢計劃器可以給出查詢在數據庫中的執行方式。它還會分析查詢并在執行之前進行優化。計劃期只能提供一些大致的估計。怎樣才能得到下述查詢的結果呢?
SELECT * FROM articles where author = "rakyll" order by title;
有兩種方式可以獲取結果:
-
全表掃描:掃描表中的每一條記錄,返回作者名字匹配的所有文章,然后排序
-
索引掃描:使用索引找到匹配的ID,獲取這些行,然后排序
查詢計劃器的職責就是判斷哪種方式最合適。查詢計劃器得到的信號很有限,因此可能會導致不理想的決定。DBA或開發者應該使用這些信息來診斷并對效率不高的查詢進行調優。新版數據庫可以調整查詢計劃器并進行自我診斷。慢查詢報告、延遲問題報告、執行時間報告等也有助于優化查詢。
查詢計劃器提供的某些指標可能很混亂,特別是有關延遲或CPU時間的指標。作為查詢計劃器的輔助,跟蹤和執行路徑工具也有助于診斷這些問題。不過并不是每個數據庫都提供這些工具。
在線遷移非常復雜,但不是不可能
實時在線遷移意味著從一個數據庫遷移到另一個數據庫,中途不停止服務,還要保證數據的正確性。如果在同種數據庫之間遷移,那么在線遷移就能容易些,但如果使用新的數據庫,再加上不同的性能要求和表結構,遷移就會變得更加復雜。
在線遷移有不同的模型,下面是其中一種:
-
開始對兩個數據庫進行雙重寫入。在此階段,新數據庫不會擁有所有數據,但新數據會出現在新數據庫中。一旦對這一步充滿信心,就可以繼續進行第二步。
-
在兩個數據庫上同時啟用讀取路徑。
-
主要使用新的數據庫進行讀寫操作。
-
停止在舊的數據庫上進行寫入操作,但讀取操作依然在舊的數據庫上進行。此時,新的數據庫并不包含所有新數據,在讀取舊數據時,可能依然需要使用舊的數據庫。
-
此時,舊的數據庫是只讀的。將新數據庫中缺少的數據填入。遷移完成后,所有的讀寫路徑都可以使用新的數據庫,舊的數據庫就可以移除了。
如果需要更多案例研究,可以閱讀Stripe的這篇詳盡的文章(https://stripe.com/blog/online-migrations)。
數據庫的顯著增長表明不確定性
數據庫增長會帶來不可預料的規模問題。對數據庫內部原理的理解越多,我們對于規模的預見就越少,但有些東西是無法預料的。
數據庫增長會導致以前對于數據規模和網絡容量需求的假設變得無效。這時就需要進行大規模結構修改、大規模操作改善、容量問題改善、重新考慮部署、遷移到其他數據庫等方式來避免服務中斷。
永遠不要假設你只需要理解當前數據庫的內部原理,因為規模會引發新的問題。無法預料的熱點、不均衡的數據分布、無法預料的容量和硬件問題,日益增長的流量和新的網絡分區,都會讓你不得不重新考慮數據庫、數據模型、部署模型和部署規模等問題。
原文鏈接:
https://medium.com/@rakyll/things-i-wished-more-developers-knew-about-databases-2d0178464f78
本文為CSDN翻譯文章,轉載請注明出處