1. 程式人生 > >胖子哥的大資料之路(6)- NoSQL生態圈全景介紹

胖子哥的大資料之路(6)- NoSQL生態圈全景介紹

引言:

NoSQL高階培訓課程的基礎理論篇的部分課件,是從一本英文原著中做的摘選,中文部分參考自網際網路。給大家分享。

正文:

 The NoSQL Ecosystem

 目錄

The NoSQL Ecosystem... 1

13.1. What's in a Name?. 5

13.1.1. SQL and the Relational Model 6

13.1.2. NoSQL Inspirations. 8

13.1.3. Characteristics and Considerations. 9

13.2. NoSQL Data and Query Models. 11

13.2.1. Key-based NoSQL Data Models

. 11

13.2.1.1 Key-Value Stores. 12

13.2.1.2 Key-Data Structure Stores. 12

13.2.1.3 Key-Document Stores. 13

13.2.1.4 BigTable Column Family Stores. 13

13.2.2. Graph Storage. 14

13.2.3. Complex Queries. 16

13.2.4. Transactions. 16

13.2.5. Schema-free Storage. 17

13.3. Data Durability. 18

13.3.1. Single-server Durability

. 18

13.3.1.1 Control fsync Frequency. 19

13.3.1.2 Increase Sequential Writes by Logging.. 20

13.3.1.2 Increase Throughput by Grouping Writes. 20

13.3.2. Multi-server Durability. 21

13.4. Scaling for Performance. 22

13.4.1. Do Not Shard Until You Have To.. 23

13.4.1.1 Read Replicas. 23

13.4.1.1 Caching

.. 24

13.4.2. Sharding Through Coordinators. 24

13.4.3. Consistent Hash Rings. 25

13.4.3.1 Hash Rings by Example. 25

13.4.3.2 Replicating Data. 27

13.4.3.3 Achieving Better Distribution.. 27

13.4.4. Range Partitioning.. 28

13.4.4.1 The BigTable Way. 28

13.4.4.2 Handling Failures. 30

13.4.4.3 Range Partitioning-based NoSQL Projects. 31

13.4.5. Which Partitioning Scheme to Use. 32

13.5. Consistency. 32

13.5.1. A Little Bit About CAP.. 33

13.5.2. Strong Consistency. 34

13.5.3. Eventual Consistency. 36

13.5.3.1  Versioning and Conflicts. 37

13.5.3.2 Conflict Resolution.. 38

13.5.3.3 Read Repair. 38

13.5.3.4 Hinted Handoff. 39

13.5.3.5 Anti-Entropy. 39

13.5.3.6 Gossip.. 40

13.6. A Final Word.. 40

13.7. Acknowledgments. 41

Footnotes. 41

內容

NoSQL不是一個工具,而是由一些具有互補性和競爭性的工具組成的一個概念,是一個生態圈。這些被稱作NoSQL的工具,在儲存資料的方式上,提供了一種與基於SQL語言的關係型資料截然不同的思路。要想了解NoSQL,我們必須先了解現有的這些工具,去理解那些讓他們開拓出新的儲存領域的設計思路。

如果你正在考慮使用NoSQL,你應該會馬上發現你有很多種選擇。NoSQL系統捨棄了許了傳統關係型資料庫的方便之處,而把一些通常由關係型資料庫本身來完成的任務交給了應用層來完成。這需要開發人員更深入的去了解儲存系統的架構和具體實現。

13.1. What's in a Name?

在給NoSQL下定義之前,我們先來試著從它的名字上做一下解讀,顧名思義,NoSQL系統的資料操作介面應該是非SQL型別的。但在NoSQL社群,NoSQL被賦予了更具有包容性的含義,其意為Not Only SQL,即NoSQL提供了一種與傳統關係型資料庫不太一樣的儲存模式,這為開發者提供了在關係型資料庫之外的另一種選擇。有時候你可能會完全用NoSQL資料庫代替關係型資料庫,但你也可以同時使用關係型和非關係型儲存來解決具體的問題。

在進入NoSQL的大門之前,我們先來看看哪些場景下使用關係型資料庫更合適,哪些使用NoSQL更合適。

13.1.1. SQL and the Relational Model

SQL是一種任務描述性的查詢語言,所謂任務描述性的查詢語言,就是說它只描述他需要系統做什麼,而不告訴系統如何去做。例如:查出39號員工的資訊,查出員工的名字和電話,只查詢做會計工作的員工資訊,計算出每個部門的員工總數,或者是對員工表和經理表做一個聯合查詢。

簡單的說,SQL讓我們可以直接向資料庫提出上述問題而不必考慮資料是如何在磁碟上儲存的,使用哪些索引來查詢資料,或者說用哪種演算法來處理資料。在關係型資料庫中有一個重要的元件,叫做查詢優化器,正是它來推算用哪種操作方式能夠更快的完成操作。查詢優化器通常比一般的資料庫使用者更聰明,但是有時候由於沒有充足的資訊或者系統模型過於簡單,也會導致查詢優化器不能得出最有效的操作方式。

作為目前應用最廣的資料庫系統,關係型資料庫系統以其關聯型的資料模型而命名。在關聯型的資料模型中,在現實世界中的不同型別的個體被儲存在不同的表裡。比如有一個專門存員工的員工表,有一個專門存部門的部門表。每一行資料又包含多個列,比如員工表裡可能包含了員工號,員工工資,生日以及姓名等,這些資訊項被存在員工表中的某一列中。

關聯型的模型與SQL是緊密想連的。簡單的查詢操作,比如查詢符合某個條件的所有行(例:employeeid = 3, 或者 salary > $20000)。更復雜一些的任務會讓資料庫做一些額外的工作,比如跨表的聯合查詢(例:查出3號員的部門名稱是什麼)。一些複雜的查詢,比如統計操作(例:算出所有員工的平均工資),甚至可能會導致全表掃描。

關聯型的資料模型定義了高度結構化的資料結構,以及對這些結構之間關係的嚴格定義。在這樣的資料模型上執行的查詢操作會比較侷限,而且可能會導致複雜的資料遍歷操作。資料結構的複雜性及查詢的複雜性,會導致系統產生如下的一些限制:

    複雜導致不確定性。使用SQL的一個問題就是計算某個查詢的代價或者產生的負載幾乎是不可能的。使用簡單的查詢語言可能會導致應用層的邏輯更復雜,但是這樣可以將儲存系統的工作簡單化,讓它只需要響應一些簡單的請求。

    對一個問題建模有很多種方式。其中關聯型的資料模型是非常嚴格的一種:表結構的定義規定了表中每一行資料的儲存內容。如果你的資料結構化並沒有那麼強,或者對每一行資料的要求比較靈活,那可能關聯型的資料模型就太過嚴格了。類似的,應用層的開發人員可能對關聯型的資料結構並不滿意。比如很多應用程式是用面向物件的語言寫的,資料在這些語言中通常是以列表、佇列或集合的形式組織的,程式設計師們當然希望他們的資料儲存層也能和應用層的資料模型一致。

    當資料量增長到一臺機器已經不能容納,我們需要將不同的資料表分佈到不同的機器。而為了避免在不同機器上的資料表在進行聯合查詢時需要跨網路進行。我們必須進行反正規化的資料庫設計,這種設計方式要求我們把需要一次性查詢到的資料儲存在一起。這樣做使得我們的系統變得就像一個主鍵查詢系統一樣,於是我們開始思考,是否有其它更適合我們資料的資料模型。

