資料密集型系統設計
資料系統的基石
本文將會介紹資料系統底層的基礎概念,⽆論是在單臺機器上運⾏的單點資料系統,還是分佈在多臺機器上的分散式資料系統都適⽤。
- 第⼀部分將介紹本書使⽤的術語和⽅法。可靠性,可擴充套件性和可維護性 ,這些詞彙到底意味著什麼?如何實現這些⽬標?
- 第⼆部分將對⼏種不同的資料模型和查詢語⾔進⾏⽐較。從程式設計師的⻆度看,這是資料庫之間最明顯的區別。不同的資料模型適⽤於不同的應⽤場景。
- 第三部分將深⼊儲存引擎內部,研究資料庫如何在磁碟上擺放資料。不同的儲存引擎針對不同的負載進⾏優化,選擇合適的儲存引擎對系統效能有巨⼤影響。
- 第四部分將對⼏種不同的 資料編碼進⾏⽐較。特別研究了這些格式在應⽤需求經常變化、模式需要隨時間演變的環境中表現如何。
一、可靠性、可擴充套件性、可維護性
-
目標與意義
現今很多應⽤程式都是 資料密集型(data-intensive) 的,⽽⾮ 計算密集型(compute-intensive)
的。因此CPU很少成為這類應⽤的瓶頸,更⼤的問題通常來⾃資料量、資料複雜性、以及資料的變更速
度。
資料密集型應⽤通常由標準元件構建⽽成,標準元件提供了很多通⽤的功能;例如,許多應⽤程式都需
要:- 儲存資料,以便⾃⼰或其他應⽤程式之後能再次找到 ((資料庫(database))) ;
- 記住開銷昂貴操作的結果,加快讀取速度(快取(cache)) ;
- 允許⽤戶按關鍵字搜尋資料,或以各種⽅式對資料進⾏過濾(搜尋索引(search indexes)) ;
- 向其他程式傳送訊息,進⾏非同步處理(流處理(stream processing));
- 定期處理累積的⼤批量資料(批處理(batch processing));
如果這些功能聽上去平淡⽆奇,那是因為這些 資料系統(data system) 是⾮常成功的抽象:我們⼀直不假思索地使⽤它們並習以為常。絕⼤多數⼯程師不會幻想從零開始編寫儲存引擎,因為在開發應⽤時,資料庫已經是⾜夠完美的⼯具了。
但現實沒有這麼簡單。不同的應⽤有著不同的需求,因⽽資料庫系統也是百花⻬放,有著各式各樣的特性。實現快取有很多種⼿段,建立搜尋索引也有好⼏種⽅法,諸如此類。因此在開發應⽤前,我們依然有必要先弄清楚最適合⼿頭⼯作的⼯具和⽅法。⽽且當單個⼯具解決不了你的問題時,組合使⽤這些⼯具可能還是有些難度的。
本部分將從我們所要實現的基礎⽬標開始:可靠、可擴充套件、可維護的資料系統,以及探討考量這些⽬標的⽅法。 -
可靠性(Reliability)
-
可靠性意味著即使發⽣故障,系統也能正常⼯作。故障可能發⽣在硬體(通常是隨機的
和不相關的),軟體(通常是系統性的Bug,很難處理),和⼈類(不可避免地時不時出錯)。容錯技術可以對終端⽤戶隱藏某些型別的故障。 -
容錯
- 造成錯誤的原因叫做故障(fault),能預料並應對故障的系統特性可稱為容錯(fault tolerant)或韌性(resilient)。
“容錯”⼀詞可能會產⽣誤導,因為它暗示著系統可以容忍所有可能的錯誤,但在實際中這是不可能的時,只有談論特定型別的錯誤才有意義。 - 注意,故障(fault)不同於失效(failure)。故障通常定義為系統的⼀部分狀態偏離其標準,⽽失 效則是系統作為⼀個整體停⽌向⽤戶提供服務。故障的概率不可能降到零,因此最好設計容錯機制以防因故障⽽導致失效。而我們的目的就是要利⽤不可靠的部件構建可靠系統的技術。
- 造成錯誤的原因叫做故障(fault),能預料並應對故障的系統特性可稱為容錯(fault tolerant)或韌性(resilient)。
-
-
可擴充套件性(Scalability)
-
可擴充套件性意味著即使在負載增加(資料量、流量、複雜性)的情況下也有保持效能的策略。為了討論可擴充套件性,我們⾸先需要定量描述負載和效能的⽅法。
-
描述負載
- 負載可以⽤⼀些稱為負載引數(load parameters)的數字來描述。引數的最佳選擇取決於系統架構,它可能是每秒向Web伺服器發出的請求、資料庫中的讀寫⽐率、聊天室中同時活躍的⽤戶數量、快取命中率或其他東⻄。
除此之外,也許平均情況對你很重要,也許你的瓶頸是少數極端場景。
- 負載可以⽤⼀些稱為負載引數(load parameters)的數字來描述。引數的最佳選擇取決於系統架構,它可能是每秒向Web伺服器發出的請求、資料庫中的讀寫⽐率、聊天室中同時活躍的⽤戶數量、快取命中率或其他東⻄。
-
描述效能
-
⼀旦系統的負載被描述好,就可以研究當負載增加會發⽣什麼。我們可以從兩種⻆度來看:
a。增加負載引數並保持系統資源(CPU、記憶體、⽹絡頻寬等)不變時,系統效能將受到什麼影響?
b。增加負載引數並希望保持效能不變時,需要增加多少系統資源?
這兩個問題都需要效能資料,所以讓我們簡單地看⼀下如何描述系統效能。 -
吞吐量(throughput):即每秒可以處理的記錄數量,或者在特定規模資料集上運⾏作業的總時間。
-
響應時間(response time):即客戶端傳送請求到接收響應之間的時間。
-
測量的數值分佈(distribution)指標
-
算術平均值(arithmetic mean):平均值並不是⼀個⾮常好的指標,
-
百分位點(percentiles)法:如果你想知道“典
型(typical)”響應時間,通常使⽤百分位點會更好。如果將響應時間列表按最快到最慢排序,那麼中位數(median)就在正中間。中位數也被稱為第50百分位點,有時縮寫為p50。- 百分位點通常⽤於服務級別⽬標(SLO, service level objectives)和服務級別協議(SLA, service level agreements),即定義服務預期效能和可⽤性的合同。 SLA可能會宣告,如果服務響應時間的中位數⼩於200毫秒,且99.9百分位點低於1秒,則認為服務⼯作正常;如果響應時間更⻓,就認為服務不達標。
-
響應時間的⾼百分位點(也稱為尾部延遲(tail latencies))⾮常重要,因為它們直接影響⽤戶的服務體驗。
-
-
-
應對負載的⽅法
- ⼈們經常討論縱向擴充套件(scaling up)(垂直擴充套件(vertical scaling),轉向更強⼤的機器)和橫向擴充套件(scaling out)(⽔平擴充套件(horizontal scaling),將負載分佈到多臺⼩機器上)之間的對⽴。
跨多臺機器分配負載也稱為“⽆共享(shared-nothing)”架構。可以在單臺機器上運⾏的系統通常更簡單,但⾼端機器可能⾮常貴,所以⾮常密集的負載通常⽆法避免地需要橫向擴充套件。現實世界中的優秀架構需要將這兩種⽅法務實地結合,因為使⽤⼏臺⾜夠強⼤的機器可能⽐使⽤⼤量的⼩型虛擬機器更簡單也更便宜。 - 有些系統是彈性(elastic)的,這意味著可以在檢測到負載增加時⾃動增加計算資源,⽽其他系統則是⼿動擴充套件(⼈⼯分析容量並決定向系統新增更多的機器)。如果負載極難預測(highly
unpredictable),則彈性系統可能很有⽤,但⼿動擴充套件系統更簡單,並且意外操作可能會更少(參閱“重新平衡分割槽”)。
- ⼈們經常討論縱向擴充套件(scaling up)(垂直擴充套件(vertical scaling),轉向更強⼤的機器)和橫向擴充套件(scaling out)(⽔平擴充套件(horizontal scaling),將負載分佈到多臺⼩機器上)之間的對⽴。
-
-
可維護性(Maintainability)
-
可維護性有許多⽅⾯,但實質上是關於⼯程師和運維團隊的⽣活質量的。良好的抽象可以幫助降低複雜度,並使系統易於修改和適應新的應⽤場景。良好的可操作性意味著對系統的健康狀態具有良好的可⻅性,並擁有有效的管理⼿段。
-
我們應該以這樣⼀種⽅式來設計軟體:在設計之初就儘量考慮儘可能減少維護期間的痛苦,從⽽避免⾃⼰的軟體系統變成遺留系統。為此,我們將特別關注軟體系統的三個設計原則:
- 可操作性(Operability):便於運維團隊保持系統平穩運⾏。
- 簡單性(Simplicity):從系統中消除儘可能多的複雜度(complexity),使新⼯程師也能輕鬆理解系統。
- 可演化性(evolability):使⼯程師在未來能輕鬆地對系統進⾏更改,當需求變化時為新應⽤場景做適配。也稱為可擴充套件性(extensibility),可修改性(modifiability)或可塑性(plasticity)。
-
二、資料模型&查詢語言
-
目標與意義:資料模型可能是軟體開發中最重要的部分了,因為它們的影響如此深遠:不僅僅影響著軟體的編寫⽅式,⽽且影響著我們的解題思路。
多數應⽤使⽤層層疊加的資料模型構建。每個層都通過提供⼀個明確的資料模型來隱藏更低層次中的複雜性。這些抽象允許不同的⼈群有效地協作,例如資料庫⼚商的⼯程師和使⽤資料庫的應⽤程式開發⼈員。
因為資料模型對上層軟體的功能(能做什
麼,不能做什麼)有著⾄深的影響,所以選擇⼀個適合的資料模型是⾮常重要的。 -
常見資料模型
-
關係模型
-
現在最著名的資料模型可能是SQL。它基於Edgar Codd在1970年提出的關係模型【1】:資料被組織成關係(SQL中稱作表),其中每個關係是元組(SQL中稱作⾏)的⽆序集合。
-
特點
- 事務處理
- 批處理
- 阻抗不匹配:資料儲存在關係表中,那麼需要⼀個笨拙的轉換層,處於應⽤程式程式碼中的物件和表,⾏,列的資料庫模型之間。模型之間的不連貫有時被稱為阻抗不匹配(impedance mismatch)。
- 多對⼀和多對多的關係
- 查詢資料簡單:在關聯式資料庫中,“訪問路徑”是由查詢優化器⾃動⽣成的,⽽不是由程式設計師⽣成。
-
-
文件模型
-
Nosql
-
“NoSQL”這個名字讓⼈遺憾,因為實際上它並沒有涉及到任何特定的技術。最初它只是作為⼀個醒⽬的Twitter標籤,⽤在2009年⼀個關於分散式,⾮關聯式資料庫上的開源聚會上。後被追溯性地重新解釋為不僅是SQL(Not Only SQL)。
-
驅動Nosql資料庫的幾個因素:
- i. 需要⽐關聯式資料庫更好的可擴充套件性,包括⾮常⼤的資料集或⾮常⾼的寫⼊吞吐量。
ii. 相⽐商業資料庫產品,免費和開源軟體更受偏愛。
iii. 關係模型不能很好地⽀持⼀些特殊的查詢操作。
iv. 受挫於關係模型的限制性,渴望⼀種更具多動態性與表現⼒的資料模型。
- i. 需要⽐關聯式資料庫更好的可擴充套件性,包括⾮常⼤的資料集或⾮常⾼的寫⼊吞吐量。
-
-
資料通常是⾃我包含的,⽽且⽂檔之間的關係⾮常稀少。
-
在表示多對⼀和多對多的關係時,關聯式資料庫和⽂檔資料庫並沒有根本的不同:在這兩種情況
下,相關項⽬都被⼀個唯⼀的識別符號引⽤,這個識別符號在關係模型中被稱為外來鍵,在⽂檔模型中稱為⽂檔引⽤【9】。該識別符號在讀取時通過連線或後續查詢來解析。 -
訪問記錄的唯⼀⽅法是跟隨從根記錄起沿這些鏈路所形成的路徑。這被稱為訪問路徑(access path)。
-
-
對比關係模型和文件模型
-
使應用程式程式碼更簡單方面
- 如果應⽤程式中的資料具有類似⽂檔的結構(即,⼀對多關係樹,通常⼀次性載入整個樹),那麼使⽤⽂檔模型可能是⼀個好主意。
關係模型可能導致繁瑣的模式和不必要的複雜的應⽤程式程式碼。 - ⽂檔資料庫對連線的糟糕⽀持有可能會是⼀個問題,這取決於應⽤程式。如果你的應⽤程式確實使⽤多對多關係,文件模型通過反規範化可以減少對連線的需求,但是應⽤程式程式碼需要做額外的⼯作來保持資料的⼀致性。這也將複雜性轉移到應⽤程式中,並且通常⽐由資料庫內的專⽤程式碼執⾏的連線慢。在這種情況下,使⽤⽂檔模型會導致更復雜的應⽤程式程式碼和更差的效能。
- 如果應⽤程式中的資料具有類似⽂檔的結構(即,⼀對多關係樹,通常⼀次性載入整個樹),那麼使⽤⽂檔模型可能是⼀個好主意。
-
靈活性方面
- ⽂檔資料庫有時稱為⽆模式(schemaless),但這具有誤導性,因為讀取資料的程式碼通常假定某種結構,即存在隱式模式,但不由資料庫強制執⾏。⼀個更精確的術語是讀時模式schema-onread:資料的結構是隱含的,只有在資料被讀取時才被解釋,相應的是寫時模式schema-onwrite:傳統的關聯式資料庫⽅法中,模式明確,且資料庫確保所有的資料都符合其模式。
- 當由於某種原因(例如,資料是異構的)集合中的項⽬並不都具有相同的結構時,讀時模式更具優勢。但是,當所有記錄都具有相同的結構,那麼寫時模式是記錄並強制這種結構的有效機制。
-
查詢資料的區域性性方面
- ⽂檔通常以單個連續字串形式進⾏儲存,編碼為JSON,XML或其⼆進位制變體(如MongoDB的BSON)。如果應⽤程式經常需要訪問整個⽂檔(例如,將其渲染⾄⽹⻚),那麼儲存區域性性會帶來效能優勢。如果將資料分割到多個表中,則需要進⾏多次索引查詢才能將其全部檢索出
來,這可能需要更多的磁碟查詢並花費更多的時間。 - 區域性性僅僅適⽤於同時需要⽂檔絕⼤部分內容的情況。資料庫通常需要載入整個⽂檔,即使只訪問其中的⼀⼩部分,這對於⼤型⽂檔來說是很浪費的。更新⽂檔時,通常需要整個重寫。且只有不改變⽂檔⼤⼩的修改才可以容易地原地執⾏。這些效能限制⼤⼤減少了⽂檔資料庫的實⽤場景。
- ⽂檔通常以單個連續字串形式進⾏儲存,編碼為JSON,XML或其⼆進位制變體(如MongoDB的BSON)。如果應⽤程式經常需要訪問整個⽂檔(例如,將其渲染⾄⽹⻚),那麼儲存區域性性會帶來效能優勢。如果將資料分割到多個表中,則需要進⾏多次索引查詢才能將其全部檢索出
-
-
圖資料模型
-
如果你的應⽤程式⼤多數的關係是⼀對多關係(樹狀結構化資料),或者⼤多數記錄之間不存在關係,那麼使⽤⽂檔模型是合適的。
但是,要是多對多關係在你的資料中很常⻅,隨著資料之間的連線變得更加複雜,使用圖資料模型更加⾃然。 -
⼀個圖由兩種物件組成:頂點(vertices)(也稱為節點(nodes) 或實體(entities)),和邊 (edges)( 也稱為關係(relationships)或弧 (arcs) )。
-
有⼏種不同但相關的⽅法⽤來構建和查詢圖中的資料。如屬性圖模型和三元組儲存模型(triple-store)。
-
屬性圖模型
- 在屬性圖模型中,每個頂點(vertex)包括:
唯⼀的識別符號
- 在屬性圖模型中,每個頂點(vertex)包括:
-
-
-
-
⼀組出邊(outgoing edges)
-
⼀組⼊邊(ingoing edges)
-
⼀組屬性(鍵值對)
每條邊(edge)包括: -
唯⼀識別符號
-
邊的起點/尾部頂點(tail vertex)
-
邊的終點/頭部頂點(head vertex)
-
描述兩個頂點之間關係型別的標籤
-
⼀組屬性(鍵值對)
可以將圖儲存看作由兩個關係表組成:⼀個儲存頂點,另⼀個儲存邊。可以用頭部和尾部頂點⽤來儲存每條邊;頂點的輸⼊或輸出邊也同理。
- 關於這個模型的⼀些重要特性,這些特性為資料建模提供了很⼤的靈活性:
-
任何頂點都可以有⼀條邊連線到任何其他頂點。沒有模式限制哪種事物可不可以關聯。
-
給定任何頂點,可以⾼效地找到它的⼊邊和出邊,從⽽遍歷圖,即沿著⼀系列頂點的路徑前後移動。
-
通過對不同型別的關係使⽤不同的標籤,可以在⼀個圖中儲存⼏種不同的資訊,同時仍然保持⼀個清晰的資料模型。
4.在可演化性方面富有優勢:當嚮應⽤程式新增功能時,可以輕鬆擴充套件圖以適應應⽤程式資料結構的變化。
- Cypher查詢語⾔- Cypher是屬性圖的宣告式查詢語⾔,為Neo4j圖形資料庫⽽發明。(它是以電影“⿊客帝國”中的⼀個⻆⾊來命名的,⽽與密碼術中的密碼⽆關。)
通常對於宣告式查詢語⾔來說,在編寫查詢語句時,不需要指定執⾏細節:查詢優化程式會⾃動選擇預測效率最⾼的策略,因此你可以繼續編寫應⽤程式的其他部分。
- 三元組儲存模型
- 三元組儲存模式⼤體上與屬性圖模型相同,⽤不同的詞來描述相同的想法。在三元組儲存中,所有資訊都以⾮常簡單的三部分表示形式儲存(主語,謂語,賓語)。例如,三元組(吉姆, 喜歡 ,⾹蕉)中,吉姆是主語,喜歡是謂語(動詞),⾹蕉是賓語。
- 三元組的主語相當於圖中的⼀個頂點。⽽賓語是下⾯兩者之⼀:
-
原始資料型別中的值,例如字串或數字。在這種情況下,三元組的謂語和賓語相當於主語頂點上的屬性的鍵和值。例如, (lucy, age, 33) 就像屬性 {“age”:33} 的頂點lucy。
-
圖中的另⼀個頂點。在這種情況下,謂語是圖中的⼀條邊,主語是其尾部頂點,⽽賓語是其頭部頂點。例如,在 (lucy, marriedTo, alain) 中主語和賓語 lucy 和 alain 都是頂點,並且謂語
marriedTo 是連線他們的邊的標籤。
- SPARQL查詢語⾔- SPARQL是⼀種⽤於三元組儲存的⾯向RDF資料模型的查詢語⾔。(它是SPARQL協議和RDF查詢語⾔的縮寫,發⾳為“sparkle”。)SPARQL早於Cypher,並且由於Cypher的模式匹配借鑑於
SPARQL,這使得它們看起來⾮常相似【37】。
- 特點
- 多對多的關係:任意事物都可能與任何事物相關聯。
- 查詢和更新資料庫的程式碼變得複雜不靈活。
- 更改應⽤程式的資料模型很難。
-
查詢語言
-
當引⼊關係模型時,關係模型包含了⼀種查詢資料的新⽅法:SQL是⼀種【宣告式】查詢語⾔,⽽IMS和CODASYL使⽤【命令式】程式碼來查詢資料庫。
-
宣告式查詢語言
-
宣告式查詢語言緊密地遵循關係代數的結構。
-
關注結果不關注過程:在宣告式查詢語⾔(如SQL或關係代數)中,你只需指定所需資料的模式 - 結果必須符合哪些條件,以及如何將資料轉換(例如,排序,分組和集合) - 但不是如何實現這⼀⽬標。資料庫系統的查詢優化器決定使⽤哪些索引和哪些連線⽅法,以及以何種順序執⾏查詢的各個部分。
-
簡潔易懂:宣告式查詢語⾔是迷⼈的,因為它通常⽐命令式API更加簡潔和容易。但更重要的是,它還隱藏了資料庫引擎的實現細節,這使得資料庫系統可以在⽆需對查詢做任何更改的情況下進⾏效能提升。
-
適合並⾏執⾏:宣告式語⾔往往適合並⾏執⾏。現在,CPU的速度通過核心的增加變得更快,⽽不是以⽐以前更⾼的時鐘速度運⾏。命令程式碼很難在多個核心和多個機器之間並⾏化,因為它指定了指令必須以特定順序執⾏。
-
圖的宣告式查詢語⾔
- Cypher,SPARQL和Datalog。
-
-
命令式查詢語言
- 命令式語⾔告訴計算機以特定順序執⾏某些操作。
- 在資料庫中,使⽤像SQL這樣的宣告式查詢語⾔⽐使⽤命令式查詢API要好得多 6 。
- 宣告式查詢語⾔的優勢不僅限於資料庫。
- MapReduce既不是⼀個宣告式的查詢語⾔,也不是⼀個完全命令式的查詢API,⽽是處於兩者之間:查詢的邏輯⽤程式碼⽚斷來表示,這些程式碼⽚段會被處理框架重複性調⽤。
-
-
其他(專業)資料模型
- 使⽤基因組資料的研究⼈員通常需要執⾏序列相似性搜尋,這意味著需要⼀個很⻓的字串(代表⼀個DNA分⼦),並在⼀個擁有類似但不完全相同的字串的⼤型資料庫中尋找匹配。這⾥所描述的資料庫都不能處理這種⽤法,這就是為什麼研究⼈員編寫了像GenBank這樣的專⻔的基因組資料庫軟體的原因【48】。
- 粒⼦物理學家數⼗年來⼀直在進⾏⼤資料型別的⼤規模資料分析,像⼤型強⼦對撞機(LHC)這樣的項⽬現在可以⼯作在數百億兆位元組的範圍內!在這樣的規模下,需要定製解決⽅案來阻住硬體成本的失控【49】。
- 全⽂搜尋可以說是⼀種經常與資料庫⼀起使⽤的資料模型。資訊檢索是⼀個很⼤的專業課題,我們不會在本書中詳細介紹,但是我們將在第三章和第三章中介紹搜尋索引。
三、儲存與檢索
-
目標&意義
- 本章主題:在第2章中,我們討論了資料模型和查詢語⾔,即程式設計師將資料錄⼊資料庫的格式,以及再次找回需要的資料的機制。在本章中我們會從資料庫的視⻆來討論同樣的問題:資料庫如何儲存我們提供的資料,以及如何在我們需要時重新找到資料。
- 資料庫的根本功能:⼀個資料庫在最基礎的層次上需要完成兩件事情:當你把資料交給資料庫時,它應當把資料儲存起來;⽽後當你向資料庫要資料時,它應當把資料返回給你。
- 目標:作為⼀名應⽤程式開發⼈員,如果您掌握了有關儲存引擎內部的知識,那麼您就能更好地瞭解哪種⼯具最適合您的特定應⽤程式。以及需要調整資料庫的什麼引數,以達到期望的效果。
-
驅動資料庫的資料結構
-
雜湊索引
-
索引(index)為了⾼效查詢資料庫中特定鍵的值的一種資料結構。新增與刪除索引,不會影響資料的內容,隻影響查詢的效能。維護額外的結構會產⽣開銷,特別是在寫⼊時。
儲存系統中⼀個重要的權衡:精⼼選擇的索引加快了讀查詢的速度,但是每個索引都會拖慢寫⼊速度。 -
記憶體中的雜湊對映,其中每個鍵都對映到⼀個資料⽂件中的位元組偏移量,指明可以找到對應值的位置。當你想查詢⼀個值時,使⽤雜湊對映來查詢資料⽂件中的偏移量,尋找(seek)該位置並讀取該值。
-
如果只是追加寫⼊⼀個⽂件,如何避免最終⽤完磁碟空間?⼀種好的解決⽅案是:分段壓縮和合並演算法
- i。將⽇志分為特定⼤⼩的段,當⽇志增⻓到特定尺⼨時關閉當前段⽂件,並開始寫⼊⼀個新的段⽂件。然後,我們就可以對這些段進⾏壓縮(compaction)。壓縮意味著在⽇志中丟棄重複的鍵,只保留每個鍵的最近更新。
ii。由於壓縮經常會使得段變得很⼩(假設在⼀個段內鍵被平均重寫了好⼏次),我們也可以在執⾏壓縮的同時將多個段合併在⼀起。
iii。段被寫⼊後永遠不會被修改,所以合併的段被寫⼊⼀個新的⽂件。凍結段的合併和壓縮可以在後臺執行緒中完成,在進⾏時,我們仍然可以繼續使⽤舊的段⽂件來正常提供讀寫請求。合併過程完成後,我們將讀取請求轉換為使⽤新的合併段⽽不是舊段 —— 然後可以簡單地刪除舊的段⽂件。
- i。將⽇志分為特定⼤⼩的段,當⽇志增⻓到特定尺⼨時關閉當前段⽂件,並開始寫⼊⼀個新的段⽂件。然後,我們就可以對這些段進⾏壓縮(compaction)。壓縮意味著在⽇志中丟棄重複的鍵,只保留每個鍵的最近更新。
-
為什麼不更新⽂件,只能追加設計的原因
有⼏個:- i。追加和分段合併是順序寫⼊操作,通常⽐隨機寫⼊快得多,尤其是在磁碟旋轉硬碟上。
- ii。如果段⽂件是附加的或不可變的,併發和崩潰恢復就簡單多了。例如,您不必擔⼼在更新值時發⽣崩潰的情況,⽽將包含舊值和新值的⼀部分的⽂件保留在⼀起。
- iii。合併舊段可以避免資料⽂件隨著時間的推移⽽分散的問題。
-
雜湊表索引的侷限性:
- i。雜湊表必須能放進記憶體。磁碟雜湊對映表現較差。它需要⼤量的隨機訪問I/O,當它變滿時增⻓是很昂貴的,並且雜湊衝突需要很多的邏輯。
- ii。範圍查詢效率不⾼。
-
-
SSTable和LSM樹
-
SSTable:在普通⽇志結構儲存段都是⼀系列鍵值對,鍵值對的順序為寫⼊的順序出現,⽇志中稍後的值優先於⽇志中較早的相同鍵的值。此時,如果我們要求鍵值對的序列按鍵排序。則這種格式就是【排序字串表(Sorted String Table)】,簡稱SSTable。
-
SSTable的優勢:
- 1.合併段是簡單⽽⾼效的,即使⽂件⼤於可⽤記憶體。
- 2.因為鍵是有序的,所以在⽂件中找到⼀個特定的鍵,不再需要儲存記憶體中所有鍵的索引。
- 3.可以將記錄分組到塊中,並在寫⼊磁碟之前進⾏壓縮 ,稀疏記憶體中索引的每個條⽬。不僅節省了磁碟空間,壓縮還可以減少IO頻寬的使⽤。
-
構建和維護SSTables
- a。寫⼊時,將其新增到記憶體中的平衡樹資料結構(例如,紅⿊樹)。這個記憶體樹有時被稱為【記憶體表(memtable)】。
- b。當記憶體表⼤於某個閾值(通常為⼏兆位元組)時,將其作為SSTable⽂件寫⼊磁碟。這可以⾼效地完成,因為樹已經維護了按鍵排序的鍵值對。新的SSTable⽂件成為資料庫的最新部分。當SSTable被寫⼊磁碟時,寫⼊可以繼續到⼀個新的記憶體表例項。
- c。為了提供讀取請求,⾸先嚐試在記憶體表中找到關鍵字,然後在最近的磁碟段中,然後在下⼀個較舊的段中找到該關鍵字。
- d。有時會在後臺運⾏合併和壓縮過程以組合段⽂件並丟棄覆蓋或刪除的值。
- e。這個⽅案效果很好。它只會遇到⼀個問題:如果資料庫崩潰,則最近的寫⼊(在記憶體表中,但尚未寫⼊磁碟)將丟失。為了避免這個問題,我們可以在磁碟上儲存⼀個單獨的⽇志,每個寫⼊都會⽴即被附加到磁碟上。每當記憶體表寫出到SSTable時,相應的⽇志都可以被丟棄。該⽇志的唯⼀⽬的是在崩潰後恢復記憶體表。
-
SSTables的應用
- 以上描述的演算法本質上正是LevelDB和RocksDB所使用的,主要用於嵌入到其他應用程式的key-value儲存引擎庫。類似的引擎還被用於Cassandra和HBase。
-
LSM儲存引擎
- 基於SSTable和記憶體表memTable的這種索引結構最初被成為基於日誌的合併樹,即LSM(Log-Structure Merge Tree)。這種基於合併和壓縮排序檔案原理的儲存引擎通常都被成為LSM儲存引擎,
-
Lucene索引引擎
- Lucene是Elasticsearch和Solr使⽤的⼀種全⽂搜尋的索引引擎,它使⽤類似的⽅法來儲存它的詞典。全⽂索引⽐鍵值索引複雜得多,但是基於類似的想法:在搜尋查詢中給出⼀個單詞,找到
提及單詞的所有⽂檔(⽹⻚,產品描述等)。這是通過鍵值結構實現的,其中鍵是單詞(關鍵詞
(term)),值是包含單詞(⽂章列表)的所有⽂檔的ID的列表。在Lucene中,從術語到釋出列表的這種對映儲存在SSTable類的有序⽂件中,根據需要在後臺合併。
- Lucene是Elasticsearch和Solr使⽤的⼀種全⽂搜尋的索引引擎,它使⽤類似的⽅法來儲存它的詞典。全⽂索引⽐鍵值索引複雜得多,但是基於類似的想法:在搜尋查詢中給出⼀個單詞,找到
-
效能優化:
-
LSM樹演算法在大多數情況向效能表現良好,但當查詢資料庫中不存在的鍵時,LSM樹演算法可能會很慢:您必須檢查記憶體表,然後將這些段⼀直回到最⽼的(可能必須從磁碟讀取每⼀個),然後才能確定鍵不存在。為了優化這種訪問,儲存引擎通常使⽤額外的Bloom過濾器。
-
不同的策略來也會影響SSTables被壓縮和合並順序和時機。最常⻅的選擇是⼤⼩分級壓縮和分層壓縮。
-
大小分級壓縮
- HBase使⽤⼤⼩分級壓縮,Cassandra同時⽀持。
- 在大小分級壓縮中,較新的和較小的SSTable被連續合併到較舊和較大的SSTables。
-
分層壓縮
- LevelDB和RocksDB使⽤分層壓縮(LevelDB因此得名),Cassandra同時⽀持。
- 在分層壓縮中,鍵的範圍分裂成多個更小的SSTable,舊資料被移動到單獨的“層級”,這樣壓縮可以逐步進行並節省磁碟空間。
-
-
LSM樹的基本思想簡單⽽有效:儲存⼀系列在後臺合併的SSTables。
即使資料集⽐可⽤記憶體⼤得多,它仍能繼續正常⼯作。由於資料按排序順序儲存,因此可以⾼效地執⾏範圍查詢(掃描所有⾼於某些最⼩值和最⾼值的所有鍵),並且因為磁碟寫⼊是連續的,所以LSM樹可以⽀持⾮常⾼的寫⼊吞吐量。
-
-
-
B樹
-
以上討論的⽇志結構索引正處在逐漸被接受,而應用最廣泛的索引結構是B樹。
像SSTables⼀樣,B樹保持按鍵排序的鍵值對,這允許⾼效的鍵值查詢和範圍查詢。
⽇志結構索引將資料庫分解為可變⼤⼩的段,通常是⼏兆位元組或更⼤的⼤⼩,並且總是按順序編寫段。而B樹將資料庫分解成固定⼤⼩的塊或⻚⾯,傳統上⼤⼩為4KB(有時會更⼤),並且⼀次只能讀取或寫⼊⼀個⻚⾯。這種設計更接近於底層硬體,因為磁碟也被安排在固定⼤⼩的塊中。
每個⻚⾯都可以使⽤地址或位置來標識,這允許⼀個⻚⾯引⽤另⼀個⻚⾯,類似於指標。我們可以在磁碟中使⽤這些⻚⾯引⽤來構建⼀個⻚⾯樹,⽽不是在記憶體中。 -
讓B樹更可靠
- B樹的基本底層寫操作是⽤新資料覆蓋磁碟上的⻚⾯。假定覆蓋不改變⻚⾯的位置; 即當⻚⾯被覆蓋時,對該⻚⾯的所有引⽤保持完整。這與⽇志結構索引(如LSM樹)形成鮮明對⽐,後者只附加到⽂件(並最終刪除過時的⽂件),但從不修改⽂件。但此過程是一個複雜的操作,會產生各種問題,如下:
- 問題1: 頁分裂過程中,如果此時資料庫崩潰,可能導致損壞的索引,如產生孤兒頁面,既不是任何父頁的子頁。
解決方案:預寫式⽇志(WAL,write-ahead-log),也稱為重做⽇志(redo log)。該磁碟資料結構是⼀個僅追加的⽂件,每個B樹修改都可以應⽤到樹本身的⻚⾯上。當資料庫在崩潰後恢復時,這個⽇志被⽤來使B樹恢復到⼀致的狀態。 - 問題2: 併發問題:更新⻚⾯的⼀個額外的複雜情況是,如果多個執行緒要同時訪問B樹,則需要仔細的併發控制,否則執行緒可能會看到樹處於不⼀致的狀態。
解決方案:這種情況通常通過使⽤【鎖存器(latches)】(輕量級鎖)保護樹的資料結構來完成。⽇志結構化的⽅法在這⽅⾯更簡單,因為它們在後臺進⾏所有的合併,⽽不會⼲擾傳⼊的查詢,並且不時地將舊的分段原⼦交換為新的分段。
-
B樹優化
- a。寫時複製技術:⼀些資料庫(如LMDB)使⽤寫時複製⽅案【21】,⽽不是覆蓋⻚⾯並維護WAL進⾏崩潰恢復。修改的⻚⾯被寫⼊到不同的位置,並且樹中的⽗⻚⾯的新版本被建立,指向新的位置。這種⽅法對於併發控制也很有⽤,資料庫“快照隔離和可重複讀”中也有類似用法。
- b。壓縮鍵的⼤⼩:可以通過壓縮鍵,不儲存整個鍵來節省⻚⾯空間,特別是在樹內部的⻚⾯上,鍵需要提供⾜夠的資訊來充當鍵範圍之間的邊界。在⻚⾯中包含更多的鍵允許樹具有更⾼的分⽀因⼦,因此更少的層次。
- c。佈局樹:通常,⻚⾯可以放置在磁碟上的任何位置,如果查詢需要按照順序掃描⼤部分關鍵字範圍,每個讀取的⻚⾯都可能需要磁碟查詢,效能不太好。因此,許多B樹實現嘗試佈局樹,使得葉⼦⻚⾯按順序出現在磁碟上。但是,隨著樹的增⻓,維持這個順序是很困難的。相⽐之下,由於LSM樹在合併過程中⼀次⼜⼀次地重寫儲存的⼤部分,所以它們更容易使順序鍵在磁碟上彼此靠近。
- d。樹中新增額外的指標。例如,每個葉⼦⻚⾯可以在左邊和右邊具有對其兄弟⻚⾯的引⽤,這允許不跳回⽗⻚⾯就能順序掃描。
- e。B樹的變體如分形樹借⽤⼀些⽇志結構的思想來減少磁碟尋道(⽽且它們與分形⽆關)。
-
-
比較B樹與LSM樹
-
我們將簡要討論⼀些在衡量儲存引擎效能時值得考慮的事情
- 寫放⼤(write amplification):在資料庫的⽣命週期中寫⼊資料庫導致對磁碟的多次寫⼊,被稱為寫放⼤。
在寫⼊繁重的應⽤程式中,效能瓶頸可能是資料庫可以寫⼊磁碟的速度。在這種情況下,寫放⼤會導致直接的效能代價,降低每秒的寫入次數。
- 寫放⼤(write amplification):在資料庫的⽣命週期中寫⼊資料庫導致對磁碟的多次寫⼊,被稱為寫放⼤。
-
LSM樹
-
優點
- 通常LSM樹的寫⼊速度更快。順序寫⼊⽐隨機寫⼊快得多。
- 資料在磁碟上更靠近,減少磁碟查詢。由於LSM樹在合併過程中⼀次⼜⼀次地重寫儲存的⼤部分,所以它們更容易使順序鍵在磁碟上彼此靠近。
- 沒有併發問題。⽇志結構化的⽅法在後臺進⾏所有的合併,⽽不會⼲擾傳⼊的查詢,並且不時地將舊的分段原⼦交換為新的分段。
- 更⾼的寫⼊吞吐量:LSM樹通常能夠⽐B樹⽀持更⾼的寫⼊吞吐量,部分原因是它們有時具有【較低的寫放⼤】(這取決於儲存引擎配置和⼯作負載),部分是因為它們順序地寫⼊緊湊的SSTable⽂件⽽不是必須覆蓋樹中的⼏個⻚⾯。這種差異在磁性硬碟驅動器上尤其重要,【順序寫⼊⽐隨機寫⼊快得多】。
- LSM樹可以被壓縮得更好,碎片更少。LSM樹不是⾯向⻚⾯的,並且定期重寫SSTables以【去除碎⽚】,所以它們具有較低的儲存開銷,特別是當使⽤平坦壓縮時。
B樹儲存引擎會由於頁分裂,⽽留下⼀些不能使用的磁碟空間,從而產生磁碟碎片。
-
缺點
- LSM樹上的讀取通常⽐較慢。因為它們必須在壓縮的不同階段檢查⼏個不同的資料結構和SSTables。
- 讀寫操作與壓縮公用磁碟資源,進而影響讀寫操作速度。⽇志結構儲存的缺點是壓縮過程有時會⼲擾正在進⾏的讀寫操作。儘管儲存引擎嘗試逐步執⾏壓縮⽽不影響併發訪問,但是磁碟資源有限,所以很容易發⽣請求需要等待⽽磁碟完成昂貴的壓縮操作。
在更⾼百分⽐的情況下(參閱“描述效能”),⽇志結構化儲存引擎的查詢響應時間有時會相當⻓,⽽B樹的⾏為則相對更具可預測性。 - 寫入與壓縮公用磁碟頻寬,進而影響寫入吞吐量。壓縮的另⼀個問題出現在⾼寫⼊吞吐量:磁碟的有限寫⼊頻寬需要在初始寫⼊(記錄和重新整理記憶體表到磁碟)和在後臺運⾏的壓縮執行緒之間共享。
- 壓縮速率影響讀取速度。如果寫⼊吞吐量很⾼,並且壓縮沒有仔細配置,壓縮跟不上寫⼊速率。在這種情況下,磁碟上未合併段的數量不斷增加,直到磁碟空間⽤完,讀取速度也會減慢,因為它們需要檢查更多段⽂件。通常情況下,即使壓縮⽆法跟上,基於SSTable的儲存引擎也不會限制傳⼊寫⼊的速率,此時需要進⾏明確的監控來檢測這種情況。
- 不支援事務。⽇志結構化的儲存引擎可能在不同的段中有相同鍵的多個副本,不利於實現事務。B樹的⼀個優點是每個鍵只存在於索引中的⼀個位置,⽽這使得B樹在想要提供強⼤的事務語義的資料庫中很有吸引⼒:在許多關聯式資料庫中,事務隔離是通過在鍵範圍上使⽤鎖來實現的。
-
-
B樹
-
優點
- 通常B樹的讀取速度更快。LSM樹的寫⼊速度更快。
- 強大的事務支援。
-
缺點
- 維持資料的順序較難。隨著樹的增⻓,維持葉⼦⻚⾯按順序出現在磁碟上是很困難的,即使B樹實現使用佈局樹。
- 併發問題。需要通過使⽤鎖存器(latches)(輕量級鎖)保護樹的資料結構來完成。
- B樹的寫入速度較慢。B樹索引必須⾄少兩次寫⼊每⼀段資料:⼀次寫⼊預先寫⼊⽇志,⼀次寫⼊樹⻚⾯本身(也許再次分⻚)。即使在該⻚⾯中只有⼏個位元組發⽣了變化,也需要⼀次編寫整個⻚⾯的開銷。
- B樹索引必須⾄少兩次寫⼊每⼀段資料:⼀次寫⼊預先寫⼊⽇志,⼀次寫⼊樹⻚⾯本身(也許再次分⻚)。即使在該⻚⾯中只有⼏個位元組發⽣了變化,也需要⼀次編寫整個⻚⾯的開銷。
-
-
-
其他結構
-
主鍵索引primary index:
主鍵唯⼀標識關係表中的⼀⾏,或⽂檔資料庫中的⼀個⽂檔或圖形資料庫中的⼀個頂點。資料庫中的其他記錄可以通過其主鍵(或ID)引⽤該⾏/⽂檔/頂點,並且索引⽤於解析這樣的引⽤。 -
二級索引:
⼀個⼆級索引可以很容易地從⼀個鍵值索引構建。⼆級索引的鍵不是唯⼀的,可能有許多⾏(⽂檔,頂點)具有相同的鍵。 -
聚簇索引clustered index:
在索引中儲存所有⾏資料。在某些情況下,從索引到堆⽂件的額外跳躍對讀取來說效能損失太⼤,因此可能希望將索引⾏直接儲存在索引中,這被稱為聚集索引。例如,在MySQL的InnoDB儲存引擎中,表的主鍵總是⼀個聚簇索引,⼆級索引⽤主鍵⽽不是堆⽂件中的位置。 -
非聚簇索引nonclustered index:
僅在索引中儲存對資料的引⽤。 -
覆蓋索引covering index:
在聚集索引和⾮聚集索引之間的折衷被稱為包含列的索引(index with included columns) 或覆蓋索引,其儲存表的⼀部分在索引內。這允許通過單獨使⽤索引來回答⼀些查詢,這種情況叫做:索引覆蓋(cover)了查詢。 -
記憶體資料庫
- Memcached
- Redis
- VoltDB,MemSQL和Oracle TimesTen等
-
-
-
事務處理還是分析處理?
-
儲存引擎分類
-
a.線上事務處理(OLTP, OnLine
Transaction Processing)- 通常⾯向⽤戶
- 可能會接受處理⼤量的請求;每個查詢僅接受少量記錄,要求較低。
- 磁碟尋道時間往往是這⾥的瓶頸。
- 儲存引擎使⽤索引來提高查詢效率。
-
b.線上分析處理(OLAP, OnLine Analytice
Processing)- 主要由業務分析⼈員使⽤
- 處理⽐OLTP系統少得多的查詢量;但是每個查詢通常要求很⾼,需要在短時間內掃描數百萬條記錄。
- 磁碟頻寬(不是查詢時間)往往是瓶頸。
- 列式儲存是這種⼯作負載較為流⾏的解決
⽅案。
-
c.OLTP對比OLAP
-
-
OLTP兩大主流儲存引擎
-
a。日誌結構學派
- 特點:只允許追加寫⽂件(append only)和刪除過時的⽂件,但不會更新已經寫⼊的⽂件。主要想法是,他們系統地將隨機訪問寫⼊順序寫⼊磁碟,由於硬碟驅動器和固態硬碟的效能特點,可以實現更⾼的寫⼊吞吐量。
- 案例: Bitcask,SSTables,LSM樹,
LevelDB,Cassandra,HBase,Lucene等。
-
b。就地更新學派
- 特點:將磁碟視為⼀組可以覆蓋的固定⼤⼩的⻚⾯。
- 案例:基於B樹實現資料庫。
-
-
-
列式儲存
- OLAP⼯作負載比OLTP高的原因:當您的查詢需要在⼤量⾏中順序掃描時,索引的相關性就會降低很多。相反,⾮常緊湊地編碼資料變得⾮常重要,以最⼤限度地減少查詢需要從磁碟讀取的資料量。
四、編碼與演化
-
可演化性:應⽤程式不可避免地隨時間⽽變化。新產品的推出,對需求的深⼊理解,或者商業環境的變化,總會伴隨著功能(feature)的增增改改。第⼀章介紹了可演化性(evolvability)的概念:應該盡⼒構建能靈活適應變化的系統(參閱“可演化性:擁抱變化”)。
- 在⼤多數情況下,修改應⽤程式的功能也意味著其儲存的資料格式的更改,當資料格式(format)或模式(schema)發⽣變化時,通常需要對應⽤程式程式碼進⾏相應的更改。但在⼤型應⽤程式中,一般需要執行滾動升級。
- 滾動升級:新版本的服務逐步部署到少數節點,⽽不是同時部署到所有節點。滾動升級允許在不停機的情況下發布新版本的服務,並使部署⻛險降低。從⽽⿎勵在罕⻅的⼤型版本上頻繁釋出⼩型版本,同時允許在影響⼤量⽤戶之前檢測並回滾有故障的版本。這些屬性對於可演化性,以及對應⽤程式進⾏更改的容易性都是⾮常有利的。
- 相容性:滾動升級意味著新舊版本的程式碼,以及新舊資料格式可能會在系統中同時共處。系統想要繼續順利運⾏,就需要保持雙向相容性:
i。向後相容 (backward compatibility):新程式碼可以讀舊資料。
ii。向前相容 (forward compatibility):舊程式碼可以讀新資料。
-
編碼影響點
- 1.效率
- 2.應用程式的體系結構和部署選項
-
編碼格式及其相容性
-
概念
- 程式通常(⾄少)使⽤兩種形式的資料:
-
-
在記憶體中,資料儲存在物件,結構體,列表,陣列,雜湊表,樹等中。 這些資料結構針對CPU的⾼效訪問和操作進⾏了優化(通常使⽤指標)。
-
如果要將資料寫⼊⽂件,或通過⽹絡傳送,則必須將其編碼(encode)為某種⾃包含的位元組序列(例如,JSON⽂檔)。 由於每個程式都有⾃⼰獨⽴的地址空間,⼀個程式中的指標對任何其他程式都沒有意義,所以這個位元組序列表示會與通常在記憶體中使⽤的資料結構完全不同 1 。
- 所以,需要在兩種表示之間進⾏某種型別的翻譯。 從記憶體中表示到位元組序列的轉換稱為編碼
(Encoding)也稱為序列化(serialization)或編組(marshalling),反過來稱為解碼
(Decoding) 解析(Parsing),反序列化(deserialization),反編組(unmarshalling) 。-
1.程式設計語⾔特定的編碼
- 僅限於單⼀程式設計語⾔,並且往往⽆法提供前向和後向相容性。
-
2.JSON、XML和CSV等文字結構
- ⾮常普遍,其相容性取決於您如何使⽤它們。讓不同的組織達成⼀致的難度較大。
- 對於資料型別有些模糊,所以你必須⼩⼼數字和⼆進位制字串。
- JSON雖然區分字串和數字,但不區分整數和浮點數,⽽且不能指定精度。
- CSV沒有任何模式,因此應⽤程式需要定義每⾏和每列的含義。
-
3.Thrift、Protocol Buffers和Avro等二進位制模式驅動的格式
- 緊湊,⾼效
- 允許使⽤清晰定義的前向和後向相容性語意,以提供較好的相容性。
- 這些模式可以用於靜態型別語言的文件和程式碼生成,但是資料在解碼前不可讀
-
-
資料流的型別
-
1.資料庫中的資料流
- 資料寫入者對資料編碼,資料讀取者對資料解碼。
-
2.服務中的資料流:RPC和Rest API
- 具象狀態傳輸(REST)和遠端過程調⽤(RPC)
- 客戶端對請求編碼,服務端接受請求並解碼,並對響應編碼,客戶端對響應解碼。
-
3.訊息中的資料流:非同步訊息傳遞
- 節點之間通過傳送訊息進行通訊,訊息有傳送者編碼,由接受者解碼。
-