技術基礎 | 在Apache Cassandra中改變VNodes數量的影響

DataStax發表於2021-03-06
Apache Cassandra中num_tokens的預設值在4.0版本中將會有變化!這看起來好像只是在CHANGES.txt檔案中做了個小小的改動,但實際上這個改動將會對叢集的日常運維有著深遠的影響。
 
在這篇文章中,我們將會來仔細討論num_tokens值的改變將會如何影響叢集極其執行情況。

 
Apache Cassandra中有很多可以用於改變其行為的設定選項,num_tokens設定引數就是其中之一。像很多其他的設定引數一樣,num_tokens也是在cassandra.yaml檔案中,並且有一個預設值。不過它與其他設定引數相似的點也就到此為止了。
 
正如你所見,大多數Cassandra的設定引數只會對叢集的單一方面產生影響,但是num_tokens值的改變意味著一系列的叢集行為都將會被改變。
 
Apache Cassandra專案已經提交併解決了CASSANDRA-13701 JIRA問題,將num_tokens的預設值從256改為了16。這個改變具有重大意義,想要理解這個改變所帶來的的影響和結果,我們需要先理解num_tokens在叢集中所扮演的角色。

 
01 永遠不要在生產環境中嘗試的事
 
在我們進行深入探討之前,需要注意的是,一旦一個節點已經加入了叢集,num_tokens這個設定引數就不應該再有任何的改變。因為這樣會使得該節點在重啟時將發生故障。
 
一個資料中心中所有節點的num_tokens值應該是一樣的。從過去來說,異構的叢集是允許有不同的num_tokens值的。雖然這種情況很少見,我們也不推薦這麼做——但從理論上講,如果節點的硬體規格提升兩倍,你是可以將num_tokens的值加倍的。
 
另外,一個資料中心的節點的num_tokens值與另一個資料中心的節點的num_tokens值不同,這種情況是很常見的。這正是在保證零當機時間的前提下,可以安全的改變一個正在執行的叢集的num_tokens值的部分原因。

 
02 基礎知識
 
num_tokens這一設定影響了Cassandra如何在節點間分配資料、如何從節點中取出資料,以及如何在節點間移動資料。
 
在後臺,Cassandra用分割槽演算法(partitioner)來決定資料儲存在叢集的何處。分割槽演算法是一個具有一致性的雜湊演算法,它可以將分割槽鍵(partition key,即主鍵的第一部分)一一對映到相應的令牌,而令牌則會決定與這些分割槽鍵相關的資料將會被儲存在哪些節點。
 
叢集中的每個節點都會被分配來自令牌環(token ring)中的一個或多個唯一的令牌值(雜湊值)——這是一種形象而巧妙的說法,其實每個節點被分配的是一段首尾相接的數字範圍中的一個數字。
 
也就是說,所謂的“被分配的數字”就是前面提到的令牌雜湊值,而所謂的“首尾相接的數字範圍”就是前面提到的雜湊環。之所以說令牌環(雜湊環)是環狀的,是因為它的最大值的下一個值是它的最小值。
 
被分配的令牌定義了節點在令牌環上所負責的令牌範圍,這個範圍通常被稱為“令牌區間(token range)”。
 
一個節點所負責的“令牌區間”的邊界是兩個值所定義的:一是該節點被分配的令牌值,二是在令牌環上自該令牌值向後推所得到的最小的可用雜湊值。被分配的令牌值是包括在節點的令牌區間內的,不過令牌環上最小的可用令牌值則不包括在內——該值通常為前一個相鄰節點佔用。
 
一個首尾相接的令牌環意味著節點所負責的令牌可能包括了該令牌環的最大令牌值和最小令牌值。至少曾出現過一次這樣的狀況——在令牌環上向後推所得到的最小可用令牌值越過了令牌環中首尾相接的點,即向後推的最小令牌值越過了令牌環的最大值。
 
舉例來說,在下面的令牌環分配圖中,我們有一個令牌值範圍為0-99的令牌環。令牌10被分配給了節點1。在叢集中,節點1前面的節點是節點5,它被分配的令牌是令牌90。這樣一來,節點1所負責的令牌值的範圍就是91到10了。
 
技術基礎 | 在Apache Cassandra中改變VNodes數量的影響
 
在這個特定的例子中,節點1所負責的令牌值的範圍就越過了令牌環中最大值。
 
注意,上面的圖例只是資料副本數量為1的情況,因為這是令牌環上的每個令牌值都只對應一個節點的情況。如果資料有多個副本,節點的鄰居就會成為副本節點,負責該令牌值對應的副本資料了——請看下面的令牌環分配圖:
 
技術基礎 | 在Apache Cassandra中改變VNodes數量的影響
 
之所以將分割槽演算法定義為一個具有一致性的雜湊演算法,其實是因為它本身就有這樣的特性——無論多少次地輸入某一特定的值,它總會生成並輸出同樣的值。
 
這種特性保證了所有的節點(node)、協調節點(coordinator)或是其他的任何元件在一個給定的分割槽鍵下,總能計算出同樣的值。而這個計算得到的令牌值則可被用於可靠地定位儲存著所需資料的節點。
 
結果就是,令牌環的最大最小值就由分割槽演算法來定義。舉例來說,預設使用的基於Murmur雜湊函式的Murur3Partitioner演算法的範圍是-2^63到+2^63 - 1;而以前曾使用的基於MD5雜湊函式的RandomPartitioner演算法的範圍則是0 to 2^127 - 1。
 
這樣的一個系統有一個嚴重的副作用,就是一旦一個叢集選定了一個分割槽演算法,那麼之後就不能再更改了。若想要更改分割槽演算法,則需先重新建立一個叢集,再選擇所想使用的分割槽演算法,然後再將資料匯入到新的叢集中。

 
03 早些時候……
 
在Cassandra 1.2版本之前的時代,節點只能被手動分配單獨一個令牌。現在你依然可以通過cassandra.yaml檔案中的initial_token設定引數來實現同樣的效果。
 
那時候,預設使用的分割槽演算法是RandomPartitioner。在一個叢集從無到有的過程中,雖然令牌的分配過程是是手動的,但是RandomPartitioner分割槽演算法使得計算分配令牌的過程相當簡單直白。
 
舉例來說,如果你的叢集有3個節點,你需要做的就是用2^127 - 1除以3,這樣所得到的商就是相鄰的令牌所相差的正確增量。
 
你的第一個節點的起始令牌為0,即initial_token引數為0。接下來的節點的initial_token就會是(2^127 - 1) / 3,然後第三個節點的initial_token會是(2^127 - 1) / 3 * 2。這樣,每個節點的令牌範圍大小就會是一致的。
 
在各個節點的硬體配置完全一樣且資料在叢集中平均分佈的前提下,平均分配每個節點所負責的令牌範圍會使得節點過載的可能性較低。令牌分配的不均可能會導致所謂的“熱點問題(hot spot)”——即由於需要比別的節點處理更多請求或儲存更多資料,某個節點會處於較強的壓力之下。
 
儘管搭建一個單個節點對應單個令牌的叢集可能是一個非常手動的過程,但是它們的部署過程還是很常見,尤其是對於那些節點數通常超過1000的超大型Cassandra叢集來說。這種部署的優點之一就是可以保證令牌的分佈是均勻的。
 
雖然從頭搭建一個單個節點對應單個令牌的叢集可以保證負載的均勻分佈,想要擴大叢集可就沒有那麼容易了。如果你在擁有三個節點的叢集中再插入一個節點,結果就是四個節點當中的兩個節點的令牌範圍會小於另外兩個節點的令牌範圍。
 
想要修復這個問題並使得令牌分佈獲得再平衡,你就得執行nodetool move從而將令牌重新分配給其他節點。但是這個過程繁瑣且昂貴,因為其中牽涉了很多在整個叢集範圍內移動的資料流。一個替代方案是每次擴大叢集的時候,就將其擴大兩倍。不過這通常意味著需要使用比你實際需要的更多的硬體。
 
