Kudu的概念术语
不多說,直接上干貨!
Columnar Data Store(列式數據存儲)
Kudu 是一個 columnar data store(列式數據存儲)。列式數據存儲在強類型列中。由于幾個原因,通過適當的設計,Kudu 對 analytical(分析)或 warehousing(數據倉庫)工作會非常出色。
Read Efficiency(高效讀取)
對于分析查詢,允許讀取單個列或該列的一部分同時忽略其他列,這意味著您可以在磁盤上讀取更少塊來完成查詢。與基于行的存儲相比,即使只返回幾列的值,仍需要讀取整行數據。
Data Compression(數據壓縮)
由于給定的列只包含一種類型的數據,基于模式的壓縮比壓縮混合數據類型(在基于行的解決方案中使用)時更有效幾個數量級。結合從列讀取數據的效率,壓縮允許您在從磁盤讀取更少的塊時完成查詢。請參閱 數據壓縮。
Table(表)
一張 table 是數據存儲在 Kudu 的位置。表具有 schema 和全局有序的 primary key(主鍵)。table 被分成稱為 tablets 的 segments。
Tables 和 schemas
Kudu 提供了 table 的概念。用戶可以建立多個 table,每個 table 都有一個預先定義好的 schema。Schema 里面定義了這個 table 多個 column,每個 column 都有名字,類型,是否允許 null 等。一些 columns 組成了 primary key。
可以看到,Kudu 的數據模型非常類似關系數據庫,在使用之前,用戶必須首先建立一個 table,訪問不存在的 table 或者 column 都會報錯。用戶可以使用 DDL 語句添加或者刪除 column,但不能刪除包含 primary key 的 column。
但在 Paper 里面說到 Kudu 不支持二級索引以及除了 primary key 之外的唯一索引,這個后續可以通過更新的代碼來確定下。
其實我這里非常關注的是 Kudu 的 Online DDL 是如何做的,只是 Paper 里面貌似沒有提及,后面只能看代碼了。
Tablet(片)
一個 tablet 是一張 table 連續的 segment,與其它數據存儲引擎或關系型數據庫中的 partition(分區)相似。給定的 tablet 冗余到多個 tablet 服務器上,并且在任何給定的時間點,其中一個副本被認為是 leader tablet。任何副本都可以對讀取進行服務,并且寫入時需要在為 tablet 服務的一組 tablet server之間達成一致性。
Master
該 master 保持跟蹤所有的 tablets,tablet servers,Catalog Table 和其它與集群相關的 metadata。在給定的時間點,只能有一個起作用的 master(也就是 leader)。如果當前的 leader 消失,則選舉出一個新的 master,使用 Raft Consensus Algorithm 來進行選舉。master 還協調客戶端的 metadata operations(元數據操作)。例如,當創建新表時,客戶端內部將請求發送給 master。 master 將新表的元數據寫入 catalog table,并協調在 tablet server 上創建 tablet 的過程。所有 master 的數據都存儲在一個 tablet 中,可以復制到所有其他候選的 master。tablet server 以設定的間隔向 master 發出心跳(默認值為每秒一次)。
Tablet Server
一個 tablet server 存儲 tablet 和為 tablet 向 client 提供服務。對于給定的 tablet,一個 tablet server 充當 leader,其他 tablet server 充當該 tablet 的 follower 副本。只有 leader 服務寫請求,然而 leader 或 followers 為每個服務提供讀請求。leader 使用 Raft Consensus Algorithm 來進行選舉 。一個 tablet server 可以服務多個 tablets ,并且一個 tablet 可以被多個 tablet servers 服務著。
Raft Consensus Algorithm
Kudu 使用 Raft consensus algorithm 作為確保常規 tablet 和 master 數據的容錯性和一致性的手段。通過 Raft,tablet 的多個副本選舉出 leader,它負責接受以及復制到 follower 副本的寫入。一旦寫入的數據在大多數副本中持久化后,就會向客戶確認。給定的一組 N 副本(通常為 3 或 5 個)能夠接受最多(N - 1)/2 錯誤的副本的寫入。
Catalog Table(目錄表)
catalog table 是 Kudu 的 metadata(元數據中)的中心位置。它存儲有關 tables 和 tablets 的信息。該 catalog table(目錄表)可能不會被直接讀取或寫入。相反,它只能通過客戶端 API 中公開的元數據操作訪問。
catalog table 存儲兩類元數據:
Tables
table schemas, locations, and states(表結構,位置 和狀態)
Tablets
現有 tablet 的列表,每個 tablet 的副本所在哪些 tablet server,tablet 的當前狀態以及開始和結束的 keys(鍵)。
Logical Replication(邏輯復制)
Kudu 復制操作,不是磁盤上的數據。這被稱為 logical replication(邏輯復制),而不是 physical replication(物理復制)。這有幾個優點 :
雖然 insert(插入)和 update(更新)確實通過網絡傳輸數據,deletes(刪除)不需要移動任何數據。delete(刪除)操作被發送到每個 tablet server,它在本地執行刪除。
物理操作,如 compaction,不需要通過 Kudu 的網絡傳輸數據。這與使用 HDFS 的存儲系統不同,其中 blocks (塊)需要通過網絡傳輸以滿足所需數量的副本。
tablet 不需要在同一時間或相同的時間表上執行壓縮,或者在物理存儲層上保持同步。這會減少由于壓縮或大量寫入負載而導致所有 tablet server 同時遇到高延遲的機會。
API
Kudu 提供了 Insert,Update 和 Delete 的 write API。不支持多行事務 API,這個不知道最新的能支持了沒有,因為僅僅能對單行數據操作,還遠遠不夠。
Kudu 提供了 Scan read API 讓用戶去讀取數據。用戶可以指定一些特定的條件來過濾結果,譬如用一個常量跟一個 column 里面的值比較,或者一段 primary key 的范圍等條件。
提供 API 的好處在于實現簡單,但對于用戶來說,其實更好的使用方式仍然是 SQL,一些復雜的查詢最好能通過 SQL 搞定,而不是讓用戶自己去 scan 數據,然后自己組裝。
一致性模型
Kudu 提供兩種一致性模型:snapshot consistency 和 external consistency。
默認 Kudu 提供 Snapshot consistency, 它具有更好的讀性能,但可能會有 write skew 問題。而 External consistency 則能夠完全保證整個系統的 linearizability,也就是當寫入一條數據之后,后面的任何讀取都一定能讀到最新的數據。
為了實現 External consistency,Kudu 提供了幾種方法:
在 clients 之間顯示的傳遞時間戳。當寫入一條數據之后,用戶用要求 client 去拿一個時間戳作為 token,然后通過一個 external channel 的方式傳遞給另一個 client。然后另一個 client 就可以通過這個 token 去讀取數據,這樣就一定能保證讀取到最新的數據了。不過這個方法實在是有點復雜。
提供類似 Spanner 的 commit-wait 機制。當寫入一條數據之后,client 需要等待一段時間來確定寫入成功。Kudu 并沒有采用 Spanner TrueTime 的方案,而是使用了 HybridTime 的方案。HybridTime 依賴 NTP,這個可能導致 wait 的時間很長,但 Kudu 認為未來隨著 read-time clock 的完善,這應該不是問題了。
Kudu 是我已知的第二個采用 HybridTime 來解決 External consistency 的產品,第一個當然就是 CockroachDB 了。TiDB 跟他們不一樣,我們采用的是全局授時的方案,這個會簡單很多,但其實也有跟 PD 交互的網絡開銷。后續TiDB 可能使用類似 Spanner 的 GPS + 原子鐘,現階段相關硬件的制造方式 Google 并沒有說明,但其實難度不大。因為已經有很多硬件廠商主動找我們希望一起合作提供,只是比較貴,而現階段我們大多數客戶并沒有跨全球事務這種場景。
Kudu 的一致性模型依賴時間戳,這應該是現在所有分布式系統通用的做法。Kudu 并沒有給用戶保留時間戳的概念,主要是覺得用戶很可能會困惑,畢竟不是所有的用戶都能很好的理解 MVCC 這些概念。當然,對于 read API,還是允許用戶指定特定的一個時間戳,這樣就能讀取到歷史數據。這個 TiDB 也是類似的做法,用戶不知道時間戳,只是我們額外提供了一個設置 snapshot 的操作,讓用戶指定生成某個時間點的快照,讀取那個時間點的數據。這個功能已經幫很多公司恢復了因為錯誤操作寫壞的數據了。
分區
Kudu 支持對數據按照 Range 以及 Hash 的方式進行分區。 每個大的 table 都可以通過這種方式將數據分不到不同的 Tablet 上面。當用戶創建一個表的時候,同時也可以指定特定的 partition schema,partition schema 會將 primary key 映射成對應的 partition key。每個 Tablet 上面會覆蓋一段或者多段 partition keys 的range。當 client 需要操作數據的時候,它可以很方便的就知道這個數據在哪一個 Tablet 上面。
一個 partition schema 可以包括 0 或者多個 hash-partitioning 規則和最多一個 range-partitioning 規則。用戶可以根據自己實際的場景來設置不同的 partition 規則。
譬如有一行數據是 (host, metric, time, value),time 是單調遞增的,如果我們將 time 按照 hash 的方式分區,雖然能保證數據分散到不同的 Tablets 上面,但如果我們想查詢某一段時間區間的數據,就得需要全部掃描所有的 Tablets 了。所以通常對于 time,我們都是采用 range 的分區方式。但 range 的方式會有 hot range 的問題,也就是同一個時間會有大量的數據寫到一個 range 上面,而這個 hot range 是沒法通過 scale out 來緩解的,所以我們可以將 (host, metric) 按照 hash 分區,這樣就在 write 和 read 之間提供了一個平衡。
通過多個 partition 規則組合,能很好的應對一些場景,但同時這個這對用戶的要求比較高,他們必須更加了解 Kudu,了解自己的整個系統數據會如何的寫入以及查詢。現在 TiDB 還只是單純的支持 range 的分區方式,但未來不排除也引入 hash。
Raft
Kudu 使用 Raft 算法來保證分布式環境下面數據一致性,這里就不再詳細的說明 Raft 算法了,因為有太多的資料了。
Kudu 的 heartbeat 是 500 毫秒,election timeout 是 1500 毫秒,這個時間其實很頻繁,如果 Raft group 到了一定量級,網絡開銷會比較大。另外,Kudu 稍微做了一些 Raft 的改動:
使用了 exponential back-off 算法來處理 leader re-election 問題。
當一個新的 leader 跟 follower 進行交互的時候,Raft 會嘗試先找到這兩個節點的 log 分叉點,然后 leader 再從這個點去發送 log。Kudu 直接是通過 committedIndex 這個點來發送。
對于 membership change,Kudu 采用的是 one-by-one 算法,也就是每次只對一個節點進行變更。這個算法的好處是不像 joint consensus 那樣復雜,容易實現,但其實還是會有一些在極端情況下面的 corner case 問題。
當添加一個新的節點之后,Kudu 首先要走一個 remote bootstrap 流程。
將新的節點加入到 Raft 的 configuration 里面Leader 發送 StartEmoteBootstrap RPC,新的 follower 開始拉去 snapshot 和之后的 logFollower 接受完所有數據并 apply 成功之后,開始響應 Raft RPC可以看到,這個流程跟 TiKV 的做法類似,這個其實有一個缺陷的。假設我們有三個節點,加入第四個之后,如果新的節點還沒 apply 完 snapshot,這時候掛掉了一個節點,那么整個集群其實是沒法工作的。
為了解決這個問題,Kudu 引入了 PRR_VOTER 概念。當新的節點加入的時候,它是 PRE_VOTE 狀態,這個節點不會參與到 Raft Vote 里面,只有當這個節點接受成功 snapshot 之后,才會變成 VOTER。
當刪除一個節點的時候,Leader 直接提交一個新的 configuration,刪除這個節點,當這個 log 被 committed 之后,這個節點就把刪除了。被刪除的節點有可能不知道自己已經被刪除了,如果它長時間沒有收到其他的節點發過來的消息,就會問下 Master 自己還在不在,如果不在了,就自己干掉自己。這個做法跟 TiKV 也是類似的。
Master
Kudu 的 Master 是整個集群最核心的東西,類似于 TiKV 里面的 PD。在分布式系統里面,一些系統采用了無中心化的架構設計方案,但我個人覺得,有一個中心化的單點,能更好的用全局視角來控制和調度整個系統,而且實現起來很簡單。
在 Kudu 里面,Master 自己也是一個單一的 Tablet table,只是對用戶不可見。它保存了整個集群的元信息,并且為了性能,會將其全部緩存到內存上面。因為對于集群來說,元信息的量其實并不大,所以在很長一段時間,Master 都不會有 scale 的風險。同時 Master 也是采用 Raft 機制復制,來保證單點問題。
這個設計其實跟 PD 是一樣的,PD 也將所有的元信息放到內存。同時,PD 內部集成 etcd,來保證整個系統的可用性。跟 Kudu Master 不一樣的地方在于,PD 是一個獨立的組件,而 Kudu 的 Master 其實還是集成在 Kudu 集群里面的。
Kudu 的 Master 主要負責以下幾個事情:
(1)Catalog manager
Master 的 catalog table 會管理所有 table 的一些元信息,譬如當前 table schema 的版本,table 的 state(creating,running,deleting 等),以及這個 table 在哪些 Tables 上面。
當用戶要創建一個 table 的時候,首先 Master 在 catalog table 上面寫入需要創建 table 的記錄,table 的 state 為 CREATING。然后異步的去選擇 Tablet servers 去創建相關的元信息。如果中間 Master 掛掉了,table 記錄里面的 CREATING state 會表明這個 table 還在創建中,新的 Master leader 會繼續這個流程。
(2)Cluster coordinator
當 Tablet server 啟動之后,會給 Master 注冊,并且持續的給 Master 進行心跳匯報消后續的狀態變化。
雖然 Master 是整個系統的中心,但它其實是一個觀察者,它的很多信息都需要依賴 Tablet server 的上報,因為只有 Tablet server 自己知道當前自己有哪一些 tablet 在進行 Raft 復制,Raft 的操作是否執行成功,當前 tablet 的版本等。因為 Tablet 的狀態變更依賴 Raft,每一次變更其實就在 Raft log 上面有一個對應的 index,所以上報給 Master 的消息一定是冪等的,因為 Master 自己會比較 tablet 上報的 log index 跟當前自己保存的 index,如果上報的 log index 是舊的,那么會直接丟棄。
這個設計的好處在于極大的簡化了整個系統的設計,如果要 Master 自己去負責管理整個集群的狀態變更,譬如 Master 給一個 tablet 發送增加副本的命令,然后等待這個操作完成,在繼續處理后面的流程。整個系統光異常處理,都會變得特別復雜,譬如我們需要關注網絡是不是斷開了,超時了到底是成功了還是失敗了,要不要再去 tablet 上面查一下?
相反,如果 Master 只是給 tablet 發送一個添加副本的命令,然后不管了,剩下的事情就是一段時間后讓 tablet 自己上報回來,如果成功了繼續后面的處理,不成功則嘗試在加一次。雖然依賴 tablet 的上報會有延遲(通常情況,只要有變動,tablet 會及時的上報通知,所以這個延遲其實挺小的),整個架構簡單了很多。
其實看到這里的時候,我覺得非常的熟悉,因為我們也是采用的這一套架構方案。最開始設計 PD 的時候,我們還設想的是 PD 主動去控制 TiKV,也就是我上面說的那套復雜的發命令流程。但后來發現實在是太復雜了,于是改成 TiKV 主動上報,這樣 PD 其實就是一個無狀態的服務了,無狀態的服務好處就是如果掛了,新啟動的 PD 能立刻恢復(當然,實際還是要做一些很多優化工作的)。
(3)Tablet directory
因為 Master 知道集群所有的信息,所以當 client 需要讀寫數據的時候,它一定要先跟 Master 問一下對應的數據在哪一個 Tablet server 的 tablet 上面,然后才能發送對應的命令。
如果每次操作都從 Master 獲取信息,那么 Master 鐵定會成為一個性能瓶頸,鑒于 tablet 的變更不是特別的頻繁,所以很多時候,client 會緩存訪問的 tablet 信息,這樣下次再訪問的時候就不用從 Master 再次獲取。
因為 tablet 也可能會變化,譬如 leader 跑到了另一個 server 上面,或者 tablet 已經不在當前 server 上面,client 會收到相關的錯誤,這時候,client 就重新再去 Master 獲取一下最新的路由信息。
這個跟我們的做法仍然是一樣的,client 緩存最近的路由信息,當路由失效的時候,重新去 PD 獲取一下。當然,如果只是單純的 leader 變更,其實返回的錯誤里面通常就會帶上新的 leader 信息,這時候 client 直接刷新緩存,在直接訪問了。
Tablet storage
Tablet server 是 Kudu 用來存放實際數據的服務,為了更好的性能,Kudu 自己實現了一套 tablet storage,而沒有用現有的開源解決方案。Tablet storage 目標主要包括:
快速的按照 Column 掃描數據
低延遲的隨機更新
一致的性能
RowSets
Tablets 在 Kudu 里面被切分成更小的單元,叫做 RowSets。一些 RowSets 只存在于內存,叫做 MemRowSets,而另一些則是使用 disk 和 memory 共享存放,叫做 DiskRowSets。任何一行數據只存在一個 RowSets 里面。
在任何時候,一個 tablet 僅有一個單獨的 MemRowSet 用來保存最近插入的數據。后臺有一個線程會定期的將 這些 MemRowSets 刷到 disk 上面。
當一個 MemRowSet 被刷到 disk 之后,一個新的空的 MemRowSet 被創建出來。之前的 MemRowSet 在刷到 disk 之后,就變成了 DiskRowSet。當刷的同時,如果有新的寫入,仍然會寫到這個正在刷的 MemRowSet 上面,Kudu 有一套機制能夠保證新寫入的數據也能一起被刷到 disk 上面。
MemRowSet
MemRowSet 是一個支持并發,提供鎖優化的 B-tree,主要基于 MassTree,也有一些不同:
因為 Kudu 使用的是 MVCC,所以任何的刪除其實也是插入,所以這個 tree 沒有刪除操作。
不支持任意的 in-place 數據變更操作,除非這次操作不會改變 value 的大小。
將 Leaf link 起來,類似 B+-tree,這樣對于 scan 會有明顯的性能提升。
并沒有完全實現 trie of trees,是只是使用了一個單一 tree,因為 Kudu 并沒有太多高頻隨機訪問的場景。
DiskRowSet
當 MemRowSets 被刷到 disk 之后,就變成了 DiskRowSets。當 MemRowSets 被刷到 disk 的時候,Kudu 發現超過 32 MB 了就滾動一個新的 DiskRowSet。因為 MemRowSet 是順序的,所以 DiskRowSets 也是順序的,各滾動的 DiskRowSet 里面的 primary keys 都是不相交的。
一個 DiskRowSet 包含 base data 和 delta data。Base data 按照 column 組織,也就是通常我們說的列存。各個 column 會被獨立的寫到 disk 里面一段連續的 block 上面,數據會被切分成多個 page,使用一個 B-tree 進行高效索引。
除了刷用戶自定義的 column,Kudu 還默認將 primary key index 寫到一個 column,同時使用 Bloom filter 來保證能快速通過找到 primary key。
為了簡單,當 column 的數據刷到 disk,它就是默認 immutable 的了,但在刷的過程中,有可能有更新的數據,Kudu 將這些數據放到一個 delta stores 上面。Delta stores 可能在內存 DeltaMemStores,或者 disk DeltaFiles。
Delta store 維護的一個 map,key 是 (row_offset, timestamp),value 就是 RowChangeList 記錄。Row offset 就是 row 在 RowSet 里面的索引,譬如,有最小 primary key 的 row 在 RowSet 里面是排在最前面的,它的 offset 就是 0。Timestamp 就是通常的 MVCC timestamp。
當需要給 DiskRowSet 更新數據的時候,Kudu 首先通過 primary key 找到對應的 row。通過 B-tree 索引,能知道哪一個 page 包含了這個 row,在 page 里面,可以計算 row 在整個 DiskRowSet 的 offset,然后就把這個 offset 插入到 DeltaMemStore 里面。
當 DeltaMemStore 超過了一個閥值,一個新的 DeltaMemStore 就會生成,原先的就會被刷到 disk,變成 immutable DeltaFile。
每個 DiskRowSet 都有一個 Bloom filter,便于快速的定位一個 key 是否存在于該DiskRowSet 里面。DIskRowSet 還保存了最小和最大的 primary key,這樣外面就能通過 key 落在哪一個 key range 里面,快速的定位到這個 key 屬于哪一個 DiskRowSet。
Compaction
當做查詢操作的時候,Kudu 也會從 DeltaStore 上面讀取數據,所以如果 DeltaStore 太多,整個讀性能會急劇下降。為了解決這個問題,Kudu 在后臺會定期的將 delta data 做 compaction,merge 到 base data 里面。
同時,Kudu 還會定期的將一些 DIskRowSets 做 compaction,生成新的 DiskRowSets,對 RowSet 做 compaction 能直接去掉 deleted rows,同時也能減少重疊的 DiskRowSets,加速讀操作。
歡迎大家,加入我的微信公眾號: 大數據躺過的坑 人工智能躺過的坑 Java從入門到架構師
同時,大家可以關注我的個人博客:
http://www.cnblogs.com/zlslch/和 http://www.cnblogs.com/lchzls/ http://www.cnblogs.com/sunnyDream/
詳情請見:http://www.cnblogs.com/zlslch/p/7473861.html
人生苦短,我愿分享。本公眾號將秉持活到老學到老學習無休止的交流分享開源精神,匯聚于互聯網和個人學習工作的精華干貨知識,一切來于互聯網,反饋回互聯網。
目前研究領域:大數據、機器學習、深度學習、人工智能、數據挖掘、數據分析。 語言涉及:Java、Scala、Python、Shell、Linux等 。同時還涉及平常所使用的手機、電腦和互聯網上的使用技巧、問題和實用軟件。 只要你一直關注和呆在群里,每天必須有收獲
對應本平臺的討論和答疑QQ群:大數據和人工智能躺過的坑(總群)(161156071)
總結
- 上一篇: 超经典,百度最爱考的安卓Android百
- 下一篇: JSON是什么?