通常來說,捨棄多年以來的設計思路是不明智的。當你要把資料存到資料庫,當考慮到SQL與關聯型的資料模型,這些都是數十年的研究的開發成果,提供豐富的資料模型,提供複雜操作的保證。而當你的問題涉及到大資料量,高負載或者說你的資料結構在SQL與關聯型資料模型下很難得到優化,NoSQL可能是更好的選擇。

13.1.2. NoSQL Inspirations

NoSQL運動受到了很多相關研究論文的啟示,這所有論文中,最核心的有兩個。

Google的BigTable[CDG+06]提出了一種很有趣的資料模型,它將各列資料進行排序儲存。資料值按範圍分佈在多臺機器,資料更新操作有嚴格的一致性保證。

Amazon的Dynamo[DHJ+07]使用的是另外一種分散式模型。Dynamo的模型更簡單,它將資料按key進行hash儲存。其資料分片模型有比較強的容災性,因此它實現的是相對鬆散的弱一致性:最終一致性。

接下來我們會深入介紹這些設計思想,而實際上在現實中這些思想經常是混搭使用的。比如像HBase及其它一些NoSQL系統他們在設計上更接受BigTable的模型,而像Voldemort 系統它就和Dynamo更像。同時還有像Cassandra這種兩種特性都具備的實現(它的資料模型和BigTable類似,分片策略和一致性機制和Dynamo類似)。

13.1.3. Characteristics and Considerations

NoSQL系統捨棄了一些SQL標準中的功能,取而代之的是提供了一些簡單靈活的功能。NoSQL 的構建思想就是儘量簡化資料操作,儘量讓執行操作的效率可預知。在很多NoSQL系統裡,複雜的操作都是留給應用層來做的,這樣的結果就是我們對資料層進行的操作得到簡化,讓操作效率可預知。

NoSQL系統不僅捨棄了很多關係資料庫中的操作。它還可能不具備關係資料庫以下的一些特性:比如通常銀行系統中要求的事務保證,一致性保證以及資料可靠性的保證等。事務機制提供了在執行多個命令時的all-or-nothing保證。一致性保證瞭如果一個數據更新後,那麼在其之後的操作中都能看到這個更新。可靠性保證如果一個數據被更新,它就會被寫到持久化的儲存裝置上(比如說磁碟),並且保證在資料庫崩潰後資料可恢復。

通過放寬對上述幾點特性的要求,NoSQL系統可以為一些非銀行類的業務提供以效能換穩定的策略。而同時,對這幾點要求的放寬,又使得NoSQL系統能夠輕鬆的實現分片策略,將遠遠超出單機容量的大量資料分佈在多臺機器上的。

由於NoSQL系統還處在萌芽階段,本章中提到的很多NoSQL架構都是用於滿足各種不同使用者的需求的。對這些架構進行總結不太可能,因為它們總在變化。所以希望你能記住的一點是,不同的NoSQL系統的特點是不同的,通過本章的內容,希望你能該根據自己的業務情況來選擇合適的NoSQL系統,而本章內容不可能給你直接的答案。

當你去考查一個NoSQL系統的時候,下面的的幾點是值得注意的:

l  資料模型及操作模型:你的應用層資料模型是行、物件還是文件型的呢?這個系統是否能支援你進行一些統計工作呢?

l  可靠性:當你更新資料時,新的資料是否立刻寫到持久化儲存中去了?新的資料是否同步到多臺機器上了?

l  擴充套件性:你的資料量有多大,單機是否能容下?你的讀寫量求單機是否能支援?

l  分割槽策略:考慮到你對擴充套件性,可用性或者永續性的要求,你是否需要一份資料被存在多臺機器上?你是否需要知道資料在哪臺機器上,以及你能否知道。

l  一致性:你的資料是否被複制到了多臺機器上,這些分佈在不同點的資料如何保證一致性?

l  事務機制:你的業務是否需要ACID的事務機制?

l  單機效能:如果你打算持久化的將資料存在磁碟上,哪種資料結構能滿足你的需求(你的需求是讀多還是寫多)?寫操作是否會成為磁碟瓶頸?

l  負載可評估:對於一個讀多寫少的應用,諸如響應使用者請求的web應用,我們總會花很多精力來關注負載情況。你可能需要進行資料規模的監控,對多個使用者的資料進行彙總統計。你的應用場景是否需要這樣的功能呢?

儘管後三點在本章沒有獨立的小節進行描述,但它們和其它幾點一樣重要,下面就讓我們對以上的幾點進行闡述。

13.2. NoSQL Data and Query Models

資料庫的資料模型指的是資料在資料庫中的組織方式,資料庫的操作模型指的是存取這些資料的方式。通常資料模型包括關係模型、鍵值模型以及各種圖結構模型。的操作語言可能包括SQL、鍵值查詢及MapReduce等。NoSQL通常結合了多種資料模型和操作模型,提供了不一樣的架構方式。

13.2.1. Key-based NoSQL Data Models

在鍵值型系統中,複雜的聯合操作以及滿足多個條件的取資料操作就不那麼容易了,需要我們創造性的建立和使用鍵名。如果一個程式設計師既想通過員工號查到員工資訊,又想通過部門號查到員工資訊,那麼他必須建立兩種型別的鍵值對。例如 emplyee:30 這個鍵用於指向員工號為30的員工資訊。employee_departments:20 可能用到指向一個包含在20號部門工作的所有員工工號的列表。這樣資料庫中的聯合操作就被轉換成業務層的邏輯了:要獲取部門號為20的所有員工的資訊,應用層可以先獲取employee_departments:20 這個列表,然後再迴圈地拿這個列表中的ID通過獲取employee:ID得到所有員工的資訊。

鍵值查詢的一個好處是,對資料庫的操作模式是固定的,這些操作所產生的負載也是相對固定且可預知的。這樣像分析整個應用中的效能瓶頸這種事,就變得簡單多了。因為複雜的邏輯操作並不是放在資料庫裡面黑箱操作了。不過這樣做之後,業務邏輯和資料邏輯可能就沒那麼容易分清了。

下面我們快速地把各種鍵值模型的資料結構簡單描述一下。看看不同的NoSQL系統的在這方面的不同實現方式。

型別

部分代表

特點

列儲存

Hbase

Cassandra

Hypertable

顧名思義,是按列儲存資料的。最大的特點是方便儲存結構化和半結構化資料,方便做資料壓縮,對針對某一列或者某幾列的查詢有非常大的IO優勢。

文件儲存

MongoDB

CouchDB

