設計數據密集型應用第二部分:分佈式系統的機遇與挑戰

  在《Designing Data-Intensive Applications》的第一部分(參考上文),介紹了數據系統的基礎理論與知識,都是基於single node。而在DDIA的第二部分(Distributed Data),則是將視野擴展到了分佈式數據系統。數據的分佈式主要有如下三個緣由:html

  • Scalability
  • Fault tolerance/high availability
  • Reduce latency

  當負載增長的時候,有兩種應對方式,scale up vs scale out,前者指使用更強大但昂貴的設備:更快更多核的CPU、更大的RAM、更大容量更快讀寫速度的磁盤,這中shared-memory的形式,不只造價昂貴,並且容錯性較差。然後者,是分佈式數據系統採用的shared-nothing 架構,經過增長普通機器節點(node)來應對負載的增長,這也是目前主流的應對大容量數據的方式。node

  如何將數據分佈在多個節點上,有兩種方式,replication and partition。《Distributed systems for fun and profit 》中這個圖形象說明了這兩種方式:python

     

  固然,分佈式系統並非銀彈,分佈式在帶來可擴展性、高可用性的同時,也帶來了諸多挑戰,如分佈式事務,共識。算法

replication

  如上圖所示,replication(複製集)就是將一份數據(副本)保存在多個節點上,數據的冗餘有如下好處sql

  • reduce latency:To keep data geographically close to your users
  • increase availability:To allow the system to continue working even if some of its parts have failed
  • increase read throughput : To scale out the number of machines that can serve read queries

  複製集的最大挑戰在於數據的一致性:如何在必定的約束條件下保證複製集中全部副本的數據是一致的。按照在複製集中的不一樣角色(Leader、Follower),有三種算法 single leader, multi leader, no leader。其中,關於中心化的複製集協議(single leader)我在帶着問題學習分佈式之中心化複製集一文中已經有較爲詳細的介紹.mongodb

Single leader

  中心化複製集協議須要考慮如下問題:數據庫

  (1)數據在多節節點間的寫入是同步仍是異步編程

  (2)新增的Follower(secondary)如何快速同步數據安全

  (3)如何處理節點的故障:對於Follower(Secondary)故障,須要catch up; 對於Leader(Primary)故障,須要選舉出新的Leader,如何判斷Leader故障,如何保證在Leader Failover的過程當中不丟失數據,以及避免腦裂(同時存在多個Leader)都是挑戰。網絡

 

  不少狀況下,數據的異步寫入是更好的方式,由於有更好的可用性,併發量更高。但異步寫入,須要處理replication lag問題,即Leader與Follower之間的數據延遲,這樣用戶經過複製集中不一樣節點讀取到的數據多是不一致的。下面針對幾種具體的狀況下,來看看如何保證必定程度上的一致。

Reading Your Own Writes

  用戶可以查詢到本身成功更新的內容,但並不關心別的用戶可否當即查詢。這就須要read-after-write consistency

  實現方法:

  (1)當讀取的是可能被用戶修改的內容是,從leader讀取,不然能夠從follower

  (2)記錄更新時間,超過必定時間則從follower讀

Monotonic reads: (單調讀)

only means that if one user makes several reads in sequence, they will not see time go backward

  即一個用戶若是讀到了新版本的數據,那麼重複讀取的時候,不能讀到舊版本的數據

  實現辦法:

  (1)每一個用戶從固定一個副本讀取

Consistent prefix reads 

  因果關係:好比」問一個問題「與」回答該問題「,必定是前者先發生。但在複製集多個節點間異步通訊的時候,第三者(Observer)可能先看到答案,後看到問題,這就違背的因果性。如圖所示:

  

This guarantee says that if a sequence of writes happens in a certain order, then anyone reading those writes will see them appear in the same order.

One solution is to make sure that any writes that are causally related to each other are written to the same partition

  如圖所示,這個問題在單個複製集(單個partition)中是不會出現的,只有partitioned (sharded)的環境下才會出現。

  解決辦法:

  (1)有因果關係的操做路由到同一個partition

