Redis 分布式扩展之 Redis Cluster 方案
主从切换的过程中会丢失数据,因为只有一个 master,只能单点写,没有解决水平扩容的问题。而且每个节点都保存了所有数据,一个是内存的占用率较高,另外就是如果进行数据恢复时,非常慢。而且数据量过大对数据 IO 操作的性能也会有影响。
所以我们同样也有对 Redis 数据分片的需求,所谓分片就是把一份大数据拆分成多份小数据,在 3.0 之前,我们只能通过构建多个 redis 主从节点集群,把不同业务数据拆分到不冉的集群中,这种方式在业务层需要有大量的代码来完成数据分片、路由等工作,导致维护成本高、增加、移除节点比较繁琐。
Redis3.0 之后引入了 Redis Cluster 集群方案,它用来解决分布式扩展的需求,同时也实现了高可用机制。
1. Redis Cluster 架构
一个 Redis Cluster 由多个 Redis 节点构成,不同节点组服务的数据没有交集,也就是每个一节点组对应数据 sharding 的一个分片。
节点组内部分为主备两类节点,对应 master 和 slave 节点。两者数据准实时一致,通过异步化的主备复制机制来保证。
一个节点组有且只有一个 master 节点,同时可以有 0 到多个 slave 节点,在这个节点组中只有 master 节点对用户提供写服务,读服务可以由 master 或者 slave 提供。如下图,包含三个 master 节点以及三个 master 对应的 slave 节点,一般一组集群至少要 6 个节点才能保证完整的高可用。
其中三个 master 会分配不同的 slot(表示数据分片区间),当 master 出现故障时,slave 会自动选举成为 master 顶替主节点继续提供服务。
1.1 关于 gossip 协议
在上图描述的架构中,其他的点都好理解,就是关于 gossip 协议是干嘛的,需要单独说明一下。
在整个 redis cluster 架构中,如果出现以下情况
- 新加入节点
- slot 迁移
- 节点宕机
- slave 选举成为 master
我们希望这些变化能够让整个集群中的每个节点都能够尽快发现,传播到整个集群并且集群中所有节点达成一致,那么各个节点之间就需要相互连通并且携带相关状态数据进行传播,
按照正常的逻辑是采用广播的方式想集群中的所有节点发送消息,优点是集群中的数据同步较快,但是每条消息都需要发送给所有节点,对 CPU 和带宽的消耗过大,所以这里采用了 gossip 协议。
Gossip protocol 也叫 Epidemic Protocol (流行病协议),别名很多比如:“流言算法”、“疫情传播算法” 等。
它的特点是,在节点数量有限的网络中,每个节点都会 “随机”(不是真正随机,而是根据规则选择通信节点)与部分节点通信,经过一番杂乱无章的通信后,每个节点的状态在一定时间内会达成一致,如下图所示。
假设我们提前设置如下规则:
1、Gossip 是周期性的散播消息,把周期限定为 1 秒
2、被感染节点随机选择 k 个邻接节点(fan-out)散播消息,这里把 fan-out 设置为 3,每次最多往 3 个节点散播。
3、每次散播消息都选择尚未发送过的节点进行散播
4、收到消息的节点不再往发送节点散播,比如 A -> B,那么 B 进行散播的时候,不再发给 A。
这里一共有 16 个节点,节点 1 为初始被感染节点,通过 Gossip 过程,最终所有节点都被感染:
1.2 gossip 协议消息
gossip 协议包含多种消息,包括 ping,pong,meet,fail 等等。
ping:每个节点都会频繁给其他节点发送 ping,其中包含自己的状态还有自己维护的集群元数据,互相通过 ping 交换元数据;
pong: 返回 ping 和 meet,包含自己的状态和其他信息,也可以用于信息广播和更新;
fail: 某个节点判断另一个节点 fail 之后,就发送 fail 给其他节点,通知其他节点,指定的节点宕机了。
meet:某个节点发送 meet 给新加入的节点,让新节点加入集群中,然后新节点就会开始与其他节点进行通信,不需要发送形成网络的所需的所有 CLUSTER MEET 命令。发送 CLUSTER MEET 消息以便每个节点能够达到其他每个节点只需通过一条已知的节点链就够了。由于在心跳包中会交换 gossip 信息,将会创建节点间缺失的链接。
1.3 gossip 的优缺点
优点: gossip 协议的优点在于元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新有一定的延时,降低了压力; 去中心化、可扩展、容错、一致性收敛、简单。 由于不能保证某个时刻所有节点都收到消息,但是理论上最终所有节点都会收到消息,因此它是一个最终一致性协议。
缺点: 元数据更新有延时可能导致集群的一些操作会有一些滞后。 消息的延迟 , 消息冗余 。
1.4 Redis Cluster 集群搭建
集群至少需要 6 个节点(3 主 3 从模式),每一个节点可以搭建在同一台机器上,也可以搭建在不同的服务器上。
- 192.168.183.130 7000 、 7001
- 192.168.183.131 7002 、 7003
- 192.168.183.132 7004 、 7005
1.5 分别启动 6 个节点。
-
在 redis 安装目录下,分别创建以下目录,这些目录必须要提前创建好,redis 启动时不会主动创建这些目录。
mkdir -p /usr/local/redis/run mkdir -p /usr/local/redis/logs mkdir -p /usr/local/redis/data/7000、7001 mkdir -p /usr/local/redis/conf mkdir -p /usr/local/redis/redis-cluster
-
拷贝一份 redis.conf 到 redis-cluster 目录下,由于只有三台机器,所以每个机器上需要运行两个 redis-server,因此需要修改 redis.conf 文件的名字来做区分,redis_7000.conf。并且修改该文件的一下内容。
pidfile "/usr/local/redis/run/redis_7000.pid" #pid存储目录 logfile "/usr/local/redis/logs/redis_7000.log" #日志存储目录 dir "/usr/local/redis/data/7000" #数据存储目录,目录要提前创建好 cluster-enabled yes #开启集群 cluster-config-file nodes-7000.conf #集群节点配置文件,这个文件是不能手动编辑的。确保每一个集群节点的配置文件不同 cluster-node-timeout 15000 #集群节点的超时时间,单位:ms,超时后集群会认为该节点失败
-
每个节点需要启动两个 redis-server,所以对配置文件做一份拷贝,然后修改以下配置
pidfile "/usr/local/redis/run/redis_7001.pid" #pid存储目录 logfile "/usr/local/redis/logs/redis_7001.log" #日志存储目录 dir "/usr/local/redis/data/7001" #数据存储目录,目录要提前创建好 cluster-enabled yes #开启集群 cluster-config-file nodes-7001.conf #集群节点配置文件,这个文件是不能手动编辑的。确保每一个集群节点的配置文件不同 cluster-node-timeout 15000 #集群节点的超时时间,单位:ms,超时后集群会认为该节点失败
-
创建两个脚本用来进行统一的服务运行
- cluster-start.sh
./redis-server ../conf/redis_7000.conf ./redis-server ../conf/redis_7001.conf
- cluster-shutdown.sh
pgrep redis-server | xargs -exec kill -9
- 通过下面命令让上述脚本拥有执行权限
chmod +x cluster-*.sh
-
其他两个节点重复上述的过程,完成 6 个节点的启动。
如果遇到相关问题大部分原因是由于配置文件错误,重新编辑配置文件;如果是复制的主从复制环境的配置环境记得去除主从复制的相关配置;
记得修改对应的端口号;
1.6 配置 redis 集群
启动完这 6 台服务器后,需要通过下面的操作来配置集群节点。在 redis6.0 版本中,创建集群的方式为 redis-cli 方式直接创建,以下命令在任意一台服务器上执行即可
用以下命令创建集群,–cluster-replicas 1 参数表示希望每个主服务器都有一个从服务器,这里则代表 3 主 3 从,通过该方式创建的带有从节点的机器不能够自己手动指定主节点,redis 集群会尽量把主从服务器分配在不同机器上:
./redis-cli --cluster create 192.168.183.131:7002 192.168.183.131:7003 192.168.183.130:7001 192.168.183.130:7000 192.168.183.132:7004 192.168.183.132:7005 --cluster-replicas 1
如果不加–cluster-replicas 1则会创建6个主节点
执行上述命令后,会得到以下执行结果,
Performing hash slots allocation on 6 nodes…
Master[0] -> Slots 0 – 5460
Master[1] -> Slots 5461 – 10922
Master[2] -> Slots 10923 – 16383
Adding replica 192.168.183.130:7000 to 192.168.183.131:7002
Adding replica 192.168.183.131:7003 to 192.168.183.130:7001
Adding replica 192.168.183.132:7005 to 192.168.183.132:7004
Trying to optimize slaves allocation for anti-affinity
[OK] Perfect anti-affinity obtained!
M: 97dda24ba8316ab5f024e8c7f63f811f99b8c558 192.168.183.131:7002
slots:[0-5460] (5461 slots) master
S: 891fc60e90e834b09361f35ad625cd195b31c666 192.168.183.131:7003
replicates c24500833eeeb3b119d58c5c7b52208f2cc066c5
M: c24500833eeeb3b119d58c5c7b52208f2cc066c5 192.168.183.130:7001
slots:[5461-10922] (5462 slots) master
S: 03e4710a488b33b0e9d2bb645a247580eafb6b4c 192.168.183.130:7000
replicates f7aa50102081d901bd754e72605746631f7fa3f8
M: f7aa50102081d901bd754e72605746631f7fa3f8 192.168.183.132:7004
slots:[10923-16383] (5461 slots) master
S: 7a834ae821e47c3803100784db2fe28f0ac6a9a2 192.168.183.132:7005
replicates 97dda24ba8316ab5f024e8c7f63f811f99b8c558
Can I set the above configuration? (type ‘yes’ to accept): yes
Nodes configuration updated
Assign a different config epoch to each node
Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join
…
Performing Cluster Check (using node 192.168.183.131:7002)
M: 97dda24ba8316ab5f024e8c7f63f811f99b8c558 192.168.183.131:7002
slots:[0-5460] (5461 slots) master
1 additional replica(s)
S: 891fc60e90e834b09361f35ad625cd195b31c666 192.168.183.131:7003
slots: (0 slots) slave
replicates c24500833eeeb3b119d58c5c7b52208f2cc066c5
M: f7aa50102081d901bd754e72605746631f7fa3f8 192.168.183.132:7004
slots:[10923-16383] (5461 slots) master
1 additional replica(s)
S: 7a834ae821e47c3803100784db2fe28f0ac6a9a2 192.168.183.132:7005
slots: (0 slots) slave
replicates 97dda24ba8316ab5f024e8c7f63f811f99b8c558
M: c24500833eeeb3b119d58c5c7b52208f2cc066c5 192.168.183.130:7001
slots:[5461-10922] (5462 slots) master
1 additional replica(s)
S: 03e4710a488b33b0e9d2bb645a247580eafb6b4c 192.168.183.130:7000
slots: (0 slots) slave
replicates f7aa50102081d901bd754e72605746631f7fa3f8
[OK] All nodes agree about slots configuration.
Check for open slots…
Check slots coverage…
[OK] All 16384 slots covered.
从上述结果中看到两个点:
- 预先分配三个节点的 slot 区间
- 自动选择合适的节点作为 master
1.7 移除集群
如果想要重新设置集群,首先通过之前的cluster-shutdown.sh停止服务,然后删除run和data中的数据,重新启动;随后重新执行配置命令接口。
1.8 查看集群状态等信息
- cluster info 查看集群状态信息
[root@localhost bin]# ./redis-cli -p 7000
127.0.0.1:7000> cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:6
cluster_my_epoch:5
cluster_stats_messages_ping_sent:397
cluster_stats_messages_pong_sent:398
cluster_stats_messages_meet_sent:4
cluster_stats_messages_sent:799
cluster_stats_messages_ping_received:397
cluster_stats_messages_pong_received:401
cluster_stats_messages_meet_received:1
cluster_stats_messages_received:799
- 查看集群节点信息
127.0.0.1:7000> cluster nodes
7a834ae821e47c3803100784db2fe28f0ac6a9a2 192.168.183.132:7005@17005 slave 97dda24ba8316ab5f024e8c7f63f811f99b8c558 0 1638079692000 6 connected
c24500833eeeb3b119d58c5c7b52208f2cc066c5 192.168.183.130:7001@17001 master - 0 1638079693576 3 connected 5461-10922
03e4710a488b33b0e9d2bb645a247580eafb6b4c 192.168.183.130:7000@17000 myself,slave f7aa50102081d901bd754e72605746631f7fa3f8 0 1638079694000 4 connected
891fc60e90e834b09361f35ad625cd195b31c666 192.168.183.131:7003@17003 slave c24500833eeeb3b119d58c5c7b52208f2cc066c5 0 1638079692000 3 connected
97dda24ba8316ab5f024e8c7f63f811f99b8c558 192.168.183.131:7002@17002 master - 0 1638079692560 1 connected 0-5460
f7aa50102081d901bd754e72605746631f7fa3f8 192.168.183.132:7004@17004 master - 0 1638079694591 5 connected 10923-16383
2. 数据分布
Redis Cluster 中,Sharding 采用 slot (槽) 的概念,一共分成 16384 个槽,这有点儿类似 pre sharding 思路。对于每个进入 Redis 的键值对,根据 key 进行散列,分配到这 16384 个 slot 中的某一个中。使用的 hash 算法也比较简单,就是 CRC16 后 16384 取模 [crc16(key)%16384]。
Redis 集群中的每个 node (节点) 负责分摊这 16384 个 slot 中的一部分,也就是说,每个 slot 都对应一个 node 负责处理。
如下图所示,假设现在我们是三个主节点分别是:A, B, C 三个节点,它们可以是一台机器上的三个端口,也可以是三台不同的服务器。那么,采用哈希槽 (hash slot) 的方式来分配 16384 个 slot 的话,它们三个节点分别承担的 slot 区间是:
- 节点 A 覆盖 0-5000;
- 节点 B 覆盖 5001-10000;
- 节点 C 覆盖 10001-16383
3. 客户端重定向
如上图所示,假设 k 这个 key 应该存储在 node3 上,而此时用户在 node1 或者 node2 上调用 set k v
指令,这个时候 redis cluster 怎么处理呢?
127.0.0.1:7000> set name 16300
(error) MOVED 5798 192.168.183.130:7001
服务端返回 MOVED,也就是根据 key 计算出来的 slot 不归当前节点管理,服务端返回 MOVED 告诉客户端去 7001端口操作。
这个时候更换端口,用 redis-cli –p 7001操作,才会返回 OK。或者用./redis-cli -c -p port 的命令。但是导致的问题是,客户端需要连接两次才能完成操作。
127.0.0.1:7000> set name 16300
(error) MOVED 5798 192.168.183.130:7001
127.0.0.1:7000>
[root@localhost bin]# ./redis-cli -p 7001
127.0.0.1:7001> set name 16300
OK
所以大部分的 redis 客户端都会在本地维护一份 slot 和 node 的对应关系,在执行指令之前先计算当前 key 应该存储的目标节点,然后再连接到目标节点进行数据操作。
在 redis 集群中提供了下面的命令来计算当前 key 应该属于哪个 slot
redis> cluster keyslot key1
4. 高可用主从切换原理
如果主节点没有从节点,那么当它发生故障时,集群就将处于不可用状态。
一旦某个 master 节点进入到 FAIL 状态,那么整个集群都会变成 FAIL 状态,同时触发 failover 机制,failover 的目的是从 slave 节点中选举出新的主节点,使得集群可以恢复正常,这个过程实现如下:
当 slave 发现自己的 master 变为 FAIL 状态时,便尝试进行 Failover,以期成为新的 master。由于挂掉的 master 可能会有多个 slave,从而存在多个 slave 竞争成为 master 节点的过程, 其过程如下:
- slave 发现自己的 master 变为 FAIL
- 将自己记录的集群 currentEpoch 加 1,并广播 FAILOVER_AUTH_REQUEST 信息
- 其他节点收到该信息,只有 master 响应,判断请求者的合法性,并发送 FAILOVER_AUTH_ACK,对每一个 epoch 只发送一次 ack
- 尝试 failover 的 slave 收集 master 返回的 FAILOVER_AUTH_ACK
- slave 收到超过半数 master 的 ack 后变成新 Master (这里解释了集群为什么至少需要三个主节点,如果只有两个,当其中一个挂了,只剩一个主节点是不能选举成功的)
- 广播 Pong 消息通知其他集群节点。
从节点并不是在主节点一进入 FAIL 状态就马上尝试发起选举,而是有一定延迟,一定的延迟确保我们等待 FAIL 状态在集群中传播,slave 如果立即尝试选举,其它 masters 或许尚未意识到 FAIL 状态,可能会拒绝投票。
延迟计算公式: DELAY = 500ms + random (0 ~ 500ms) + SLAVE_RANK * 1000ms
SLAVE_RANK 表示此 slave 已经从 master 复制数据的总量的 rank。Rank 越小代表已复制的数据越新。这种方式下,持有最新数据的 slave 将会首先发起选举
5. 常见问题分析
问题1:怎么让相关的数据落到同一个节点上?
比如有些 multi key 操作是不能跨节点的,例如用户 2673 的基本信息和金融信息?
在 key 里面加入 {hash tag} 即可。Redis 在计算槽编号的时候只会获取 {} 之间的字符串进行槽编号计算,这样由于上面两个不同的键,{} 里面的字符串是相同的,因此他们可以被计算出相同的槽
user{2673}base=…
user{2673}fin=…
操作步骤如下,下面这些 key 都会保存到同一个 node 中。
127.0.0.1:7001> set a{name} 1
OK
127.0.0.1:7001> set b{name} 2
OK
127.0.0.1:7001> set c{name} 3
OK
127.0.0.1:7001> keys *
1) "a{name}"
2) "name"
3) "c{name}"
4) "b{name}"
6. 总结
优势
- 无中心架构。
- 数据按照 slot 存储分布在多个节点,节点间数据共享,可动态调整数据分布。
- 可扩展性,可线性扩展到 1000 个节点(官方推荐不超过 1000 个),节点可动态添加或删除。
- 高可用性,部分节点不可用时,集群仍可用。通过增加 Slave 做 standby 数据副本,能够实现故障自动 failover,节点之间通过 gossip 协议交换状态信息,用投票机制完成 Slave 到 Master 的角色提升。
- 降低运维成本,提高系统的扩展性和可用性。
不足
- Client 实现复杂,驱动要求实现 Smart Client,缓存 slots mapping 信息并及时更新,提高了开发难度,客户端的不成熟影响业务的稳定性。
- 节点会因为某些原因发生阻塞(阻塞时间大于 clutser-node-timeout),被判断下线,这种 failover 是没有必要的。
- 数据通过异步复制,不保证数据的强一致性。
- 多个业务使用同一套集群时,无法根据统计区分冷热数据,资源隔离性较差,容易出现相互影响的情况。
- Slave 在集群中充当 “冷备”,不能缓解读压力,当然可以通过 SDK 的合理设计来提高 Slave 资源的利用率。
7. Redission 连接 cluster
修改 redisson.yml 文件,参考 springboot-redis-2 这个项目
PROPERTIES
clusterServersConfig:
nodeAddresses:
- "redis://192.168.183.130:7000"
- "redis://192.168.183.131:7002"
- "redis://192.168.183.132:7004"
codec: !<org.redisson.codec.JsonJacksonCodec> {}
注意,nodeAddresses 对应的节点都是 master。
Codis
Codis 是一个分布式 Redis 解决方案,对于上层的应用来说,连接到 Codis Proxy 和连接原生的 Redis Server 没有明显的区别 (不支持的命令列表), 上层应用可以像使用单机的 Redis 一样使用,Codis 底层会处理请求的转发,不停机的数据迁移等工作,所有后边的一切事情,对于前面的客户端来说是透明的,可以简单的认为后边连接的是一个内存无限大的 Redis 服务。
codis 的架构
如下图所示,表示 Codis 的整体架构图。
Codis Proxy: 客户端连接的 Redis 代理服务,实现了 Redis 协议。 除部分命令不支持以外 (不支持的命令列表),表现的和原生的 Redis 没有区别(就像 Twemproxy)。对于同一个业务集群而言,可以同时部署多个 codis-proxy 实例;不同 codis-proxy 之间由 codis-dashboard 保证状态同
codis-redis-group: 代表一个 redis 服务集群节点,一个 RedisGroup 里有一个 Master,和多个 Slave
**Zookeeper:**Codis 依赖 ZooKeeper 来存放数据路由表和 codis-proxy 节点的元信息,codis-config 发起的命令都会通过 ZooKeeper 同步到各个存活的 codis-proxy。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/16797.html