三主三从
集群概念
Redis 的哨兵模式,提高了系统的可用性,但是正在用来存储数据的还是 master 和 slave 节点,所有的数据都需要存储在单个 master 和 salve 节点中。
如果数据量很大,接近超出了 master / slave 所在机器的物理内存,就可能出现严重问题了。
虽然硬件价格在不断降低,一些中大厂的服务器内存已经可以达到 TB 级别了,但是1TB 在当前这个“大数据”时代,俨然不算什么,有的时候我们确实需要更大的内存空间来保存更多的数据。
Redis 的集群就是在上述的思路之下,引入多组 master / slave,每一组 master / slave 存储数据全集的一部分,从而构成一个更大的整体,称为 Redis 集群(Cluster)。
假定整个数据全集是1TB,引入三组 master / slave来存储。那么每⼀组机器只需要存储整个数据全集的 1/3 即可。
- Master1 和 Slave11 和 Slave12 保存的是同样的数据占总数据的 1/3
- Master2 和 Slave21 和 Slave22 保存的是同样的数据占总数据的 1/3
- Master3 和 Slave31 和 Slave32 保存的是同样的数据占总数据的 1/3
这三组机器存储的数据都是不同的
每个 Slave 都是对应 Master 的备份(当 Master 挂了,对应的 Slave 会补位成 Master)。
每个红框部分都可以称为是一个分片(Sharding)。
如果全量数据进一步增加,只要再增加更多的分片,即可解决。
可能有的人认为,数据量多了,使用硬盘来保存不就行了?不要忘了硬盘只是存储多了,但是访问速度是比内存慢很多的。但是事实上,还是存在很多的应用场景,既希望存储较多的数据,又希望有非常高的读写速度。
数据分片算法
Redis cluster 的核心思路是用多组机器来存数据的每个部分,那么接下来的核心问题就是,给定一个数据(一个具体的 key)那么这个数据应该存储在哪个分片上?读取的时候又应该去哪个分片读取?
围绕这个问题,业界有三种比较主流的实现方式
哈希求余
设有N个分片,使用 [0,N-1] 这样序号进行编号
针对某个给定的 key,先计算 hash 值,再把得到的结果%N,得到的结果即为分片编号。
例如,N为3。给定 key 为 hello,对 hello 计算 hash 值(比如使用 md5 算法),得到的结果为bc4b2a76b9719d91,再把这个结果 %3,结果为 0,那么就把 hello 这个 key 放到 0 号分片上。当然,实际工作中涉及到的系统,计算 hash 的方式不一定是 md5,但是思想是一致的。
后续如果要取某个 key 的记录,也是针对 key 计算 hash,再对 N 求余,就可以找到对应的分片编号
优缺点:
优点:简单高效,数据分配均匀。
缺点:一旦需要进行扩容,N 改变了,原有的映射规则被破坏,就需要让节点之间的数据相互传输,重新排列,以满足新的映射规则此时需要搬运的数据量是比较多的,开销较大
一致性哈希算法
为了降低上述的搬运开销,能够更高效扩容,业界提出了“一致性哈希算法”。key 映射到分片序号的过程不再是简单求余了,而是改成以下过程:
第一步:把 0->2^32-1 这个数据空间,映射到⼀个圆环上。数据按照顺时针方向增长。
第二步:假设当前存在三个分片,就把分片放到圆环的某个位置上。
第三步:假定有⼀个 key,计算得到 hash 值 H,那么这个key映射到哪个分片呢?规则很简单,就是从 H 所在位置,顺时针往下找,找到的第⼀个分片,即为该 key 所从属的分片。
这就相当于,N个分片的位置,把整个圆环分成了 N 个管辖区间 key 的 hash 值落在某个区间内,就归对应区间管理。
在这个情况下,如果扩容一个分片,如何处理呢?
原有分片在环上的位置不动,只要在环上新安排一个分片位置即可。
优缺点:
优点:大大降低了扩容时数据搬运的规模,提高了扩容操作的效率。
缺点:数据分配不均匀(有的多有的少,数据倾斜)
hash slots算法(Redis 使用)
为了解决上述问题(搬运成本高和数据分配不均),Redis cluster 引入了哈希槽(hash slots)算法。
hash_slot = crc16(key) % 16384 #crc16也是⼀种hash算法,16384其实是16*1024,也就是2^14
相当于是把整个哈希值,映射到 16384 个槽位上,也就是 [0,16383]。
然后再把这些槽位比较均匀的分配给每个分片,每个分片的节点都需要记录自己持有哪些槽位。
假设当前有三个分片,一种可能的分配方式:
- 0号分片:[0,5461],共 5462 个槽位
- 1号分片:[5462,10923],共 5462 个槽位
- 2号分片:[10924,16383],共5460个槽位
这里的分片规则是很灵活的,每个分片持有的槽位也不一定连续。
每个分片的节点使用位图来表示自己持有哪些槽位,对于 16384 个槽位来说,需要 2048 个字节(2KB)大小的内存空间表示
如果需要进行扩容比如新增一个3 号分片就可以针对原有的槽位进行重新分配。
比如可以把之前每个分片持有的槽位,各拿出一点,分给新分片
一种可能的分配方式:
- 0号分片:[0,40951],共 4096 个槽位
- 1号分片:[5462,9557],共 4096 个槽位
- 2号分片:[10924,15019],共 4096个槽位
- 3号分片:[4096,5461] +[9558,10923]+[15019,16383],共 4096 个槽位
我们在实际使用 Redis 集群分片的时候,不需要手动指定哪些槽位分配给某个分片,只需要告诉某个分片应该持有多少个槽位即可,Redis 会自动完成后续的槽位分配。以及对应的 key 搬运的工作
此处还有两个问题:
问题一:Redis集群是最多有16384个分片吗?
并非如此,如果一个分片只有一个槽位,这对于集群的数据均匀其实是难以保证的。
实际上 Redis 的作者建议集群分片数不应该超过1000。
而且,16000 这么大规模的集群,本身的可用性也是一个大问题,一个系统越复杂,出现故障的概率是越高的。
问题二:为什么是16384个槽位?
- 节点之间通过心跳包通信,心跳包中包含了该节点持有哪些 slots,这个是使用位图这样的数据结构表示的,表示16384(16k) 个 slots,需要的位图大小是2KB。如果给定的 slots 数更多了,比如 65536 个了,此时就需要消耗更多的空间,8 KB 位图表示了 8KB,对于内存来说不算什么,但是在频繁的网络心跳包中,还是一个不小的开销的。
- 另一方面,Redis 集群一般不建议超过 1000 个分片所以 16k 对于最大 1000个分片来说是足够用的,同时也会使对应的槽位配置位图体积不至于很大。
集群搭建(基于 docker)
接下来基于 docker,搭建一个集群每个节点都是一个容器
此处我们先创建出 11 个 redis 节点,其中前 9 个用来演示集群的搭建,后两个用来演示集群扩容
第一步:生成配置文件
[root@localhost redis]# cat conf/redis.conf
port 6379
bind 0.0.0.0
protected-mode no
appendonly yes
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
cluster-announce-ip 172.16.10.42
cluster-announce-port 6379
cluster-announce-bus-port 16379
[root@localhost redis]# cat conf/generate.sh
for port in $(seq 1 3); \
do \
mkdir -p redis${port}/
touch redis${port}/redis.conf
cat << EOF > redis${port}/redis.conf
port 6379
bind 0.0.0.0
protected-mode no
appendonly yes
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
cluster-announce-ip 172.16.10.4${port}
cluster-announce-port 6379
cluster-announce-bus-port 16379
EOF
done
for port in $(seq 4 6); \
do \
mkdir -p redis${port}/
touch redis${port}/redis.conf
cat << EOF > redis${port}/redis.conf
port 6379
bind 0.0.0.0
protected-mode no
appendonly yes
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
cluster-announce-ip 172.16.10.4.1${port}
cluster-announce-port 6379
cluster-announce-bus-port 16379
EOF
done
- bind:指定Redis服务绑定的IP地址。在本例中,使用0.0.0.0表示服务将绑定到所有可用的IPv4地址上。
- daemonize:决定Redis是否以守护进程方式运行。当设置为yes时,Redis将在后台运行,不会向终端输出日志信息。
- protected-mode:控制Redis是否启用保护模式。当设置为no时,Redis将不会检查访问请求是否来自本地主机。
- port:指定Redis服务监听的端口号。在本例中,服务将监听6381端口。
- logfile:指定Redis日志文件的路径和文件名。在本例中,日志将写入/myredis/cluster/cluster6381.log文件。
- pidfile:指定Redis进程ID文件的路径和文件名。在本例中,文件名为cluster6381.pid。
- dir:指定Redis数据库文件和AOF文件所在的目录。在本例中,它们将存储在/myredis/cluster目录下。
- dbfilename:指定Redis数据库文件的文件名。在本例中,文件名为dump6381.rdb。
- appendonly:决定Redis是否启用AOF持久化。当设置为yes时,Redis将记录所有修改操作,并将其写入AOF文件中。
- appendfilename:指定AOF文件的文件名。在本例中,文件名为appendonly6381.aof。
- requirepass:设置Redis的访问密码。在本例中,密码为111111。
- masterauth:设置Redis主节点的访问密码。在本例中,主节点密码也为111111。
- cluster-enabled:决定Redis是否启用集群模式。当设置为yes时,Redis将启用集群模式。
- cluster-config-file:指定Redis集群配置文件的路径和文件名。在本例中,文件名为nodes-6381.conf。
- cluster-node-timeout:指定Redis集群节点之间的超时时间。在本例中,超时时间为5000毫秒
[root@localhost redis]#
[root@localhost redis]# pwd
/opt/redis
[root@localhost redis]# tree
.
├── conf
│ ├── generate.sh
│ └── redis.conf
└── data
├── appendonly.aof
├── dump.rdb
└── nodes.conf
2 directories, 5 files
三主三从 6台机器
启动服务器
[root@localhost redis]# docker run -it --rm --name redis1 -p 6379:6379 -p 16379:16379 -v /opt/redis/conf:/etc/redis/ -v /opt/redis/data:/data -d --sysctl net.core.somaxconn=1024 harbor.jettech.com/jettechtools/redis:6.2 redis-server /etc/redis/redis.conf
创建集群
root@f604b21ceae6:/# redis-cli --cluster create 172.16.10.41:6379 172.16.10.42:6379 172.16.10.43:6379 172.16.10.44:6379 172.16.10.45:6379 172.16.10.46:6379 --cluster-replicas 1
- cluster create 表示建立集群。后面填写每个节点的 ip 和地址
- cluster-replicas 1 表示每个主节点需要1个从节点备份
日志中会描述哪些是主节点,哪些从节点跟随哪个主节点。
root@e68581597b49:/data# redis-cli --cluster create 172.16.10.41:6379 172.16.10.42:6379 172.16.10.43:6379 172.1