Leaderless Replication

  Leaderless Replication,去中心化的副本協議,就是說副本集中沒有中心節點,全部節點的地位是平等的,你們均可以接受更新請求,相互經過協商達成數據的一致。在Amazon的Dynamo: Amazon’s Highly Available Key-value Store及其開源實現(Riak, Cassandra, and Voldemort)中就使用了Leaderless replication。

  Leaderless的最大優勢在於高可用性,不會由於單個(少數)節點的故障致使系統的不可用,高可用性的核心在於Quorum協議:複製集中節點數目爲N,當一份數據成功寫入到W個節點,每次讀取的時候獲得R個節點的返回,只要W + R > N,那麼R中就必定包含最新的數據。以下圖所示:

  

  事實上,每次寫入或者讀取的時候都是發給全部的節點,可是隻用等到W(R)個節點的成功返回便可通知客戶端結果。

  如上圖所示,Node 3(replica 3)因爲數據寫入時故障,返回了過期的數據,數據系統須要使複製集的數據趨於一致,達到最終一致性。有兩種方法

  read repair: 讀取的時候多讀幾個replica的數據,修復過期的數據。

  Anti-entropy process : 後臺進程檢查差別

 

  Quorum並非萬能的,在Leaderless中,即便使用了Quorum,還有如下潛在的問題

  • 在不一樣節點的併發寫致使的衝突,這是Leaderless最大的挑戰
  • 在讀寫併發的狀況下,缺少隔離性,可能讀取到舊的數據
  • 寫失敗時(少於w個節點寫入成功),不會回滾

Detecting Concurrent Writes

  leader less下併發寫會可能衝突,在 read-repair 或者 hinted handoff 的時候也可能產生衝突。下面是一個衝突的示例:

  

  在併發的狀況下,若是每一個節點收到請求就寫數據,那麼複製集就沒法達成一致,如上圖所示,不一樣節點數據是不一致的。如何解決併發衝突,其中一種方式是 Last write win,cassandra就是這麼解決衝突的,做用前提:準確無誤判斷recent;每個寫操做拷貝到全部的副本。缺點是存在數據丟失的狀況:在寫入W份告知客戶端寫入成功的前提下,某些寫入會被silently discard

The 「happens-before」 relationship and concurrency

  如何判斷兩個操做是否是併發:有沒有happened before關係

An operation A happens before another operation B if B knows about A, or depends on A, or builds upon A in some way.

In fact, we can simply say that two operations are concurrent if neither happens before the other

  若是存在happened before:那麼後者覆蓋前者是可行的;只有concurrent纔會有衝突。

  使用version vector來判斷多個寫操做的依賴關係。

 

Partition

  關於Partitioning(Sharding),我在帶着問題學習分佈式系統之數據分片一文中也有詳細介紹,可供參考。所以在本章節,只補充新知識。

  Partitioning的主要緣由是伸縮性(scalability)。如何對數據進行劃分,如何rebalance數據是Partition須要解決的兩個基礎問題。

  若是某個Partition上的數據或查詢比其餘Partition多,那麼稱這個現象爲skewed,高負載的Partition爲hot spot

Partitioning and Secondary Indexes

  partitioning是按照primary index來分片的,那麼secondary indexes是如何解決的呢

  two main approaches to partitioning a database with secondary indexes: document-based partitioning and term-based partitioning.

Partitioning Secondary Indexes by Document

each partition maintains its own secondary indexes, covering only the documents in that partition.

  每一個分片維護本身的輔助索引,只包含了在該分片上的數據的輔助索引信息。

a document-partitioned index is also known as a local index

  所以寫數據的時候只用修改本地的輔助索引文件。

  用輔助索引查詢時,查詢語句須要在全部分片上執行,並彙總(scatter-gather).以下圖所示,color就是一個輔助索引。

  

  local index很是使用普遍:MongoDB, Riak, Cassandra, Elasticsearch SolrCloud and VoltDB

Partitioning Secondary Indexes by Term

  

  也稱之爲global index,輔助索引數據也分片。

  相比Local index,優勢是使用輔助索引讀取數據時更高效(無需scatter gather) reads more efficient. 缺點是使得寫入操做變慢並且複雜(須要分佈式事務來保證)

