1. 程式人生 > >Cassandra 分散式資料庫詳解,第 1 部分:配置、啟動與叢集

Cassandra 分散式資料庫詳解,第 1 部分:配置、啟動與叢集

瞭解一個軟體的配置項的意義是使用這個軟體的前提,這裡詳細介紹 Cassandra 的配置檔案(storage-config.xml)中各個配置項的意義,這其中包含有很多配置引數,我們可以對其進行調整以達到理想的效能。為了節省篇幅這裡沒有列出 storage-config.xml 檔案的內容,你可以對照著這個檔案看下面的內容。

Cluster Name 代表一個族的標識,它通常代表一個叢集。這個配置項在 Cassandra 沒有儲存資料時就必須指定,當 Cassandra 第一次啟動後,它就會被寫到 Cassandra 的系統表中,如果你要修改 Cluster Name 必須要刪除 Cassandra 中資料。

這個配置項看起來十分簡單,但是如果你對 Cassandra 沒有深入瞭解的話,恐怕不知道當你改變這個配置項時 Cassandra 可能會發生什麼?

我們知道 Cassandra 叢集是通過維護一個自適應的 Token 環來達到叢集中的節點的自治理,它們不僅要保證每臺機器的狀態的同步和一致性還要保證它們之間 Token 分佈的合理性,通過重新劃分 Token 來達到每臺機器的負載的均衡性。

那這個配置項與 Token 和負載又有何關聯性?其實表面上看起來這個配置項是當這個節點啟動時是否自動加入叢集。但是,當你設定成 False 時它是不是就不加入叢集呢?顯然不是,這還要看你有沒有配置 seeds,如果你配置了其它 seed,那麼它仍然會去加入叢集。

那麼到底有何區別,通過分析其啟動程式碼發現,這個配置項不僅跟 seed 配置項有關而且和 Cassandra 是否是第一次啟動也有關。Cassandra 的啟動規則大慨如下:

  1. 當 AutoBootstrap 設為 FALSE,第一次啟動時 Cassandra 會在系統表中記錄 AutoBootstrap=TRUE,以表示這是由系統自動設定的,其實這個只是作為一個標誌來判斷你以後的啟動情況。
  2. 當 AutoBootstrap 設為 TRUE,第一次啟動,Cassandra 會判斷當前節點有沒有被配置成 seed 節點,也就是在本機 ip 有沒有在 seeds 中。如果在 seeds 中,Cassandra 的啟動情況和 1 是一樣的。
  3. 當 AutoBootstrap 設為 TRUE,第一次啟動,並且沒有配置為 seed,Cassandra 將會有一個漫長的啟動過程,當然這個時間的長短和你的當前的叢集的資料量有很大的關係。這時 Cassandra 將會根據當前叢集的負載,來動態調整它們的均衡。調整均衡的方式就是根據當前的 Token 環分配一個合適的 Token 給這個節點,並將這個符合這個 Token 的資料傳給它。

從以上分析可以看出,AutoBootstrap 設定的主要目的是是否調整當前叢集中的負載均衡。這其實還有一個很重要的問題就是,如果按照第一種情況啟動,如果沒有指定 Token,這個節點的 Token 將會是隨機生成的,那麼問題就來了,當這個隨機生成是 Token 加入叢集的 Token 環時,Cassandra 如何保證 Token 和 Token 所對應的資料的一致性,這個問題將在後面說明。

Cassandra 中 Keyspace 相當於關係資料庫中的表空間的概念,可以理解為操作表的一個容器,它下面可以定義多個 ColumnFamily,這個 ColumnFamily 就相當於表了,它是儲存資料的實體。

ColumnFamily 中幾個屬性的意義如下:

  • ColumnType。列的型別,有兩種:Standard 和 Super,分別是標準列和超列,超列的含義是列還有一個父列。
  • CompareWith。表示的是列的排序規則,可以根據不同的資料型別進行排序如 TimeUUIDType,可以根據插入的時間排序
  • CompareSubcolumnsWith。子列的排序規則與 CompareWith 類似
  • RowsCached。查詢時快取的資料量,可以是多少條,也可以是百分比,如 10% 就是快取 10% 的資料量,這個對查詢效能影響很大,如果命中率高的話,可以顯著提高查詢效率。
  • KeysCached。快取 ColumnFamily 中的 key,這個 key 就是對應到 Index.db 中的資料,如果沒有在 RowsCached 中命中,那麼就要到每個 SSTable 中查詢,這時必然要查詢 key,如果在 KeysCached 能命中就不需要到 Index.db 中查詢了,省去了 IO 操作。