在一個單個節點對應單個令牌的叢集中保持令牌的均勻分佈就像是管理一個整潔無暇的後花園,你需要投入時間、養護以及注意力,或者要有大量的智慧自動化方案。
 
對於單個節點對應單個令牌的叢集來說,擴充套件性只是所有挑戰的一半。而挑戰的另一半,則是某些故障場景會極大地延長恢復所需要的時間。
 
讓我們來舉個例子,假設你在一個資料中心裡有一個擁有6個節點的叢集,叢集資料的副本數為3 (Replication Factor = 3)。這些副本可能會儲存在節點1和節點4,或節點2和節點5,或節點3和節點6。在這種情景下,每個節點負責3套副本中每套副本的六分之一。
 
技術基礎 | 在Apache Cassandra中改變VNodes數量的影響
 
在上面的圖中,令牌環上的每一個令牌區間都被分配了一個英文字母,這是為了更容易地記錄每個節點被分配到的令牌。
 
如果這個叢集出現了故障,導致節點1和節點6變成不可用狀態,你就只能通過節點2和節點5來回復它們所擁有的那獨特的1/6的資料。也就是說,只有節點2可以被用於恢復與令牌區間F相關的資料,也只有節點5可以被用於恢復與令牌區間E相關的資料。下圖展示了這個原理:
 
技術基礎 | 在Apache Cassandra中改變VNodes數量的影響

 
04 vnodes前來救場
 
為了解決單個節點對應單個令牌這種分配方案的多個缺點,增強後的Cassandra 1.2版本允許一個節點可以被分配多個令牌,即一個節點可以負責多個令牌區間。Cassandra的這項特性被稱為“虛擬節點”,簡稱vnode。
 
vnode這項特性是在CASSANDRA-4119這個JIRA中被引入的,根據任務描述,vnode的目標是:
  • 降低叢集伸縮的運維複雜性
  • 縮短故障重建時間
  • 故障發生時能夠平均分配負載
  • 平均分配資料流操作帶來的影響
  • 針對硬體異構性提供更可行的支援
Vnode這項特性的引入催生了cassandra.yaml檔案中的num_tokens設定引數,它定義了一個節點負責的vnode(即令牌區間)的數量。
 
增加每個節點所負責的vnode的數量,則每個令牌區間會相應縮小。這是因為令牌環上的令牌數目總是有限的,分出越多的區間,則每個區間的範圍就會越小。
 
為了能夠保持對更老的1.x系列版本的叢集的向下相容性,num_tokens的預設值為1。而且在出廠設定(vanilla installation)裡,這個設定引數被有效地禁用了——具體來說,就是該值在cassandra.yaml檔案中已經被註釋掉了。但是這個註釋行和以前的多次開發提交確實讓大家得以窺見vnode這項新特性的未來。
 
就像cassandra.yaml檔案和那些git提交記錄所預示的,當Cassandra 2.0版本推出之時,vnode這項特性被預設啟用了。num_tokens的所在行並不再被註釋掉,在出廠設定(vanilla installation)裡,它有效的預設值為256。
 
這開創了叢集的新時代——令牌相對還能保持平均分配,同時叢集可以簡單地擴張。
 
有了由256個vnode組成的節點以及附帶的其他功能,擴張叢集的過程就像是做夢一樣——你可以只管在你的叢集中插入一個新的節點,Cassandra就會自動計算並分配令牌!令牌值是隨機計算出來的,所以隨著時間的推移,當你新增更多節點時,叢集會收斂到一種平衡的狀態。
 
這個就像是魔法一般的工程專案使得人們再也不需要花費數小時做計算,更無需多次用nodetool move操作來實現叢集的擴張——雖然這種方案仍然還可以被使用。
 
如果你有一個非常大的叢集或者有其他的要求,你仍可以使用在Cassandra 2.0中已經被註釋掉的initial_token設定引數。如果要這樣做,那麼num_token的值仍然要手動設定成在initial_token設定引數中所定義的令牌數量。

 
05 記得了解相關的限制條件
 
這個特性就像是我們擁有了個人開發助手——你交給他們一個節點,告訴他們將節點插入叢集,然後過一會兒他們就會將令牌分配好,並且節點也已經成了叢集的一部分。但是,塞翁得馬焉知非禍……
 
雖然使用256個vnode時令牌分佈會更為均勻,但會出現可用性降低更快的問題——諷刺的是,我們越多地分拆令牌區間,我們的資料將越快面臨不可用的問題。而當vnode數量較少時,更容易會出現令牌區間不均的問題。
 
這裡所說的“數量較少”是指vnode的數量少於32個。當vnode數量較少時,Cassandra的隨機令牌分配機制就變得無能為力。其原因是面對生成的長度差異很大的令牌區間,系統沒有足夠的令牌用於平衡令牌的分配。

 
06 一圖勝千言
 
藉助用於測試的叢集,很容易能演示出上面提到的可用性和令牌區間分配不均的問題。我們可以用ccm搭建一個擁有6個節點的單個節點對應單個令牌的叢集。在計算了令牌分配,並配置啟動我們的測試叢集后,會得到如下輸出結果。
 
$ ccm node1 nodetool status

Datacenter: datacenter1
=======================
Status=Up/Down
|/ State=Normal/Leaving/Joining/Moving
--  Address    Load       Tokens       Owns (effective)  Host ID                               Rack
UN  127.0.0.1  71.17 KiB  1            33.3%             8d483ae7-e7fa-4c06-9c68-22e71b78e91f  rack1
UN  127.0.0.2  65.99 KiB  1            33.3%             cc15803b-2b93-40f7-825f-4e7bdda327f8  rack1
UN  127.0.0.3  85.3 KiB   1            33.3%             d2dd4acb-b765-4b9e-a5ac-a49ec155f666  rack1
UN  127.0.0.4  104.58 KiB  1            33.3%             ad11be76-b65a-486a-8b78-ccf911db4aeb  rack1
UN  127.0.0.5  71.19 KiB  1            33.3%             76234ece-bf24-426a-8def-355239e8f17b  rack1
UN  127.0.0.6  30.45 KiB  1            33.3%             cca81c64-d3b9-47b8-ba03-46356133401b  rack1
 
接著,我們可以使用cqlsh建立一個測試鍵空間(keyspace),並向其中輸入資料。
$ ccm node1 cqlsh
Connected to SINGLETOKEN at 127.0.0.1:9042.
[cqlsh 5.0.1 | Cassandra 3.11.9 | CQL spec 3.4.4 | Native protocol v4]
Use HELP for help.
cqlsh> CREATE KEYSPACE test_keyspace WITH REPLICATION = { 'class' : 'NetworkTopologyStrategy', 'datacenter1' : 3 };
cqlsh> CREATE TABLE test_keyspace.test_table (
...   id int,
...   value text,
...   PRIMARY KEY (id));
cqlsh> CONSISTENCY LOCAL_QUORUM;
Consistency level set to LOCAL_QUORUM.
cqlsh> INSERT INTO test_keyspace.test_table (id, value) VALUES (1, 'foo');
cqlsh> INSERT INTO test_keyspace.test_table (id, value) VALUES (2, 'bar');
cqlsh> INSERT INTO test_keyspace.test_table (id, value) VALUES (3, 'net');
cqlsh> INSERT INTO test_keyspace.test_table (id, value) VALUES (4, 'moo');
cqlsh> INSERT INTO test_keyspace.test_table (id, value) VALUES (5, 'car');
cqlsh> INSERT INTO test_keyspace.test_table (id, value) VALUES (6, 'set');

 

想要確認該叢集是否已經完美地實現令牌的平均分配,我們可以檢視該叢集的令牌環。
$ ccm node1 nodetool ring test_keyspace


Datacenter: datacenter1
==========
Address    Rack   Status  State   Load        Owns     Token
                                                       6148914691236517202
127.0.0.1  rack1  Up      Normal  125.64 KiB  50.00%   -9223372036854775808
127.0.0.2  rack1  Up      Normal  125.31 KiB  50.00%   -6148914691236517206
127.0.0.3  rack1  Up      Normal  124.1 KiB   50.00%   -3074457345618258604
127.0.0.4  rack1  Up      Normal  104.01 KiB  50.00%   -2
127.0.0.5  rack1  Up      Normal  126.05 KiB  50.00%   3074457345618258600
127.0.0.6  rack1  Up      Normal  120.76 KiB  50.00%   6148914691236517202

  

