這篇文檔是對 Redis 集群的介紹,沒有使用復雜難懂的東西來理解分布式系統(tǒng)的概念。本文提供了如何建立,測試和操作一個集群的相關指導,但沒有涉及在 Redis 集群規(guī)范(參考本系列其他文章,譯者注)中的諸多細節(jié),只是從用戶的視角來描述系統(tǒng)是如何運作的。
注意,如果你打算來一次認真的 Redis 集群的部署,更正式的規(guī)范文檔(關注本系列文章,譯者注)強烈建議你好好讀一讀。
Redis 集群當前處于 alpha 階段,如果你發(fā)現(xiàn)任何問題,請聯(lián)系 Redis 郵件列表,或者在 Redis 的 Github 倉庫中開啟一個問題(issue)。
Redis 集群提供一種運行 Redis 的方式,數(shù)據(jù)被自動的分片到多個 Redis 節(jié)點。
集群不支持處理多個鍵的命令,因為這需要在 Redis 節(jié)點間移動數(shù)據(jù),使得 Redis 集群不能提供像 Redis 單點那樣的性能,在高負載下會表現(xiàn)得不可預知。
Redis 集群也提供在網(wǎng)絡分割(partitions)期間的一定程度的可用性,這就是在現(xiàn)實中當一些節(jié)點失敗或者不能通信時能繼續(xù)進行運轉(zhuǎn)的能力。
所以,在實踐中,你可以從 Redis 集群中得到什么呢?
每個 Redis 集群節(jié)點需要兩個 TCP 連接打開。正常的 TCP 端口用來服務客戶端,例如 6379,加 10000 的端口用作數(shù)據(jù)端口,在上面的例子中就是 16379。
第二個大一些的端口用于集群總線(bus),也就是使用二進制協(xié)議的點到點通信通道。集群總線被節(jié)點用于錯誤檢測,配置更新,故障轉(zhuǎn)移授權等等??蛻舳瞬粦搰L試連接集群總線端口,而應一直與正常的 Redis 命令端口通信,但是要確保在防火墻中打開了這兩個端口,否則 Redis 集群的節(jié)點不能相互通信。
命令端口和集群總線端口的偏移量一直固定為 10000。
注意,為了讓 Redis 集群工作正常,對每個節(jié)點:
如果你不打開這兩個 TCP 端口,你的集群就不會像你期待的那樣去工作。
Redis 集群沒有使用一致性哈希,而是另外一種不同的分片形式,每個鍵概念上是被我們稱為哈希槽(hash slot)的東西的一部分。
Redis 集群有 16384 個哈希槽,我們只是使用鍵的 CRC16 編碼對 16384 取模來計算一個指定鍵所屬的哈希槽。
每一個 Redis 集群中的節(jié)點都承擔一個哈希槽的子集,例如,你可能有一個 3 個節(jié)點的集群,其中:
這可以讓在集群中添加和移除節(jié)點非常容易。例如,如果我想添加一個新節(jié)點 D,我需要從節(jié)點 A,B,C 移動一些哈希槽到節(jié)點 D。同樣地,如果我想從集群中移除節(jié)點 A,我只需要移動 A 的哈希槽到 B 和 C。當節(jié)點 A 變成空的以后,我就可以從集群中徹底刪除它。
因為從一個節(jié)點向另一個節(jié)點移動哈希槽并不需要停止操作,所以添加和移除節(jié)點,或者改變節(jié)點持有的哈希槽百分比,都不需要任何停機時間(downtime)。
為了當部分節(jié)點失效時,或者無法與大多數(shù)節(jié)點通信時仍能保持可用,Redis 集群采用每個節(jié)點擁有 1(主服務自身)到 N 個副本(N-1 個附加的從服務器)的主從模型。
在我們的例子中,集群擁有 A,B,C 三個節(jié)點,如果節(jié)點 B 失效集群將不能繼續(xù)服務,因為我們不再有辦法來服務在 5501-11000 范圍內(nèi)的哈希槽。
但是,如果當我們創(chuàng)建集群后(或者稍后),我們?yōu)槊恳粋€主服務器添加一個從服務器,這樣最終的集群就由主服務器 A,B,C 和從服務器 A1,B1,C1 組成,如果 B 節(jié)點失效系統(tǒng)仍能繼續(xù)服務。
B1 節(jié)點復制 B 節(jié)點,于是集群會選舉 B1 節(jié)點作為新的主服務器,并繼續(xù)正確的運轉(zhuǎn)。
Redis 集群不保證強一致性。實踐中,這意味著在特定的條件下,Redis 集群可能會丟掉一些被系統(tǒng)收到的寫入請求命令。
Redis 集群為什么會丟失寫請求的第一個原因,是因為采用了異步復制。這意味著在寫期間下面的事情發(fā)生了:
你可以看到,B 在回復客戶端之前沒有等待從 B1,B2,B3 的確認,因為這是一個過高的延遲代價,所以如果你的客戶端寫入什么東西,B 確認了這個寫操作,但是在發(fā)送寫操作到其從服務器前崩潰了,其中一個從服務器被提升為主服務器,永久性的丟失了這個寫操作。
這非常類似于在大多數(shù)被配置為每秒刷新數(shù)據(jù)到磁盤的數(shù)據(jù)庫發(fā)生的事情一樣,這是一個可以根據(jù)以往不包括分布式系統(tǒng)的傳統(tǒng)數(shù)據(jù)庫系統(tǒng)的經(jīng)驗來推理的場景。同樣的,你可以通過在回復客戶端之前強制數(shù)據(jù)庫刷新數(shù)據(jù)到磁盤來改進一致性,但這通常會極大的降低性能。
基本上,有一個性能和一致性之間的權衡。
注意:未來,Redis 集群在必要時可能或允許用戶執(zhí)行同步寫操作。
Redis 集群丟失寫操作還有另一個場景,發(fā)生在網(wǎng)絡分割時,客戶端與至少包含一個主服務器的少數(shù)實例被孤立起來了。
舉個例子,我們的集群由 A,B,C,A1,B1,C1 共 6 個節(jié)點組成,3 個主服務器,3 個從服務器。還有一個客戶端,我們稱為 Z1。
分割發(fā)生以后,有可能分割的一側(cè)是 A,C,A1,B1,C1,分割的另一側(cè)是 B 和 Z1。
Z1 仍然可以寫入到可接受寫請求的 B。如果分割在很短的時間內(nèi)恢復,集群會正常的繼續(xù)。但是,如果分割持續(xù)了足夠的時間,B1 在分割的大多數(shù)這一側(cè)被提升為主服務器,Z1 發(fā)送給 B 的寫請求會丟失。
注意,Z1 發(fā)送給 B 的寫操作數(shù)量有一個最大窗口:如果分割的大多數(shù)側(cè)選舉一個從服務器為主服務器后過了足夠多的時間,少數(shù)側(cè)的每一個主服務器節(jié)點將停止接受寫請求。
這個時間量是 Redis 集群一個非常重要的配置指令,稱為節(jié)點超時(node timeout)。
節(jié)點超時時間過后,主服務器節(jié)點被認為失效,可以用其一個副本來取代。同樣地,節(jié)點超時時間過后,主服務器節(jié)點還不能感知其它主服務器節(jié)點的大多數(shù),則進入錯誤狀態(tài),并停止接受寫請求。
要創(chuàng)建一個集群,我們要做的第一件事情就是要有若干運行在集群模式下的 Redis 實例。這基本上意味著,集群不是使用正常的 Redis 實例創(chuàng)建的,而是需要配置一種特殊的模式 Redis 實例才會開啟集群特定的特性和命令。
下面是最小的 Redis 集群配置文件:
port 7000
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
正如你所看到的,簡單的 cluster-enabled 指令開啟了集群模式。每個實例包含一個保存這個節(jié)點配置的文件的路徑,默認是 nodes.conf。這個文件不會被用戶接觸到,啟動時由 Redis 集群實例生成,每次在需要時被更新。
注意,可以正常運轉(zhuǎn)的最小集群需要包含至少 3 個主服務器節(jié)點。在你的第一次嘗試中,強烈建議開始一個 6 個節(jié)點的集群,3 個主服務器,3 個從服務器。
要這么做,先進入一個新的目錄,創(chuàng)建下面這些以端口號來命名的目錄,我們后面會在每個目錄中運行實例。
像這樣:
mkdir cluster-test
cd cluster-test
mkdir 7000 7001 7002 7003 7004 7005
在從 7000 到 7005 的每個目錄內(nèi)創(chuàng)建一個 redis.conf 文件。作為你的配置文件的模板,只使用上面的小例子,但是要確保根據(jù)目錄名來使用正確的端口號來替換端口號 7000。
現(xiàn)在,復制你從 Github 的不穩(wěn)定分支的最新的源代碼編譯出來的 redis-server 可執(zhí)行文件到 cluster-test 目錄中,最后在你喜愛的終端應用程序中打開 6 個終端標簽。
像這樣在每個標簽中啟動實例:
cd 7000
../redis-server ./redis.conf
你可以從每個實例的日志中看到,因為 nodes.conf 文件不存在,每個節(jié)點都為自己賦予了一個新 ID。
[82462] 26 Nov 11:56:55.329 * No cluster configuration found, I'm 97a3a64667477371c4479320d683e4c8db5858b1
這個 ID 會一直被這個實例使用,這樣實例就有一個在集群上下文中唯一的名字。每個節(jié)點使用這個 ID 來記錄每個其它節(jié)點,而不是靠 IP 和端口。IP 地址和端口可能會變化,但是唯一的節(jié)點標識符在節(jié)點的整個生命周期中都不會改變。我們稱這個標識符為節(jié)點 ID(Node ID)。
現(xiàn)在,我們已經(jīng)有了一些運行中的實例,我們需要創(chuàng)建我們的集群,寫一些有意義的配置到節(jié)點中。
這很容易完成,因為我們有稱為 redis-trib 的 Redis 集群命令行工具來幫忙,這是一個 Ruby 程序,可以在實例上執(zhí)行特殊的命令來創(chuàng)建一個新的集群,檢查或重分片一個已存在的集群,等等。
redis-trib 工具在 Redis 源代碼分發(fā)版本的 src 目錄中。要創(chuàng)建你的集群,簡單輸入:
./redis-trib.rb create --replicas 1 127.0.0.1:7000 127.0.0.1:7001 \
127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005
這里使用的命令是 create,因為我們想創(chuàng)建一個新的集群。--replicas 1 選項意思是我們希望每個創(chuàng)建的主服務器有一個從服務器。其他參數(shù)是我想用來創(chuàng)建新集群的實例地址列表。
顯然,我們要求的唯一布局就是創(chuàng)建一個擁有 3 個主服務器和 3 個從服務器的集群。
Redis-trib 會建議你一個配置。輸入 yes 接受。集群會被配置和連接在一起,也就是說,實例會被引導為互相之間對話。最后,如果一切順利你會看到一個類似這樣的消息:
[OK] All 16384 slots covered
這表示,16384 個槽中的每一個至少有一個主服務器在處理。
在當前階段,Redis 集群的一個問題是缺少客戶端庫的實現(xiàn)。
據(jù)我所知有以下實現(xiàn):
測試 Redis 集群的簡單辦法就是嘗試上面這些客戶端,或者只是使用 redis-cli 命令行工具。下面的交互例子使用的是后者:
$ redis-cli -c -p 7000
redis 127.0.0.1:7000> set foo bar
-> Redirected to slot [12182] located at 127.0.0.1:7002
OK
redis 127.0.0.1:7002> set hello world
-> Redirected to slot [866] located at 127.0.0.1:7000
OK
redis 127.0.0.1:7000> get foo
-> Redirected to slot [12182] located at 127.0.0.1:7002
"bar"
redis 127.0.0.1:7000> get hello
-> Redirected to slot [866] located at 127.0.0.1:7000
"world"
redis-cli 的集群支持非常基本,所以總是依賴 Redis 集群節(jié)點重定向客戶端到正確的節(jié)點。一個真正的客戶端可以做得更好,緩存哈希槽和節(jié)點地址之間的映射,直接使用到正確節(jié)點的正確連接。映射只在集群的配置發(fā)生某些變化時才重新刷新,例如,故障轉(zhuǎn)移以后,或者系統(tǒng)管理員通過添加或移除節(jié)點改變了集群的布局以后。