Cassandra 是一個 Key/Value 系統,從它的儲存的邏輯結構來看分為:Keyspace、Key、ColumnFamily、Super Column 以及 Column 幾個部分。很明顯我們能看出每一對 Key/Value 都有一個寄生的容器,所以它實際上是由一個個 Map 容器構成的。這個容器結構可以用圖 1 和圖 2 來表示:


圖 1. 標準的 Column 結構圖
圖 1. 標準的 Column 結構圖

圖 2. 含有 Super Column 的結構圖
圖 2. 含有 Super Column 的結構圖

定義資料複製策略,預設是 org.apache.cassandra.locator.RackUnawareStrategy,資料複製到其它節點沒有特別的規定。org.apache.cassandra.locator.RackAwareStrategy 是將節點分為不同的 Rack,這種方式不管是存資料還是查資料,都從不同的 Rack 的節點取資料或寫資料。org.apache.cassandra.locator.DatacenterShardStategy 又將節點劃分為不同的 Data Center,讓資料放在不同資料中心,從而保證資料的安全性,例如可以按機房劃分 Data Center,從而避免一個機房出現故障,會影響整個叢集。

定義資料要儲存幾個備份,結合 ReplicaPlacementStrategy 可以把資料放在不同的地方。

org.apache.cassandra.locator.EndPointSnitch 可以根據當前的網路情況選擇更好的節點路由,一般預設即可。

這個配置項可以控制資料訪問的安全性,可以在 access.properties 和 passwd.properties 設定使用者和密碼。

控制資料的分佈規則,org.apache.cassandra.dht.RandomPartitioner 是隨機分佈,Cassandra 控制資料在不同的節點是通過 key 的來劃分的,這個方式是將 key 進行 MD5 Hash,從而形成隨機分佈的 Token,然後根據這個 Token 將資料分佈到不同的節點上。

org.apache.cassandra.dht.OrderPreservingPartitioner 是取 key 的 Ascii 字元來劃分的,因此我們可以根據 key 來主動控制資料的分佈,例如我們可以給 key 加一個字首,相同字首的 key 分佈在同一個節點中。

給節點分配一個初始 Token,當節點第一次啟動後這個 Token 就被寫在系統表中。結合 Partitioner 就可以控制資料的分佈。這個配置項可以讓我們能調整叢集的負載均衡。

這兩個配置項是設定 CommitLog 和 SSTable 儲存的目錄。

Seeds

關於 Seeds 節點的配置有這樣幾個疑問:

  1. 是不是叢集中的所有節點都要配置在 seed 中。
  2. 本機需不需要配置在 seed 中。

關於第二個問題在前面中已經說明了,是否配置就決定是否作為 seed 節點來啟動。關於第一個問題,答案是否定的,因為即使你把叢集中的所有節點都配置在 seed 中,當 Cassandra 在啟動時它也不會往每個 seed 傳送心跳資訊,而是隨機選擇一個節點與其同步叢集中的其他所有節點狀態。幾個回合後這個節點同樣能夠獲取叢集中所有的節點的列表。這就是叢集自治理的優點,只要能發現其中一個節點就能發現全部節點。

ListenAddress 這個配置是用來監聽叢集中其它節點與本節點交換狀態資訊和資料的地址。需要注意的是當你配置為本機的 ip 地址沒有問題,不配置通常也沒問題,但是如果你沒有配置或者配置成主機名,而你又把你的主機名繫結到 127.0.0.1 時,這時將會導致本節點不能加入到叢集中,因為它接受不到其他節點過來的任何資訊,防止出錯直接繫結本機 ip 最好。

監聽 Client 的連線請求,不設或者配置成 0.0.0.0,監聽所有地址的請求。

當 Cassandra 壓縮時,如果一個 row 超出了配置的大小時列印 warn 日誌,沒有任何其它作用。

分別是用來配置,根據 Slice 和 Column Name 來查詢時 Cassandra 快取資料的大小,當查詢範圍較小時可以適當設定大一點以提高命中率。

這兩個配置項是設定 Cassandra 在將記憶體中的資料寫到磁碟時一次寫入的快取量,適當提高這個兩個值可以提高 Cassandra 的寫效能。