文件儲存一般用類似json的格式儲存,儲存的內容是文件型的。這樣也就有有機會對某些欄位建立索引,實現關係資料庫的某些功能。

key-value儲存

Tokyo Cabinet / Tyrant

Berkeley DB

MemcacheDB

Redis

可以通過key快速查詢到其value。一般來說,儲存不管value的格式,照單全收。(Redis包含了其他功能)

圖儲存

Neo4J

FlockDB

InfoGrid

圖形關係的最佳儲存。使用傳統關係資料庫來解決的話效能低下,而且設計使用不方便。

物件儲存

db4o

Versant

通過類似面嚮物件語言的語法操作資料庫,通過物件的方式存取資料。

xml資料庫

Berkeley DB XML

BaseX

高效的儲存XML資料,並支援XML的內部查詢語法,比如XQuery,Xpath。

13.2.1.1 Key-Value Stores

Key-Value儲存可以說是最簡單的NoSQL儲存。每個key值對應一個任意的資料值。對NoSQL 系統來說,這個任意的資料值是什麼,它並不關心。比如在員工資訊資料庫裡,exployee:30 這個key對應的可能就是一段包含員工所有資訊的二進位制資料。這個二進位制的格式可能是Protocol Buffer、Thrift或者Avro都無所謂。

如果你使用上面說的Key-Value儲存來儲存你的結構化資料,那麼你就得在應用層來處理具體的資料結構:單純的Key-Value儲存是不提供針對資料中特定的某個屬性值來進行操作。通常它只提供像set、get和delete這樣的操作。以Dynamo為原型的Voldemort資料庫,就只提供了分散式的Key-Value儲存功能。BDB 是一個提供Key-Value操作的持久化資料儲存引擎。

13.2.1.2 Key-Data Structure Stores

Key結構化資料儲存,其典型代表是Redis,Redis將Key-Value儲存的Value變成了結構化的資料型別。Value的型別包括數字、字串、列表、集合以及有序集合。除了set/get/delete 操作以為,Redis還提供了很多針對以上資料型別的特殊操作,比如針對數字可以執行增、減操作,對list可以執行 push/pop 操作,而這些對特定資料型別的特定操作並沒有對效能造成多大的影響。通過提供這種針對單個Value進行的特定型別的操作,Redis可以說實現了功能與效能的平衡。

13.2.1.3 Key-Document Stores

Key – 文件儲存的代表有CouchDB、MongoDB和Riak。這種儲存方式下Key-Value的Value是結構化的文件,通常這些文件是被轉換成JSON或者類似於JSON的結構進行儲存。文件可以儲存列表,鍵值對以及層次結構複雜的文件。

MongoDB 將Key按業務分到各個collection裡,這樣以collection作為名稱空間,員工資訊和部門資訊的Key就被隔開了。CouchDB和Riak把型別跟蹤這種事留給了開發者去完成。文件型儲存的靈活性和複雜性是一把雙刃劍:一方面,開發者可以任意組織文件的結構,另一方面,應用層的查詢需求會變得比較複雜。

13.2.1.4 BigTable Column Family Stores

HBase和Cassandra的資料模型都借鑑自Google 的BigTable。這種資料模型的特點是列式儲存,每一行資料的各項被儲存在不同的列中(這些列的集合稱作列簇)。而每一列中每一個數據都包含一個時間戳屬性,這樣列中的同一個資料項的多個版本都能儲存下來。

列式儲存可以理解成這樣,將行ID、列簇號,列號以及時間戳一起,組成一個Key,然後將Value按Key的順序進行儲存。Key值的結構化使這種資料結構能夠實現一些特別的功能。最常用的就是將一個數據的多個版本存成時間戳不同的幾個值,這樣就能很方便的儲存歷史資料。這種結構也能天然地進行高效的鬆散列資料(在很多行中並沒有某列的資料)儲存。當然,另一方面,對於那些很少有某一行有NULL值的列,由於每一個數據必須包含列標識,這又會造成空間的浪費。

這些NoSQL系統對BigTable資料模型的實現多少有些差別,這其中以Cassandra進行的變更最為顯著。Cassandra引入了超級列(supercolumn)的概念,通過將列組織到相應的超級列中,可以在更高層級上進行資料的組織,索引等。這一做法也取代了locality groups的概念(這一概念的實現可以讓相關的幾個行的資料儲存在一起,以提高存取效能)。

13.2.2. Graph Storage

圖結構儲存是NoSQL的另一種儲存實現。圖結構儲存的一個指導思想是:資料並非對等的,關係型的儲存或者鍵值對的儲存,可能都不是最好的儲存方式。圖結構是電腦科學的基礎結構之一,Neo4j和HyperGraphDB是當前最流行的圖結構資料庫。圖結構的儲存與我們之前討論過的幾種儲存方式很不同,這種不同幾乎體現在每一個方面,包括:資料模型、資料查詢方式、資料在磁碟上的組織方式、在多個結點上的分佈方式,甚至包括對事務機制的實現等等。

圖形資料庫是一種非關係型資料庫,它應用圖形理論儲存實體之間的關係資訊。最常見的一個例子,就是社會網路中人與人之間的關係。關係型資料庫用於儲存“關係型”資料的效果並不好,其查詢複雜、緩慢、超出預期,而圖形資料庫的獨特設計恰恰彌補了這個缺陷。

由於篇幅的限制,對這些方面我們暫時不做深入的討論,這裡需要你瞭解的是,某些資料結構使用圖結構的資料庫進行儲存可能會更好。

13.2.3. Complex Queries

在NoSQL儲存系統中,有很多比鍵值查詢更復雜的操作。比如MongoDB可以在任意資料行上建立索引,可以使用Javascript語法設定複雜的查詢條件。BigTable型的系統通常支援對單獨某一行的資料進行遍歷,允許對單列的資料進行按特定條件地篩選。CouchDB允許你建立同一份資料的多個檢視,通過執行MapReduce任務來實現一些更為複雜的查詢或者更新操作。很多NoSQL系統都支援與Hadoop或者其它一些MapReduce框架結合來進行一些大規模資料分析工作。

13.2.4. Transactions

傳統的關係型資料庫在功能支援上通常很寬泛,從簡單的鍵值查詢,到複雜的多表聯合查詢,再到事務機制的支援。而與之不同的是,NoSQL系統通常注重效能和擴充套件性,而非事務機制。

傳統的SQL資料庫的事務通常都是支援ACID的強事務機制。A代表原子性,即在事務中執行多個操作是原子性的,要麼事務中的操作全部執行,要麼一個都不執行;C代表一致性,即保證進行事務的過程中整個資料加的狀態是一致的,不會出現資料花掉的情況;I代表隔離性,即兩個事務不會相互影響,覆蓋彼此資料等;D表示持久化,即事務一量完成,那麼資料應該是被寫到安全的,持久化儲存的裝置上(比如磁碟)。

