Redis(3.2.3)叢集部署實戰
阿新 • • 發佈:2018-12-25
一、Redis簡介
Redis是一個開源的使用ANSI C語言編寫、支援網路、可基於記憶體亦可持久化的日誌型、Key-Value資料庫,並提供多種語言的API。
Redis官網地址:http://redis.io/
Redis中文網地址:http://redis.cn
Redis中文文件地址:http://redisdoc.com
二、Redis安裝
系統環境:CentOS 6.8 mininal 初始化完成
下載,解壓,編譯:
wget http://download.redis.io/releases/redis-3.2.3.tar.gz
-P /usr/local/src/
cd /usr/local/src && tar zxvf redis-3.2.3.tar.gz
&& cd redis-3.2.3 && make
#make test 報錯
解決:
yum -y install tcl
#make test 報錯
*** [err]: Test replication partial
resync: ok psync
■ 解決辦法:
1,只用單核執行 make test:
taskset -c 1 sudo make test
2,更改 tests/integration/replication-psync.tcl 檔案:
vi tests/integration/replication-psync.tcl
把對應報錯的那段程式碼中的 after後面的數字,從100改成 500。我個人覺得,這個引數貌似是等待的毫秒數。
這兩種方法都可以解決這個報錯。
#設定系統核心引數echo 512 > /proc/sys/net/core/somaxconn
sysctl vm.overcommit_memory=1
三、Redis叢集教程
3.1、Redis叢集介紹 Redis 叢集是一個提供在多個Redis間節點間共享資料的程式集。 Redis叢集並不支援處理多個keys的命令,因為這需要在不同的節點間移動資料,從而達不到像Redis那樣的效能,在高負載的情況下可能會導致不可預料的錯誤. Redis 叢集通過分割槽來提供一定程度的可用性,在實際環境中當某個節點宕機或者不可達的情況下繼續處理命令.
Redis 叢集是一個分散式(distributed)、容錯(fault-tolerant)的 Redis 實現, 叢集可以使用的功能是普通單機 Redis 所能使用的功能的一個子集(subset)。 Redis 叢集中不存在中心(central)節點或者代理(proxy)節點, 叢集的其中一個主要設計目標是達到線性可擴充套件性(linear scalability)。 Redis 叢集為了保證一致性(consistency)而犧牲了一部分容錯性: 系統會在保證對網路斷線(net split)和節點失效(node failure)具有有限(limited)抵抗力的前提下, 儘可能地保持資料的一致性。
3.2、Redis 叢集的優勢:
自動分割資料到不同的節點上。 整個叢集的部分節點失敗或者不可達的情況下能夠繼續處理命令。 Redis 叢集的資料分片 Redis 叢集沒有使用一致性hash, 而是引入了 雜湊槽的概念. Redis 叢集有16384個雜湊槽,每個key通過CRC16校驗後對16384取模來決定放置哪個槽.叢集的每個節點負責一部分hash槽,舉個例子,比如當前叢集有3個節點,那麼: 節點 A 包含 0 到 5500號雜湊槽. 節點 B 包含5501 到 11000 號雜湊槽. 節點 C 包含11001 到 16384號雜湊槽. 這種結構很容易新增或者刪除節點. 比如如果我想新添加個節點D, 我需要從節點 A, B, C中得部分槽到D上. 如果我像移除節點A,需要將A中得槽移到B和C節點上,然後將沒有任何槽的A節點從叢集中移除即可. 由於從一個節點將雜湊槽移動到另一個節點並不會停止服務,所以無論新增刪除或者改變某個節點的雜湊槽的數量都不會造成叢集不可用的狀態. Redis 叢集的主從複製模型 為了使在部分節點失敗或者大部分節點無法通訊的情況下叢集仍然可用,所以叢集使用了主從複製模型,每個節點都會有N-1個複製品. 在我們例子中具有A,B,C三個節點的叢集,在沒有複製模型的情況下,如果節點B失敗了,那麼整個叢集就會以為缺少5501-11000這個範圍的槽而不可用. 然而如果在叢集建立的時候(或者過一段時間)我們為每個節點新增一個從節點A1,B1,C1,那麼整個叢集便有三個master節點和三個slave節點組成,這樣在節點B失敗後,叢集便會選舉B1為新的主節點繼續服務,整個叢集便不會因為槽找不到而不可用了 不過當B和B1 都失敗後,叢集是不可用的. Redis 一致性保證 Redis 並不能保證資料的強一致性. 這意味這在實際中叢集在特定的條件下可能會丟失寫操作. 第一個原因是因為叢集是用了非同步複製. 寫操作過程: 客戶端向主節點B寫入一條命令. 主節點B向客戶端回覆命令狀態. 主節點將寫操作複製給他得從節點 B1, B2 和 B3. 主節點對命令的複製工作發生在返回命令回覆之後, 因為如果每次處理命令請求都需要等待複製操作完成的話, 那麼主節點處理命令請求的速度將極大地降低 —— 我們必須在效能和一致性之間做出權衡。 注意:Redis 叢集可能會在將來提供同步寫的方法。 Redis 叢集另外一種可能會丟失命令的情況是叢集出現了網路分割槽, 並且一個客戶端與至少包括一個主節點在內的少數例項被孤立。 舉個例子 假設叢集包含 A 、 B 、 C 、 A1 、 B1 、 C1 六個節點, 其中 A 、B 、C 為主節點, A1 、B1 、C1 為A,B,C的從節點, 還有一個客戶端 Z1 假設叢集中發生網路分割槽,那麼叢集可能會分為兩方,大部分的一方包含節點 A 、C 、A1 、B1 和 C1 ,小部分的一方則包含節點 B 和客戶端 Z1 . Z1仍然能夠向主節點B中寫入, 如果網路分割槽發生時間較短,那麼叢集將會繼續正常運作,如果分割槽的時間足夠讓大部分的一方將B1選舉為新的master,那麼Z1寫入B中得資料便丟失了. 注意, 在網路分裂出現期間, 客戶端 Z1 可以向主節點 B 傳送寫命令的最大時間是有限制的, 這一時間限制稱為節點超時時間(node timeout), 是 Redis 叢集的一個重要的配置選項:
3.3、搭建並使用Redis叢集
#隨意移動到一個新目錄下,本文以/opt為例:
cd /opt && mkdir cluster && cd cluster
mkdir 7000 7001 7002 7003 7004 7005
#拷貝redis.conf到上面建立的6個子目錄中,本文以7000為例
#修改redis.conf相關配置引數
daemonize yes
pidfile /var/run/redis/7000.pid
port 7000
logfile "/var/log/redis/7000.log"
#註釋掉以下資訊,不需要RDB持久化
#save 900 1
#save 300 10
#save 60 10000
#更改以下引數
appendonly yes
appendfilename "appendonly-7000.aof"
#取消以下引數註釋,使redis工作在叢集模式下
cluster-enabled yes #啟動cluster模式
cluster-config-file nodes-7000.conf #叢集資訊檔名。由redis自己維護
cluster-node-timeout 15000 #15秒聯絡不到對方的node,即認為對方有故障可能
#修改MAXMEMORY POLICY
maxmemory-policy allkeys-lru
maxmemory-samples 5
#完整redis.conf配置如下:
bind 192.168.0.51 127.0.0.1
protected-mode yes
port 7000
tcp-backlog 511
timeout 0
tcp-keepalive 300
daemonize yes
supervised no
pidfile /var/run/redis/7000.pid
loglevel notice
logfile "/var/log/redis/7000.log"
databases 16
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump_7000.rdb
dir /opt/cluster/rdb/
slave-serve-stale-data yes
slave-read-only yes
repl-diskless-sync no
repl-diskless-sync-delay 5
repl-disable-tcp-nodelay no
slave-priority 100
maxmemory-policy allkeys-lru
maxmemory-samples 5
appendonly yes
appendfilename "appendonly-7000.aof"
appendfsync everysec
protected-mode yes
port 7000
tcp-backlog 511
timeout 0
tcp-keepalive 300
daemonize yes
supervised no
pidfile /var/run/redis/7000.pid
loglevel notice
logfile "/var/log/redis/7000.log"
databases 16
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump_7000.rdb
dir /opt/cluster/rdb/
slave-serve-stale-data yes
slave-read-only yes
repl-diskless-sync no
repl-diskless-sync-delay 5
repl-disable-tcp-nodelay no
slave-priority 100
maxmemory-policy allkeys-lru
maxmemory-samples 5
appendonly yes
appendfilename "appendonly-7000.aof"
appendfsync everysec
no-appendfsync-on-rewrite no
appendfilename "appendonly-7000.aof"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
lua-time-limit 5000
cluster-enabled yes
cluster-config-file /opt/cluster/nodes/nodes-7000.conf
cluster-node-timeout 15000
slowlog-log-slower-than 10000
slowlog-max-len 128
latency-monitor-threshold 0
notify-keyspace-events ""
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-size -2
list-compress-depth 0
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
hll-sparse-max-bytes 3000
activerehashing yes
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit slave 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
hz 10
aof-rewrite-incremental-fsync yes
#拷貝redis的相關命令到/opt/cluster/bin目錄下,並設定環境變數
mkdir /opt/cluster/bin
cd /usr/local/src/redis-3.2.3/src/
cp mkreleasehdr.sh redis-benchmark redis-sentinel redis-server redis-trib.rb redis-cli /opt/cluster/bin
cp /usr/local/src/redis-3.2.3/^Cntest runtest-sentinel runtest-cluster /opt/cluster/bin/
#檢視
#建立軟連結
ln -s /opt/cluster/bin/* /usr/bin/
#把修改好的redis.conf檔案,分別拷貝到6個子目錄下,以目錄名命名配置檔案(自定義),然後依次修改配置檔案中的埠號,以7000.conf檔案為例:
sed -i 's/7000/7001/g' /opt/cluster/7001/7001.conf
sed -i 's/7000/7002/g' /opt/cluster/7002/7002.conf
...
sed -i 's/7000/7005/g' /opt/cluster/7005/7005.conf
#建立配置檔案中修改的目錄
mkdir -p /var/run/redis /var/log/redis /opt/cluster/{rdb,nodes}
#依次啟動redis例項
/opt/cluster/bin/redis-server /opt/cluster/7000/7000.conf
/opt/cluster/bin/redis-server /opt/cluster/7001/7001.conf
...
#檢視redis啟動日誌,檢查是否有啟動報錯,如下圖:
#warning處理
1432:M 17 Oct 11:21:35.952 # Server started, Redis version 3.2.3
1432:M 17 Oct 11:21:35.952 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
1432:M 17 Oct 11:21:35.952 * The server is now ready to accept connections on port 7005
#啟動redis需要調整個別核心引數,根據提示修改sysct.conf,執行下面命令(如果之前設定過,不需要重複操作)
sysctl -a | grep vm.overcommit_memory #檢視vm.overcommit_memory的值,如為1,不需要修改,若為0 ,執行
sysctl vm.overcommit_memory=1
#從redis的啟動日誌及服務狀態可以看出,到此redis的6個例項都已啟動完成。
四、搭建redis叢集
現在有了六個正在執行中的 Redis 例項, 接下來需要使用這些例項來建立叢集, 併為每個節點編寫配置檔案。通過使用 Redis 叢集命令列工具 redis-trib , 編寫節點配置檔案的工作可以非常容易地完成: redis-trib 位於 Redis 原始碼的 src 資料夾中, 它是一個 Ruby 程式, 這個程式通過向例項傳送特殊命令來完成建立新叢集, 檢查叢集, 或者對叢集進行重新分片(reshared)等工作。
4.1、安裝ruby環境,及安裝redis的ruby依賴介面
yum install ruby ruby-devel rubygems -y && gem install redis
4.2、 使用Redis 叢集命令列工具 redis-trib建立叢集
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
這個命令在這裡用於建立一個新的叢集, 選項–replicas 1 表示我們希望為叢集中的每個主節點建立一個從節點。
之後跟著的其他引數則是這個叢集例項的地址列表,3個master3個slave redis-trib 會打印出一份預想中的配置給你看, 如果你覺得沒問題的話, 就可以輸入 yes , redis-trib 就會將這份配置應用到叢集當中,讓各個節點開始互相通訊,最後可以得到如下資訊:
#此過程將6個節點組成叢集,3主3從
4.3、叢集的使用
測試 Redis 叢集比較簡單的辦法就是使用 redis-rb-cluster 或者 redis-cli , 接下來我將使用 redis-cli 為例來進行演示:
#redis-cli 對叢集的支援是非常基本的, 所以它總是依靠 Redis 叢集節點來將它轉向(redirect)至正確的節點。一個真正的(serious)叢集客戶端應該做得比這更好: 它應該用快取記錄起雜湊槽與節點地址之間的對映(map), 從而直接將命令傳送到正確的節點上面。這種對映只會在叢集的配置出現某些修改時變化, 比如說, 在一次故障轉移(failover)之後, 或者系統管理員通過新增節點或移除節點來修改了叢集的佈局(layout)之後, 諸如此類。
#使用redis-rb-cluster寫一個例子
在展示如何使用叢集進行故障轉移、重新分片等操作之前, 我們需要建立一個示例應用, 瞭解一些與 Redis 叢集客戶端進行互動的基本方法。在執行示例應用的過程中, 我們會嘗試讓節點進入失效狀態, 又或者開始一次重新分片, 以此來觀察 Redis 叢集在真實世界執行時的表現, 並且為了讓這個示例儘可能地有用, 我們會讓這個應用向叢集進行寫操作。
本節將通過兩個示例應用來展示 redis-rb-cluster 的基本用法, 以下是本節的第一個示例應用, 它是一個名為 example.rb 的檔案, 包含在redis-rb-cluster 專案裡面:
require './cluster' startup_nodes = [ {:host => "127.0.0.1", :port => 7000}, {:host => "127.0.0.1", :port => 7001} ] rc = RedisCluster.new(startup_nodes,32,:timeout => 0.1) last = false while not last begin last = rc.get("__last__") last = 0 if !last rescue => e puts "error #{e.to_s}" sleep 1 end end ((last.to_i+1)..1000000000).each{|x| begin rc.set("foo#{x}",x) puts rc.get("foo#{x}") rc.set("__last__",x) rescue => e puts "error #{e.to_s}" end sleep 0.1 }#這個應用所做的工作非常簡單: 它不斷地以 foo<number> 為鍵, number 為值, 使用 SET 命令向資料庫設定鍵值對:SET foo0 0
SET foo1 1
SET foo2 2
And so forth…
程式碼中的每個叢集操作都使用一個 begin 和 rescue 程式碼塊(block)包裹著, 因為我們希望在程式碼出錯時, 將錯誤列印到終端上面, 而不希望應用因為異常(exception)而退出。
程式碼的第七行是程式碼中第一個有趣的地方, 它建立了一個 Redis 叢集物件, 其中建立物件所使用的引數及其意義如下:第一個引數是記錄了啟動節點的 startup_nodes 列表, 列表中包含了兩個叢集節點的地址。第二個引數指定了對於叢集中的各個不同的節點, Redis 叢集物件可以獲得的最大連線數 ,第三個引數 timeout 指定了一個命令在執行多久之後, 才會被看作是執行失敗。
啟動列表中並不需要包含所有叢集節點的地址, 但這些地址中至少要有一個是有效的: 一旦 redis-rb-cluster 成功連線上叢集中的某個節點時, 叢集節點列表就會被自動更新, 任何真正的的叢集客戶端都應該這樣做。
現在, 程式建立的 Redis 叢集物件例項被儲存到 rc 變數裡面, 我們可以將這個物件當作普通 Redis 物件例項來使用。
在十一至十九行, 我們先嚐試閱讀計數器中的值, 如果計數器不存在的話, 我們才將計數器初始化為 0 : 通過將計數值儲存到 Redis 的計數器裡面, 我們可以在示例重啟之後, 仍然繼續之前的執行過程, 而不必每次重啟之後都從 foo0 開始重新設定鍵值對。為了讓程式在叢集下線的情況下, 仍然不斷地嘗試讀取計數器的值, 我們將讀取操作包含在了一個 while 迴圈裡面, 一般的應用程式並不需要如此小心。
二十一至三十行是程式的主迴圈, 這個迴圈負責設定鍵值對, 並在設定出錯時列印錯誤資訊。程式在主迴圈的末尾添加了一個 sleep 呼叫, 讓寫操作的執行速度變慢, 幫助執行示例的人更容易看清程式的輸出。執行 example.rb 程式將產生以下輸出:
ruby ./example.rb
1
2
3
4
5
6
7
8
9
^C (I stopped the program here)
這個程式並不是十分有趣, 稍後我們就會看到一個更有趣的叢集應用示例, 不過在此之前, 讓我們先使用這個示例來演示叢集的重新分片操作。
叢集重新分片
現在, 讓我們來試試對叢集進行重新分片操作。在執行重新分片的過程中, 請讓你的 example.rb 程式處於執行狀態, 這樣你就會看到, 重新分片並不會對正在執行的叢集程式產生任何影響, 你也可以考慮將 example.rb 中的 sleep 呼叫刪掉, 從而讓重新分片操作在近乎真實的寫負載下執行 重新分片操作基本上就是將某些節點上的雜湊槽移動到另外一些節點上面, 和建立叢集一樣, 重新分片也可以使用 redis-trib 程式來執行 執行以下命令可以開始一次重新分片操作:
./redis-trib.rb reshard 127.0.0.1:7000
你只需要指定叢集中其中一個節點的地址, redis-trib 就會自動找到叢集中的其他節點。
目前 redis-trib 只能在管理員的協助下完成重新分片的工作, 要讓 redis-trib 自動將雜湊槽從一個節點移動到另一個節點, 目前來說還做不到
你想移動多少個槽( 從1 到 16384)?
我們嘗試從將100個槽重新分片, 如果 example.rb 程式一直執行著的話, 現在 1000 個槽裡面應該有不少鍵了。
除了移動的雜湊槽數量之外, redis-trib 還需要知道重新分片的目標, 也即是, 負責接收這 1000 個雜湊槽的節點。
$ redis-cli -p 7000 cluster nodes | grep myself
97a3a64667477371c4479320d683e4c8db5858b1 :0 myself,master - 0 0 0 connected 0-5460
我得目標節點是 97a3a64667477371c4479320d683e4c8db5858b1.
現在需要指定從哪寫節點來移動keys到目標借調 我輸入的是all ,這樣就會從其他每個master上取一些雜湊槽。
最後確認後你將會看到每個redis-trib移動的槽的資訊,每個key的移動的資訊也會打印出來 在重新分片的過程中,你得例子程式是不會受到影響的,你可以停止或者重新啟動多次。
在重新分片結束後你可以通過如下命令檢查叢集狀態:
./redis-trib.rb check 127.0.0.1:7000
一個更有趣的程式
我們在前面使用的示例程式 example.rb 並不是十分有趣, 因為它只是不斷地對叢集進行寫入, 但並不檢查寫入結果是否正確。 比如說, 叢集可能會錯誤地將 example.rb 傳送的所有 SET 命令都改成了 SET foo 42 , 但因為 example.rb 並不檢查寫入後的值, 所以它不會意識到叢集實際上寫入的值是錯誤的 因為這個原因, redis-rb-cluster 專案包含了一個名為 consistency-test.rb 的示例應用, 這個應用比起 example.rb 有趣得多: 它建立了多個計數器(預設為 1000 個), 並通過傳送 INCR 命令來增加這些計數器的值。
在增加計數器值的同時, consistency-test.rb 還執行以下操作: 每次使用 INCR 命令更新一個計數器時, 應用會記錄下計數器執行 INCR 命令之後應該有的值。 舉個例子, 如果計數器的起始值為 0 , 而這次是程式第 50 次向它傳送 INCR 命令, 那麼計數器的值應該是 50 。
在每次傳送 INCR 命令之前, 程式會隨機從叢集中讀取一個計數器的值, 並將它與自己記錄的值進行對比, 看兩個值是否相同。
換句話說, 這個程式是一個一致性檢查器(consistency checker): 如果叢集在執行 INCR 命令的過程中, 丟失了某條 INCR 命令, 又或者多執行了某條客戶端沒有確認到的 INCR 命令, 那麼檢查器將察覺到這一點 —— 在前一種情況中, consistency-test.rb 記錄的計數器值將比叢集記錄的計數器值要大; 而在後一種情況中, consistency-test.rb 記錄的計數器值將比叢集記錄的計數器值要小。
#consistency-test.rbrequire './cluster'class ConsistencyTester def initialize(redis) @r = redis @working_set = 1000 @keyspace = 10000 @writes = 0 @reads = 0 @failed_writes = 0 @failed_reads = 0 @lost_writes = 0 @not_ack_writes = 0 @delay = 0 @cached = {} # We take our view of data stored in the DB. @prefix = [Process.pid.to_s,Time.now.usec,@r.object_id,""].join("|") @errtime = {} end def genkey # Write more often to a small subset of keys ks = rand() > 0.5 ? @keyspace : @working_set @prefix+"key_"+rand(ks).to_s end def check_consistency(key,value) expected = @cached[key] return if !expected # We lack info about previous state. if expected > value @lost_writes += expected-value elsif expected < value @not_ack_writes += value-expected end end def puterr(msg) if !@errtime[msg] || Time.now.to_i != @errtime[msg] puts msg end @errtime[msg] = Time.now.to_i end def test last_report = Time.now.to_i while true # Read key = genkey begin val = @r.get(key) check_consistency(key,val.to_i) @reads += 1 rescue => e puterr "Reading: #{e.to_s}" @failed_reads += 1 end # Write begin @cached[key] = @r.incr(key).to_i @writes += 1 rescue => e puterr "Writing: #{e.to_s}" @failed_writes += 1 end # Report sleep @delay if Time.now.to_i != last_report report = "#{@reads} R (#{@failed_reads} err) | " + "#{@writes} W (#{@failed_writes} err) | " report += "#{@lost_writes} lost | " if @lost_writes > 0 report += "#{@not_ack_writes} noack | " if @not_ack_writes > 0 last_report = Time.now.to_i puts report end end endendif ARGV.length != 2 puts "Usage: consistency-test.rb <hostname> <port>" exit 1else startup_nodes = [ {:host => ARGV[0], :port => ARGV[1].to_i