數據密集型系統設計
數據系統的基石
本文將會介紹數據系統底層的基礎概念,⽆論是在單台機器上運⾏的單點數據系統,還是分布在多台機器上的分布式數據系統都適⽤。
- 第⼀部分將介紹本書使⽤的術語和⽅法。可靠性,可擴展性和可維護性 ,這些詞匯到底意味着什么?如何實現這些⽬標?
- 第⼆部分將對⼏種不同的數據模型和查詢語⾔進⾏⽐較。從程序員的⻆度看,這是數據庫之間最明顯的區別。不同的數據模型適⽤於不同的應⽤場景。
- 第三部分將深⼊存儲引擎內部,研究數據庫如何在磁盤上擺放數據。不同的存儲引擎針對不同的負載進⾏優化,選擇合適的存儲引擎對系統性能有巨⼤影響。
- 第四部分將對⼏種不同的 數據編碼進⾏⽐較。特別研究了這些格式在應⽤需求經常變化、模式需要隨時間演變的環境中表現如何。
一、可靠性、可擴展性、可維護性
-
目標與意義
現今很多應⽤程序都是 數據密集型(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.消息中的數據流:異步消息傳遞
- 節點之間通過發送消息進行通信,消息有發送者編碼,由接受者解碼。
-