在“Owns”那列,我們可以看到所有的節點都擁有50%的資料。為了讓這個例子更容易理解,我們可以在每個令牌的右邊手動新增一個代表該令牌的字母。這樣一來,這些令牌區間就會被如下這樣表示:
 
$ ccm node1 nodetool ring test_keyspace


Datacenter: datacenter1
==========
Address    Rack   Status  State   Load        Owns     Token                 Token Letter
                                                       6148914691236517202   F
127.0.0.1  rack1  Up      Normal  125.64 KiB  50.00%   -9223372036854775808  A
127.0.0.2  rack1  Up      Normal  125.31 KiB  50.00%   -6148914691236517206  B
127.0.0.3  rack1  Up      Normal  124.1 KiB   50.00%   -3074457345618258604  C
127.0.0.4  rack1  Up      Normal  104.01 KiB  50.00%   -2                    D
127.0.0.5  rack1  Up      Normal  126.05 KiB  50.00%   3074457345618258600   E
127.0.0.6  rack1  Up      Normal  120.76 KiB  50.00%   6148914691236517202   F

接著,我們可以捕獲ccm node1 nodetool describering test_keyspace的輸出結果,並且將令牌序號換成上面令牌環輸出結果中相應的字母。
 
$ ccm node1 nodetool describering test_keyspace

Schema Version:6256fe3f-a41e-34ac-ad76-82dba04d92c3
TokenRange:
  TokenRange(start_token:A, end_token:B, endpoints:[127.0.0.2, 127.0.0.3, 127.0.0.4], rpc_endpoints:[127.0.0.2, 127.0.0.3, 127.0.0.4], endpoint_details:[EndpointDetails(host:127.0.0.2, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.3, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.4, datacenter:datacenter1, rack:rack1)])
  TokenRange(start_token:C, end_token:D, endpoints:[127.0.0.4, 127.0.0.5, 127.0.0.6], rpc_endpoints:[127.0.0.4, 127.0.0.5, 127.0.0.6], endpoint_details:[EndpointDetails(host:127.0.0.4, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.5, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.6, datacenter:datacenter1, rack:rack1)])
  TokenRange(start_token:B, end_token:C, endpoints:[127.0.0.3, 127.0.0.4, 127.0.0.5], rpc_endpoints:[127.0.0.3, 127.0.0.4, 127.0.0.5], endpoint_details:[EndpointDetails(host:127.0.0.3, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.4, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.5, datacenter:datacenter1, rack:rack1)])
  TokenRange(start_token:D, end_token:E, endpoints:[127.0.0.5, 127.0.0.6, 127.0.0.1], rpc_endpoints:[127.0.0.5, 127.0.0.6, 127.0.0.1], endpoint_details:[EndpointDetails(host:127.0.0.5, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.6, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.1, datacenter:datacenter1, rack:rack1)])
  TokenRange(start_token:F, end_token:A, endpoints:[127.0.0.1, 127.0.0.2, 127.0.0.3], rpc_endpoints:[127.0.0.1, 127.0.0.2, 127.0.0.3], endpoint_details:[EndpointDetails(host:127.0.0.1, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.2, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.3, datacenter:datacenter1, rack:rack1)])
  TokenRange(start_token:E, end_token:F, endpoints:[127.0.0.6, 127.0.0.1, 127.0.0.2], rpc_endpoints:[127.0.0.6, 127.0.0.1, 127.0.0.2], endpoint_details:[EndpointDetails(host:127.0.0.6, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.1, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.2, datacenter:datacenter1, rack:rack1)])

  

根據上面的輸出結果,特別是end_token這一列,我們就可以知道所有的節點被分配的令牌區間。就像前文“基礎知識”部分中提到的那樣,令牌區間是由前一個令牌(start_token)開始(不包括在內),直到本節點被分配的令牌(end_token)為止(包括在內)。
每個節點被分配的令牌區間如下圖所示:
 
技術基礎 | 在Apache Cassandra中改變VNodes數量的影響
 
在這種配置下,如果節點3(node3)和節點6(node6)變為不可用狀態,我們將會丟失一整個資料副本。不過即使應用程式使用的一致性級別(consistency level)為LOCAL_QUORUM,所有的資料仍然可用,因為我們在剩下的四個節點中依然存有兩個資料副本。
 
現在,讓我們來考慮一下叢集使用vnode的情況。為了舉例,我們可以將num_tokens設為3,這樣比較小的令牌數會讓我們的例子更容易理解。在使用ccm配置並啟動多個節點後,我們的測試叢集的初始情況如下:
 
對於大多數叢集大小小於500個節點的生產部署,建議使用更大的num_tokens值。
 
$ ccm node1 nodetool status

Datacenter: datacenter1
=======================
Status=Up/Down
|/ State=Normal/Leaving/Joining/Moving
--  Address    Load       Tokens  Owns (effective)  Host ID                               Rack
UN  127.0.0.1  71.21 KiB  3       46.2%             7d30cbd4-8356-4189-8c94-0abe8e4d4d73  rack1
UN  127.0.0.2  66.04 KiB  3       37.5%             16bb0b37-2260-440c-ae2a-08cbf9192f85  rack1
UN  127.0.0.3  90.48 KiB  3       28.9%             dc8c9dfd-cf5b-470c-836d-8391941a5a7e  rack1
UN  127.0.0.4  104.64 KiB  3      20.7%             3eecfe2f-65c4-4f41-bbe4-4236bcdf5bd2  rack1
UN  127.0.0.5  66.09 KiB  3       36.1%             4d5adf9f-fe0d-49a0-8ab3-e1f5f9f8e0a2  rack1
UN  127.0.0.6  71.23 KiB  3       30.6%             b41496e6-f391-471c-b3c4-6f56ed4442d6  rack1

 

在上面的輸出結果中我們可以直觀迅速地看到該叢集可能已經處於失衡狀態。

就像我們針對單個節點對應單個令牌的叢集所做的那樣,這裡我們可以用cqlsh建立一個測試鍵空間,並向其中輸入資料。接著,我們通過讀取資料來看一下令牌環的情況。同樣的,為了使這個例子更容易理解,我們在每個令牌的右邊手動新增一個代表該令牌的字母。
 
$ ccm node1 nodetool ring test_keyspace

Datacenter: datacenter1
==========
Address    Rack   Status  State   Load        Owns    Token                 Token Letter
                                                      8828652533728408318   R
127.0.0.5  rack1  Up      Normal  121.09 KiB  41.44%  -7586808982694641609  A
127.0.0.1  rack1  Up      Normal  126.49 KiB  64.03%  -6737339388913371534  B
127.0.0.2  rack1  Up      Normal  126.04 KiB  66.60%  -5657740186656828604  C
127.0.0.3  rack1  Up      Normal  135.71 KiB  39.89%  -3714593062517416200  D
127.0.0.6  rack1  Up      Normal  126.58 KiB  40.07%  -2697218374613409116  E
127.0.0.1  rack1  Up      Normal  126.49 KiB  64.03%  -1044956249817882006  F
127.0.0.2  rack1  Up      Normal  126.04 KiB  66.60%  -877178609551551982   G
127.0.0.4  rack1  Up      Normal  110.22 KiB  47.96%  -852432543207202252   H
127.0.0.5  rack1  Up      Normal  121.09 KiB  41.44%  117262867395611452    I
127.0.0.6  rack1  Up      Normal  126.58 KiB  40.07%  762725591397791743    J
127.0.0.3  rack1  Up      Normal  135.71 KiB  39.89%  1416289897444876127   K
127.0.0.1  rack1  Up      Normal  126.49 KiB  64.03%  3730403440915368492   L
127.0.0.4  rack1  Up      Normal  110.22 KiB  47.96%  4190414744358754863   M
127.0.0.2  rack1  Up      Normal  126.04 KiB  66.60%  6904945895761639194   N
127.0.0.5  rack1  Up      Normal  121.09 KiB  41.44%  7117770953638238964   O
127.0.0.4  rack1  Up      Normal  110.22 KiB  47.96%  7764578023697676989   P
127.0.0.3  rack1  Up      Normal  135.71 KiB  39.89%  8123167640761197831   Q
127.0.0.6  rack1  Up      Normal  126.58 KiB  40.07%  8828652533728408318   R

  