ACID的支援使得應用者能夠很清楚他們當前的資料狀態。即使如對於多個事務同時執行的情況下也能夠保證資料狀態的正常性。但是如果要保證資料的一致性,通常多個事務是不可能交叉執行的,這樣就導致了可能一個很簡單的操作需要等等一個複雜操作完成才能進行的情況。

對很多NoSQL系統來說,對效能的考慮遠在ACID的保證之上。通常NoSQL系統僅提供對行級別的原子性保證,也就是說同時對同一個Key下的資料進行的兩個操作,在實際執行的時候是會序列的執行,保證了每一個Key-Value對不會被破壞。對絕大多數應用場景來說,這樣的保證並不會引起多大的問題,但其換來的執行效率卻是非常可觀的。當然,使用這樣的系統可能需要我們在應用層的設計上多做容錯性和修正機制的考慮。

在NoSQL中,Redis在事務支援這方面上不太一樣,它提供了一個MULTI命令用來將多個命令進行組合式的操作,通過一個WATCH命令提供操作隔離性。這和其它一些提供test-and-set這樣的低層級的隔離機制類似。

13.2.5. Schema-free Storage

還有一個很多NoSQL都有的共同點,就是它通常並沒有強制的資料結構約束。即使是在文件型儲存或者列式儲存上,也不會要求某一個數據列在每一行資料上都必須存在。這在非結構化資料的儲存上更方便,同時也省去了修改表結構的代價。而這一機制對應用層的容錯性要求可能會更高。比如程式可能得確定如果某一個員工的資訊裡缺少lastname這一項,是否算是錯誤。或者說某個表結構的變更是否對所有資料行起了作用。還有一個問題,就是資料庫的表結構可能會因為專案的多次迭代而變得混亂不堪。

13.3. Data Durability

最理想狀態是,資料庫會把所有寫操作立刻寫到持久化儲存的裝置,同時複製多個副本到不同地理位置的不同節點上以防止資料丟失。但是這種對資料安全性的要求對效能是有影響的,所以不同的NoSQL系統在自身效能的考慮下,在資料安全上採取了不太一樣的策略。

一種典型的出錯情況是重啟機器或者機器斷電了,這時候需要讓資料從記憶體轉存到磁碟才能保證資料的安全性,因為磁碟資料不會因斷電而丟失。而要避免磁碟損壞這種故障,就需要將資料冗餘的存在其它磁碟上,比如使用RAID來做映象,或者將資料同步到不同機器。但是有些時候針對同一個資料中心來說,是不可能做到完全的資料安全的,比如一旦發生颶風地震這種天災,整個機房機器可能都會損壞,所以,在這種情況下要保證資料的安全性,就必須將資料備份儲存到其它地理位置上比較遠的資料中心。所以,具體各個NoSQL系統在資料安全性和效能上的權衡策略也不太一樣。

13.3.1. Single-server Durability

單機可靠性理解起來非常簡單,它的定義是寫操作不會由於機器重啟或者斷電而丟失。通常單機可靠性的保證是通過把資料寫到磁碟來完成的,而這通常會造成磁碟IO成為整個系統的瓶頸。而事實上,即使你的程式每次都把資料寫到了磁碟,實際上由於作業系統buffer層的存在,資料還是不會立刻被寫到物理磁碟上,只有當你呼叫fsync這個系統呼叫的時候,作業系統才會儘可能的把資料寫到磁碟。

一般的磁碟大概能進行每秒100-200次的隨機訪問,每秒30-100MB的順序寫入速度。而記憶體在這兩方面的效能都有數量級上的提升。通過儘量減少隨機寫,取而代之的對每個磁碟裝置進行順序寫,這樣能夠減少單機可靠性保證的代價。也就是說我們需要減少在兩次fsync呼叫之間的寫操作次數,而增加順序寫操作的數量。下面我們說一下一些在單機可靠性的保證下提高效能的方法。

13.3.1.1 Control fsync Frequency

Memcached是一個純記憶體的儲存,由於其不進行磁碟上的持久化儲存,從而換來了很高的效能。當然,在我們進行伺服器重啟或者伺服器意外斷電後,這些資料就全丟了。因此Memcached 是一個非常不錯的快取,但是做不了持久化儲存。

Redis則提供了幾種對fsync呼叫頻率的控制方法。應用開發者可以配置Redis在每次更新操作後都執行一次fsync,這樣會比較安全,當然也就比較慢。Redis也可以設定成N秒種呼叫一次fsync,這樣效能會更好一點。但這樣的後果就是一旦出現故障,最多可能導致N秒內的資料丟失。而對一些可靠性要求不太高的場合(比如僅僅把Redis當Cache用的時候),應用開發者甚至可以直接關掉fsync的呼叫:讓作業系統來決定什麼時候需要把資料flush到磁碟。

(這只是Redis append only file的機制,Redis是可以關閉aof日誌的,另外請注意:Redis 本身支援將記憶體中資料dump成rdb檔案的機制)

13.3.1.2 Increase Sequential Writes by Logging

像B+ 樹這樣的一些資料結構,使得NoSQL系統能夠快速的定位到磁碟上的資料,但是通常這些資料結構的更新操作是隨機寫操作,如果你在每次操作後再呼叫一次fsync,那就會造成頻繁的磁碟隨機訪問了。為了避免產生這樣的問題,像Cassandra、HBase、Redis和Riak都會把寫操作順序的寫入到一個日誌檔案中。相對於儲存系統中的其它資料結構,上面說到的日誌檔案可以頻繁的進行fsync操作。這個日誌檔案記錄了操作行為,可以用於在出現故障後恢復丟失那段時間的資料,這樣就把隨機寫變成順序寫了。

雖然有的NoSQL系統,比如MongoDB,是直接在原資料上進行更新操作的,但也有許多NoSQL系統是採用了上面說到的日誌策略。Cassandra和HBase借鑑了BigTable的做法,在資料結構上實現了一個日誌型的查詢樹。Riak也使用了類似的方法實現了一個日誌型的hash表(也就是Riak的BitCask模型)。CouchDB對傳統的B+樹結構進行了修改,使得對樹的更新可以使用順序的追加寫操作來實現(這種B+樹被稱作append-only B-Tree)。這些方法的使用,使得儲存系統的寫操作承載力更高,但同時為了防止資料異構後膨脹得過大,需要定時進行一些合併操作。

13.3.1.3 Increase Throughput by Grouping Writes

Cassandra有一個機制,它會把一小段時間內的幾個併發的寫操作放在一起進行一次fsync呼叫。這種做法叫group commit,它導致的一個結果就是更新操作的返回時間可能會變長,因為一個更新操作需要等就近的幾個更新操作一起進行提交。這樣做的好處是能夠提高寫操作承載力。作為HBase底層資料支援的Hadoop 分散式檔案系統HDFS,它最近的一些補丁也在實現一些順序寫和group commit的機制。

13.3.2. Multi-server Durability

由於硬體層面有時候會造成無法恢復的損壞,單機可靠性的保證在這方面就鞭長莫及了。對於一些重要資料,跨機器做備份儲存是必備的安全措施。一些NoSQL系統提供了多機可靠性的支援。