MemtableOperationsInMillions 是定義當前 Keyspace 對應的資料在記憶體中的快取大小,Cassandra 預設是 64M,也就是當寫到 Cassandra 的資料達到 64M 時,Cassandra 會將記憶體的資料寫到本地磁碟中。

MemtableOperationsInMillions 是定義當前這個 Memtable 中所持有資料物件的個數,真實的個數是 MemtableOperationsInMillions*1024*1024。當超出這個數值時 Memtable 同樣會被寫到磁碟中。

MemtableFlushAfterMinutes 的作用是,當前兩個條件都長時間不滿足時,Memtable 中資料會一直不會寫到磁碟,這也不合適,所以設定了一個時間限制,當超過這個時間長度時 Memtable 中的資料也會被寫到磁碟中。

所以 Memtable 中的資料何時被寫到寫到磁碟是由這三個值決定,任何一個條件滿足都會寫到磁碟。

這兩個是定義 Cassandra 用來處理 read 和 write 的執行緒池中執行緒的個數,根據當前的測試結果,讀寫的效能大慨是 1:10,適當的設定這兩個值不僅要根據讀寫的效能,還要參考當前機器的處理效能。當機器的 load 很高,但是 cpu 的利用率卻很低時,很明顯是由於連線數過多,Cassandra 的已經處理不過來都處於等待狀態。這樣就可以適當增加讀寫的執行緒數,同樣如果當讀的請求大於寫的請求時,也應該適當增加讀的執行緒數,反之亦然。

我們知道 Cassandra 是先寫到 CommitLog 中再寫到 Memtable 和磁碟中。如果每寫一條資料都要寫一次到磁碟那樣效能將會大打折扣。Cassandra 為了提高寫 CommitLog 的效能提供了兩種寫的方式。

  1. Periodic。週期性的把 CommitLog 資料寫到磁碟中,這個時間週期由 CommitLogSyncPeriodInMS 指定,預設是 10000MS, 如果是這種方式,可想而知 Cassandra 並不能完全保證寫到 Cassandra 的資料不會丟失,最壞的情況就是在這個時間段的資料會被丟失,但是 Cassandra 的解釋是通過資料的多個備份,來能提高安全性。但是如果是單機儲存資料,最壞的情況仍然會丟失 10000MS 時間段寫入的資料。可以說這種方式寫 CommitLog 是完全的非同步的方式。
  2. Batch。這種方式是等待資料被寫到磁碟中才會返回,與前面相比安全性會得到保證,它能保證 100% 資料的正確性。但也並不是每寫一條資料都立即寫到磁碟中,而是有一個延遲時間,這個延遲時間就是由 CommitLogSyncBatchWindowInMS 指定的,也就是寫一條資料到 CommitLog 的最大時間是 CommitLogSyncBatchWindowInMS 指定的時間,理想的時間範圍是 0.1~10MS 之間。這個時間既要平衡客戶端的相應時間也要考慮伺服器寫資料到磁碟的效能。

這兩種方式各有好處,如果資料是儲存在有多個備份的叢集中,第一種情況下,丟資料的情況幾乎為零,但是效能肯定會比第二種要好很多。如果是單機情況下,要保證資料的安全性第二種較合適。

這個配置項不是 Java 中的 gc 回收記憶體,但是其功能類似於 jvm 中 gc,它也是回收已經沒有被關聯的資料,例如已經被標識為刪除的資料,Cassandra 處理資料有點奇怪,即使資料被標識為刪除,但是隻要是沒有超過 GCGraceSeconds 的時間這個資料仍然是存在的,也就是可以定製資料的實效時間,超出這個時間資料將會被回收。