就像上面“Owns”那列所顯示的,令牌區間的分配有著較大的失衡現象,導致各個節點所負責的資料量有很大的不同。
 
IP地址為127.0.0.3的節點有著最小的令牌區間,該節點擁有39.89%的資料副本;而IP地址為127.0.0.2的節點有著最大的令牌區間,該節點擁有66.6%的資料副本——這兩者居然幾乎相差了26%。
 
就像前面所做的那樣,我們可以捕獲ccm node1 nodetool describering test_keyspace的輸出結果,並且將令牌序號換成上面令牌環輸出結果中相應的字母。
 
$ ccm node1 nodetool describering test_keyspace

Schema Version:4b2dc440-2e7c-33a4-aac6-ffea86cb0e21
TokenRange:
    TokenRange(start_token:J, end_token:K, endpoints:[127.0.0.3, 127.0.0.1, 127.0.0.4], rpc_endpoints:[127.0.0.3, 127.0.0.1, 127.0.0.4], endpoint_details:[EndpointDetails(host:127.0.0.3, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.1, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.4, datacenter:datacenter1, rack:rack1)])
    TokenRange(start_token:K, end_token:L, endpoints:[127.0.0.1, 127.0.0.4, 127.0.0.2], rpc_endpoints:[127.0.0.1, 127.0.0.4, 127.0.0.2], endpoint_details:[EndpointDetails(host:127.0.0.1, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.4, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.2, datacenter:datacenter1, rack:rack1)])
    TokenRange(start_token:E, end_token:F, endpoints:[127.0.0.1, 127.0.0.2, 127.0.0.4], rpc_endpoints:[127.0.0.1, 127.0.0.2, 127.0.0.4], endpoint_details:[EndpointDetails(host:127.0.0.1, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.2, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.4, datacenter:datacenter1, rack:rack1)])
    TokenRange(start_token:D, end_token:E, endpoints:[127.0.0.6, 127.0.0.1, 127.0.0.2], rpc_endpoints:[127.0.0.6, 127.0.0.1, 127.0.0.2], endpoint_details:[EndpointDetails(host:127.0.0.6, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.1, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.2, datacenter:datacenter1, rack:rack1)])
    TokenRange(start_token:I, end_token:J, endpoints:[127.0.0.6, 127.0.0.3, 127.0.0.1], rpc_endpoints:[127.0.0.6, 127.0.0.3, 127.0.0.1], endpoint_details:[EndpointDetails(host:127.0.0.6, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.3, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.1, datacenter:datacenter1, rack:rack1)])
    TokenRange(start_token:A, end_token:B, endpoints:[127.0.0.1, 127.0.0.2, 127.0.0.3], rpc_endpoints:[127.0.0.1, 127.0.0.2, 127.0.0.3], endpoint_details:[EndpointDetails(host:127.0.0.1, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.2, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.3, datacenter:datacenter1, rack:rack1)])
    TokenRange(start_token:R, end_token:A, endpoints:[127.0.0.5, 127.0.0.1, 127.0.0.2], rpc_endpoints:[127.0.0.5, 127.0.0.1, 127.0.0.2], endpoint_details:[EndpointDetails(host:127.0.0.5, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.1, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.2, datacenter:datacenter1, rack:rack1)])
    TokenRange(start_token:M, end_token:N, endpoints:[127.0.0.2, 127.0.0.5, 127.0.0.4], rpc_endpoints:[127.0.0.2, 127.0.0.5, 127.0.0.4], endpoint_details:[EndpointDetails(host:127.0.0.2, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.5, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.4, datacenter:datacenter1, rack:rack1)])
    TokenRange(start_token:H, end_token:I, endpoints:[127.0.0.5, 127.0.0.6, 127.0.0.3], rpc_endpoints:[127.0.0.5, 127.0.0.6, 127.0.0.3], endpoint_details:[EndpointDetails(host:127.0.0.5, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.6, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.3, datacenter:datacenter1, rack:rack1)])
    TokenRange(start_token:L, end_token:M, endpoints:[127.0.0.4, 127.0.0.2, 127.0.0.5], rpc_endpoints:[127.0.0.4, 127.0.0.2, 127.0.0.5], endpoint_details:[EndpointDetails(host:127.0.0.4, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.2, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.5, datacenter:datacenter1, rack:rack1)])
    TokenRange(start_token:N, end_token:O, endpoints:[127.0.0.5, 127.0.0.4, 127.0.0.3], rpc_endpoints:[127.0.0.5, 127.0.0.4, 127.0.0.3], endpoint_details:[EndpointDetails(host:127.0.0.5, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.4, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.3, datacenter:datacenter1, rack:rack1)])
    TokenRange(start_token:P, end_token:Q, endpoints:[127.0.0.3, 127.0.0.6, 127.0.0.5], rpc_endpoints:[127.0.0.3, 127.0.0.6, 127.0.0.5], endpoint_details:[EndpointDetails(host:127.0.0.3, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.6, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.5, datacenter:datacenter1, rack:rack1)])
    TokenRange(start_token:Q, end_token:R, endpoints:[127.0.0.6, 127.0.0.5, 127.0.0.1], rpc_endpoints:[127.0.0.6, 127.0.0.5, 127.0.0.1], endpoint_details:[EndpointDetails(host:127.0.0.6, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.5, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.1, datacenter:datacenter1, rack:rack1)])
    TokenRange(start_token:F, end_token:G, endpoints:[127.0.0.2, 127.0.0.4, 127.0.0.5], rpc_endpoints:[127.0.0.2, 127.0.0.4, 127.0.0.5], endpoint_details:[EndpointDetails(host:127.0.0.2, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.4, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.5, datacenter:datacenter1, rack:rack1)])
    TokenRange(start_token:C, end_token:D, endpoints:[127.0.0.3, 127.0.0.6, 127.0.0.1], rpc_endpoints:[127.0.0.3, 127.0.0.6, 127.0.0.1], endpoint_details:[EndpointDetails(host:127.0.0.3, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.6, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.1, datacenter:datacenter1, rack:rack1)])
    TokenRange(start_token:G, end_token:H, endpoints:[127.0.0.4, 127.0.0.5, 127.0.0.6], rpc_endpoints:[127.0.0.4, 127.0.0.5, 127.0.0.6], endpoint_details:[EndpointDetails(host:127.0.0.4, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.5, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.6, datacenter:datacenter1, rack:rack1)])
    TokenRange(start_token:B, end_token:C, endpoints:[127.0.0.2, 127.0.0.3, 127.0.0.6], rpc_endpoints:[127.0.0.2, 127.0.0.3, 127.0.0.6], endpoint_details:[EndpointDetails(host:127.0.0.2, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.3, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.6, datacenter:datacenter1, rack:rack1)])
    TokenRange(start_token:O, end_token:P, endpoints:[127.0.0.4, 127.0.0.3, 127.0.0.6], rpc_endpoints:[127.0.0.4, 127.0.0.3, 127.0.0.6], endpoint_details:[EndpointDetails(host:127.0.0.4, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.3, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.6, datacenter:datacenter1, rack:rack1)])

  

最後,我們就可以知道所有節點所被分配到的令牌區間了——它們如下圖所示:
 
技術基礎 | 在Apache Cassandra中改變VNodes數量的影響
根據上圖,我們可以看看如果發生像是前面提到的單個節點對應單個令牌的叢集所遇到的故障(即節點3和節點6處於不可用狀態),結果會如何。
 
上圖中我們可以看到節點3和節點6都負責令牌區間C、D、I、J、Q。所以如果我們的應用程式的一致性級別是LOCAL_QUORUM,那麼與這些令牌相關的資料都會處於不可用狀態。
 