Redis採用了傳統的主從資料同步的方式。所有在master上執行的操作,都會通過類似於操作日誌的結構順序地傳遞給slave上再執行一遍。如果master發生宕機等事故,slave可以繼續執行完master傳來的操作日誌並且成為新的master。可能這中間會導致一些資料丟失,因為master同步操作到slave是非阻塞的,master並不知道操作是否已經同步線slave了。CouchDB 實現了一個類似的指向性的同步功能,它使得一個寫操作可以同步到其它節點上。

MongoDB提供了一個叫Replica Sets的架構方制,這個架構策略使得每一個文件都會儲存在組成Replica Sets的所有機器上。MongoDB提供了一些選項,讓開發者可以確定一個寫操作是否已經同步到了所有節點上,也可以在節點資料並不是最新的情況下執行一些操作。很多其它的分散式NoSQL儲存都提供了類似的多機可靠性支援。由於HBase的底層儲存是HDFS,它也就自然的獲得了HDFS提供的多機可靠性保證。HDFS的多機可靠性保證是通過把每個寫操作都同步到兩個以上的節點來實現的。

Riak、Cassandra和Voldemort提供了一些更靈活的可配置策略。這三個系統提供一個可配置的引數N,代表每一個數據會被備份的份數,然後還可以配置一個引數W,代表每個寫操作需要同步到多少能機器上才返回成功。當然W是小於N的。

為了應對整個資料中心出現故障的情況,需要實現跨資料中心的多機備份功能。Cassandra、HBase和Voldemort都實現了一個機架位置可知的配置,這種配置方式使得整個分散式系統可以瞭解各個節點的地理位置分佈情況。如果一個操作需要等待另外的資料中心的同步操作成功才返回給使用者,那時間就太長了,所以通常跨資料中心的同步備份操作都是非同步進行的。使用者並不需要等待另一個數據中心同步的同步操作執行成功。

13.4. Scaling for Performance

上面我們討論了對出錯情況的處理,下面我們討論另一種情況:成功!如果資料儲存操作成功執行了,那麼你的系統就要負責對這些資料進行處理。就要承擔資料帶來的負載。一個比較粗暴的解決方法是通過升級你的機器來提升單機效能:通過加大記憶體和新增硬碟來應對越來越大的負載。但是隨著資料量的增大,投入在升級機器上的錢將將不會產生原來那麼大的效果。這時候你就必須考慮把資料同步到不同的機器上,利用橫向擴充套件來分擔訪問壓力。

橫向擴充套件的目標是達到線性的效果,即如果你增加一倍的機器,那麼負載能力應該也能相應的增加一倍。其主要需要解決的問題是如何讓資料在多臺機器間分佈。資料分片技術實際上就是將對資料和讀寫請求在多個機器節點上進行分配的技術,分片技術在很多NoSQL系統中都有實現,比如Cassandra、HBase、Voldemort和Riak等等,最近MongoDB和Redis也在做相應的實現。而有的專案並不提供內建的分片支援,比如CouchDB更加註重單機效能的提升。對於這些專案,通常我們可以藉助一些其它的技術在上層來進行負載分配。

下面讓我們來整理一些通用的概念。對於資料分片和分割槽,我們可以做同樣的理解。對機器,伺服器或者節點,我們可以統一的理解成物理上的儲存資料的機器。最後,叢集或者機器環都可以理解成為是組成你儲存系統的集合。

分片的意思是,沒有任何一臺機器可以處理所有寫請求,也沒有任何一臺機器可以處理對所有資料的讀請求。很多NoSQL系統都是基於鍵值模型的,因此其查詢條件也基本上是基於鍵值的查詢,基本不會有對整個資料進行查詢的時候。由於基本上所有的查詢操作都是基本鍵值形式的,因此分片通常也基於資料的鍵來做:鍵的一些屬性會決定這個鍵值對儲存在哪臺機器上。下面我們將會對hash分片和範圍分片兩種分片方式進行描述。

13.4.1. Do Not Shard Until You Have To

分片原則:分片會導致系統複雜程式大增,所以,如果沒有必要,請不要使用分片。下面我們先講兩種不用分片就能讓系統具有擴充套件性的方法。

13.4.1.1 Read Replicas

大多數應用場景都是讀多寫少的場景。所以在這種情況下,可以用一個簡單的方法來分擔負載,就是把資料同步到多臺機器上。這時候寫請求還是由master機器處理,而讀請求則可以分擔給那些同步到資料的機器了。而同步資料的操作,通常是不會對master帶來多大的壓力的。

如果你已經使用了主從配置,將資料同步到多臺機器以提供高可靠性了,那麼你的slave機器應該能夠為master分擔不少壓力了。對有些實時性要求不是非常高的查詢請求,比如一些統計操作,你完全可以放到slave上來執行。通常來說,你的應用對實時性要求越低,你的slave機器就能承擔越多的任務。

13.4.1.1 Caching

將一些經常訪問的資料放到快取層中,通常會帶來很好的效果。Memcached 主要的作用就是將資料層的資料進行分散式的快取。Memcached 通過客戶端的演算法(常見的一致性hash演算法)來實現橫向擴充套件,這樣當你想增大你快取池的大小時,只需要新增一臺新的快取機器即可。

由於Memcached僅僅是一個快取儲存,它並不具備一些持久儲存的複雜特性。當你在考慮使用複雜的擴充套件方案時,希望你先考慮一下使用快取來解決你的負載問題。注意,快取並不是臨時的處理方案:Facebook 就部署了總容量達到幾十TB的Memcahced記憶體池。

通過讀寫分離和構建有效的快取層,通常可以大大分擔系統的讀負載,但是當你的寫請求越來越頻繁的時候,你的master機器還是會承受越來越大的壓力。對於這種情況,我們可能就要用到下面說到的資料分片技術了。

13.4.2. Sharding Through Coordinators

通過協調器進行分片示例:

由於CouchDB專注於單機效能,沒有提供類似的橫向擴充套件方案,於是出現了兩個專案:Lounge 和 BigCouch,他們通過提供一個proxy層來對CouchDB中的資料進行分片。在這種架構中,proxy作為CouchDB叢集的前端機器,接受和分配請求到後端的多臺CouchDB上。後端的CouchDB 之間並沒有互動。協調器會將按操作的key值將請求分配到下層的具體某臺機器。

Twitter 自己實現了一個叫Gizzard的協調器,可以實現資料分片和備份功能。Gizzard不關心資料型別,它使用樹結構來儲存資料範圍標識,你可以用它來對SQL或者NoSQL系統進行封裝。通過對 Gizzard 進行配置,可以實現將特定範圍內的資料進行冗餘儲存,以提高系統的容災能力。

13.4.3. Consistent Hash Rings

好的hash演算法可以使資料保持比較均勻的分佈。這使得我們可以按這種分佈將資料儲存布多臺機器上。一致性hash是一種被廣泛應用的技術,其最早在一個叫distributed hash tables (DHTs)的系統中進行使用。那些類Dynamo的應用,比如Cassandra、Voldemort和Riak,基本上都使用了一致性hash演算法。