按照我的理解我將 Cassandra 的功能模組劃分為三個部分:

  1. 客戶端協議解析。目前這個版本 Cassandra 支援兩個客戶端 avro 和 thrift,使用的較多的是後者,它們都是通過 socket 協議作為網路層協議,然後再包裝一層應用層協議,這個應用層協議的包裝和解析都是由它們的客戶端和相應的服務端模組來完成的。這樣設計的目的是解決多種多樣的客戶端的連線方式,既可以是短連線也可以是長連線。既可以是 Java 程式呼叫也可以是 PHP 呼叫或者多種其它程式語言都可以呼叫。
  2. 叢集 Gossip 協議。叢集中節點之間相互通訊是通過 Gossip 協議來完成的,它的實現都在 org.apache.cassandra.gms.Gossiper 類中。它的主要作用就是每個節點向叢集中的其它節點發送心跳,心跳攜帶的資訊是本身這個節點持有的其它節點的狀態資訊包括本節點的狀態,如果發現兩邊的狀態資訊不是不一致,則會用最新的狀態資訊替換,同時通過心跳來判斷某個節點是否還線上,把這種狀態變化通知感興趣的事件監聽者,以做出相應的修改,包括新增節點、節點死去、節點復活等。除了維護節點狀態資訊外,還需做另外一些事,如叢集之間的資料的轉移,這些資料包括:讀取的資料、寫入的資料、狀態檢查的資料、修復的資料等等。
  3. 資料的儲存。資料的儲存包括,記憶體中資料的組織形式,它又包括 CommitLog 和 Memtable。磁碟的資料組織方式,它又包括 date、filter 和 index 的資料。

其它剩下的就是如何讀取和操作這些資料了,可以用下圖來描述 Cassandra 是如何工作的:


圖 3. Cassandra 的工作模型
圖 3. Cassandra 的工作模型

這裡將詳細介紹 Cassandra 的啟動過程。Cassandra 的啟動過程大慨分為下面幾個階段:

storage-config.xml 配置檔案的解析

配置檔案的讀取和解析都是在 org.apache.cassandra.config.DatabaseDescriptor 類中完成的,這個類的作用非常簡單,就是讀取配置檔案中各個配置項所定義的值,經過簡單的驗證,符合條件就將其值賦給 DatabaseDescriptor 的私有靜態常量。值得注意的是關於 Keyspace 的解析,按照 ColumnFamily 的配置資訊構建成 org.apache.cassandra.config.CFMetaData 物件,最後把這些所有 ColumnFamily 放入 Keyspace 的 HashMap 物件 org.apache.cassandra.config.KSMetaData 中,每個 Keyspace 就是一個 Table。這些資訊都是作為基本的元資訊,可以通過 DatabaseDescriptor 類直接獲取。DatabaseDescriptor 類相關的類結構如下圖 4 所示:


圖 4. DatabaseDescriptor 類相關的類結構
圖 4. DatabaseDescriptor 類相關的類結構

建立每個 Table 的例項

建立 Table 的例項將完成:1)獲取該 Table 的元資訊 TableMatedate。2)建立改 Table 下每個 ColumnFamily 的儲存操作物件 ColumnFamilyStore。3)啟動定時程式,檢查該 ColumnFamily 的 Memtable 設定的 MemtableFlushAfterMinutes 是否已經過期,過期立即寫到磁碟。與 Table 相關的類如圖 5 所示:


圖 5. Table 相關的類圖
圖 5. Table 相關的類圖

一個 Keyspace 對應一個 Table,一個 Table 持有多個 ColumnFamilyStore,而一個 ColumnFamily 對應一個 ColumnFamilyStore。Table 並沒有直接持有 ColumnFamily 的引用而是持有 ColumnFamilyStore,這是因為 ColumnFamilyStore 類中不僅定義了對 ColumnFamily 的各種操作而且它還持有 ColumnFamily 在各種狀態下資料物件的引用,所以持有了 ColumnFamilyStore 就可以操作任何與 ColumnFamily 相關的資料了。與 ColumnFamilyStore 相關的類如圖 6 所示


圖 6. ColumnFamilyStore 相關的類
圖 6. ColumnFamilyStore 相關的類

CommitLog 日誌恢復

這裡主要完成這幾個操作,發現是否有沒有被寫到磁碟的資料,恢復這個資料,構建新的日誌檔案。CommitLog 日誌檔案的恢復策略是,在標頭檔案中發現沒有被序列化的最新的

ColumnFamily Id,然後取出這個這個被序列化 RowMutation 物件的起始地址,反序列化成為 RowMutation 物件,後面的操作和新添一條資料的流程是一樣的,如果這個 RowMutation 物件中的資料被成功寫到磁碟中,那麼會在 CommitLog 去掉已經被持久化的 ColumnFamily Id。關於 CommitLog 日誌檔案的儲存格式以及資料如何寫到 CommitLog 檔案中,將在後面第三部分詳細介紹。

啟動儲存服務

