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