換句話說,與單個節點對應單個令牌的叢集不同,在這種情況下,33.3%的資料可能再也無法被讀取了。

 
07 上機架
 
經驗豐富的Cassandra使用者會注意到,到目前為止,我們只在一個單獨的機架(rack)上針對我們的叢集進行了令牌分配測試。在使用vnode時,我們可以通過部署更多的機架來提高可用性。
 
當使用多個機架時,Cassandra會試著在每個機架上都只存放一個單獨的資料副本——即Cassandra會試著確保在同一個機架上,不會出現兩個完全相同的令牌區間。
 
這裡的重點是要做好叢集的配置工作——對於一個給定的資料中心,其機架的數量應該與其複製因子(replication factor)相等。
 
讓我們再用一下前面num_tokens被設為3的例子,不過這次我們在測試叢集中會定義3個機架。在用ccm配置並啟動節點後,我們重新配置好的測試叢集的初始狀態如下:
 
$ ccm node1 nodetool status

Datacenter: datacenter1
=======================
Status=Up/Down
|/ State=Normal/Leaving/Joining/Moving
--  Address    Load       Tokens  Owns (effective)  Host ID                               Rack
UN  127.0.0.1  71.08 KiB  3       31.8%             49df615d-bfe5-46ce-a8dd-4748c086f639  rack1
UN  127.0.0.2  71.04 KiB  3       34.4%             3fef187e-00f5-476d-b31f-7aa03e9d813c  rack2
UN  127.0.0.3  66.04 KiB  3       37.3%             c6a0a5f4-91f8-4bd1-b814-1efc3dae208f  rack3
UN  127.0.0.4  109.79 KiB  3      52.9%             74ac0727-c03b-476b-8f52-38c154cfc759  rack1
UN  127.0.0.5  66.09 KiB  3       18.7%             5153bad4-07d7-4a24-8066-0189084bbc80  rack2
UN  127.0.0.6  66.09 KiB  3       25.0%             6693214b-a599-4f58-b1b4-a6cf0dd684ba  rack3

  

我們仍能看到一些表明叢集可能處於失衡狀態的跡象,不過這是次要問題——在上面的程式碼中,我們的主要關注點在於現在我們在1個叢集中定義了3個機架,並且為每個機架分配了2個節點。
 
與前面我們對單個節點的叢集所做的操作類似,我們可以用cqlsh建立一個測試鍵空間,並向其中輸入資料。接著,我們通過讀取資料來看一下令牌環的情況。與前面的測試相同,為了使這個例子更容易理解,我們在每個令牌的右邊手動新增一個代表該令牌的字母。
 
ccm node1 nodetool ring test_keyspace


Datacenter: datacenter1
==========
Address    Rack   Status  State   Load        Owns    Token                 Token Letter
                                                      8993942771016137629   R
127.0.0.5  rack2  Up      Normal  122.42 KiB  34.65%  -8459555739932651620  A
127.0.0.4  rack1  Up      Normal  111.07 KiB  53.84%  -8458588239787937390  B
127.0.0.3  rack3  Up      Normal  116.12 KiB  60.72%  -8347996802899210689  C
127.0.0.1  rack1  Up      Normal  121.31 KiB  46.16%  -5712162437894176338  D
127.0.0.4  rack1  Up      Normal  111.07 KiB  53.84%  -2744262056092270718  E
127.0.0.6  rack3  Up      Normal  122.39 KiB  39.28%  -2132400046698162304  F
127.0.0.2  rack2  Up      Normal  121.42 KiB  65.35%  -1232974565497331829  G
127.0.0.4  rack1  Up      Normal  111.07 KiB  53.84%  1026323925278501795   H
127.0.0.2  rack2  Up      Normal  121.42 KiB  65.35%  3093888090255198737   I
127.0.0.2  rack2  Up      Normal  121.42 KiB  65.35%  3596129656253861692   J
127.0.0.3  rack3  Up      Normal  116.12 KiB  60.72%  3674189467337391158   K
127.0.0.5  rack2  Up      Normal  122.42 KiB  34.65%  3846303495312788195   L
127.0.0.1  rack1  Up      Normal  121.31 KiB  46.16%  4699181476441710984   M
127.0.0.1  rack1  Up      Normal  121.31 KiB  46.16%  6795515568417945696   N
127.0.0.3  rack3  Up      Normal  116.12 KiB  60.72%  7964270297230943708   O
127.0.0.5  rack2  Up      Normal  122.42 KiB  34.65%  8105847793464083809   P
127.0.0.6  rack3  Up      Normal  122.39 KiB  39.28%  8813162133522758143   Q
127.0.0.6  rack3  Up      Normal  122.39 KiB  39.28%  8993942771016137629   R

  

與前面的測試步驟一樣,我們可以捕獲ccm node1 nodetool describering test_keyspace的輸出結果,並且將令牌序號換成上面令牌環輸出結果中相應的字母。
$ ccm node1 nodetool describering test_keyspace

