Note:本文实际绑定的版本是branch5.0(2018-7-25)。
先过一遍cluster的原理和使用方法,再来看代码。
cluster设计
slots
#define CLUSTER_SLOTS 16384
redis集群用了槽的概念,总共限定16384个slots。
每个kv的key,用CRC16(hash)然后取模16384,得到槽索引值,就知道对应哪个槽了。
另外还有个节点的概念,节点是指redis集群里单个redis服务器。
节点数量上限是16384,即一个节点一个槽,下限是1。
节点和槽的对应关系:
这个架构下的2个关键点:
- 添加删除节点,槽需要在节点之间移动
- 槽的移动过程中,服务依然可用
单节点的问题
因为不同节点存的数据是不一样的,所以单节点故障就会导致那部分数据不可用,实际上,这会导致整个redis集群不可用,因为要保证一致性。
解决方案是单节点要实现主从复制,例如一个主节点可以有一个从节点,这样节点数量会翻一倍。
集群的主从一致性问题
redis并不能保证强一致。这是因为使用了异步复制的设计:
- 客户端向某主节点调用会触发write的指令,例如hmset
- 该主节点立即执行并立即回复客户端执行结果
- 主节点将写操作复制给它的所有从节点
如果2和3调换,就是强一致,但是就会损失可用性(C和A不能同时达到)。
本机搭建一个集群教程
- 随便创建一个文件夹如test
- 在test里面创建6个文件夹:7000、7001、7002、7003、7004、7005
- 复制redis的redis-server和redis-cli到test文件夹
- 每个700x子文件夹放一个redis.conf,并启动: ../redis-server ./redis.conf
- 在test文件夹执行:./redis-cli --cluster create 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 --cluster-replicas 1
redis.conf:
port 7002
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes
执行了create指令后,会输出:
>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 127.0.0.1:7003 to 127.0.0.1:7000
Adding replica 127.0.0.1:7004 to 127.0.0.1:7001
Adding replica 127.0.0.1:7005 to 127.0.0.1:7002
>>> Trying to optimize slaves allocation for anti-affinity
[WARNING] Some slaves are in the same host as their master
M: 10921696574cc509be40ae48d704ef6ca67b4171 127.0.0.1:7000
slots:[0-5460] (5461 slots) master
M: c1f776cd30527cffb9046e01ca30031e1d5bc578 127.0.0.1:7001
slots:[5461-10922] (5462 slots) master
M: 77001d974c1f5f6db0256622b276293ec36359fb 127.0.0.1:7002
slots:[10923-16383] (5461 slots) master
S: 235238c0774c50721e8f8f0fad6ca8efb13a6f19 127.0.0.1:7003
replicates 10921696574cc509be40ae48d704ef6ca67b4171
S: 248661b074edd363e91d151391577c4ac0c43bdf 127.0.0.1:7004
replicates c1f776cd30527cffb9046e01ca30031e1d5bc578
S: 61199063df69d57a096a16ab2c97138f59aeb36d 127.0.0.1:7005
replicates 77001d974c1f5f6db0256622b276293ec36359fb
Can I set the above configuration? (type 'yes' to accept):
槽的分配都显示出来了:
- 主节点0,对应槽[0, 5460]
- 主节点1,对应槽[5461, 10922]
- 主节点2,对应槽[10923, 16383]
主节点是端口分别为7000、7001、7002的节点;7003、7004、7005是相应的从节点。
这里还有个细节:
>>> Trying to optimize slaves allocation for anti-affinity
[WARNING] Some slaves are in the same host as their master
这2行表示redis分析出了这些节点之间组织得并不好,从节点和主节点在同一个主机上。当然现在是测试,忽略它。
键入yes后:
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
就会把配置告诉所有节点,并且让节点建立起相互通讯,完成集群初始化。
看下各个节点的输出信息
7000:
9047:M 28 Jul 13:07:50.634 # configEpoch set to 1 via CLUSTER SET-CONFIG-EPOCH
69047:M 28 Jul 13:07:50.679 # IP address for this node updated to 127.0.0.1
69047:M 28 Jul 13:07:55.360 * Slave 127.0.0.1:7003 asks for synchronization
69047:M 28 Jul 13:07:55.360 * Partial resynchronization not accepted: Replication ID mismatch (Slave asked for '33f68b0a5ab33163019bb1897036d3b7dd1ac982', my replication IDs are '36b868ab0e64403a1fae3b9257c799b4cd3a3808' and '0000000000000000000000000000000000000000')
69047:M 28 Jul 13:07:55.360 * Starting BGSAVE for SYNC with target: disk
69047:M 28 Jul 13:07:55.361 * Background saving started by pid 69655
69655:C 28 Jul 13:07:55.364 * DB saved on disk
69047:M 28 Jul 13:07:55.426 * Background saving terminated with success
69047:M 28 Jul 13:07:55.427 * Synchronization with slave 127.0.0.1:7003 succeeded
69047:M 28 Jul 13:07:55.633 # Cluster state changed: ok
- 第三行显示从节点7003开始请求同步数据
- 第四行显示不能执行部分重同步,因为复制ID不匹配,这是因为这是主从之间的第一次同步
- 第五行,主节点7000启动了BGSAVE,这是为了把数据同步到硬盘
- 第六行,提示bg存盘进程启动
- 第七行,提示数据库已经保存到硬盘上
- 第八行,提示bg存盘进程成功结束并销毁
- 第九行,提示和slave的同步也完成了
再看下从节点的7003的输出:
69054:M 28 Jul 13:07:50.636 # configEpoch set to 4 via CLUSTER SET-CONFIG-EPOCH
69054:M 28 Jul 13:07:50.716 # IP address for this node updated to 127.0.0.1
69054:S 28 Jul 13:07:54.662 * Before turning into a slave, using my master parameters to synthesize a cached master: I may be able to synchronize with the new master with just a partial transfer.
69054:S 28 Jul 13:07:54.662 # Cluster state changed: ok
69054:S 28 Jul 13:07:55.358 * Connecting to MASTER 127.0.0.1:7000
69054:S 28 Jul 13:07:55.359 * MASTER <-> SLAVE sync started
69054:S 28 Jul 13:07:55.359 * Non blocking connect for SYNC fired the event.
69054:S 28 Jul 13:07:55.359 * Master replied to PING, replication can continue...
69054:S 28 Jul 13:07:55.360 * Trying a partial resynchronization (request 33f68b0a5ab33163019bb1897036d3b7dd1ac982:1).
69054:S 28 Jul 13:07:55.361 * Full resync from master: ab929efaa31dd35b6b4a83c7c6c7dd6c75ce157f:0
69054:S 28 Jul 13:07:55.361 * Discarding previously cached master state.
69054:S 28 Jul 13:07:55.427 * MASTER <-> SLAVE sync: receiving 177 bytes from master
69054:S 28 Jul 13:07:55.427 * MASTER <-> SLAVE sync: Flushing old data
69054:S 28 Jul 13:07:55.427 * MASTER <-> SLAVE sync: Loading DB in memory
69054:S 28 Jul 13:07:55.427 * MASTER <-> SLAVE sync: Finished with success
69054:S 28 Jul 13:07:55.428 * Background append only file rewriting started by pid 69657
69054:S 28 Jul 13:07:55.453 * AOF rewrite child asks to stop sending diffs.
69657:C 28 Jul 13:07:55.453 * Parent agreed to stop sending diffs. Finalizing AOF...
69657:C 28 Jul 13:07:55.453 * Concatenating 0.00 MB of AOF diff received from parent.
69657:C 28 Jul 13:07:55.453 * SYNC append only file rewrite performed
69054:S 28 Jul 13:07:55.459 * Background AOF rewrite terminated with success
69054:S 28 Jul 13:07:55.459 * Residual parent diff successfully flushed to the rewritten AOF (0.00 MB)
69054:S 28 Jul 13:07:55.460 * Background AOF rewrite finished successfully
- 从节点因为不能做部分重同步,所以变成完整同步,并清除了旧数据,并载入收到的完整DB数据
- 从节点还做了AOF文件的处理
测试
- 启动一个客户端连接到7000节点,并发送一个set指令:
/redis-cli -c -p 7000
127.0.0.1:7000> set foo bar
-> Redirected to slot [12182] located at 127.0.0.1:7002
OK
127.0.0.1:7002>
提示这个set指令被重定向到槽12182,即7002节点,根据前面的配置信息,7002是一个主节点。指令返回后,发现终端变成127.0.0.1:7002> ,说明之后的指令都是发往7002主机(也被重定向了)。
- 再发一个set指令:
127.0.0.1:7002> set hello world
-> Redirected to slot [866] located at 127.0.0.1:7000
OK
127.0.0.1:7000>
现在又回到了7000,hello对应的槽是866。
- 测试读指令,发送一个get:
127.0.0.1:7000> get foo
-> Redirected to slot [12182] located at 127.0.0.1:7002
"bar"
127.0.0.1:7002>
因为foo是在7002上的,所以又回到了7002,并拿到了刚才写入的"bar"。
- 另外开一个redis-cli来测试下:
./redis-cli -c -p 7001
127.0.0.1:7001> get hello
-> Redirected to slot [866] located at 127.0.0.1:7000
"world"
127.0.0.1:7000>
这次连的是另外一台主服务器,被重定向到7000,没什么问题。
- 测试从节点:
./redis-cli -c -p 7005
127.0.0.1:7005> get hello
-> Redirected to slot [866] located at 127.0.0.1:7000
"world"
127.0.0.1:7000>
和连接主节点7001的一样,也是重定向到7000,没什么问题。
- 再测试下在从节点执行del指令:
./redis-cli -c -p 7005
127.0.0.1:7005> del hello
-> Redirected to slot [866] located at 127.0.0.1:7000
(integer) 1
127.0.0.1:7000>
和预期的一样,都是先算出槽号并找出对应的目标节点,重定向到目标节点上执行。
sentinel的基础:raft算法
脱离sentinel,简单地介绍下raft:
节点状态
有且只有三种,任一时刻处于其中之一:
- Follower
- Candidate
- Leader
初始状态为follower。
从follower变成candidate
如果follower节点一段时间内(election timeout,150ms到300ms)都没有收到leader节点的心跳包(heartbeat timeout,不带数据的AppendEntry协议),那么自动把自己升级成candidate节点。
candidate节点做的第一件事是,向其他和自己相连的节点广播一个叫RequestVote的协议,收到该协议的节点会自己做决策,决定是否给这个candidate节点投票,同一个term只能投一次,投票后会重置自己的timer。
Leader election
接着,如果一个candidate节点获得了大多数节点的投票(majority of nodes,超过二分之一),那么这个candidate节点升级成leader节点。
leader节点的责任是:所有对raft节点集群的写操作,都要交给leader节点处理。
log entry和log replication
每条写操作被称为log entry。
log entry刚被添加到(leader节点的)log列表时,状态是uncommited。意味着这个写操作未产生实际作用。
为了让log entry变成commited状态,leader节点会向其他节点广播AppendEntry协议,然后等待它们的写完成回复,但只需要等其中的大多数节点(majority of nodes),而不是所有节点。
当确实收到大多数节点的写完成回复时,此时leader节点会把这个log entry改为commited状态,并应用log entry,改变节点的值(数据库状态变更)。还有就是回复客户端。
leader节点还广播告诉其他节点,“这个log entry已经commited了,你们也可以commit它了”。
其他节点都commit这个log entry后,此时集群就达成了一致状态。
term
只要leader存在,leader一直能够给其他节点发心跳包,那么term是不会增加的。
换leader才会导致term增加。
同时获得相同数量投票的问题
假设有4个节点,其中有2个节点A、B同时变成候选人并同时发出了3个RequestVote:
- A收到B的RequestVote,B收到A的RequestVote
- C收到A的RequestVote,D收到B的RequestVote
- B投票给A,A投票给B,A有1票(1/4),B有1票(1/4)
- C投票给A,D投票给B,A有2票(2/4),B有2票(2/4)
- 已经没人可以投票了,因为A和B都不能获得大多数节点的投票(起码要3票),于是重新开了一轮选举
- 此时只要增加时间随机性,就可以使得有一个节点先获得大部分选票,称为leader,结束”死锁状态“
sentinel
sentinel是用来自动化管理的:
- 监控集群的运行状态
- 避免人工处理节点故障
- 主节点故障时,实现新的主节点选举
- 通知用户发生故障转移,让用户可以去处理故障节点
- 如果用了sentinel,那么客户端需要先连接sentinel才能知道master节点位置
当主节点故障时,要等待一段时间,确认主节点不会恢复时,才开始从节点的选举(故障转移)。
从节点升级到主节点后,如果旧的主节点恢复,那么并不会恢复成主节点,而只是个从节点。
raft算法中的投票方,对应的是sentinel节点集群,而不是redis集群。但master节点、follower节点、log复制,却是redis节点集群之间的。个中区别只能看redis源码来了解。
资料
写作不易,您的支持是我写作的动力!