13.4.3.1 Hash Rings by Example

Figure 13.1: A Distributed Hash Table Ring

一致性hash演算法的工作原理如下:首先我們有一個hash函式H,可以通過資料的key值計算出一個數字型的hash值。然後我們將整個hash環的範圍定義為[1,L]這個區間,我們將剛才算出的hash值對L進行取餘,就能算出一個key值在這個環上的位置。而每一臺真實伺服器結點就會負責[1-L]之間的某個區間的資料。如上圖,就是一個五個結點的hash環。

上面hash環的L值為1000,然後我們對ABCDE 5個點分別進行hash運算,H(A) mod L = 7, H(B) mod L = 234, H(C) mod L = 447, H(D) mod L = 660, and H(E) mod L = 875 ,這樣,hash值在7-233之間的所有資料,我們都讓它儲存在A節點上。在實際動作中,我們對資料進行hash,算出其應該在哪個節點儲存即可,例:H(‘employee30′) mod L = 899 那麼它應該在E節點上,H(‘employee31′) mod L = 234 那麼這個資料應該在B節點上。

13.4.3.2 Replicating Data

一致性hash下的資料備份通常採用下面的方法:將資料冗餘的存在其歸屬的節點的順序往下的節點,例如你的冗餘係數為3(即資料會在不同節點中儲存三份),那麼如果通過hash計算你的資料在A區間[7,233],你的資料會被同時儲存在A,B,C三個節點上。這樣如果A節點出現故障,那麼B,C節點就能處理這部分資料的請求了。而某些設計會使E節點將自己的範圍擴大到A233,以接受對出故障的A節點的請求。

13.4.3.3 Achieving Better Distribution

雖然hash演算法能夠產生相對均勻的hash值。而且通常是節點數量越多,hash演算法會越平均的分配key值。然而通常在專案初期不會有太多的資料,當然也不需要那麼多的機器節點,這時候就會造成資料分配不平均的問題。比如上面的5個節點,其中A節點需要負責的hash區間範圍大小為227,而E節點負責的區間範圍為132。同時在這種情況下,出故障後資料請求轉移到相鄰節點的策略也可能不好實施了。

為了解決由於節點比較少導致資料分配不均的問題,很多DHT系統都實現了一種叫做虛擬節點的技術。例如4個虛擬節點的系統中,A節點可能被虛擬化成A_1,A_2,A_3,A_4這四個虛擬節點,然後對這四個虛擬節點再進行hash運算,A節點負責的key值區間就比較分散了。Voldemort 使用了與上面類似的策略,它允許對虛擬節點數進行配置,通常這個節點數會大於真實節點數,這樣每個真實節點實際上是負責了N個虛擬節點上的資料。

Cassandra 並沒有使用虛擬節點到真實節點對映的方法。這導致它的資料分配是不均勻的。為了解決這種不平衡,Cassandra 利用一個非同步的程序根據各節點的歷史負載情況來調節資料的分佈。

13.4.4. Range Partitioning

使用連續範圍分割槽的方法進行資料分片,需要我們儲存一份對映關係表,標明哪一段key值對應存在哪臺機器上。和一致性hash類似,連續範圍分割槽會把key值按連續的範圍分段,每段資料會被指定儲存在某個節點上,然後會被冗餘備份到其它的節點。和一致性hash不同的是,連續範圍分割槽使得key值上相鄰的兩個資料在儲存上也基本上是在同一個資料段。這樣資料路由表只需記錄某段資料的開始和結束點[start,end]就可以了。

通過動態調整資料段到機器結點的對映關係,可以更精確的平衡各節點機器負載。如果某個區段的資料負載比較大,那麼負載控制器就可以通過縮短其所在節點負責的資料段,或者直接減少其負責的資料分片數目。通過新增這樣一個監控和路由模組,使我們能夠更好的對資料節點進行負載均衡。

13.4.4.1 The BigTable Way

Google BigTable 論文中描述了一種範圍分割槽方式,它將資料切分成一個個的tablet資料塊。每個tablet儲存一定數量的鍵值對。然後每個Tablet 伺服器會儲存多個tablet塊,具體每個Tablet伺服器儲存的tablet資料塊數,則是由伺服器壓力來決定的。

每個tablet大概100-200MB大。如果tablet的尺寸變小,那麼兩個tablet可能會合併成一個tablet,同樣的如果一個tablet過大,它也會被分裂成兩個tablet,以保持每個tablet的大小在一定範圍內。在整個系統中有一個master機器,會根據tablet的大小、負載情況以及機器的負載能力等因素動態地調整tablet在各個機器上的分佈。

Figure 13.2: BigTable-based Range Partitioning

master伺服器會把 tablet 的歸屬關係存在元資料表裡。當資料量非常大時,這個元資料表實際也會變得非常大,所以歸屬關係表實際上也是被切分成一個個的tablet儲存在tablet伺服器中的。這樣整個資料儲存就被分成了如上圖的三層模型。

下面我們解釋一下上面圖中的例子。當某個客戶端要從BigTable系統中獲取key值為900的資料時,首先他會到第一級元資料伺服器A(METADATA0)去查詢,第一級元資料伺服器查詢自己的元資料表,500-1500這個區間中的所有元資料都存在B伺服器中,於是會返回客戶端說去B伺服器查詢,客戶端再到B伺服器中進行查詢,B伺服器判斷到850-950這個區間中的資料都存在tablet伺服器C中,於是會告知客戶端到具體的tablet伺服器C去查詢。然後客戶端再發起一次向C伺服器的請求,就能獲取到900對應的資料了。然後,客戶端會把這個查詢結果進行快取,以避免對元資料伺服器的頻繁請求。在這樣的三層架構下,只需要128MB的元資料儲存,就能定位234 個tablet資料塊了(按128MB一個數據塊算,是261bytes的資料)。

13.4.4.2 Handling Failures

在BigTable中,master機器是一個故障單點,不過系統可以容忍短時間的master故障。另一方面,如果tablet 伺服器故障,那麼master可以把對其上tablet的所有請求分配到其它機器節點。

為了監測和處理節點故障,BigTable實現了一個叫Chubby的模組,Chubby是一個分散式的鎖系統,用於管理叢集成員及檢測各成員是否存活。ZooKeeper是Chubby的一個開源實現,有很多基於 Hadoop 的專案都使用它來進行二級master和tablet節點的排程。

13.4.4.3 Range Partitioning-based NoSQL Projects

HHBase 借鑑了BigTable的分層理論來實現範圍分割槽策略。tablet相關的資料存在HDFS裡。HDFS 會處理資料的冗餘備份,並負責保證各備份的一致性。而像處理資料請求,修改儲存結構或者執行tablet的分裂和合並這種事,是具體的tablet伺服器來負責的。