Schema Version:aff03498-f4c1-3be1-b133-25503becf208
TokenRange:
    TokenRange(start_token:B, end_token:C, endpoints:[127.0.0.3, 127.0.0.1, 127.0.0.2], rpc_endpoints:[127.0.0.3, 127.0.0.1, 127.0.0.2], endpoint_details:[EndpointDetails(host:127.0.0.3, datacenter:datacenter1, rack:rack3), EndpointDetails(host:127.0.0.1, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.2, datacenter:datacenter1, rack:rack2)])
    TokenRange(start_token:L, end_token:M, endpoints:[127.0.0.1, 127.0.0.3, 127.0.0.5], rpc_endpoints:[127.0.0.1, 127.0.0.3, 127.0.0.5], endpoint_details:[EndpointDetails(host:127.0.0.1, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.3, datacenter:datacenter1, rack:rack3), EndpointDetails(host:127.0.0.5, datacenter:datacenter1, rack:rack2)])
    TokenRange(start_token:N, end_token:O, endpoints:[127.0.0.3, 127.0.0.5, 127.0.0.4], rpc_endpoints:[127.0.0.3, 127.0.0.5, 127.0.0.4], endpoint_details:[EndpointDetails(host:127.0.0.3, datacenter:datacenter1, rack:rack3), EndpointDetails(host:127.0.0.5, datacenter:datacenter1, rack:rack2), EndpointDetails(host:127.0.0.4, datacenter:datacenter1, rack:rack1)])
    TokenRange(start_token:P, end_token:Q, endpoints:[127.0.0.6, 127.0.0.5, 127.0.0.4], rpc_endpoints:[127.0.0.6, 127.0.0.5, 127.0.0.4], endpoint_details:[EndpointDetails(host:127.0.0.6, datacenter:datacenter1, rack:rack3), EndpointDetails(host:127.0.0.5, datacenter:datacenter1, rack:rack2), EndpointDetails(host:127.0.0.4, datacenter:datacenter1, rack:rack1)])
    TokenRange(start_token:K, end_token:L, endpoints:[127.0.0.5, 127.0.0.1, 127.0.0.3], rpc_endpoints:[127.0.0.5, 127.0.0.1, 127.0.0.3], endpoint_details:[EndpointDetails(host:127.0.0.5, datacenter:datacenter1, rack:rack2), EndpointDetails(host:127.0.0.1, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.3, datacenter:datacenter1, rack:rack3)])
    TokenRange(start_token:R, end_token:A, endpoints:[127.0.0.5, 127.0.0.4, 127.0.0.3], rpc_endpoints:[127.0.0.5, 127.0.0.4, 127.0.0.3], endpoint_details:[EndpointDetails(host:127.0.0.5, datacenter:datacenter1, rack:rack2), EndpointDetails(host:127.0.0.4, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.3, datacenter:datacenter1, rack:rack3)])
    TokenRange(start_token:I, end_token:J, endpoints:[127.0.0.2, 127.0.0.3, 127.0.0.1], rpc_endpoints:[127.0.0.2, 127.0.0.3, 127.0.0.1], endpoint_details:[EndpointDetails(host:127.0.0.2, datacenter:datacenter1, rack:rack2), EndpointDetails(host:127.0.0.3, datacenter:datacenter1, rack:rack3), EndpointDetails(host:127.0.0.1, datacenter:datacenter1, rack:rack1)])
    TokenRange(start_token:Q, end_token:R, endpoints:[127.0.0.6, 127.0.0.5, 127.0.0.4], rpc_endpoints:[127.0.0.6, 127.0.0.5, 127.0.0.4], endpoint_details:[EndpointDetails(host:127.0.0.6, datacenter:datacenter1, rack:rack3), EndpointDetails(host:127.0.0.5, datacenter:datacenter1, rack:rack2), EndpointDetails(host:127.0.0.4, datacenter:datacenter1, rack:rack1)])
    TokenRange(start_token:E, end_token:F, endpoints:[127.0.0.6, 127.0.0.2, 127.0.0.4], rpc_endpoints:[127.0.0.6, 127.0.0.2, 127.0.0.4], endpoint_details:[EndpointDetails(host:127.0.0.6, datacenter:datacenter1, rack:rack3), EndpointDetails(host:127.0.0.2, datacenter:datacenter1, rack:rack2), EndpointDetails(host:127.0.0.4, datacenter:datacenter1, rack:rack1)])
    TokenRange(start_token:H, end_token:I, endpoints:[127.0.0.2, 127.0.0.3, 127.0.0.1], rpc_endpoints:[127.0.0.2, 127.0.0.3, 127.0.0.1], endpoint_details:[EndpointDetails(host:127.0.0.2, datacenter:datacenter1, rack:rack2), EndpointDetails(host:127.0.0.3, datacenter:datacenter1, rack:rack3), EndpointDetails(host:127.0.0.1, datacenter:datacenter1, rack:rack1)])
    TokenRange(start_token:D, end_token:E, endpoints:[127.0.0.4, 127.0.0.6, 127.0.0.2], rpc_endpoints:[127.0.0.4, 127.0.0.6, 127.0.0.2], endpoint_details:[EndpointDetails(host:127.0.0.4, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.6, datacenter:datacenter1, rack:rack3), EndpointDetails(host:127.0.0.2, datacenter:datacenter1, rack:rack2)])
    TokenRange(start_token:A, end_token:B, endpoints:[127.0.0.4, 127.0.0.3, 127.0.0.2], rpc_endpoints:[127.0.0.4, 127.0.0.3, 127.0.0.2], endpoint_details:[EndpointDetails(host:127.0.0.4, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.3, datacenter:datacenter1, rack:rack3), EndpointDetails(host:127.0.0.2, datacenter:datacenter1, rack:rack2)])
    TokenRange(start_token:C, end_token:D, endpoints:[127.0.0.1, 127.0.0.6, 127.0.0.2], rpc_endpoints:[127.0.0.1, 127.0.0.6, 127.0.0.2], endpoint_details:[EndpointDetails(host:127.0.0.1, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.6, datacenter:datacenter1, rack:rack3), EndpointDetails(host:127.0.0.2, datacenter:datacenter1, rack:rack2)])
    TokenRange(start_token:F, end_token:G, endpoints:[127.0.0.2, 127.0.0.4, 127.0.0.3], rpc_endpoints:[127.0.0.2, 127.0.0.4, 127.0.0.3], endpoint_details:[EndpointDetails(host:127.0.0.2, datacenter:datacenter1, rack:rack2), EndpointDetails(host:127.0.0.4, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.3, datacenter:datacenter1, rack:rack3)])
    TokenRange(start_token:O, end_token:P, endpoints:[127.0.0.5, 127.0.0.6, 127.0.0.4], rpc_endpoints:[127.0.0.5, 127.0.0.6, 127.0.0.4], endpoint_details:[EndpointDetails(host:127.0.0.5, datacenter:datacenter1, rack:rack2), EndpointDetails(host:127.0.0.6, datacenter:datacenter1, rack:rack3), EndpointDetails(host:127.0.0.4, datacenter:datacenter1, rack:rack1)])
    TokenRange(start_token:J, end_token:K, endpoints:[127.0.0.3, 127.0.0.5, 127.0.0.1], rpc_endpoints:[127.0.0.3, 127.0.0.5, 127.0.0.1], endpoint_details:[EndpointDetails(host:127.0.0.3, datacenter:datacenter1, rack:rack3), EndpointDetails(host:127.0.0.5, datacenter:datacenter1, rack:rack2), EndpointDetails(host:127.0.0.1, datacenter:datacenter1, rack:rack1)])
    TokenRange(start_token:G, end_token:H, endpoints:[127.0.0.4, 127.0.0.2, 127.0.0.3], rpc_endpoints:[127.0.0.4, 127.0.0.2, 127.0.0.3], endpoint_details:[EndpointDetails(host:127.0.0.4, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.2, datacenter:datacenter1, rack:rack2), EndpointDetails(host:127.0.0.3, datacenter:datacenter1, rack:rack3)])
    TokenRange(start_token:M, end_token:N, endpoints:[127.0.0.1, 127.0.0.3, 127.0.0.5], rpc_endpoints:[127.0.0.1, 127.0.0.3, 127.0.0.5], endpoint_details:[EndpointDetails(host:127.0.0.1, datacenter:datacenter1, rack:rack1), EndpointDetails(host:127.0.0.3, datacenter:datacenter1, rack:rack3), EndpointDetails(host:127.0.0.5, datacenter:datacenter1, rack:rack2)])

  

最後,我們就如同前面的測試一樣,可以知道所有節點所被分配到的令牌區間了:
 
技術基礎 | 在Apache Cassandra中改變VNodes數量的影響
 
從Cassandra分配令牌的方式中可以看出,一套完整的資料副本會被分別儲存在三個機架的每個機架的兩個節點上。如果我們回頭再看看前面節點3和節點6同時不可用的故障場景,我們會發現此時系統仍然能夠為一致性級別(consistency level)為LOCAL_QUORUM的查詢請求提供服務。
 
唯一剩下的主要問題在於分配給節點3的令牌數量遠大於其他節點,而與節點3在同一個機架的節點6則與之相反,被分配的節點數量比其他節點要少一些。

 
08 過多的vnode會毀掉叢集
 
鑑於令牌分配不均的問題通常在vnode數量較少的情況下發生,可能有人會覺得使用大量的vnode會是最好的選項。然而,除了由多節點故障導致的資料不可用的情況更有可能發生以外,大量的vnode還會影響資料流的操作。
 
為了修復節點上的資料,Cassandra會在每個vnode上開啟一輪修復會話。這些修復會話需要按順序處理。所以,vnode數量越多,修復所需的時間就越多,執行一輪修復的開銷也就越大。
 
為了修正由vnode數量較大引起的修復過程緩慢的問題,Cassandra 3.0版本引入了CASSANDRA-5220問題。這個改變使得Cassandra能將一組節點共同的令牌區間集中在一個修復會話中。雖然同時修復多個令牌區間使得一輪修復會話所包含的內容更多,但這減少了同時執行的修復會話的數量。
 
我們可以通過在一個真實的硬體的叢集上做一個簡單的測試,來看看修復vnode會產生的效果。
 
為了做這個測試,首先我們需要建立一個用單個令牌執行修復任務的叢集。然後我們可以再建立一個一樣的叢集,不過vnode的數量是256,然後也用這個叢集執行同樣的修復任務。
 
我們將使用tlp-cluster在AWS上建立一個Cassandra叢集,並且應用如下特性:
  • 物件大小:i3.2xlarge
  • 節點數量:12
  • 機架數量:3(每個機架上4個節點)
  • Cassandra版本:3.11.9(寫作本文時最新的穩定版本)
下面是建立叢集所用到的命令語句。
 
$ tlp-cluster init --azs a,b,c --cassandra 12 --instance i3.2xlarge --stress 1 TLP BLOG "Blogpost repair testing"
$ tlp-cluster up
$ tlp-cluster use --config "cluster_name:SingleToken" --config "num_tokens:1" 3.11.9
$ tlp-cluster install

  