這裡是啟動過程中最重要的一步。這裡將會啟動一系列服務,主要包括如下步驟。

  1. 建立 StorageMetadata。StorageMetadata 將包含三個關鍵資訊:本節點的 Token、當前 generation 以及 ClusterName,Cassandra 判斷如果是第一次啟動,Cassandra 將會建立三列分別儲存這些資訊並將它們存在在系統表的 LocationInfo ColumnFamily 中,key 是“L”。如果不是第一次啟動將會更新這三個值。這裡的 Token 是判斷使用者是否指定,如果指定了使用使用者指定的,否則隨機生成一個 Token。但是這個 Token 有可能在後面被修改。這三個資訊被存在 StorageService 類的 storageMetadata_ 屬性中,以便後面隨時呼叫。
  2. GCInspector.instance.start 服務。主要是統計統計當前系統中資源的使用情況,將這個資訊記錄到日誌檔案中,這個可以作為系統的監控日誌使用。
  3. 啟動訊息監聽服務。這個訊息監聽服務就是監聽整個叢集中其它節點發送到本節點的所有訊息,Cassandra 會根據每個訊息的型別,做出相應的反應。關於訊息的處理將在後面詳細介紹。
  4. StorageLoadBalancer.instance.startBroadcasting 服務。這個服務是每個一段時間會收集當前這個節點所存的資料總量,也就是節點的 load 資料。把這個資料更新到本節點的 ApplicationState 中,然後就可以通過這個 state 來和其它節點交換資訊。這個 load 資訊在資料的儲存和新節點加入的時候,會有參考價值。
  5. 啟動 Gossiper 服務。在啟動 Gossiper 服務之前,將 StorageService 註冊為觀察者,一旦節點的某些狀態發生變化,而這些狀態是 StorageService 感興趣的,StorageService 的 onChange 方法就會觸發。Gossiper 服務就是一個定時程式,它會向本節點加入一個 HeartBeatState 物件,這個物件標識了當前節點是 Live 的,並且記錄當前心跳的 generation 和 version。這個 StorageMetadata 和前面的 StorageMetadata 儲存的 generation 是一致的,version 是從 0 開始的。這個定時程式每隔一秒鐘隨機向 seed 中定義的節點發送一個訊息,而這個訊息是保持叢集中節點狀態一致的唯一途徑。這個訊息如何同步,將在後面詳細介紹。
  6. 判斷啟動模式。是否是 AutoBootstrap 模式啟動,又是如何判斷的,以及應作出那些相應的操作,在前面的第一部分中已有介紹,這裡不再贅述。這裡主要說一下,當是 Bootstrap 模式啟動時,Cassandra 都做了那些事情。這一步很重要,因為它關係到後面的很多操作,對 Cassandra 的效能也會有影響。

這個過程如下:

  1. 通過之前的訊息同步獲取叢集中所有節點的 load 資訊
  2. 找出 load 最大的節點的 ip 地址
  3. 向這個節點發送訊息,獲取其一半 key 範圍所對應的 Token,這個 Token 是前半部分值。
  4. 將這個 Token 寫到本地節點
  5. 本地節點會根據這個 Token 計算以及叢集中的 Token 環,計算這個 Token 應該分攤叢集中資料的一個範圍(range)這個環應該就是,最大 load 節點的一半 key 的所對應的 range。
  6. 向這個 range 所在的節點請求資料。傳送 STREAM-STAGE 型別的訊息,要經過 STREAM_REQUEST、STREAM_INITIATE、STREAM_INITIATE_DONE、STREAM_FINISHED 幾次握手,最終才將正確的資料傳輸到本節點。
  7. 資料傳輸完成時設定 SystemTable.setBootstrapped(true) 標記 Bootstrap 已經啟動,這個標記的目的是防止再次重啟時,Cassandra 仍然會執行相同的操作。

這個過程可以用下面的時序圖來描述:


圖 7. StorageService 服務啟動時序圖
圖 7. StorageService 服務啟動時序圖

檢視大圖

以上是 AutoBootstrap 模式啟動,如果是以非 AutoBootstrap 模式啟動,那麼啟動將會非常簡單,這個過程如下:

  1. 檢查配置項 InitialToken 有沒有指定,如果指定了初始 Token,使用使用者指定的 Token,否則將根據 Partitioner 配置項指定的資料分配策略生成一個預設的 Token,並把它寫到系統表中。
  2. 更新 generation=generation+1 到系統表中
  3. 設定 SystemTable.setBootstrapped(true),標記啟動方式,防止使用者再修改 AutoBootstrap 的啟動模式。