Rebalancing Partitions

  Rebalance的目標是:

  1. rebalance以後 各節點間負載均衡

  2. rebalance不影響(不中斷)讀寫服務

  3. 節點間遷移數據很少很多(不要多)

Request Routing

  分片環境下,客戶端如何得知改與哪一個節點通訊。

  This is an instance of a more general problem called service discovery

  方式:

  (1)客戶端鏈接任一節點,若是該節點不能處理請求,那麼轉發到正確的節點

  (2)客戶端發送請求到路由(routing tier)

   This routing tier does not itself handle any requests; it only acts as a partition-aware load balancer.

  (3)客戶端知道分片信息與節點的映射關係

  

transaction

  事務是在軟硬件出現各類異常(fault)的狀況下,提高系統可靠性(reliable)的重要手段。

A transaction is a way for an application to group several reads and writes together into a logical unit.

Conceptually, all the reads and writes in a transaction are executed as one operation: either the entire transaction succeeds (commit) or it fails (abort, rollback).

  組成一個事務的多個操做要麼都成功(commit),要麼都不執行(rollback、abort),而不會存在部分執行成功的狀況,即 all-or-nothing。

  事務簡化了應用層對異常的處理,系統是否須要事務,取決於事務帶來的安全性保障,以及對應的代價。傳統的關係型數據庫都會選擇支持事務,而在分佈式數據庫,如Nosql中,則(部分)放棄了對事務的支持,緣由在於事務是可伸縮性的對立面,會影響大型系統的性能與可靠性。

  當咱們談論事務的時候,通常都是指事務的ACID特性。

  一個數據庫對ACID的實現(甚至是理解)是不必定等同於其餘數據庫的,其中,最複雜的是Isolation(隔離性)。

  隔離性是指併發的兩個事務的執行互不干擾,一個事務不能看到其餘事務運行過程的中間狀態。固然,併發的讀是不會相互干擾的,只有併發的讀寫、或者併發的寫,纔會帶來race condition。實現隔離性最好的方式是可串行化serializable,達到和順序執行同樣的效果,但這樣的方法存在性能問題。所以,數據庫提供不一樣的隔離性級別來兼顧隔離線與併發性能。

  關於隔離型這一部分,筆者打算另外寫一篇筆記。

The Trouble with Distributed Systems

  分佈式系統帶來了更多的挑戰,更多意向不到的錯誤和異常,除了單點系統的問題,分佈式系統還需應對的兩個難題是:

  • problems with networks
  • clocks and timing issues

  與單點系統不一樣的是,分佈式系統容易出現partial failure:即部分工做、部分異常。partial failure的最大問題是nondeterministic,不肯定性。分佈式系統須要在軟件層面實現容錯(fault tolerance),以應對partial failure。

Unreliable neteork vs Detecting Faults vs timeout

  分佈式系統使用的網絡是不可靠的,數據包可能丟失,可能延遲。並且丟失或者延遲既可能發生在request的路上,也可能發生在response的路上,這都是不肯定的。

  網絡消息其中一個重要的應用就是心跳。

  系統須要檢測到異常的節點,如load balancer須要監測到不工做的節點,如中心化複製集協議對leader的監測。

  在節點crash的時候,若是能準確判斷且通知到系統中的其餘節點,那最好不過。可是不少時候,沒法判斷一個節點是否crash,並且,一個節點雖然沒有crash但也沒法繼續工做,這個時候仍是得靠心跳超時,以前寫過這麼一篇文章《Hey,man,are you ok? -- 關於心跳、故障監測、lease機制》來介紹相關問題。

  當在網絡信息中使用超時時,超時時長是個問題:超時時間太長,那麼須要等很長時間;過短,又很容易容易誤判。

If the system is already struggling with high load, declaring nodes dead prematurely can make the problem worse. cascading failure

  而網絡延時在各類環境下變化又很大,擁塞控制致使發送方排隊、網絡交換機排隊、虛擬機管理排隊、CPU忙時排隊、多租戶環境下(超賣)受其餘服務影響都有可能影響到網絡延時。比較厲害的就是根據網絡延時自動調節的超時時間,如Phi Accrual failure detector , TCP的超時重傳就使用了相似的思想。

