本文不是一篇Spanner的介紹文章,主要想對於Spanner和F1解決的幾個有表明性的問題作一個歸納和梳理。接下來的行文安排將主要以問答的形式展開。對Spanner和F1不熟悉的盆友能夠參考最後一節列出的引用。html
嚴格的CP系統以及遠超5個9的可用性git
基於2PC協議的內部順序一致性github
外部一致性,支持讀寫事務、只讀事務、快照讀算法
全球化的分佈式存儲系統,延遲是可以接受的數據庫
基於模式化的半關係表的數據模型緩存
在一系列鍵值映射的上層,Spanner 實現支持一個被稱爲「目錄」的桶抽象,也就是包含公共前綴的連續鍵的集合。一個目錄是數據放置的基本單元。屬於一個目錄的全部數據,都具備相同的副本配置。 當數據在不一樣的 Paxos 組之間進行移動時,會一個目錄一個目錄地轉移。安全
一個 Paxos 組能夠包含多個目錄,這意味着一個 Spanner tablet 是不一樣於一個 BigTable tablet 的。一個 Spanner tablet 沒有必要是一個行空間內按照詞典順序連續的分區,相反,它能夠是行空間內的多個分區。這樣作可讓多個被頻繁一塊兒訪問的目錄被整合到一塊兒,也就是說多個directory會被映射到一個tablet上。服務器
每一個表都和關係數據庫表相似,具有行、列和版本值。每一個表都需 要有包含一個或多個主鍵列的排序集合。主鍵造成了一個行的名稱,每一個表都定義了從主鍵列到非主鍵列的映射。當一個行存在時,必需要求已經給行的一些鍵定義了一些值(即便是 NULL)。網絡
Spanner中的表具備層次結構,這有些相似於傳統關係數據庫中一對多關係。戶端應用會使用 INTERLEAVE IN 語句在數據庫模式中聲明這個層次結構。這個層次結構上面的表,是一個目錄表。目錄表中的每行都具備鍵 K,和子孫表中的全部以 K 開始(以字典順序排序)的行一塊兒,構成了一個目錄。這樣作是想把想關聯的表放在一樣的位置,利用數據的局部性提升性能,畢竟單個Paxos組內的操做比跨paxos組要來的高效。架構
一個 Spanner 部署稱爲一個 universe。假設 Spanner 在全球範圍內管理數據,那麼,將會只有可數的、運行中的 universe。咱們當前正在運行一個測試用的 universe,一個部署/線上用的 universe 和一個只用於線上應用的 universe。
Spanner 被組織成許多個 zone 的集合,每一個 zone 都大概像一個 BigTable 服務器的部署。 zone 是管理部署的基本單元。zone 的集合也是數據能夠被複制到的位置的集合。當新的數據中心加入服務,或者老的數據中心被關閉時,zone 能夠被加入到一個運行的系統中,或者從中移除。zone 也是物理隔離的單元,在一個數據中心中,可能有一個或者多個 zone, 例如,當屬於不一樣應用的數據必須被分區存儲到同一個數據中心的不一樣服務器集合中時,一個數據中心就會有多個 zone 。
spanner數據的實際存儲依靠於分佈式文件系統Colossus,在一個存儲分區分區單元tablet上運行一個Paxos狀態機,基於Paxos實現備份和容錯。咱們的 Paxos 實現支持長壽命的領導者(採用基於時間的領導者租約),時間一般在 0 到 10 秒之間。當前的 Spanner 實現中,會對每一個 Paxos 寫操做進行兩次記錄:一次是寫入到 tablet 日誌中,一次是寫入到 Paxos 日誌中。
Spanner 的 Paxos 實現中使用了時間化的租約,來實現長時間的領導者地位(默認是 10秒)。一個潛在的領導者會發起請求,請求時間化的租約投票,在收到指定數量的投票後,這個領導者就能夠肯定本身擁有了一個租約。一個副本在成功完成一個寫操做後,會隱式地延期本身的租約。對於一個領導者而言,若是它的租約快要到期了,就要顯示地請求租約延期。另外一個領導者的租約有個時間區間,這個時間區間的起點就是這個領導者得到指定數量的投票那一刻,時間區間的終點就是這個領導者失去指定數量的投票的那一刻(由於有些投 票已通過期了)。Spanner 依賴於下面這些「不連貫性」:對於每一個 Paxos組,每一個Paxos領導者的租約時間區間,是和其餘領導者的時間區間徹底隔離的。
爲了保持這種彼此隔離的不連貫性,Spanner 會對何時退位作出限制。把 smax 定義爲一個領導者可使用的最大的時間戳。在退位以前,一個領導者必須等到 TT.after(smax)是真。
上面是論文中關於領導着租約的解釋,這裏咱們的問題是爲何須要一個長期的leader lease,不加入這個特性是否能夠?下面是我我的的一些見解:
Multi-Paxos協議自己在只有一個proposer的時候,對於每一個instance能夠將兩階段變爲一階段,提升Paxos協議的效率。固然這只是微小的一方面,畢竟谷歌使用的Paxos實現應該是標準的變體。
Leader變更頻率降低有助於減小在操做中查詢leader的次數,有助於減輕元信息管理服務的壓力
這種作法的前提一定是故障的頻率低,若是故障時常發生,一個較長的租約時間將使得故障的發現和處理變慢
若是一個事務 T2 在事務 T1 提交之後開始執行, 那麼,事務 T2 的時間戳必定比事務 T1 的時間戳大。
TrueTime 會顯式地把時間表達成 TTinterval,這是一個時間區間,具備有界限的時間不肯定性。表示一個事件 e 的絕對時間,能夠利用函數 tabs(e)。若是用更加形式化的術語,TrueTime 能夠保證,對於一個調用 tt=TT.now(),有 tt.earliest≤tabs(enow)≤tt.latest,其中, enow 是調用的事件。
這裏須要再問一個問題:
每一個節點在同一時刻調用TT.now()等到的區間是相同的嗎?顯然不是,若是那樣不就等同於獲得一個全球相同的時間點了,但這不影響外部一致性正確的證實。
咱們先來說作法:是基於時間戳分配和commit wait實現的。
Start. 爲一個事務 Ti 擔任協調者的領導者分配一個提交時間戳 si,不會小於 TT.now().latest 的值,TT.now().latest的值是在esierver事件以後計算獲得的。要注意,擔任參與者的領導者, 在這裏不起做用。第 4.2.1 節描述了這些擔任參與者的領導者是如何參與下一條規則的實現的。
Commit Wait. 擔任協調者的領導者,必須確保客戶端不能看到任何被 Ti 提交的數據,直到 TT.after(si)爲真。提交等待,就是要確保 si 會比 Ti 的絕對提交時間小。
那麼證實以下:
讀事務能夠分爲只讀事務讀當前最新的值,以及快照讀。前者能夠經過分配時間戳轉變爲後者,因此咱們先討論快照讀的實現。
首先問一個問題,讀必須走Paxos Group 的Leader嗎?不是的,首先在同一個Paxos Group內,寫操做時間戳的單調性是毫無疑問的,那麼只須要找到足夠新的副本。這就有一個新問題:
如何判斷一個副本是否足夠新?
每一個副本都會跟蹤記錄一個值,這個值被稱爲安全時間 tsafe,它是一個副本最近更新後的最大時間戳。若是一個讀操做的時間戳是 t,當知足 t<=tsafe 時, 這個副本就能夠被這個讀操做讀取。這種情形下小於t的寫操做一定已經被該副本catch up了(基於Paxos協議)。
這一小節內容請先參閱讀寫事務部分。
tsafe能夠從兩個值推導而來,Paxos狀態機最大的apply操做的時間戳(這一部分象徵着已經生效的寫),和事務管理器決定的tasfe(TM),tsafe=min(tsafe(Paxos),tsafe(TM)),後者是什麼意思呢?
若是如今該replica沒有參與任何事務中,那麼理應這一部分不形成任何影響,因此tsafe(TM)=無窮大。(記住tsafe越大,讀越容易經過)。若是當前replica參與了一個讀寫事務Ti,那麼本Paxos組的leader會分配一個準備時間戳(見後文讀寫事務),那麼理應tsafe比全部正在參與的事務Ti的準備時間戳都小,而且是知足這個條件時間戳裏的最大一個。答案呼之欲出了。
可是,上述的方法依然存在問題,一個未完成的事務將阻止tsafe增加,以後的以有讀事務將被阻塞,即便它讀的值和該事務寫的值沒啥關係。這裏有點相似於Java對ConcurrentHashMap作的優化了——分段加鎖,好了,咱們繼續接着往下看。咱們能夠對一個範圍的key range來維護參與事務的準備時間戳,那麼對於一個讀操做,只用檢查和它衝突的key range的準備時間戳就好。
tsafe(Paxos)也有一個問題,那就是缺少Paxos寫的時候,也無法增加。若是上次的apply時間在讀被分配的時間戳t以前,接下來沒有Paxos寫的話,快照讀t無法執行,這就尷尬了。能夠爲每個instance n預估一個將分配給n+1的時間戳。那這裏要有一個問題了,預估的這個時間須要知足什麼條件呢?我的認爲首先是要超過期間戳的最大偏差值。
以前提到過對於只讀事務能夠經過分配時間戳來轉化爲快照讀,那麼該如何分配呢,首先看這種分配須要知足什麼?
知足寫後讀一致性,也就是其分配的時間戳要大於全部已提交事務的時間戳
在一個事務開始後的任意時刻,能夠簡單地分配 sread=TT.now().latest。但這樣有一個問題,對於時間戳 sread 而言,若是 tsafe 沒有增長到足夠大,可能須要對 sread 時刻的讀操做進行阻塞。擇一個 sread 的值可 能也會增長 smax 的值,從而保證不連貫性。爲了減小阻塞的機率,_Spanner 應該分配能夠保持外部一致性的最老(小)的時間戳_。
分配一個時間戳須要一個協商階段,這個協商發生在全部參與到該讀操做中的 Paxos 組之間。其過程以下:
對於單個Paxos組內的讀操做,把 LastTS()定義爲在 Paxos 組中最後提交的寫操做的時間戳。若是沒有準備提交的事務,這個分配到的時間戳 sread=LastTS()就很容易知足外部一致性要求
對於跨Paxos組的讀操做,最複雜的作法是基於多Paxos Leader的LastTS()協商得出;但實際採用的簡單作法是採用TT.now().latest,容許某種程度上的阻塞。
不管事務讀不讀,都按讀寫事務來處理
這種狀況下只須要給讀寫事務分配時間戳,進行快照讀,最後發生在一個事務中的寫操做會在客戶端進行緩存,直到提交。由此致使的結果是,在一個事務中的讀操做,不會看到這個事務的寫操做的結果。這種設計在 Spanner 中能夠很好地工做,由於一個讀操做能夠返回任何數據讀的時間戳,未提交的寫操做尚未被分配時間戳。
至於讀寫事務如何分配時間戳,請參照True Time一章。
客戶端對須要讀的Paxos組Leader申請加讀鎖,按分配時間戳讀取最新數據。
客戶端向leader持續發送「保持活躍信息「,防止leader認爲會話過期
讀操做結束,緩衝全部寫操做,開始2PC,選擇協調組發送緩衝信息
對於參與協調的非領導者而言,獲取寫鎖,會選擇一個比以前分配給其餘事務的任什麼時候間戳都要大的預備時間戳,而且經過 Paxos 把準備提交記錄寫入日誌。而後把本身的準備時間戳通知給協調者。
對於協調者,也會首先得到寫鎖,可是,會跳過準備階段。在從全部其餘的、扮演參與者的領導者那裏得到信息後,它就會爲整個事務選擇一個時間戳。這個提交時間戳s必須大於或等於全部的準備時間戳,而且s應該大於這個領導者爲以前的其餘全部事務分配的時間戳,以後就會經過 Paxos 在日誌中寫入一個提交記錄。
在容許任何協調者副本去提交記錄以前,扮演協調者的領導者會一直等待到 TT.after(s),知足提交等待條件。
在提交等待以後,協調者就會發送一個提交時間戳給客戶端和全部其餘參與的領導者。每一個參與的領導者會經過 Paxos 把事務結果寫入日誌。全部的參與者會在同一個時間戳進行提交,而後釋放鎖。
注意,這裏的每一個協調者其實都是一個Paxos組,2PC自己是一個反可用性的協議(也就是它要求沒有故障才能順利完成),可是Paxos協議可以在協調者故障時快速選出新主,因爲信息已經經過Paxos日誌同步了,新主能夠繼續參與2PC過程,提高了可用性。
原子模式變動。使用一個標準的事務是不可行的,由於參與者的數量(即數據庫中組的數量)可能達到幾百萬個。在一個數據中心內進行原子模式變動,這個操做會阻塞全部其餘操做。
一個 Spanner 模式變動事務一般是一個標準事務的、非阻塞的變種。首先,它會顯式地分配一個將來的時間戳,這個時間戳會在準備階段進行註冊。由此,跨越幾千個服務器的模式變動,能夠在不打擾其餘併發活動的前提下完成。其次,讀操做和寫操做,它們都是隱式地依賴於模式,它們都會和任何註冊的模式變動時間戳t保持同步:當它們的時間戳小於 t 時, 讀寫操做就執行到時刻 t;當它們的時間戳大於時刻 t 時,讀寫操做就必須阻塞,在模式變動事務後面進行等待。
那麼,接下來又有幾個問題:
如何爲模式變動選擇時間戳?選擇的時間戳須要知足哪些條件?
上述的方法會形成服務不可用嗎?若是是,不可用的時間區間是多少?
兩次模式變動之間在重合的時間段裏有重合組的時候,經過什麼方式協調?樂觀仍是悲觀的併發控制?會形成死鎖嗎,會致使在分配的時間戳以後沒法按時完成嗎?仍是說在準備階段就偵測這種狀況,阻塞下一次模式變動?
關於模式變動的細節在spanner的論文中並無詳細說起,而是在另外一篇文章中闡述,若是以後有時間的話會單獨就模式變動再寫一篇博客來回答這些問題。
「三選二」的觀點在幾個方面起了誤導做用,
首先,因爲分區不多發生,那麼在系統不存在分區的狀況下沒什麼理由犧牲C或A。
其次,C與A之間的取捨能夠在同一系統內以很是細小的粒度反覆發生,而每一次的決策可能由於具體的操做,乃至由於牽涉到特定的數據或用戶而有所不一樣。
最後,這三種性質均可以在程度上衡量,並非非黑即白的有或無。可用性顯然是在0%到100%之間連續變化的,一致性分不少級別,連分區也能夠細分爲不一樣含義,如系統內的不一樣部分對因而否存在分區能夠有不同的認知。
CAP理論的經典解釋,是忽略網絡延遲的,但在實際中延遲和分區緊密相關。CAP從理論變爲現實的場景發生在操做的間歇,系統須要在這段時間內作出關於分區的一個重要決定:
取消操做於是下降系統的可用性,仍是
繼續操做,以冒險損失系統一致性爲代價
依靠屢次嘗試通訊的方法來達到一致性,好比Paxos算法或者兩階段事務提交,僅僅是推遲了決策的時間。系統終究要作一個決定;無限期地嘗試下去,自己就是選擇一致性犧牲可用性的表現。
CAP理論常常在不一樣方面被人誤解,對於可用性和一致性的做用範圍的誤解尤其嚴重,可能形成不但願看到的結果。若是用戶根本獲取不到服務,那麼其實談不上C和A之間作取捨,除非把一部分服務放在客戶端上運行。
「三選二」的時候取CA而舍P是否合理?已經有研究者指出了其中的要害——怎樣纔算「舍P」含義並不明確。設計師能夠選擇不要分區嗎?哪怕原來選了CA,當分區出現的時候,你也只能回頭從新在C和A之間再選一次。咱們最好從機率的角度去理解:選擇CA意味着咱們假定,分區出現的可能性要比其餘的系統性錯誤(如天然災難、併發故障)低不少,打個比方你在單機下就永遠不會假設分區,同一機架內部的通訊有時咱們也認爲分區不會出現。
純粹主義的答案是「否」,由於網絡分區老是可能發生,事實上在Google也確實發生過。在網絡分區時,Spanner選擇C而放棄了A。所以從技術上來講,它是一個CP系統。咱們下面探討網絡分區的影響。考慮到始終提供一致性(C),Spanner聲稱爲CA的真正問題是,它的核心用戶是否定可它的可用性(A)。若是實際可用性足夠高,用戶能夠忽略運行中斷,則Spanner是能夠聲稱達到了「有效CA」的。
實際上,c差別化可用性與spanner的實際可用性是有差距的,即用戶是否確實已經發現Spanner已停掉了。差別化可用性比Spanner的實際可用性還要高,也就是說,Spanner的短暫不可用不必定會當即形成用戶的系統不可用。
另外就是運行中斷是否因爲網絡分區形成的。若是Spanner運行中斷的主要緣由不是網絡分區,那麼聲稱CA就更充分了。對於Spanner,這意味着可用性中斷的發生,實際並不是是因爲網絡分區,而是一些其它的多種故障(由於單一故障不會形成可用性中斷)。
綜上,Spanner在技術上是個CP系統,但實際效果上能夠說其是CA的。
上面已經提到過了,對於分佈式系統的節點來講,它很難感知分區,它所能感知的只有延時。那麼這裏有幾個重要問題:
如何判斷分區
分區問題的主要來源
spanner在面對分區的時候作出了什麼樣的選擇
首先,考慮單Paxos組內事務,出現分區後會出現兩種情形:
大多數成員可用,選出了新Leader,事務繼續運行。但處於少數人的一側將再也沒法更新它的tsafe了,在某個時間戳以後的讀操做沒法在該分區被服務,犧牲了部分可用性,但數據在多數人中保持一致。
沒法維持一個多數人羣體,那麼事務將暫停,新的事務不會被接受,系統不可用
再考慮跨Paxos組的事務
對跨組事務使用2PC還意味着組內成員的網絡分區能夠阻止提交。捨棄可用性保證系統數據是安全的。
對於快照讀,快照讀對網絡分區而言更加健壯。特別的,快照讀能在如下狀況下正常工做:
對於發起讀操做的一側網絡分區,每一個組至少存在一個副本
對於這些副本,讀時間戳是過去的。
若是Leader因爲網絡分區而暫停(這可能一直持續到網絡分區結束),這時第2種狀況可能就不成立了。由於這一側的網絡分區上可能沒法選出新的Leader(譯註:見下節引用的解釋)。在網絡分區期間,時間戳在分區開始以前的讀操做極可能在分區的兩側都能成功,由於任何可達的副本有要讀取的數據就足夠了。
F1是基於Spanner之上的一個分佈式類關係數據庫,提供了一套相似於SQL(準確說是SQL的超集)的查詢語句,支持表定義和數據庫事務,同時兼具強大的可擴展性、高可用性、外部事務一致性。接下來主要從幾個方面來簡要說一說F1是怎麼在Spanner之上解決傳統數據庫的諸多問題的。
F1自己不負責數據的存儲,只是做爲中間層預處理數據並解析SQL生成實際的讀寫任務。咱們知道,大多數時候移動數據要比移動計算昂貴的多,F1節點自身不負責數據的底層讀寫,那麼節點的加入和移除還有負載均衡就變得廉價了。下面放一張F1的結構圖:
大部分的F1是無狀態的,意味着一個客戶端能夠發送不一樣請求到不一樣F1 server,只有一種情況例外:客戶端的事務使用了悲觀鎖,這樣就不能分散請求了,只能在這臺F1 server處理剩餘的事務。
F1支持層級表結構和protobuf複合數據域,示例以下:
這樣作的好處主要是:
能夠並行化,是由於在子表中能夠get到父表主鍵,對於不少查詢能夠並行化操做,不用先查父表再查子表
數據局部性,減小跨Paxos組的事務.update通常都有where 字段=XX這樣的條件,在層級存儲方式下相同row值的都在一個directory裏
protobuf支持重複字段,這樣也是爲了對於array一類的結構在取數據時提高性能
最後,對於索引:
全部索引在F1裏都是單獨用個表存起來的,並且都爲複合索引,由於除了被索引字段,被索引表的主鍵也必須一併包含.除了對常見數據類型的字段索引,也支持對Buffer Protocol裏的字段進行索引.
索引分兩種類型:
Local:包含root row主鍵的索引爲local索引,由於索引和root row在同一個directory裏;同時,這些索引文件也和被索引row放在同一個spanserver裏,因此索引更新的效率會比較高.
global:同理可推global索引不包含root row,也不和被索引row在同一個spanserver裏.這種索引通常被shard在多個spanserver上;當有事務須要更新一行數據時,由於索引的分佈式,必需要2PC了.當須要更新不少行時,就是個災難了,每插入一行都須要更新可能分佈在多臺機器上的索引,開銷很大;因此建議插入行數少許屢次.
同步的模式變動可行嗎?
顯然是不行的,這違反了咱們對可用性的追求。
然而在線的、異步的模式變動會形成哪些問題呢?
全部F1服務器的Schema變動是沒法同步的,也就是說不一樣的F1服務器會在不一樣的時間點切換至新Schema。因爲全部的F1服務器共享同一個kv存儲引擎,Schema的異步更新可能形成嚴重的數據錯亂。例如咱們發起給一次添加索引的變動,更新後的節點會很負責地在添加一行數據的同時寫入一條索引,隨後另外一個還沒來得及更新的節點收到了刪除同一行數據的請求,這個節點還徹底不知道索引的存在,天然也不會去刪除索引了,因而錯誤的索引就被遺留在數據庫中。
在F1 Schema變動的過程當中,因爲數據庫自己的複雜性,有些變動沒法由一箇中間狀態隔離,咱們須要設計多個逐步遞進的狀態來進行演化。只要咱們保證任意相鄰兩個狀態是相互兼容的,整個演化的過程就是可依賴的。
F1中Schema以特殊的kv對存儲於Spanner中,同時每一個F1服務器在運行過程當中自身也維護一份拷貝。爲了保證同一時刻最多隻有2份Schema生效,F1約定了長度爲數分鐘的Schema租約,全部F1服務器在租約到期後都要從新加載Schema。若是節點沒法從新完成續租,它將會自動終止服務並等待被集羣管理設施重啓。
定義的中間狀態:
delete-only 指的是Schema元素的存在性只對刪除操做可見。
write-only 指的是Schema元素對寫操做可見,對讀操做不可見。
reorg:取到當前時刻的snapshot,爲每條數據補寫對應的索引
演化過程:
absent --> delete only --> write only --(reorg)--> public
F1支持三種事務
快照事務
悲觀事務
樂觀事務
前面兩類都是Spanner直接支持的,這裏主要講講樂觀事務,分爲兩階段:第一階段是讀,無鎖,可持續任意長時間;第二階段是寫,持續很短.但在寫階段可能會有row記錄值衝突(可能多個事務會寫同一行),爲此,每行都有個隱藏的lock列,包含最後修改的timestamp.任何事務只要commit,都會更新這個lock列,發出請求的客戶端收集這些timestamp併發送給F1 Server,一旦F1 Server發現更新的timestamp與本身事務衝突,就會中斷自己的事務。
其優點主要以下:
在樂觀事務下能長時間運行而不被超時機制中斷,也不會影響其餘客戶端
樂觀事務的狀態值都在client端,即便F1 Server處理事務失敗了,client也能很好轉移到另外一臺F1 Server繼續運行.
一個悲觀事務一旦失敗從新開始也須要上層業務邏輯從新處理,而樂觀事務自包含的--即便失敗了重來一次對客戶端也是透明的.
但在高併發的情景下,衝突變得常見,樂觀事務的吞吐率將變得很低。
F1的查詢處理有點相似於Storm一類流式計算系統,先生成能夠由有向無環圖表示的執行計劃,下面給出一個示例:
F1的運算都在內存中執行,再加上管道的運用,沒有中間數據會存放在磁盤上,但缺點也是全部內存數據庫都有的--一個節點掛就會致使整個SQL失敗要從新再來.F1的實際運行經驗代表執行時間不遠超過1小時的SQL通常足夠穩定。
F1自己不存儲數據,由Spanner遠程提供給它,因此網絡和磁盤就影響重大了;爲了減小網絡延遲,F1使用批處理和管道技術,同時還有一些優化手段:
對於層級表之間的join,一次性取出全部知足條件的行
支持客戶端多進程併發接受數據,每一個進程都會收到本身的那部分結果,爲避免多接受或少接受,會有一個endpoint標示.
對protobuf數據的處理,即便是隻取部分字段,也必須取整個對象並解析,這也是爲了換取減小子表開銷作出的權衡。