為什么需要 Redis 集群
為什么需要集群?
性能
Redis 本身的 QPS 已經(jīng)很高了,但是如果在一些并發(fā)量非常高的情況下,性能還是會(huì)受到影響.這個(gè)時(shí)候我們希望有更多的 Redis 服務(wù)來完成工作
擴(kuò)展
第二個(gè)是出于存儲(chǔ)的考慮.因?yàn)?Redis 所有的數(shù)據(jù)都放在內(nèi)存中,如果數(shù)據(jù)量大,很容易受到硬件的限制.升級(jí)硬件收效和成本比太低,所以我們需要有一種橫向擴(kuò)展的方法
可用性
第三個(gè)是可用性和安全的問題.如果只有一個(gè) Redis 服務(wù),一旦服務(wù)宕機(jī),那么所有的客戶端都無法訪問,會(huì)對(duì)業(yè)務(wù)造成很大的影響.另一個(gè),如果硬件發(fā)生故障,而單機(jī)的數(shù)據(jù)無法恢復(fù)的話,帶來的影響也是災(zāi)難性的
可用性,數(shù)據(jù)安全,性能都可以通過搭建多個(gè) Reids 服務(wù)實(shí)現(xiàn).其中有一個(gè)是主節(jié)點(diǎn)(master),可以有多個(gè)從節(jié)點(diǎn)(slave).主從之間通過數(shù)據(jù)同步,存儲(chǔ)完全相同的數(shù)據(jù).如果主節(jié)點(diǎn)發(fā)生故障,則把某個(gè)從節(jié)點(diǎn)改成主節(jié)點(diǎn),訪問新的主節(jié)點(diǎn)
Redis 主從復(fù)制(replication)
主從復(fù)制配置
例如一主多從,203 是主節(jié)點(diǎn),在每個(gè) slave 節(jié)點(diǎn)的 redis.conf 配置文件增加一行
slaveof 192.168.8.203 6379
//在主從切換的時(shí)候,這個(gè)配置會(huì)被重寫成:
# Generated by CONFIG REWRITE
replicaof 192.168.8.203 6379
或者在啟動(dòng)服務(wù)時(shí)通過參數(shù)指定 master 節(jié)點(diǎn):
./redis-server --slaveof 192.168.8.203 6379
或在客戶端直接執(zhí)行 slaveof xxxx,使該 Redis 實(shí)例成為從節(jié)點(diǎn)
啟動(dòng)后,查看集群狀態(tài):
redis> info replication
從節(jié)點(diǎn)不能寫入數(shù)據(jù)(只讀),只能從 master 節(jié)點(diǎn)同步數(shù)據(jù).get 成功,set 失敗
127.0.0.1:6379> set gupao 666
(error)READONLY You can't write against a read only replica
主節(jié)點(diǎn)寫入后,slave 會(huì)自動(dòng)從 master 同步數(shù)據(jù)
斷開復(fù)制:
redis> slaveof no one
此時(shí)從節(jié)點(diǎn)會(huì)變成自己的主節(jié)點(diǎn),不再?gòu)?fù)制數(shù)據(jù)
主從復(fù)制原理
連接階段
- slave node 啟動(dòng)時(shí)(執(zhí)行 slaveof 命令),會(huì)在自己本地保存 master node 的信息,包括 master node 的 host 和 ip
- slave node 內(nèi)部有個(gè)定時(shí)任務(wù) replicationCron(源碼 replication.c),每隔 1 秒鐘檢查是否有新的 master node 要連接和復(fù)制,如果發(fā)現(xiàn),就跟 master node 建立 socket 網(wǎng)絡(luò)連接,如果連接成功,從節(jié)點(diǎn)為該 socket 建立一個(gè)專門處理復(fù)制工作的文件事件處理器,負(fù)責(zé)后續(xù)的復(fù)制工作,如接收 RDB 文件,接收命令傳播等
當(dāng)從節(jié)點(diǎn)變成了主節(jié)點(diǎn)的一個(gè)客戶端之后,會(huì)給主節(jié)點(diǎn)發(fā)送 ping 請(qǐng)求
數(shù)據(jù)同步階段
- master node 第一次執(zhí)行全量復(fù)制,通過 bgsave 命令在本地生成一份 RDB 快照,將 RDB 快照文件發(fā)給 slave node(如果超時(shí)會(huì)重連,可以調(diào)大 repl-timeout 的值).slave node 首先清除自己的舊數(shù)據(jù),然后用 RDB 文件加載數(shù)據(jù)
問題:生成 RDB 期間,master 接收到的命令怎么處理
開始生成 RDB 文件時(shí),master 會(huì)把所有新的寫命令緩存在內(nèi)存中.在 slave node 保存了 RDB 之后,再將新的寫命令復(fù)制給 slave node
命令傳播階段
- master node 持續(xù)將寫命令,異步復(fù)制給 slave node
延遲是不可避免的,只能通過優(yōu)化網(wǎng)絡(luò)
repl-disable-tcp-nodelay no
當(dāng)設(shè)置為 yes 時(shí),TCP 會(huì)對(duì)包進(jìn)行合并從而減少帶寬,但是發(fā)送的頻率會(huì)降低,從節(jié)點(diǎn)數(shù)據(jù)延遲增加,一致性變差亭饵;具體發(fā)送頻率與 Linux 內(nèi)核的配置有關(guān),默認(rèn)配置為 40ms.當(dāng)設(shè)置為 no 時(shí),TCP 會(huì)立馬將主節(jié)點(diǎn)的數(shù)據(jù)發(fā)送給從節(jié)點(diǎn),帶寬增加但延遲變小
一般來說,只有當(dāng)應(yīng)用對(duì) Redis 數(shù)據(jù)不一致的容忍度較高,且主從節(jié)點(diǎn)之間網(wǎng)絡(luò)狀況不好時(shí),才會(huì)設(shè)置為 yes;多數(shù)情況使用默認(rèn)值 no
問題:如果從節(jié)點(diǎn)有一段時(shí)間斷開了與主節(jié)點(diǎn)的連接是不是要重新全量復(fù)制一遍?如果可以增量復(fù)制,怎么知道上次復(fù)制到哪里?
通過 master_repl_offset 記錄的偏移量
redis> info replication
主從復(fù)制的不足
主從模式解決了數(shù)據(jù)備份和性能(通過讀寫分離)的問題,但是還是存在一些不足:
- RDB 文件過大的情況下,同步非常耗時(shí)
- 在一主一從或者一主多從的情況下,如果主服務(wù)器掛了,對(duì)外提供的服務(wù)就不可用了,單點(diǎn)問題沒有得到解決.如果每次都是手動(dòng)把之前的從服務(wù)器切換成主服務(wù)器,這個(gè)比較費(fèi)時(shí)費(fèi)力,還會(huì)造成一定時(shí)間的服務(wù)不可用
可用性保證之 Sentinel
Sentinel 原理
如何實(shí)現(xiàn)主從的自動(dòng)切換?我們的思路:
創(chuàng)建一臺(tái)監(jiān)控服務(wù)器來監(jiān)控所有 Redis 服務(wù)節(jié)點(diǎn)的狀態(tài),比如,master 節(jié)點(diǎn)超過一定時(shí)間沒有給監(jiān)控服務(wù)器發(fā)送心跳報(bào)文,就把 master 標(biāo)記為下線,然后把某一個(gè) slave 變成 master.應(yīng)用每一次都是從這個(gè)監(jiān)控服務(wù)器拿到 master 的地址
問題是:如果監(jiān)控服務(wù)器本身出問題了怎么辦?那我們就拿不到 master 的地址了,應(yīng)用也沒有辦法訪問
那我們?cè)賱?chuàng)建一個(gè)監(jiān)控服務(wù)器,來監(jiān)控監(jiān)控服務(wù)器……似乎陷入死循環(huán)了,這個(gè)問題怎么解決?這個(gè)問題先放著
Redis 的 Sentinel 就是這種思路:通過運(yùn)行監(jiān)控服務(wù)器來保證服務(wù)的可用性
官網(wǎng):https://redis.io/topics/sentinel
從 Redis2.8 版本起,提供了一個(gè)穩(wěn)定版本的 Sentinel(哨兵),用來解決高可用的問題.它是一個(gè)特殊狀態(tài)的 redis 實(shí)例
我們會(huì)啟動(dòng)一個(gè)或者多個(gè) Sentinel 的服務(wù)(通過 src/redis-sentinel),它本質(zhì)上只是一個(gè)運(yùn)行在特殊模式之下的 Redis,Sentinel 通過 info 命令得到被監(jiān)聽 Redis 機(jī)器的 master,slave 等信息
為了保證監(jiān)控服務(wù)器的可用性,我們會(huì)對(duì) Sentinel 做集群的部署.Sentinel 既監(jiān)控所有的 Redis 服務(wù),Sentinel 之間也相互監(jiān)控
注意:Sentinel 本身沒有主從之分,只有 Redis 服務(wù)節(jié)點(diǎn)有主從之分
概念梳理:master,slave(redisgroup),sentinel,sentinel 集合
服務(wù)下線
Sentinel 默認(rèn)以每秒鐘 1 次的頻率向 Redis 服務(wù)節(jié)點(diǎn)發(fā)送 PING 命令.如果在 down-after-milliseconds 內(nèi)都沒有收到有效回復(fù),Sentinel 會(huì)將該服務(wù)器標(biāo)記為下線(主觀下線)
# sentinel.conf
sentinel down-after-milliseconds <master-name> <milliseconds>
這個(gè)時(shí)候 Sentinel 節(jié)點(diǎn)會(huì)繼續(xù)詢問其他的 Sentinel 節(jié)點(diǎn),確認(rèn)這個(gè)節(jié)點(diǎn)是否下線,如果多數(shù) Sentinel 節(jié)點(diǎn)都認(rèn)為 master 下線,master 才真正確認(rèn)被下線(客觀下線),這個(gè)時(shí)候就需要重新選舉 master
故障轉(zhuǎn)移
如果 master 被標(biāo)記為下線,就會(huì)開始故障轉(zhuǎn)移流程
既然有這么多的 Sentinel 節(jié)點(diǎn),由誰(shuí)來做故障轉(zhuǎn)移的事情呢?
故障轉(zhuǎn)移流程的第一步就是在 Sentinel 集群選擇一個(gè) Leader,由 Leader 完成故障轉(zhuǎn)移流程.Sentinle 通過 Raft 算法,實(shí)現(xiàn) Sentinel 選舉
Ratf 算法
在分布式存儲(chǔ)系統(tǒng)中,通常通過維護(hù)多個(gè)副本來提高系統(tǒng)的可用性,那么多個(gè)節(jié)點(diǎn)之間必須要面對(duì)數(shù)據(jù)一致性的問題.Raft 的目的就是通過復(fù)制的方式,使所有節(jié)點(diǎn)達(dá)成一致,但是這么多節(jié)點(diǎn),以哪個(gè)節(jié)點(diǎn)的數(shù)據(jù)為準(zhǔn)呢?所以必須選出一個(gè) Leader
大體上有兩個(gè)步驟:領(lǐng)導(dǎo)選舉,數(shù)據(jù)復(fù)制
Raft 是一個(gè)共識(shí)算法(consensus algorithm).比如比特幣之類的加密貨幣,就需要共識(shí)算法.SpringCloud 的注冊(cè)中心解決方案 Consul 也用到了 Raft 協(xié)議
Raft 的核心思想:先到先得,少數(shù)服從多數(shù)
Raft 算法演示:http://thesecretlivesofdata.com/raft/
總結(jié):Sentinle 的 Raft 算法和 Raft 論文略有不同
- master 客觀下線觸發(fā)選舉,而不是過了 election timeout 時(shí)間開始選舉
- Leader 并不會(huì)把自己成為 Leader 的消息發(fā)給其他 Sentinel.其他 Sentinel 等待 Leader 從 slave 選出 master 后,檢測(cè)到新的 master 正常工作后,就會(huì)去掉客觀下線的標(biāo)識(shí),從而不需要進(jìn)入故障轉(zhuǎn)移流程
故障轉(zhuǎn)移
問題:怎么讓一個(gè)原來的 slave 節(jié)點(diǎn)成為主節(jié)點(diǎn)?
- 選出 Sentinel Leader 之后,由 Sentinel Leader 向某個(gè)節(jié)點(diǎn)發(fā)送 slaveof no one 命令,讓它成為獨(dú)立節(jié)點(diǎn)
- 然后向其他節(jié)點(diǎn)發(fā)送 slaveof x.x.x.xxxxx(本機(jī)服務(wù)),讓它們成為這個(gè)節(jié)點(diǎn)的子節(jié)點(diǎn),故障轉(zhuǎn)移完成
問題:這么多從節(jié)點(diǎn),選誰(shuí)成為主節(jié)點(diǎn)?
關(guān)于從節(jié)點(diǎn)選舉,一共有四個(gè)因素影響選舉的結(jié)果,分別是斷開連接時(shí)長(zhǎng),優(yōu)先級(jí)排序,復(fù)制數(shù)量,進(jìn)程 id
如果與哨兵連接斷開的比較久,超過了某個(gè)閾值,就直接失去了選舉權(quán).如果擁有選舉權(quán),那就看誰(shuí)的優(yōu)先級(jí)高,這個(gè)在配置文件里可以設(shè)置(replica-priority 100),數(shù)值越小優(yōu)先級(jí)越高
如果優(yōu)先級(jí)相同,就看誰(shuí)從 master 中復(fù)制的數(shù)據(jù)最多(復(fù)制偏移量最大),選最多的那個(gè),如果復(fù)制數(shù)量也相同,就選擇進(jìn)程 id 最小的那個(gè)
Sentinel 的功能總結(jié)
- Monitoring.Sentinelconstantlychecksifyourmasterandslaveinstancesareworkingasexpected
- Notification.Sentinelcannotifythesystemadministrator,anothercomputerprograms,viaanAPI,thatsomethingiswrongwithoneofthemonitoredRedisinstances
- Automaticfailover.Ifamasterisnotworkingasexpected,Sentinelcanstartafailoverprocesswhereaslaveispromotedtomaster,theotheradditionalslavesarereconfiguredtousethenewmaster,andtheapplicationsusingtheRedisserverinformedaboutthenewaddresstousewhenconnecting
- Configurationprovider.Sentinelactsasasourceofauthorityforclientsservicediscovery:clientsconnecttoSentinelsinordertoaskfortheaddressofthecurrentRedismasterresponsibleforagivenservice.Ifafailoveroccurs,Sentinelswillreportthenewaddress
監(jiān)控:Sentinel 會(huì)不斷檢查主服務(wù)器和從服務(wù)器是否正常運(yùn)行
通知:如果某一個(gè)被監(jiān)控的實(shí)例出現(xiàn)問題,Sentinel 可以通過 API 發(fā)出通知
自動(dòng)故障轉(zhuǎn)移(failover):如果主服務(wù)器發(fā)生故障,Sentinel 可以啟動(dòng)故障轉(zhuǎn)移過程.把某臺(tái)服務(wù)器升級(jí)為主服務(wù)器,并發(fā)出通知
配置管理:客戶端連接到 Sentinel,獲取當(dāng)前的 Redis 主服務(wù)器的地址
Sentinel 實(shí)戰(zhàn)
Sentinel 配置
為了保證 Sentinel 的高可用,Sentinel 也需要做集群部署,集群中至少需要三個(gè) Sentinel 實(shí)例(推薦奇數(shù)個(gè),防止腦裂)
hostname | IP 地址 | 節(jié)點(diǎn)角色&端口 |
---|---|---|
master | 192.168.8.203 | Master:6379/Sentinel:26379 |
slave1 | 192.168.8.204 | Slave:6379/Sentinel:26379 |
Slave2 | 192.168.8.205 | Slave:6379/Sentinel:26379 |
以 Redis 安裝路徑/usr/local/soft/redis-5.0.5/為例,在 204 和 205 的 src/redis.conf 配置文件中添加
slaveof 192.168.8.203 6379
在 203,204,205 創(chuàng)建 sentinel 配置文件(安裝后根目錄下默認(rèn)有 sentinel.conf):
cd /usr/local/soft/redis-5.0.5
mkdir logs
mkdir rdbs
mkdir sentinel-tmp
vim sentinel.conf
三臺(tái)服務(wù)器內(nèi)容相同:
daemonize yes
port 26379
protected-mode no
dir "/usr/local/soft/redis-5.0.5/sentinel-tmp"
sentinel monitor redis-master 192.168.8.203 6379 2
sentinel down-after-milliseconds redis-master 30000
sentinel failover-timeout redis-master 180000
sentinel parallel-syncs redis-master 1
上面出現(xiàn)了 4 個(gè)'redis-master',這個(gè)名稱要統(tǒng)一,并且使用客戶端(比如 Jedis)連接的時(shí)候名稱要正確
hostname | IP 地址 |
---|---|
protected-mode | 是否允許外部網(wǎng)絡(luò)訪問 |
dir | sentinel 的工作目錄 |
sentinel | monitorsentinel 監(jiān)控的 redis 主節(jié)點(diǎn) |
down-after-milliseconds(毫秒) | master 宕機(jī)多久,才會(huì)被 Sentinel 主觀認(rèn)為下線 |
sentinelfailover-timeout(毫秒) | 1.同一個(gè) sentinel 對(duì)同一個(gè) master 兩次 failover 之間的間隔時(shí)間,2.當(dāng)一個(gè) slave 從一個(gè)錯(cuò)誤的 master 那里同步數(shù)據(jù)開始計(jì)算時(shí)間.直到 slave 被糾正為向正確的 master 那里同步數(shù)據(jù)時(shí),3.當(dāng)想要取消一個(gè)正在進(jìn)行的 failover 所需要的時(shí)間,4.當(dāng)進(jìn)行 failover 時(shí),配置所有 slaves 指向新的 master 所需的最大時(shí)間 |
parallel-syncs | 這個(gè)配置項(xiàng)指定了在發(fā)生 failover 主備切換時(shí)最多可以有多少個(gè) slave 同時(shí)對(duì)新的 master 進(jìn)行同步,這個(gè)數(shù)字越小,完成 failover 所需的時(shí)間就越長(zhǎng),但是如果這個(gè)數(shù)字越大,就意味著越多的 slave 因?yàn)?replication 而不可用.可以通過將這個(gè)值設(shè)為 1 來保證每次只有一個(gè) slave 處于不能處理命令請(qǐng)求的狀態(tài) |
Sentinel 驗(yàn)證
啟動(dòng) Redis 服務(wù)和 Sentinel
cd /usr/local/soft/redis-5.0.5/src
# 啟動(dòng) Redis 節(jié)點(diǎn)
./redis-server ../redis.conf
# 啟動(dòng) Sentinel 節(jié)點(diǎn)
./redis-sentinel ../sentinel.conf
# 或者
./redis-server ../sentinel.conf --sentinel
查看集群狀態(tài):
redis> info replication
203
204和205
模擬 master 宕機(jī),在 203 執(zhí)行:
redis> shutdown
205 被選為新的 Master,只有一個(gè) Slave 節(jié)點(diǎn)
注意看 sentinel.conf 里面的 redis-master 被修改了!
模擬原 master 恢復(fù),在 203 啟動(dòng) redis-server.它還是 slave,但是 master 又有兩個(gè) slave 了
slave 宕機(jī)和恢復(fù)省略
Sentinel 連接使用
Jedis 連接 Sentinel(gupao-jedis:JedisSentinelTest.java)
mastername 來自于 sentinel.conf 的配置
private static JedisSentinelPool createJedisPool(){
String masterName = "redis-master";
Set<String> sentinels = new HashSet<String>();
sentinels.add("192.168.8.203:26379");
sentinels.add("192.168.8.204:26379");
sentinels.add("192.168.8.205:26379");
pool = new JedisSentinelPool(masterName, sentinels);
return pool;
}
SpringBoot 連接 Sentinel(springboot-redis:RedisAppTest.java)
spring.redis.sentinel.master=redis-master
spring.redis.sentinel.nodes=192.168.8.203:26379,192.168.8.204:26379,192.168.8.205:26379
無論是 Jedis 還是 SpringBoot(2.x 版本默認(rèn)是 Lettuce),都只需要配置全部哨兵的地址,由哨兵返回當(dāng)前的 master 節(jié)點(diǎn)地址
哨兵機(jī)制的不足
主從切換的過程中會(huì)丟失數(shù)據(jù),因?yàn)橹挥幸粋€(gè) master
只能單點(diǎn)寫,沒有解決水平擴(kuò)容的問題
如果數(shù)據(jù)量非常大,這個(gè)時(shí)候我們需要多個(gè) master-slave 的 group,把數(shù)據(jù)分布到不同的 group 中
問題來了,數(shù)據(jù)怎么分片?分片之后,怎么實(shí)現(xiàn)路由?
Redis 分布式方案
如果要實(shí)現(xiàn) Redis 數(shù)據(jù)的分片,我們有三種方案.第一種是在客戶端實(shí)現(xiàn)相關(guān)的邏輯,例如用取模或者一致性哈希對(duì) key 進(jìn)行分片,查詢和修改都先判斷 key 的路由
第二種是把做分片處理的邏輯抽取出來,運(yùn)行一個(gè)獨(dú)立的代理服務(wù),客戶端連接到這個(gè)代理服務(wù),代理服務(wù)做請(qǐng)求的轉(zhuǎn)發(fā)
第三種就是基于服務(wù)端實(shí)現(xiàn)
客戶端 Sharding
Jedis 客戶端提供了 RedisSharding 的方案,并且支持連接池
ShardedJedis
public class ShardingTest {
public static void main(String[] args){
JedisPoolConfig poolConfig = new JedisPoolConfig(); // Redis 服務(wù)器
JedisShardInfo shardInfo1 = new JedisShardInfo("127.0.0.1", 6379);
JedisShardInfo shardInfo2 = new JedisShardInfo("192.168.8.205", 6379); // 連接池
List<JedisShardInfo> infoList = Arrays.asList(shardInfo1, shardInfo2);
ShardedJedisPool jedisPool = new ShardedJedisPool(poolConfig, infoList);
ShardedJedis jedis = null;
try {
jedis = jedisPool.getResource();
for(int i=0; i<100; i++){
jedis.set("k"+i, ""+i);
}
for(int i=0; i<100; i++){
System.out.println(jedis.get("k"+i));
}
} finally {
if(jedis!=null){
jedis.close();
}
}
}
}
(作業(yè))Sharded 分片的原理?怎么連接到某一個(gè) Redis 服務(wù)?
使用 ShardedJedis 之類的客戶端分片代碼的優(yōu)勢(shì)是配置簡(jiǎn)單,不依賴于其他中間件,分區(qū)的邏輯可以自定義,比較靈活.但是基于客戶端的方案,不能實(shí)現(xiàn)動(dòng)態(tài)的服務(wù)增減,每個(gè)客戶端需要自行維護(hù)分片策略,存在重復(fù)代碼
第二種思路就是把分片的代碼抽取出來,做成一個(gè)公共服務(wù),所有的客戶端都連接到這個(gè)代理層.由代理層來實(shí)現(xiàn)請(qǐng)求和轉(zhuǎn)發(fā)
代理 Proxy
典型的代理分區(qū)方案有 Twitter 開源的 Twemproxy 和國(guó)內(nèi)的豌豆莢開源的 Codis
Twemproxy
two-em-proxy https://github.com/twitter/twemproxy
Twemproxy 的優(yōu)點(diǎn):比較穩(wěn)定,可用性高
不足:
- 出現(xiàn)故障不能自動(dòng)轉(zhuǎn)移,架構(gòu)復(fù)雜,需要借助其他組件(LVS/HAProxy + Keepalived)實(shí)現(xiàn) HA
- 擴(kuò)縮容需要修改配置,不能實(shí)現(xiàn)平滑地?cái)U(kuò)縮容(需要重新分布數(shù)據(jù))
Codis
https://github.com/CodisLabs/codis
Codis 是一個(gè)代理中間件,用 Go 語(yǔ)言開發(fā)
功能:客戶端連接 Codis 跟連接 Redis 沒有區(qū)別
Codis | Tewmproxy | RedisCluster | |
---|---|---|---|
重新分片不需要重啟 | Yes | No | Yes |
pipeline | Yes | Yes | |
多 key 操作的 hash tags {} | Yes | Yes | Yes |
重新分片時(shí)的多 key 操作 | Yes | - | No |
客戶端支持 | 所有 | 所有 | 支持 cluster 協(xié)議的客戶端 |
分片原理:Codis 把所有的 key 分成了 N 個(gè)槽(例如 1024),每個(gè)槽對(duì)應(yīng)一個(gè)分組,一個(gè)分組對(duì)應(yīng)于一個(gè)或者一組 Redis 實(shí)例.Codis 對(duì) key 進(jìn)行 CRC32 運(yùn)算,得到一個(gè) 32 位的數(shù)字,然后模以 N(槽的個(gè)數(shù)),得到余數(shù),這個(gè)就是 key 對(duì)應(yīng)的槽,槽后面就是 Redis 的實(shí)例.比如 4 個(gè)槽:
Codis 的槽位映射關(guān)系是保存在 Proxy 中的,如果要解決單點(diǎn)的問題,Codis 也要做集群部署,多個(gè) Codis 節(jié)點(diǎn)怎么同步槽和實(shí)例的關(guān)系呢?需要運(yùn)行一個(gè) Zookeeper(或者 etcd/本地文件)
在新增節(jié)點(diǎn)的時(shí)候,可以為節(jié)點(diǎn)指定特定的槽位.Codis 也提供了自動(dòng)均衡策略
Codis 不支持事務(wù),其他的一些命令也不支持
不支持的命令
https://github.com/CodisLabs/codis/blob/release3.2/doc/unsupported_cmds.md
獲取數(shù)據(jù)原理(mget):在 Redis 中的各個(gè)實(shí)例里獲取到符合的 key,然后再匯總到 Codis 中
Codis 是第三方提供的分布式解決方案,在官方的集群功能穩(wěn)定之前,Codis 也得到了大量的應(yīng)用
Redis Cluster
https://redis.io/topics/cluster-tutorial/
Redis Cluster 是在 Redis3.0 的版本正式推出的,用來解決分布式的需求,同時(shí)也可以實(shí)現(xiàn)高可用.跟 Codis 不一樣,它是去中心化的,客戶端可以連接到任意一個(gè)可用節(jié)點(diǎn)
數(shù)據(jù)分片有幾個(gè)關(guān)鍵的問題需要解決:
- 數(shù)據(jù)怎么相對(duì)均勻地分片
- 客戶端怎么訪問到相應(yīng)的節(jié)點(diǎn)和數(shù)據(jù)
- 重新分片的過程,怎么保證正常服務(wù)
架構(gòu)
Redis Cluster 可以看成是由多個(gè) Redis 實(shí)例組成的數(shù)據(jù)集合.客戶端不需要關(guān)注數(shù)據(jù)的子集到底存儲(chǔ)在哪個(gè)節(jié)點(diǎn),只需要關(guān)注這個(gè)集合整體
以3主3從為例,節(jié)點(diǎn)之間兩兩交互,共享數(shù)據(jù)分片,節(jié)點(diǎn)狀態(tài)等信息
搭建
https://gper.club/articles/7e7e7f7ff7g5egc7g6d
配置啟動(dòng)進(jìn)入客戶端:
redis-cli -p 7291
redis-cli -p 7292
redis-cli -p 7293
批量插入數(shù)據(jù)
問題:Cluster 解決分片的問題,數(shù)據(jù)怎么分布?
數(shù)據(jù)分布
如果是希望數(shù)據(jù)分布相對(duì)均勻的話,我們首先可以考慮哈希后取模
哈希后取模
例如,hash(key)%N,根據(jù)余數(shù),決定映射到那一個(gè)節(jié)點(diǎn).這種方式比較簡(jiǎn)單,屬于靜態(tài)的分片規(guī)則.但是一旦節(jié)點(diǎn)數(shù)量變化,新增或者減少,由于取模的 N 發(fā)生變化,數(shù)據(jù)需要重新分布
為了解決這個(gè)問題,我們又有了一致性哈希算法
一致性哈希
一致性哈希的原理:
把所有的哈希值空間組織成一個(gè)虛擬的圓環(huán)(哈希環(huán)),整個(gè)空間按順時(shí)針方向組織.因?yàn)槭黔h(huán)形空間,0 和 2^32-1 是重疊的
假設(shè)我們有四臺(tái)機(jī)器要哈希環(huán)來實(shí)現(xiàn)映射(分布數(shù)據(jù)),我們先根據(jù)機(jī)器的名稱或者 IP 計(jì)算哈希值,然后分布到哈希環(huán)中(紅色圓圈)
現(xiàn)在有 4 條數(shù)據(jù)或者 4 個(gè)訪問請(qǐng)求,對(duì) key 計(jì)算后,得到哈希環(huán)中的位置(綠色圓圈).沿哈希環(huán)順時(shí)針找到的第一個(gè) Node,就是數(shù)據(jù)存儲(chǔ)的節(jié)點(diǎn)
在這種情況下,新增了一個(gè) Node5 節(jié)點(diǎn),不影響數(shù)據(jù)的分布
刪除了一個(gè)節(jié)點(diǎn) Node4,只影響相鄰的一個(gè)節(jié)點(diǎn)
谷歌的 MurmurHash 就是一致性哈希算法.在分布式系統(tǒng)中,負(fù)載均衡,分庫(kù)分表等場(chǎng)景中都有應(yīng)用
一致性哈希解決了動(dòng)態(tài)增減節(jié)點(diǎn)時(shí),所有數(shù)據(jù)都需要重新分布的問題,它只會(huì)影響到下一個(gè)相鄰的節(jié)點(diǎn),對(duì)其他節(jié)點(diǎn)沒有影響
但是這樣的一致性哈希算法有一個(gè)缺點(diǎn),因?yàn)楣?jié)點(diǎn)不一定是均勻地分布的,特別是在節(jié)點(diǎn)數(shù)比較少的情況下,所以數(shù)據(jù)不能得到均勻分布.解決這個(gè)問題的辦法是引入虛擬節(jié)點(diǎn)(Virtual Node)
比如:2 個(gè)節(jié)點(diǎn),5 條數(shù)據(jù),只有 1 條分布到 Node2,4 條分布到 Node1,不均勻
Node1 設(shè)置了兩個(gè)虛擬節(jié)點(diǎn),Node2 也設(shè)置了兩個(gè)虛擬節(jié)點(diǎn)(虛線圓圈)
這時(shí)候有 3 條數(shù)據(jù)分布到 Node1,1 條數(shù)據(jù)分布到 Node2
Redis 虛擬槽分區(qū)
Redis 既沒有用哈希取模,也沒有用一致性哈希,而是用虛擬槽來實(shí)現(xiàn)的
Redis 創(chuàng)建了 16384 個(gè)槽(slot),每個(gè)節(jié)點(diǎn)負(fù)責(zé)一定區(qū)間的 slot.比如 Node1 負(fù)責(zé) 0-5460,Node2 負(fù)責(zé) 5461-10922,Node3 負(fù)責(zé) 10923-16383
3 個(gè) Redis 節(jié)點(diǎn),各自負(fù)責(zé)的 slot
Redis 的每個(gè) master 節(jié)點(diǎn)維護(hù)一個(gè) 16384 位(2048bytes=2KB)的位序列,比如:序列的第 0 位是 1,就代表第一個(gè) slot 是它負(fù)責(zé)砂豌;序列的第 1 位是 0,代表第二個(gè) slot 不歸它負(fù)責(zé)
對(duì)象分布到 Redis 節(jié)點(diǎn)上時(shí),對(duì) key 用 CRC16 算法計(jì)算再%16384,得到一個(gè) slot 的值,數(shù)據(jù)落到負(fù)責(zé)這個(gè) slot 的 Redis 節(jié)點(diǎn)上
查看 key 屬于哪個(gè) slot:
redis> cluster keyslot qingshan
注意:key 與 slot 的關(guān)系是永遠(yuǎn)不會(huì)變的,會(huì)變的只有 slot 和 Redis 節(jié)點(diǎn)的關(guān)系
問題:怎么讓相關(guān)的數(shù)據(jù)落到同一個(gè)節(jié)點(diǎn)上?
比如有些 multikey 操作是不能跨節(jié)點(diǎn)的,如果要讓某些數(shù)據(jù)分布到一個(gè)節(jié)點(diǎn)上,例如用戶 2673 的基本信息和金融信息,怎么辦?
在 key 里面加入{hashtag}即可.Redis 在計(jì)算槽編號(hào)的時(shí)候只會(huì)獲取{}之間的字符串進(jìn)行槽編號(hào)計(jì)算,這樣由于上面兩個(gè)不同的鍵,{}里面的字符串是相同的,因此他們可以被計(jì)算出相同的槽
user{2673}base=…
user{2673}fin=…
127.0.0.1:7293> set a{qs}a 1
OK
127.0.0.1:7293> set a{qs}b 1
OK
127.0.0.1:7293> set a{qs}c 1
OK
127.0.0.1:7293> set a{qs}d 1
OK
127.0.0.1:7293> set a{qs}e 1
OK
問題:客戶端連接到哪一臺(tái)服務(wù)器?訪問的數(shù)據(jù)不在當(dāng)前節(jié)點(diǎn)上,怎么辦?
客戶端重定向
比如在 7291 端口的 Redis 的 redis-cli 客戶端操作:
127.0.0.1:7291> set qs 1
(error)MOVED 13724 127.0.0.1:7293
服務(wù)端返回 MOVED,也就是根據(jù) key 計(jì)算出來的 slot 不歸 7191 端口管理,而是歸 7293 端口管理,服務(wù)端返回 MOVED 告訴客戶端去 7293 端口操作
這個(gè)時(shí)候更換端口,用 redis-cli–p7293 操作,才會(huì)返回 OK.或者用./redis-cli-c-pport 的命令(c 代表 cluster).這樣客戶端需要連接兩次
Jedis 等客戶端會(huì)在本地維護(hù)一份 slot——node 的映射關(guān)系,大部分時(shí)候不需要重定向,所以叫做 smartjedis(需要客戶端支持)
問題:新增或下線了 Master 節(jié)點(diǎn),數(shù)據(jù)怎么遷移(重新分配)?
數(shù)據(jù)遷移
因?yàn)?key 和 slot 的關(guān)系是永遠(yuǎn)不會(huì)變的,當(dāng)新增了節(jié)點(diǎn)的時(shí)候,需要把原有的 slot 分配給新的節(jié)點(diǎn)負(fù)責(zé),并且把相關(guān)的數(shù)據(jù)遷移過來
添加新節(jié)點(diǎn)(新增一個(gè)7297):
redis-cli --cluster add-node 127.0.0.1:7291 127.0.0.1:7297
新增的節(jié)點(diǎn)沒有哈希槽,不能分布數(shù)據(jù),在原來的任意一個(gè)節(jié)點(diǎn)上執(zhí)行:
redis-cli --cluster reshard 127.0.0.1:7291
輸入需要分配的哈希槽的數(shù)量(比如 500),和哈希槽的來源節(jié)點(diǎn)(可以輸入 all 或者 id)
問題:只有主節(jié)點(diǎn)可以寫,一個(gè)主節(jié)點(diǎn)掛了,從節(jié)點(diǎn)怎么變成主節(jié)點(diǎn)?
高可用和主從切換原理
當(dāng) slave 發(fā)現(xiàn)自己的 master 變?yōu)?FAIL 狀態(tài)時(shí),便嘗試進(jìn)行 Failover,以期成為新的 master.由于掛掉的 master 可能會(huì)有多個(gè) slave,從而存在多個(gè) slave 競(jìng)爭(zhēng)成為 master 節(jié)點(diǎn)的過程,其過程如下:
- slave 發(fā)現(xiàn)自己的 master 變?yōu)?FAIL
- 將自己記錄的集群 currentEpoch 加 1,并廣播 FAILOVER_AUTH_REQUEST 信息
- 其他節(jié)點(diǎn)收到該信息,只有 master 響應(yīng),判斷請(qǐng)求者的合法性,并發(fā)送 FAILOVER_AUTH_ACK,對(duì)每一個(gè) epoch 只發(fā)送一次 ack
- 嘗試 failover 的 slave 收集 FAILOVER_AUTH_ACK
- 超過半數(shù)后變成新 Master
- 廣播 Pong 通知其他集群節(jié)點(diǎn)
Redis Cluster 既能夠?qū)崿F(xiàn)主從的角色分配,又能夠?qū)崿F(xiàn)主從切換,相當(dāng)于集成了 Replication 和 Sentinal 的功能
總結(jié):
優(yōu)勢(shì)
- 無中心架構(gòu)
- 數(shù)據(jù)按照 slot 存儲(chǔ)分布在多個(gè)節(jié)點(diǎn),節(jié)點(diǎn)間數(shù)據(jù)共享,可動(dòng)態(tài)調(diào)整數(shù)據(jù)分布
- 可擴(kuò)展性,可線性擴(kuò)展到1000個(gè)節(jié)點(diǎn)(官方推薦不超過1000個(gè)),節(jié)點(diǎn)可動(dòng)態(tài)添加或刪除
- 高可用性,部分節(jié)點(diǎn)不可用時(shí),集群仍可用.通過增加 Slave 做 standby 數(shù)據(jù)副本,能夠?qū)崿F(xiàn)故障自動(dòng) failover,節(jié)點(diǎn)之間通過 gossip 協(xié)議交換狀態(tài)信息,用投票機(jī)制完成 Slave 到 Master 的角色提升
- 降低運(yùn)維成本,提高系統(tǒng)的擴(kuò)展性和可用性
不足
- Client 實(shí)現(xiàn)復(fù)雜,驅(qū)動(dòng)要求實(shí)現(xiàn) Smart Client,緩存 slot smapping 信息并及時(shí)更新,提高了開發(fā)難度,客戶端的不成熟影響業(yè)務(wù)的穩(wěn)定性
- 節(jié)點(diǎn)會(huì)因?yàn)槟承┰虬l(fā)生阻塞(阻塞時(shí)間大于 clutser-node-timeout),被判斷下線,這種 failover 是沒有必要的
- 數(shù)據(jù)通過異步復(fù)制,不保證數(shù)據(jù)的強(qiáng)一致性
- 多個(gè)業(yè)務(wù)使用同一套集群時(shí),無法根據(jù)統(tǒng)計(jì)區(qū)分冷熱數(shù)據(jù),資源隔離性較差,容易出現(xiàn)相互影響的情況