Unreliable Clocks vs Process pause

  時間很重要,由於時間意味着:order,duration,points in time。

  咱們經常使用的時間,即time-of-day(wall-clock time).:是指根據某種日曆返回的時間。在程序中,wall-clock time存在一些問題

  • NTP可能致使時間回退
  • 一般會忽略閏秒

  所以wall-clock time不適合衡量時間差(measuring elapsed time)

  所以,操做系統提供了另外一種時間Monotonic clocks,如Linux上的clock_gettime(CLOCK_MONOTONIC),Monotonic clocks保證了時間不會jump back。

 

  當分佈式系統中各個節點的時鐘不一致時,會出現各類問題,如一個經常使用但容易出問題的場景:用時間(timestamp)來判斷多個節點上事件發生的順序

  

  在LeaderLess複製集中,last write win(lww)是解決併發衝突的一個方法,若是這個時候不一樣節點數據不一致,可能致使數據被 悄無聲息 地丟失。

  即便使用了NTP,也沒法徹底保障各節點間數據的一致。一種有意思的想法是使用置信區間:

Clock readings have a confidence interval:

it doesn’t make sense to think of a clock reading as a point in time—it is more like a range of times, within a confidence interval:

  不少算法與協議,依賴對本地時間的判斷,如Lease,即便各節點的數據一致,在某些狀況下也會出問題,那就是Process Pause。

  好比,某段代碼執行前會去check lease,check的時候知足lease,而後發生了Process Pause,恢復的時候可能已經再也不知足lease了。由於不知道哪裏可能會pause,也就無從再次檢查

  什麼會致使Process Pause呢,不少:

  • gc
  • virtual machine can be suspended and resumed
  • 多線程
  • 磁盤IO: 非預期的disk access,如python import
  • swap
  • Unix SIGSTOP(Ctrl z)

  特色是gc這種stop the world的行爲,在有內存管理的編程語言Java、Python中時有發生。

  gc致使process Pause,在Hbase中就發生過,如圖所示:

  

  分佈式鎖的實現中,使用了lease,即便在stop-the-world-gc pause,client 1任然認爲本身持有lease,而事實上client 1持有的lease已通過期。所以在分佈式系統中:

The Truth Is Defined by the Majority。

A node cannot necessarily trust its own judgment of a situation.

  解決辦法很簡單:fencing token

   

System Model and Reality

  當咱們說起算法和協議,老是基於必定的系統模型,系統模型是算法工做環境的前提或者假設

  system model, which is an abstraction that describes what things an algorithm may assume。

  對於時間的假設:

  1. Synchronous model
  2. Partially synchronous model: 絕大多數是同步的,bounded;偶爾超出bound問題不大 ,依靠imeout機制
  3. Asynchronous model

  對於Node failure的假設

  1. Crash-stop faults
  2. Crash-recovery faults(nodes are assumed to have stable storage)
  3. Byzantine (arbitrary) faults
 

  如何衡量一個算法設計與實現是否正確呢:在系統模型下,所承諾的屬性( properties)都得以知足。好比unique屬性,好比事務中的Atomic屬性。

  屬性能夠分爲兩類:

Safety:nothing bad happens,

liveness: something good eventually happens.

  分佈式算法,在任何系統模型下,都須要知足safety屬性

For distributed algorithms, it is common to require that safety properties always hold, in all possible situations of a system model,However, with liveness properties we are allowed to make caveats:

Consistency and Consensus

  本章討論在分佈式系統中的容錯算法、協議。

  構建容錯性系統的最好方式是:找出並實現通用的抽象模型(這些抽象解決一類問題),這樣應用層代碼就無需考慮、處理這些問題,即便發生各類異常。如數據庫提供的事務。在分佈式系統中:重要的抽象就是共識 consensus: that is, getting all of the nodes to agree on something。