我們知道 Cassandra 叢集中節點是通過自治理來對外提供服務的,它不像 Hadoop 這種 Master/Slave 形式的叢集結構,會有一個主服務節點來管理所有節點中的原資訊和對外提供服務的負載均衡。這種方式管理叢集中的節點邏輯上比較簡單也很方便,但是也有其弱點,那就是這個 Master 容易形成瓶頸,其穩定性也是一種挑戰。而 Cassandra 的叢集管理方式就是一種自適應的管理方式,叢集中的節點沒有 Master、Slave 之分,它們都是平等的,每個節點都可以單獨對外提供服務,某個節點 Crash 也不會影響到其它節點。但是一旦某個節點的狀態發生變化,整個叢集中的所有節點都要知道,並且都會執行預先設定好的應對方案,這會造成節點間要傳送大量的訊息交換各自狀態,這樣也增加了叢集中狀態和資料一致性的複雜度,但是優點是它是一個高度自治的組織,健壯性比較好。

訊息交換

那麼 Cassandra 是如何做到這麼高度自治的呢?這個問題的關鍵就是它們如何同步各自的狀態資訊,同步訊息的前提是它們有一種約定的訊息交換機制。這個機制就是 Gossip 協議,Cassandra 就是通過 Gossip 協議相互交換訊息。

前面在 Cassandra 服務啟動時提到了 Gossiper 服務的啟動,一旦 Cassandra 啟動成功,Gossiper 服務就是一直執行下去,它是一個定時程式。這個服務的程式碼在 org.apache.cassandra.gms.Gossiper 類中,下面是定時程式執行的關鍵程式碼如清單 1 所示:


清單 1. Gossiper.GossipTimerTask.run

Cassandra 通過向其它節點發送心跳來證明自己仍然是活著的,心跳裡面包含有當前的 generation,用來表示有的節點是不是死了又復活的。

本地節點所儲存的所有其它節點的狀態資訊都被放在了 GossipDigest 集合中。一個 GossipDigest 物件將包含這個節點的 generation、maxVersion 和節點地址。接著將會組裝一個 Syn 訊息(關於 Cassandra 中的訊息格式將在後面介紹),同步一次狀態資訊 Cassandra 要進行三次會話,這三次會話分別是 Syn、Ack 和 Ack2。當組裝成 Syn 訊息後 Cassandra 將隨機在當前活著的節點列表中選擇一個向其傳送訊息。

Cassandra 中的訊息格式如下:

  1. header:訊息頭 org.apache.cassandra.net.Header,訊息頭中包含五個屬性:訊息編號(messageId)、傳送方地址(from)、訊息型別(type)、所要做的動作(verb)和一個 map 結構(details)
  2. body:訊息內容,是一個 byte 陣列,用來存放序列化的訊息主體。

可以用下面的圖 8 更形象的表示:


圖 8. message 訊息結構
圖 8. message 訊息結構

當組裝成一個 message 後,再將這個訊息按照 Gossip 協議組裝成一個 pocket 傳送到目的地址。關於這個 pocket 資料包的結構如下:

  1. header:包頭,4 bytes。前兩個是 serializer type;第三個是是否壓縮包,預設是否;最後一個 byte 表示是否是 streaming mode。
  2. body:包體,message 的序列化位元組資料。

這個 pocket 的序列化位元組結構如下:


圖 9. 通訊協議包的結構
圖 9. 通訊協議包的結構

當另外一個節點接受到 Syn 訊息後,反序列化 message 的 byte 陣列,它會取出這個訊息的 verb 執行相應的動作,Syn 的 verb 就是解析出傳送節點傳過來的節點的狀態資訊與本地節點的狀態資訊進行比對,看哪邊的狀態資訊更新,如果傳送方更新,將這個更新的狀態所對應的節點加入請求列表,如果本地更新,則將本地的狀態再回傳給傳送方。回送的訊息是 Ack,當傳送方接受到這個 Ack 訊息後,將接受方的狀態資訊更新的本地對應的節點。再將接收方請求的節點列表的狀態傳送給接受方,這個訊息是 Ack2,接受方法接受到這個 Ack2 訊息後將請求的節點的狀態更新到本地,這樣一次狀態同步就完成了。

