1 集群的意义
Redis集群不断发展,可实现在多台机器,部署多实例,每个实例存部分数据。 同时每个实例可以带上Redis从实例,保证若Redis主实例挂了,自动切换到redis从实例。
上古时代,很多用codis之类客户端支持集群。现在版本大家都用Redis cluster,即官方提供的集群模式,必须深入研究了。
从单机的一主多从复制架构到现在的分布式架构
主要有如下维度:
业务
追求更高QPS
数据量
Scale Up已经无法满足,超过了单机极限,考虑Scale Out分布式。
Redis内存淘汰策略,保证不可能超过master节点内存阈值。master节点数据和slave节点的数据保持一致。 一个redis即可横向扩容了。若要支撑更大数据缓存,就横向扩容更多master节点,每个master节点就能存放更多数据。单台服务器32G,30台即可达1TB。
网络流量
业务流量超过服务器网卡上限,考虑分布式分流
离线计算
需要中间环节缓冲等需求
2 meet
节点之间完成相互通信的基础,有一定的频率和规则。
CLUSTER MEET命令被用来连接不同的开启集群支持的 Redis 节点,以进入工作集群。
2.1 基本思想
每个节点默认互不信任,并且被认为是未知节点,以防系统管理错误或地址被修改,而不太可能将多个不同的集群节点混成一个集群。
因此,为了使给定节点能将另一个节点接收到组成 Redis Cluster 的节点列表中,这里只有两种方法:
- 系统管理员发送一个CLUSTER MEET命令强制一个节点会见另一个节点
- 一个已知节点发送一个保存在 gossip 部分的节点列表,包含着未知节点。如果接收的节点已经将发送节点信任为已知节点,它会处理 gossip 部分并且发送一个握手消息给未知的节点。
Redis Cluster 需要形成一个完整的网络(每个节点都连接着其他每个节点),但为创建一个集群,不需要发送形成网络所需的所有CLUSTER MEET命令。发送CLUSTER MEET消息以便每个节点能够到达其他每个节点只需通过一条已知的节点链就够了。由于在心跳包中会交换 gossip 信息,将会创建节点间缺失的链接。
所以,如果我们通过CLUSTER MEET链接节点 A 和 B ,并且 B 和 C 有链接,那么节点 A 和 C 会发现他们握手和创建链接的方法。
2.2 案例
假设某一集群有A、B、C、D四个节点,可以只发送以下一组命令给 A :
代码语言:javascript复制CLUSTER MEET B-ip B-port
CLUSTER MEET C-ip C-port
CLUSTER MEET D-ip D-port
由于 A 知道及被其他所有节点知道,它将会在发送的心跳包中包含gossip部分,这将允许其他每个节点彼此都创建一个链接,即使集群很大,也能在数秒内形成一个完整网络。
CLUSTER MEET无需相互执行,即若发送命令给 A 以加入B ,那就不必也发送给 B 以加入 A。
2.3 实现细节:MEET 和 PING 包
当某一给定节点接收到一个MEET消息时,命令中指定的节点仍不知发送了该命令,所以为使节点强制将接收命令的节点将它作为信任的节点接受它,它会发送MEET包而非PING包。两个消息包有相同的格式,但是MEET强制使接收消息包的节点确认发送消息包的节点为可信任的。
3 指派槽
把16384个槽平分给节点管理,每个节点只对自己负责的槽进行读写。
每个节点间都相互通信,所以每个节点都知道其它节点所管理槽的范围
- 客户端与指派槽
4 集群伸缩
集群的伸缩包括新节点的加入和旧节点退出。
4.1 加入新节点
- 准备新节点 启动一个集群模式下的Redis节点
- 加入集群 通过与任意一集群中的节点握手加入新节点
- 迁移slot到新节点 再向新节点分配它负责的slot并向其迁移slot对应数据。
由于Redis采用Gossip协议,所以可让新节点与任一现有集群节点握手,一段时间后整个集群都会知道加入了新节点。
4.1.1 案例
向如下集群中新加入一个节点6385。由于负载均衡的要求,加入后四个节点每个节点负责4096个slots,但集群中原来的每个节点都负责5462个slots,所以6379、6380、6381节点都需要向新的节点6385迁移1366个slots。
Redis集群并没有一个自动实现负载均衡的工具,把多少slots从哪个节点迁移到哪个节点完全是由用户指定。
- 迁移数据的流程图
迁移key可以用pipeline进行批量的迁移。
对于扩容,原理已经很清晰了,至于具体操作,网上很多。 至于缩容,也是先手动完成数据迁移,再关闭redis。收缩时如果下线的节点有负责的槽需要迁移到其他节点,再通过cluster forget命令让集群内所有节点忘记被下线节点
5 客户端路由
5.1 moved重定向
每个节点通信共享Redis Cluster中槽和集群中对应节点的关系。
- 客户端向Redis Cluster的任一节点发送命令
- 接收命令的节点再计算自己的槽和对应节点
- 如果保存数据的槽被分配给当前节点,则去槽中执行命令,并把命令执行结果返回给客户端
- 如果保存数据的槽不在当前节点的管理范围内,则向客户端返回moved重定向异常
- 客户端接收到节点返回的结果,如果是moved异常,则从moved异常中获取目标节点的信息
- 客户端向目标节点发送命令,获取命令执行结果
客户端不会自动找到目标节点执行命令,需要二次执行
5.2 ask重定向
由于集群伸缩时,需要数据迁移。
- 当客户端访问某key,节点告诉客户端key在源节点,再去源节点访问时,却发现key已迁移到目标节点,就会返回ask。
- 客户端向目标节点发送命令,目标节点中的槽已经迁移到其它节点
- 目标节点会返回ask转向给客户端
- 客户端向新节点发送Asking命令
- 再向新节点发送命令
- 新节点执行命令,把命令执行结果返回给客户端
为什么不能简单使用MOVED重定向?
虽然MOVED意味着我们认为哈希槽由另一个节点永久提供,并且应该对指定节点尝试下一个查询,所以ASK意味着仅将下一个查询发送到指定节点。
之所以需要这样做,是因为下一个关于哈希槽的查询可能是关于仍在A中的键的,因此我们始终希望客户端尝试A,然后在需要时尝试B。由于只有16384个可用的哈希槽中有一个发生,因此群集上的性能下降是可以接受的。
5.3 moved V.S ask
都是客户端重定向:
- moved:槽已经确定转移
- ask:槽还在迁移中
5.4 智能客户端
目标
追求性能
设计思路
- 从集群中选一个可运行节点,使用
Cluster slots
初始化槽和节点映射 - 将Cluster slots的结果映射在本地,为每个节点创建JedisPool,然后就可以进行数据读写操作
注意事项
- 每个JedisPool中缓存了slot和节点node的关系
- key和slot的关系:对key进行CRC16规则进行hash后与16383取余得到的结果就是槽
- JedisCluster启动时,已经知道key,slot和node之间的关系,可以找到目标节点
- JedisCluster对目标节点发送命令,目标节点直接响应给JedisCluster
- 如果JedisCluster与目标节点连接出错,则JedisCluster会知道连接的节点是一个错误的节点
- 此时JedisCluster会随机节点发送命令,随机节点返回moved异常给JedisCluster
- JedisCluster会重新初始化slot与node节点的缓存关系,然后向新的目标节点发送命令,目标命令执行命令并向JedisCluster响应
- 如果命令发送次数超过5次,则抛出异常"Too many cluster redirection!"
- 基本图示
- 全面图示
6 批量操作
mget、mset须在同一槽。
Redis Cluster 不同于 Redis 单节点,甚至和一个 Sentinel 监控的主从模式也不一样。主要因为集群自动分片,将一个key 映射到16384槽之一,这些槽分布在多节点。因此操作多 key 的命令必须保证所有的key都映射同一槽,避免跨槽执行错误。
一个单独的集群节点,只服务一组专用的keys,请求一个命令到一个Server,只能得到该Server上拥有keys的对应结果。 一个非常简单的例子是执行KEYS命令,当发布该命令到集群中某节点时,只能得到该节点上拥有key,并非集群中所有key。要得到集群中所有key,必须从集群的所有主节点上获取所有key。
对于分散在redis集群中不同节点的数据,如何比较高效地批量获取数据呢?
6.1 串行mget
定义for循环,遍历所有key,分别去所有的Redis节点中获取值并进行汇总,简单,但效率不高,需n次网络时间。
6.2 串行I/O
优化串行的mget,在客户端本地做内聚,对每个key hash,然后取余,知道key对应槽
本地已缓存了槽与节点的对应关系,然后对key按节点进行分组,成立子集,然后使用pipeline把命令发送到对应的node,需要nodes次网络时间,大大减少了网络时间开销。
6.3 并行I/O
优化串行IO,分组key后,根据节点数量启动对应的线程数,根据多线程模式并行向node节点请求数据,只需1次网络时间
6.4 hash_tag
不做任何改变,hash后就比较均匀地散在每个节点上
是否能像单机,一次IO将所有key取出呢?hash-tag提供了这样功能:若将上述key改为如下,即大括号括起来相同的内容,保证所有的key只向一个node请求数据,这样执行类似mget命令只需要去一个节点获取数据即可,效率更高。
6.5 选型对比
方案 | 优点 | 缺点 | 网络 I/O |
---|---|---|---|
串行mget | 1.编程简单 2.少量keys,性能满足要求 | 大量keys请求延迟严重 | O(keys) |
串行IO | 1.编程简单 2.少量节点,性能满足要求 | 大量 node延迟严重 | O(nodes) |
并行IO | 1.利用并行特性 2.延迟取决于最慢的节点 | 1.编程复杂 2.超时定位较难 | O(max_slow(node)) |
hash tags | 性能最高 | 1.tag-key业务维护成本较高 2.tag分布容易出现数据倾斜 | O(1) |
第一种方式,使用多线程解决批量问题,减少带宽时延,提高效率,这种做法就如上面所说简单便捷(我们目前批量操作类型比较多),有效。但问题比较明显。批量操作数量不大即可满足。 搜狐的cachecloud采用第二点,先将key获取槽点,然后分node pipeline操作。这种做法相对比第一种做法较优。
7 故障转移
7.1 故障发现
Redis Cluster通过ping/pong消息实现故障发现:不需要sentinel。ping/pong不仅能传递节点与槽的对应消息,也能传递其他状态,比如:节点主从状态,节点故障等
故障发现就是通过这种模式来实现,分为主观下线和客观下线:
7.1.1 主观下线
定义
某节点认为另一节点不可用,这仅代表一个节点对另一节点的判断,不代表所有节点的认知。
流程
- 节点-1定时发ping消息给节点-2
- 若发送成功,代表节点-2正常运行,节点-2会响应PONG消息给节点1,节点1更新与2的最后通信时间
- 若发送失败,则节点-1与节点-2间通信异常判断连接,在下一定时任务周期时,仍然会与节点2发送ping消息
- 若节点-1发现与节点-2最后通信时间超过
node-timeout
,则把节点2标识为pfail
状态
7.1.2 客观下线
定义
当半数以上持有槽的主节点都标记某节点主观下线。可以保证判断的公平性。
集群模式下,只有主节点(master)才有读写权限和集群槽的维护权限,从节点(slave)只有复制的权限。
流程
- 某个节点接收到其他节点发送的ping消息,如果接收到的ping消息中包含了其他pfail节点,这个节点会将主观下线的消息内容添加到自身的故障列表中,故障列表中包含了当前节点接收到的每一个节点对其他节点的状态信息
- 当前节点把主观下线的消息内容添加到自身的故障列表之后,会尝试对故障节点进行客观下线操作
7.2 故障恢复
从节点接收到它的主节点客观下线的通知,则进行故障恢复。
资格检查
- 对从节点的资格进行检查,只有难过检查的从节点才可以开始进行故障恢复
- 每个从节点检查与故障主节点的断线时间
- 超过cluster-node-timeout * cluster-slave-validity-factor数字,则取消资格
- cluster-node-timeout默认为15秒,cluster-slave-validity-factor默认值为10
- 如果这两个参数都使用默认值,则每个节点都检查与故障主节点的断线时间,如果超过150秒,则这个节点就没有成为替换主节点的可能性
准备选举时间
使偏移量最大的从节点具备优先级成为主节点的条件。
选举投票
对选举出来的多个从节点进行投票,选出新的主节点。
替换主节点
- 当前从节点取消复制变成离节点。(slaveof no one)
- 执行cluster del slot撤销故障主节点负责的槽,并执行cluster add slot把这些槽分配给自己
- 向集群广播自己的pong消息,表明已经替换了故障从节点
8 开发运维常见问题
8.1 集群完整性
- cluster-require-full-coverage默认为yes,即集群中所有节点都在服务且16384个槽都可用,集群才会提供服务,以保证集群完整性。
- 当某节点故障或者正在故障转移时获取数据会提示:(error)CLUSTERDOWN The cluster is down
但是大多数业务都无法容忍,建议把
cluster-require-full-coverage
设为no
8.2 带宽消耗
- Redis Cluster节点之间会定期交换Gossip消息,以及做一些心跳检测
- 官方建议Redis Cluster节点数量不要超过1000个,当集群中节点数量过多时,会产生不容忽视的带宽消耗
- 消息发送频率:节点发现与其他节点最后通信时间超过cluster-node-timeout /2时,会直接发送PING消息
- 消息数据量:slots槽数组(2kb空间)和整个集群1/10的状态数据(10个节点状态数据约为1kb)
- 节点部署的机器规模:集群分布的机器越多且每台机器划分的节点数越均匀,则集群内整体的可用带宽越高
带宽优化
- 避免使用’大’集群:避免多业务使用一个集群,大业务可以多集群
- cluster-node-timeout:带宽和故障转移速度的均衡
- 尽量均匀分配到多机器上:保证高可用和带宽
8.3 Pub/Sub广播
- 在任意一个cluster节点执行publish,则发布的消息会在集群中传播,集群中的其他节点都会订阅到消息,这样节点的带宽的开销会很大
- publish在集群每个节点广播,加重带宽
解决方案
单独“走”一套redis sentinel。就是针对目标的几个节点构建redis sentinel,在这个里面实现广播。
8.4 集群倾斜
分布式数据库存在倾斜问题是比较常见的。集群倾斜也就是各个节点使用的内存不一致
数据倾斜原因
- 节点和槽分配不均,如果使用redis-trib.rb工具构建集群,则出现这种情况的机会不多
redis-trib.rb info ip:port查看节点,槽,键值分布
redis-trib.rb rebalance ip:port进行均衡(谨慎使用)
- 不同槽对应键值数量差异比较大
- CRC16算法正常情况下比较均匀
- 可能存在hash_tag
- cluster countkeysinslot {slot}获取槽对应键值个数
- 包含bigkey:例如大字符串,几百万的元素的hash,set等
- 在从节点:redis-cli --bigkeys
- 优化:优化数据结构
- 内存相关配置不一致
- hash-max-ziplist-value:满足一定条件情况下,hash可以使用ziplist
- set-max-intset-entries:满足一定条件情况下,set可以使用intset
- 在一个集群内有若干个节点,当其中一些节点配置上面两项优化,另外一部分节点没有配置上面两项优化
- 当集群中保存hash或者set时,就会造成节点数据不均匀
- 优化:定期检查配置一致性
- 请求倾斜:热点key
- 重要的key或者bigkey
- Redis Cluster某个节点有一个非常重要的key,就会存在热点问题
优化
- 避免bigkey
- 热键不要用hash_tag
- 当一致性不高时,可以用本地缓存 MQ(消息队列)
9 读写分离
- 只读连接 集群模式下,从节点不接受任何读写请求。
- 当向从节点执行读请求时,重定向到负责槽的主节点
- readonly命令可以读:连接级别命令,当连接断开之后,需要再次执行
- readonlyredis cluster 默认slave 也是不能读的,如果要读取,需要执行 readonly,就可以了。
读写分离:更加复杂(成本很高,尽量不要使用)
- 同样的问题:复制延迟,读取过期数据,从节点故障
- 修改客户端:cluster slaves {nodeId}
10 集群优劣
集群的限制
- key批量操作支持有限:例如mget,mset必须在一个slot
- key事务和Lua支持有限:操作的key必须在一个节点
- key是数据分区的最小粒度:不支持bigkey分区
- 不支持多个数据库:集群模式下只有一个db0
- 复制只支持一层:不支持树形复制结构
集群不一定好
- Redis Cluster满足容量和性能的扩展性,很多业务’不需要’
- 大多数时客户端性能会’降低’
- 命令无法跨节点使用:mget,keys,scan,flush,sinter等
- Lua和事务无法跨节点使用
- 客户端维护更复杂:SDK和应用本身消耗(例如更多的连接池) 很多场景Redis Sentinel已经够用了
参考
- https://www.slideshare.net/iammutex/redis-cluster
- https://zhuanlan.zhihu.com/p/105569485
- https://sunweiguo.github.io/2019/02/01/redis/Redis-Cluster理论详解/
- https://redis.io/topics/cluster-spec
- http://trumandu.github.io/2016/05/09/RedisCluster构建批量操作探讨/