Linearizability & Causality

  在CAP理論與MongoDB一致性、可用性的一些思考 一文中介紹過CAP理論,CAP理論是說對於分佈式數據存儲,最多隻能同時知足一致性(C,Consistency)、可用性(A, Availability)、分區容錯性(P,Partition Tolerance)中的二者。強一致性能保證對於每一次讀操做,要麼都可以讀到最新寫入的數據,要麼錯誤。

  linearizability 能實現強一致性,由於

make a system appear as if there were only one copy of the data, and all operations on it are atomic

  線性一致性是一個頗有用的特性:好比經過lock的形式來選舉leader,那麼鎖必須是線性的linearizable:;好比unqueness約束。

  不一樣的複製集協議可否保持線性呢?對於single leader:若是隻從leader讀數據,那麼基本上是線性的,也有例外,如數據被回滾,這個時候就不能保證線性。對於leaderless,理論上使用Quorum來保證線性,但實際中,也會出現非線性,以下圖所示

   

  這個圖說明了在知足quorum的狀況下,也不能保證線性,上圖是dirty read的狀況,另外若是出現部分節點寫失敗,讀取的時候也不能保證線性。

  linearizability其實就是強一致性,雖然linearizability容易理解,易於使用,但分佈式系統大多選擇不支持linearizability,緣由在於線性一致性容錯性差,性能也很差。

 

  在分佈式一致性語義下,線性就是隻有一份數據,且每一個操做在某個時間點原子性執行,這就意味着某種順序

  Linearizability是total order,只有一份拷貝,且操做原子性發生,全部操做都有相對順序。但事實上,不少操做是能夠併發執行的,只要相互不影響。

  Causality consistenc(因果一致性)是partial order,某些操做間是有順序的,其餘操做則是能夠併發的。

In fact, causal consistency is the strongest possible consistency model that does not slow down due to network delays, and remains available in the face of network failures 線性一致性代價大,且不少時候沒有必要

  要在多個節點間記錄因果順序是比較複雜的,具體參考 lamport timestamp
 

consensus & epoch & quorum

  上述的因果一致性並不能解決全部問題,好比當兩個用戶併發登記同一個username,是沒有因果的,但並不知足username的uniqueness約束,所以須要共識算法。共識就是幾個節點對某件事情達成一致,顯然共識能解決uniqueness constraint問題。初次以後,好比single leader的選舉,好比 分佈式事務的atomic commit,都須要共識。

  Two-Phase Commit (2PC)是實現分佈式事務的經典手段,經過2PC,也能實現共識。可是2PC的問題在於容錯性差,節點故障和網絡超時都會致使重試,直到節點或者網絡恢復

  consensus算法定義:

one or more nodes may propose values, and the consensus algorithm decides on one of those values

  公式算法要知足的屬性:

Uniform agreement

  No two nodes decide differently.

Integrity

  No node decides twice.

Validity

  If a node decides value v, then v was proposed by some node.

Termination

  Every node that does not crash eventually decides some value.

  前三是safety屬性,最後一個是liveness屬性,最後一個也要求了系統要有容錯性(2pc就不能知足這個屬性)

   single leader能保證共識,但single leader的選舉依賴於共識算法,常見的容錯的共識算法包括(Viewstamped Replication (VSR) , Paxos , Zab)

  共識算法依賴leader,但leader不是固定的:the protocols define an epoch number (called the ballot number in Paxos, view number in Viewstamped Replication, and term number in Raft) and guarantee that within each epoch, the leader is unique

  所以,single leader只是緩兵之計,不是不須要共識,而是不須要頻繁的共識。

  不一樣的數據系統選擇不一樣的形式來知足leader 選舉等共識需求,如mongodb,在replica node間使用相似raft的算法來選舉leader。而其餘系統,如hbase,使用outsourced的服務(如zookeeper)來達成共識、故障檢測,把專業的事交給專業的人,大大簡化了數據系統的複雜度。

總結

  DDIA的第二部分信息量很大,設計到大量的算法和理論,僅僅看這本書是很難搞明白的。於我而言,對LeaderLess replication與consensus這些部分還不是很清楚,好比LeaderLess因果性,vector clock、lamport clock、Paxos & Raft算法,還須要花點時間研究一下。

references 

Designing Data-Intensive Applications

Distributed systems for fun and profit  

相關文章
相關標籤/搜索