我們安排好所需的硬體之後,我們就需要單獨對每一個節點的initial_token性質進行設定。我們可以用一個簡單的Python命令來計算每一個節點的起始令牌。
 
Python 2.7.16 (default, Nov 23 2020, 08:01:20)
[GCC Apple LLVM 12.0.0 (clang-1200.0.30.4) [+internal-os, ptrauth-isa=sign+stri on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> num_tokens = 1
>>> num_nodes = 12
>>> print("\n".join(['[Node {}] initial_token: {}'.format(n + 1, ','.join([str(((2**64 / (num_tokens * num_nodes)) * (t * num_nodes + n)) - 2**63) for t in range(num_tokens)])) for n in range(num_nodes)]))
[Node 1] initial_token: -9223372036854775808
[Node 2] initial_token: -7686143364045646507
[Node 3] initial_token: -6148914691236517206
[Node 4] initial_token: -4611686018427387905
[Node 5] initial_token: -3074457345618258604
[Node 6] initial_token: -1537228672809129303
[Node 7] initial_token: -2
[Node 8] initial_token: 1537228672809129299
[Node 9] initial_token: 3074457345618258600
[Node 10] initial_token: 4611686018427387901
[Node 11] initial_token: 6148914691236517202
[Node 12] initial_token: 7686143364045646503

  

在所有節點上都啟動了Cassandra之後,使用下面的tlp-stress命令可以在每個節點上先預裝載大約3GB的資料。
在這個命令中,我們將鍵空間的複製因子(replication factor)設為3,並且將gc_grace_seconds設為0。這是為了讓hint在被建立之後立即被刪除,這樣它們就不會被傳送到終端節點了。
 
ubuntu@ip-172-31-19-180:~$ tlp-stress run KeyValue --replication "{'class': 'NetworkTopologyStrategy', 'us-west-2':3 }" --cql "ALTER TABLE tlp_stress.keyvalue WITH gc_grace_seconds = 0" --reads 1 --partitions 100M --populate 100M --iterations 1

  

資料載入完成後,叢集狀態如下:
ubuntu@ip-172-31-30-95:~$ nodetool status
Datacenter: us-west-2
=====================
Status=Up/Down
|/ State=Normal/Leaving/Joining/Moving
--  Address        Load       Tokens       Owns (effective)  Host ID                               Rack
UN  172.31.30.95   2.78 GiB   1            25.0%             6640c7b9-c026-4496-9001-9d79bea7e8e5  2a
UN  172.31.31.106  2.79 GiB   1            25.0%             ceaf9d56-3a62-40be-bfeb-79a7f7ade402  2a
UN  172.31.2.74    2.78 GiB   1            25.0%             4a90b071-830e-4dfe-9d9d-ab4674be3507  2c
UN  172.31.39.56   2.79 GiB   1            25.0%             37fd3fe0-598b-428f-a84b-c27fc65ee7d5  2b
UN  172.31.31.184  2.78 GiB   1            25.0%             40b4e538-476a-4f20-a012-022b10f257e9  2a
UN  172.31.10.87   2.79 GiB   1            25.0%             fdccabef-53a9-475b-9131-b73c9f08a180  2c
UN  172.31.18.118  2.79 GiB   1            25.0%             b41ab8fe-45e7-4628-94f0-a4ec3d21f8d0  2a
UN  172.31.35.4    2.79 GiB   1            25.0%             246bf6d8-8deb-42fe-bd11-05cca8f880d7  2b
UN  172.31.40.147  2.79 GiB   1            25.0%             bdd3dd61-bb6a-4849-a7a6-b60a2b8499f6  2b
UN  172.31.13.226  2.79 GiB   1            25.0%             d0389979-c38f-41e5-9836-5a7539b3d757  2c
UN  172.31.5.192   2.79 GiB   1            25.0%             b0031ef9-de9f-4044-a530-ffc67288ebb6  2c
UN  172.31.33.0    2.79 GiB   1            25.0%             da612776-4018-4cb7-afd5-79758a7b9cf8  2b

  

然後我們可以用下面的命令在每個節點上都執行一次全面修復(full repair)。
$ source env.sh
$ c_all "nodetool repair -full tlp_stress"

  

每個節點的修復時長如下:
[2021-01-22 20:20:13,952] Repair command #1 finished in 3 minutes 55 seconds
[2021-01-22 20:23:57,053] Repair command #1 finished in 3 minutes 36 seconds
[2021-01-22 20:27:42,123] Repair command #1 finished in 3 minutes 32 seconds
[2021-01-22 20:30:57,654] Repair command #1 finished in 3 minutes 21 seconds
[2021-01-22 20:34:27,740] Repair command #1 finished in 3 minutes 17 seconds
[2021-01-22 20:37:40,449] Repair command #1 finished in 3 minutes 23 seconds
[2021-01-22 20:41:32,391] Repair command #1 finished in 3 minutes 36 seconds
[2021-01-22 20:44:52,917] Repair command #1 finished in 3 minutes 25 seconds
[2021-01-22 20:47:57,729] Repair command #1 finished in 2 minutes 58 seconds
[2021-01-22 20:49:58,868] Repair command #1 finished in 1 minute 58 seconds
[2021-01-22 20:51:58,724] Repair command #1 finished in 1 minute 53 seconds
[2021-01-22 20:54:01,100] Repair command #1 finished in 1 minute 50 seconds

  

將這些時間加總起來得到整個修復完成所需的時間,即36分鐘44秒。
上面用到的叢集同樣可以用來測試當vnode的數量為256時,系統所需要的修復時長。我們所需要做的就是執行下面的這些步驟:
  • 在所有節點上關閉Cassandra
  • 刪除data、commitlog、hints和saved_caches這四個資料夾中的所有內容(它們位於每個節點的/var/lib/cassandra/路徑下)
  • 在cassandra.yaml配置檔案中將num_tokens的值設為256,並且刪除initial_token這一設定引數
  • 在所有節點上重新啟動Cassandra
在向叢集中輸入資料後,叢集狀態如下:
ubuntu@ip-172-31-30-95:~$ nodetool status
Datacenter: us-west-2
=====================
Status=Up/Down
|/ State=Normal/Leaving/Joining/Moving
--  Address        Load       Tokens       Owns (effective)  Host ID                               Rack
UN  172.31.30.95   2.79 GiB   256          24.3%             10b0a8b5-aaa6-4528-9d14-65887a9b0b9c  2a
UN  172.31.2.74    2.81 GiB   256          24.4%             a748964d-0460-4f86-907d-a78edae2a2cb  2c
UN  172.31.31.106  3.1 GiB    256          26.4%             1fc68fbd-335d-4689-83b9-d62cca25c88a  2a
UN  172.31.31.184  2.78 GiB   256          23.9%             8a1b25e7-d2d8-4471-aa76-941c2556cc30  2a
UN  172.31.39.56   2.73 GiB   256          23.5%             3642a964-5d21-44f9-b330-74c03e017943  2b
UN  172.31.10.87   2.95 GiB   256          25.4%             540a38f5-ad05-4636-8768-241d85d88107  2c
UN  172.31.18.118  2.99 GiB   256          25.4%             41b9f16e-6e71-4631-9794-9321a6e875bd  2a
UN  172.31.35.4    2.96 GiB   256          25.6%             7f62d7fd-b9c2-46cf-89a1-83155feebb70  2b
UN  172.31.40.147  3.26 GiB   256          27.4%             e17fd867-2221-4fb5-99ec-5b33981a05ef  2b
UN  172.31.13.226  2.91 GiB   256          25.0%             4ef69969-d9fe-4336-9618-359877c4b570  2c
UN  172.31.33.0    2.74 GiB   256          23.6%             298ab053-0c29-44ab-8a0a-8dde03b4f125  2b
UN  172.31.5.192   2.93 GiB   256          25.2%             7c690640-24df-4345-aef3-dacd6643d6c0  2c

  

和前面在單個節點對應單個令牌的叢集所做的修復測試一樣,當我們在這個啟用了vnode的節點上執行修復測試,修復所需的時長就會被記錄下來。
[2021-01-22 22:45:56,689] Repair command #1 finished in 4 minutes 40 seconds
[2021-01-22 22:50:09,170] Repair command #1 finished in 4 minutes 6 seconds
[2021-01-22 22:54:04,820] Repair command #1 finished in 3 minutes 43 seconds
[2021-01-22 22:57:26,193] Repair command #1 finished in 3 minutes 27 seconds
[2021-01-22 23:01:23,554] Repair command #1 finished in 3 minutes 44 seconds
[2021-01-22 23:04:40,523] Repair command #1 finished in 3 minutes 27 seconds
[2021-01-22 23:08:20,231] Repair command #1 finished in 3 minutes 23 seconds
[2021-01-22 23:11:01,230] Repair command #1 finished in 2 minutes 45 seconds
[2021-01-22 23:13:48,682] Repair command #1 finished in 2 minutes 40 seconds
[2021-01-22 23:16:23,630] Repair command #1 finished in 2 minutes 32 seconds
[2021-01-22 23:18:56,786] Repair command #1 finished in 2 minutes 26 seconds
[2021-01-22 23:21:38,961] Repair command #1 finished in 2 minutes 30 seconds

  

將這些時間加總起來得到整個修復完成所需的時間,即39分鐘23秒。
 
雖然對於每個節點3GB資料的情況來說,兩種情況下的修復時長並沒有相差太多。但是很容易看出,當每個節點的資料大小達到數百GB時,修復的時間差就會迅速擴大。
 
不幸的是,像是系統啟動和資料中心重建這樣的所有的資料流操作,都會遇見這種由大量vnode引起的修復問題。具體來說,當一個節點需要將資料傳輸到另一個節點,節點就會為每個令牌區間分別開啟一個流傳輸會話。由於資料是通過JVM來傳輸的,這就會導致很多沒必要的開銷。

 
09 二級索引也被影響
 
更糟的是,由於Cassandra的讀取路徑(read path)的工作原理,大量的vnode還會對二級索引(secondary indexes)產生負面影響。
 
當協調節點(coordinator node)從客戶端接收到一個帶二級索引的請求,它會將該請求分發給叢集或資料中心內的所有節點,分發範圍取決於一致性級別(consistency level)的要求。
 
接著,每個收到請求的節點在自己負責的令牌區間(token range)的SSTable中進行查詢,尋找和這個帶二級索引的請求相匹配的結果。然後這些與請求相匹配的資料就會被返回給協調節點。
 
所以,vnode數量越多,對於帶二級索引的請求的延遲的影響越大。不僅如此,這種對二級索引的效能的影響,會隨著叢集副本數量的增加以指數形式增長。在多個資料中心的節點都使用多個vnode的情況下,二級索引的效率甚至會更低。

 
10 新希望
 
至此,我們擁有的是這樣的一個Cassandra特性:它在降低叢集伸縮的複雜性方面確實達標了,但不幸的是,伴隨著這種好處而來的代價是令牌區間的分配失衡以及操作效能的降低。話雖如此,vnode的故事還遠未結束。
 
最終,這成為了一個廣為人知的事實:Apache Cassandra專案中大量的vnode會對叢集造成大家並不樂見的副作用。為了對付這個問題,聰明的Cassandra貢獻者們以及committer們在Cassandra 3.0版本中新增了CASSANDRA-7032——一個能夠感知副本的令牌分配演算法。
 
這個演算法的思路是讓num_tokens可以使用一個較低的值,同時還得保持令牌區間的相對平衡。這次對於令牌分配演算法的改進在cassandra.yaml檔案中新增了設定引數allocate_tokens_for_keyspace。
 
當一個已經存在的使用者鍵空間(user keyspace)被分配給allocate_tokens_for_keyspace這個設定引數時,系統就會使用這個新演算法,而不會使用隨機令牌分配器(random token allocator)。
 
在後臺,Cassandra會讀取節點上已有定義的鍵空間的複製因子(replication factor),並在該節點第一次進入叢集時,Cassandra會根據鍵空間的複製因子來計算該節點的令牌分配。
 
與隨機令牌生成器(random token generator)不同,這個新演算法的副本感知生成器(replica aware generator)就像是交響樂團裡富有經驗的一員,它技能嫻熟且能夠與周邊情況相適應。因此,新演算法生成令牌區間的步驟包括了:
  • 構建初始令牌環
  • 通過從中間拆分所有現有的令牌範圍來計算候選的新令牌
  • 評估所有候選令牌的預期提升,然後按優先順序形成佇列
  • 遍歷佇列中的候選令牌並找到最好的組合
  • 由於令牌被重新組合,將會重新評估佇列中的候選令牌的提升
儘管這個副本感知令牌分配演算法(replica aware token allocation algorithm)對Cassandra來說是一大提升,但是在使用這個演算法的時候還是有一些要注意的地方。
 
首先,使用這個演算法意味著只能使用Murmur3Partitioner分割槽演算法。如果你的叢集較老且使用的是其他的分割槽演算法(如RandomPartitioner),儘管該叢集隨著時間的推移已升級到3.0版本,這個新演算法仍然不適用。
 
第二個問題,也是一個更常見的阻礙:當我們從頭建立一個新叢集時,我們需要一些技巧才能使用這個新演算法。
 
由於這個問題確實很常見,點選文末“閱讀原文”檢視我們的一篇文章,在其中我們專門解釋瞭如何使用這個新演算法搭建一個令牌分配均衡的新叢集
就像你看到的,Cassandra 3.0版本確實致力於解決vnode的一些瑕疵問題。而且即將推出的Cassandra 4.0版本還將帶來更多可喜的變化和提升。
 
比如通過CASSANDRA-15260將會在cassandra.yaml檔案中新增一個新的設定引數allocate_tokens_for_local_replication_factor。它與它的姊妹引數allocate_tokens_for_keyspace有著類似的功能,當它被賦值之後,副本感知令牌分配演算法就會被啟用。
 
不過與allocate_tokens_for_keyspace不同的是,allocate_tokens_for_local_replication_factor對使用者更加友好。這是因為在從零建立平衡的新叢集時,後者不會帶來任何其他瑣碎繁雜的額外工作。在最簡單的情況下,你可以先為allocate_tokens_for_local_replication_factor設定一個值,然後就可以開始新增節點了。
 
有經驗的使用者仍可以將令牌手動分配給初始節點,以確保滿足所需的複製因子(replication factor)。在這之後,後續的節點可以在複製因子被賦值給allocate_tokens_for_local_replication_factor之後新增到叢集中。
 
可以說,Cassandra 4.0版本最長的釋出時間和重大更改之一是對num_tokens這一設定選項的預設值的更新。
 
就像在本文一開頭就提到的那樣,有賴於CASSANDRA-13701,Cassandra 4.0推出時,在cassandra.yaml檔案中的num_tokens一值將會被設定為16。另外,allocate_tokens_for_local_replication_factor這一設定引數將會預設啟用,其預設值為3。
 
這些更改都是更好的使用者預設設定。在Cassandra 4.0的出廠設定(vanilla installation)中,只要有足夠的主機來滿足複製因子為3的條件,副本感知令牌分配演算法就會被啟用。這樣做的結果就是不僅新節點令牌區間的分配會非常均衡,而且同時還可以享有vnode較少時的所有好處。

 
11 結論
 
具有一致性的雜湊令牌分配這一功能構成了Cassandra主幹的一部分。虛擬節點(vnode)則消除了在維護這一關鍵功能過程中的不確定因素,尤其是它可以幫助節點更快且更容易地伸縮。
 
根據經驗來說,vnode數量越小,令牌的分配就越不均衡,進而導致一些節點會出現過載的問題。或者如果vnode數量越大,則叢集範圍的操作就會花費越長的時間完成;與此同時,如果有多個節點當機的話,也就越有可能出現資料不可用的情況。
 
3.0版本中的功能以及4.0版本對這些功能的增強,讓Cassandra能夠在vnode數量較少的情況下同時保持相對均衡的令牌分配。最終,當新使用者使用Cassandra 4.0的出廠設定時,這會催生更好的“開箱即用”的使用體驗。

相關文章