MongoDB也用了類似於BigTable的方案來實現範圍分割槽。他用幾臺配置機器組成叢集來管理資料在節點上的分佈。這幾臺機器儲存著一樣的配置資訊,他們採用 two-phase commit 協議來保證資料的一致性。這些配置節點實際上同時扮演了BigTable中的master的路由角色,及Chubby 的高可用性排程器的角色。而MongoDB具體的資料儲存節點是通過其Replica Sets方案來實現資料冗餘備份的。

Cassandra 提供了一個有序的分割槽表,使你可以快速對資料進行範圍查詢。Cassandra也使用了一致性hash演算法進行資料分配,但是不同的是,它不是直接按單條資料進行hash,而是對一段範圍內的資料進行hash,也就是說20號資料和21號資料基本上會被分配在同一臺機器節點上。

Twitter的Gizzard框架也是通過使用範圍分割槽來管理資料在多個節點間的備份與分配。路由伺服器可以部署成多層,任何一層只負責對key值進行按範圍的分配到下層的不同節點。也就是說路由伺服器的下層既可以是真實的資料儲存節點,也可能是下層的路由節點。Gizzard的資料備份機制是通過將寫操作在多臺機器上執行多次來實現的。Gizzard的路由節點處理失敗的寫操作的方式和其它NoSQL不太一樣,他要求所有更新都是冪等的(意思是可以重複執行而不會出錯)。於是當一個節點故障後,其上層的路由節點會把當前的寫操作cache起來並且重複地讓這個節點執行,直到其恢復正常。

13.4.5. Which Partitioning Scheme to Use

上面我們說到了Hash分割槽和範圍分割槽兩種策略,哪種更好呢?這要看情況了,如果你需要經常做範圍查詢,需要按順序對key值進行操作,那麼你選擇範圍分割槽會比較好。因為如果選擇hash分割槽的話,要查詢一個範圍的資料可能就需要跨好幾個節點來進行了。

那如果我不會進行範圍查詢或者順序查詢呢?這時候hash分割槽相對來說可能更方便一點,而且hash分割槽時可能通過虛擬結點的設定來解決hash不均的問題。在hash分割槽中,基本上只要在客戶端執行相應的hash函式就能知道對應的資料存在哪個節點上了。而如果考慮到節點故障後的資料轉移情況,可能獲取到資料存放節點就會麻煩一些了。

範圍分割槽要求在查詢資料前對配置節點還要進行一次查詢,如果沒有特別好的高可用容災方案,配置節點將會是一個危險的故障單點。當然,你可以把配置節點再進行一層負載均衡來減輕負載。而範圍分割槽時如果某個節點故障了,它上面的資料可以被分配到多個節點上,而不像在一致性hash時,只能遷移到其順序的後一個節點,造成下一個節點的負載飆升。

13.5. Consistency

上面我們講到了通過將資料冗餘儲存到不同的節點來保證資料安全和減輕負載,下面我們來看看這樣做引發的一個問題:保證資料在多個節點間的一致性是非常困難的。在實際應用中我們會遇到很多困難,同步節點可能會故障,甚至會無法恢復,網路可能會有延遲或者丟包,網路原因導致叢集中的機器被分隔成兩個不能互通的子域等等。在NoSQL中,通常有兩個層次的一致性:第一種是強一致性,既叢集中的所有機器狀態同步保持一致。第二種是最終一致性,既可以允許短暫的資料不一致,但資料最終會保持一致。我們先來講一下,在分散式叢集中,為什麼最終一致性通常是更合理的選擇,然後再來討論兩種一致性的具體實現結節。

13.5.1. A Little Bit About CAP

為什麼我們會考慮削弱資料的一致性呢?其實這背後有一個關於分散式系統的理論依據。這個理論最早被 Eric Brewer 提出,稱為CAP理論,爾後Gilbert 和 Lynch 對CAP進行了理論證明。這一理論首先把分散式系統中的三個特性進行了如下歸納:

  • Consistency: do all replicas of a piece of data always logically agree on the same version of that data by the time you read it? (This concept of consistency is different than the C in ACID.)
  • Availability: Do replicas respond to read and write requests regardless of how many replicas are inaccessible?
  • Partition tolerance: Can the system continue to operate even if some replicas temporarily lose the ability to communicate with each other over the network?

而CAP理論就是說在分散式儲存系統中,最多隻能實現上面的兩點。而由於當前的網路硬體肯定會出現延遲丟包等問題,所以分割槽容忍性是我們必須需要實現的。所以我們只能在一致性和可用性之間進行權衡,沒有NoSQL系統能同時保證這三點。

要保證資料一致性,最簡單的方法是令寫操作在所有資料節點上都執行成功才能返回成功。而這時如果某個結點出現故障,那麼寫操作就成功不了了,需要一直等到這個節點恢復。也就是說,如果要保證強一致性,那麼就無法提供7×24的高可用性。

而要保證可用性的話,就意味著節點在響應請求時,不用完全考慮整個叢集中的資料是否一致。只需要以自己當前的狀態進行請求響應。由於並不保證寫操作在所有節點都寫成功,這可能會導致各個節點的資料狀態不一致。

CAP理論導致了最終一致性和強一致性兩種選擇。當然,事實上還有其它的選擇,比如在Yahoo! 的PNUTS中,採用的就是鬆散的一致性和弱可用性結合的方法。但是我們討論的NoSQL系統沒有類似的實現,所以我們在後續不會對其進行討論。

13.5.2. Strong Consistency

強一致性的保證,要求所有資料節點對同一個key值在同一時刻有同樣的value值。雖然實際上可能某些節點儲存的值是不一樣的,但是作為一個整體,當客戶端發起對某個key的資料請求時,整個叢集對這個key對應的資料會達成一致。下面就舉例說明這種一致性是如何實現的。

假設在我們的叢集中,一個數據會被備份到N個結點。這N個節點中的某一個可能會扮演協調器的作用。它會保證每一個數據寫操作會在成功同步到W個節點後才向客戶端返回成功。而當客戶端讀取資料時,需要至少R個節點返回同樣的資料才能返回讀操作成功。而NWR之間必須要滿足下面關係:R+W>N

下面舉個實在的例子。比如我們設定N=3(資料會備份到A、B、C三個結點)。比如值 employee30:salary 當前的值是20000,我們想將其修改為30000。我們設定W=2,下面我們會對A、B、C三個節點發起寫操作(employee30:salary, 30000),當A、B兩個節點返回寫成功後,協調器就會返回給客戶端說寫成功了。至於節點C,我們可以假設它從來沒有收到這個寫請求,他儲存的依然是20000那個值。之後,當一個協調器執行一個對employee30:salary的讀操作時,他還是會發三個請求給A、B、C三個節點:

    如果設定R=1,那麼當C節點先返回了20000這個值時,那我們客戶端實際得到了一個錯誤的值。

    如果設定R=2,則當協調器收到20000和30000兩個值時,它會發現資料不太正確,並且會在收到第三個節點的30000的值後判斷20000這個值是錯誤的。

所以如果要保證強一致性,在上面的應用場景中,我們需要設定R=2,W=2