不管是傳送方還是接受方每當節點的狀態發生變化時都將通知感興趣的觀察者做出相應的反應。訊息同步所涉及到的類由下面圖 10 的關係圖表示:


圖 10. 節點狀態同步相關類結構圖
圖 10. 節點狀態同步相關類結構圖

檢視大圖

節點的狀態同步操作有點複雜,如果前面描述的還不是很清楚的話,再結合下面的時序圖,你就會更加明白了,如圖 11 所示:


圖 11. 節點狀態同步時序圖
圖 11. 節點狀態同步時序圖

檢視大圖

上圖中省去了一部分重複的訊息,還有節點是如何更新狀態也沒有在圖中反映出來,這些部分在後面還有介紹,這裡也無法完整的描述出來。

狀態更新

前面提到了訊息的交換,它的目的就是可以根據交換的資訊更新各自的狀態。Cassandra 更新狀態是通過觀察者設計模式來完成的,訂閱者被註冊在 Gossiper 的集合中,當交換的訊息中的節點的狀態和本地節點不一致時,這時就會更新本地狀態,更改本地狀態本身並沒有太大的意義,有意義的是狀態發生變化這個動作,這個動作發生時,就會通知訂閱者來完成這個狀態發生變化後應該做出那些相應的改動,例如,發現某個節點已經不在叢集中時,那麼對這個節點應該要在本地儲存的 Live 節點列表中移去,防止還會有資料傳送到這個無法到達的節點。和狀態相關的類如下:


圖 12. 更新狀態相關的類
圖 12. 更新狀態相關的類

從上圖可以看出節點的狀態資訊由 ApplicationState 表示,並儲存在 EndPointState 的集合中。狀態的修改將會通知 IendPointStateChangeSubscriber,繼而再更新 Subscriber 的具體實現類修改相應的狀態。

下面是新節點加入的時序圖,如圖 13 所示:


圖 13. 新加入節點的時序圖
圖 13. 新加入節點的時序圖

上圖基本描述了 Cassandra 更新狀態的過程,需要說明的點是,Cassandra 為何要更新節點的狀態,這實際上就是關於 Cassandra 對叢集中節點的管理,它不是集中管理的方式,所以每個節點都必須儲存叢集中所有其它節點的最新狀態,所以將本節點所持有的其它節點的狀態與另外一個節點交換,這樣做有一個好處就是,並不需要和某個節點通訊就能從其它節點獲取它的狀態資訊,這樣就加快了獲取狀態的時間,同時也減少了叢集中節點交換資訊的頻度。另外,節點狀態資訊的交換的根本還是為了控制叢集中 Cassandra 所維護的一個 Token 環,這個 Token 是 Cassandra 叢集管理的基礎。因為資料的儲存和資料流動都在這個 Token 環上進行,一旦環上的節點發生變化,Cassandra 就要馬上調整這個 Token 環,只有這樣才能始終保持整個叢集正確執行。

到底哪些狀態資訊對整個叢集是重要的,這個在 TokenMetadata 類中,它主要記錄了當前這個叢集中,哪些節點是 live 的哪些節點現在已經不可用了,哪些節點可能正在啟動,以及每個節點它們的 Token 是多少。而這些資訊都是為了能夠精確控制叢集中的那個 Token 環。只要每個叢集中每個節點所儲存的是同一個 Token 環,整個叢集中的節點的狀態就是同步的,反之,叢集中節點的狀態就沒有同步。

當然 Cassandra 用這種叢集管理方式有其優點,但也存在一些缺點。例如現在部分使用者在大規模叢集(上千臺伺服器)的使用中發現不太穩定,這個跟 gossip 協議的本身也有關,所以這是 Cassandra 社群要致力解決的問題。

總結

本文從配置檔案開始介紹了 Cassandra 的啟動過程,以及 Cassandra 是如何管理叢集的。實際上 Cassandra 的啟動和叢集的管理是連在一起的,啟動過程中的很多步驟都是叢集管理的一部分,如節點以 AutoBootstrap 方式啟動,在啟動過程中就涉及到資料的重新分配,這個分配的過程正是在動態調整叢集中 Token 環的過程。所以當你掌握了 Cassandra 是如何動態調整這個 Token 環,你也就掌握了 Cassandra 的叢集是如何管理的了。下一篇將詳細介紹 Cassandra 內部是如何組織資料和操作資料。