如果寫操作不能收到W個節點的成功返回,或者寫操作不能得到R個一致的結果。那麼協調器可能會在某個設定的過期時間之後向客戶端返回操作失敗,或者是等到系統慢慢調整到一致。這可能就導致系統暫時處於不可用狀態。

對於R和W的不同設定,會導致系統在進行不同操作時需要不同數量的機器節點可用。比如你設定在所有備份節點上都寫入才算寫成功,既W=N,那麼只要有一個備份節點故障,寫操作就失敗了。一般設定是R+W = N+1,這是保證強一致性的最小設定了。一些強一致性的系統設定W=N,R=1,這樣就根本不用考慮各個節點資料可能不一致的情況了。

HBase是藉助其底層的HDFS來實現其資料冗餘備份的。HDFS採用的就是強一致性保證。在資料沒有完全同步到N個節點前,寫操作是不會返回成功的。也就是說它的W=N,而讀操作只需要讀到一個值即可,也就是說它R=1。為了不至於讓寫操作太慢,對多個節點的寫操作是併發非同步進行的。在直到所有的節點都收到了新的資料後,會自動執行一個swap操作將新資料寫入。這個操作是原子性和一致性的。保證了資料在所有節點有一致的值。

13.5.3. Eventual Consistency

像Voldemort,Cassandra和Riak這些類Dynamo的系統,通常都允許使用者按需要設定N,R,W三個值,即使是設定成W+R<= N也是可以的。也就是說他允許使用者在強一致性和最終一致性之間自由選擇。而在使用者選擇了最終一致性,或者是W<N的強一致性時,則總會出現一段各個節點資料不同步導致系統處理不一致的時間。為了提供最終一致性的支援,這些系統會提供一些工具來使資料更新被最終同步到所有相關節點。

下面我們會先講一講如何判斷資料是最新的還是陳舊的,然後我們再討論一下如何進行資料同步,最後我們再列舉一些Dynamo裡使用的加速同步過程的巧妙方法。

13.5.3.1  Versioning and Conflicts

由於同一份資料在不同的節點可能存在不同值,對資料的版本控制和衝突監測就變得尤為重要。類Dynamo的系統通常都使用了一種叫vector clock(向量時鐘)的版本控制機制。一個vector clock可以理解成是一個向量,它包含了這個值在每一個備份節點中修改的次數。比如說有的資料會備份到A,B,C三個節點,那麼這些值的vector clock值就是類似(NA,NB,NC),而其初始值為(0,0,0)。

每次一個key的值被修改,其vector clock相應的值就會加1。比如有一個key值當前的vector clock值為(39,1,5),然後他在B節點被修改了一次,那麼它的cector clock值就會相應的變成(39,2,5)。而當另一個節點C在收到B節點的同步請求時,他會先用自己儲存的vector clock值與B傳來的vector clock值進行對比,如果自己的vector clock值的每一項都小於等於B傳來的這個值,那麼說明這次的修改值是在自已儲存的值上的修改,不會有衝突,直接進行相應的修改,並把自己的vector clock值更新。但是如果C發現自己的vector clock有些項比B大,而某些項比B小,比如B的是(39,2,5)C的是(39,1,6),那麼這時候說明B的這次修改並不是在C的基礎上改的,資料出現衝突了。

13.5.3.2 Conflict Resolution

不同的系統有不同的衝突解決策略。Dynamo選擇把衝突留給應用層來解決。如果是兩個節點儲存的購物車資訊衝突了,可以選擇簡單的通過把兩個資料合併進行解決。但如果是對同一份文件進行的修改衝突了,可能就需要人工來解決衝突了(譯者:像我們在SVN中做的一樣)。Voldemort就是採用的後者,它在發現衝突後,會把有衝突的幾份資料一起返回給應用層,把衝突解決留給應用層來做。

Cassandra通過為每一個操作儲存一個時間戳的方法來解決衝突,在衝突的幾個版本里,最後修改的一個會獲勝成為新的值。相對於上面的方式,它減少了通過應用層解決衝突時需要的網路訪問,同時也簡化了客戶端的操作API。但這種策略並不適合用來處理一些需要合併衝突的場合,比如上面的購物車的例子,或者是分散式的計數器這樣的應用。而Riak把Voldemort和 Cassandra 的策略都實現了。CouchDB會把衝突的key進行標識,以便應用層可以主動進行人工修復,在修復完成前,客戶端的請求是無法得到確定值的。

13.5.3.3 Read Repair

在資料讀取時,如果有R個節點返回了一致的資料,那麼協調器就可以認為這個值是正確的並返回給客戶端了。但是在總共返回的N個值中,如果協調器發現有的資料不是最新的。那麼它可以通過讀時修復機制來對這些節點進行處理。這種方式在Dynamo中有描述,在Voldemort 、 Cassandra和Riak中都得到了實現。當協調器發現有的節點資料不是最新時,它會在資料不一致的節點間啟動一個衝突解決過程。這樣主動的修復策略並不會有多大的工作量。因為讀取操作時,各個節點都已經把資料返回給協調器了,所以解決衝突越快,實際上可能造成的後續的不一致的可能性也就越小。

13.5.3.4 Hinted Handoff

Cassandra、Riak和Voldemort都實現了一種叫Hinted Handoff的技術,用來保證在有節點故障後系統的寫操作不受太大影響。它的過程是如果負責某個key值的某個節點宕機了,另一個節點會被選擇作為其臨時切換點,以臨時儲存在故障節點上面的寫操作。這些寫操作被單獨儲存起來,直到故障節點恢復正常,臨時節點會把這些寫操作重新遷移給剛剛恢復的節點。Dynamo 論文中提到一種叫“sloppy quorum”的方法,它會把通過 Hinted Handoff 寫成功的臨時節點也計算在成功寫入數中。但是Cassandra和Voldemort並不會將臨時節點也算在寫入成功節點數內,如果寫入操作並沒有成功寫在W個正式節點中,它們會返回寫入失敗。當然,Hinted Handoff 策略在這些系統中也有使用,不過只是用在加速節點恢復上。

13.5.3.5 Anti-Entropy

如果一個節點故障時間太長,或者是其 Hinted Handoff 臨時替代節點也故障了,那麼新恢復的節點就需要從其它節點中同步資料了。(譯者:實際上就是要找出經過這段時間造成的資料差異,並將差異部分同步過來)。這種情況下Cassandra和Riak都實現了在Dynamo文件中提到的一種方法,叫做anti-entropy。在anti-entropy過程中,節點間通過交換Merkle Tree來找出那些不一致的部分。Merkle Tree是一個分層的hash校驗機制:如果包含某個key值範圍的hash值在兩個資料集中不相同,那麼不同點就在這個key值範圍,同理,如果頂層的hash值相同,那麼其負責的所有key值範圍內的值都認為是相同的。這種方法的好處是,在節點恢復時,不用把所有的值都傳一遍來檢查哪些值是有變化的。只需要傳幾個hash值就能找到不一致的資料,重傳這個資料即可。

13.5.3.6