redis集群
产生背景
一句话单台机器承受不了大数据量了
假设我们在一台主从机器上配置了200G内存, 但是业务需求是需要500G的时候, 主从结构+哨兵可以实现高可用故障切换+冗余备份, 但是并不能解决数据容量的问题, 用哨兵, redis每个实例也是全量存储, 每个redis存储的内容都是完整的数据, 浪费内存且有木桶效应。
为了最大化利用内存, 可以采用cluster群集, 就是分布式存储。 即每台redis存储不同的内容。
对于redis的集群其实就和MySQL的分库分表的切分是有很大的相似性, 把数据根据规则切分在不同的节点上
1、 Redis 分布式方案一般有两种:
-
客户端分区方案, 优点是分区逻辑可控, 缺点是需要自己处理数据路由、 高可用、 故障转移等问题, 比如在redis2.8之前通常的做法是获取某个key的hashcode , 然后取余分布到不同节点, 不过这种做法无法很好的支持动态伸缩性需求, 一旦节点的增或者删操作, 都会导致key无法在redis中命中。
-
代理方案, 优点是简化客户端分布式逻辑和升级维护便利, 缺点是加重架构部署复杂度和性能损耗, 比如 twemproxy、 Codis
-
官方为我们提供了专有的集群方案: Redis Cluster, 它非常优雅地解决了 Redis 集群方面的问题, 部署方便简单, 因此理解应用好 Redis Cluster 将极大地解放我们使用分布式 Redis 的工作量。
2、redis cluster
2.1 简绍
Redis Cluster 是 Redis 的分布式解决方案, 在3.0版本正式推出, 有效地解决了 Redis 分布式方面的需求。 当遇到单机内存、 并发、 流量等瓶颈时, 可以采用 Cluster架构方案达到负载均衡的目的。
架构图:
在这个图中, 每一个蓝色的圈都代表着一个redis的服务器节点。 它们任何两个节点之间都是相互连通的。 客户端可以与任何一个节点相连接, 然后就可以访问集群中的任何一个节点, 对其进行存取和其他操作。
Redis 集群提供了以下两个好处:
1、 将数据自动切分到多个节点的能力。
2、 当集群中的一部分节点失效或者无法进行通讯时, 仍然可以继续处理命令请求的能力, 拥有自动故障转移的能力。
2.2 redis cluster vs. replication + sentinal如何选择
如果你的数据量很少, 比如你的缓存一般就几个G, 那么使用单机就足够了。
- Replication: 一个mater, 多个slave, 要几个slave跟你的要求的读吞吐量有关系, 结合sentinal集群, 去保证redis主从架构的高可用性, 就可以了。
- redis cluster: 主要是针对海量数据+高并发+高可用的场景, 海量数据, 如果你的数据量很大, 那么建议就用redis cluster。
2.3 数据分布理论
分布式数据库首先要解决把整个数据集按照分区规则映射到多个节点的问题, 即把数据集划分到多个节点上, 每个节点负责整体数据的一个子集。
常见的分区规则有哈希分区和顺序分区两种
顺序分布就是把一整块数据分散到很多机器中,顺序分布一般都是平均分配的。 如下图所示。
哈希分区
详细可看:哈希分区
如下图所示, 1~100这整块数字, 通过 hash 的函数, 取余产生的数。 这样可以保证这串数字充分的打散, 也保证了均匀的分配到各台机器上。
对比
哈希分布和顺序分布只是场景上的适用。 哈希分布不能顺序访问, 比如你想访问1~100, 哈希分布只能遍历全部数据, 同时哈希分布因为做了 hash 后导致与业务数据无关了。
分区方式 | 特点 | 代表产品 |
---|---|---|
哈希分区 | 离散度好 数据分布业务无关 无法顺序访问 |
RedisCluster Cassandra Dynamo |
顺序分区 | 离散度易倾斜 数据分布业务相关 可顺序访问 |
Bigtable HBase Hypertable |
数据倾斜与数据迁移跟节点伸缩
顺序分布是会导致数据倾斜的, 主要是访问的倾斜。 每次点击会重点访问某台机器, 这就导致最后数据都到这台机器上了, 这就是顺序分布最大的缺点。
但哈希分布其实是有个问题的, 当我们要扩容机器的时候, 专业上称之为“节点伸缩”, 这个时候, 因为是哈希算法, 会导致数据迁移。
2.3.1 哈希分区方式
1、 节点取余分区
使用特定的数据(包括redis的键或用户ID) , 再根据节点数量N, 使用公式: hash(key)%N计算出一个0~(N-1) 值, 用来决定数据映射到哪一个节点上。 即哈希值对节点总数取余。
缺点: 当节点数量N变化时(扩容或者收缩) , 数据和节点之间的映射关系需要重新计算, 这样的话, 按照新的规则映射, 要么之前存储的数据找不到, 要么之前数据被重新映射到新的节点(导致以前存储的数据发生数据迁移)
实践: 常用于数据库的分库分表规则, 一般采用预分区的方式, 提前根据数据量规划好分区数, 比如划分为512或1024张表, 保证可支撑未来一段时间的数据量,再根据负载情况将表迁移到其他数据库中。
2、 一致性哈希
bilibili讲一致性哈希视频
一致性哈希分区(Distributed Hash Table) 实现思路是为系统中每个节点分配一个 token, 范围一般在0~2^32, 这些 token 构成一个哈希环。 数据读写执行节点查找操作时, 先根据 key 计算 hash 值, 然后顺时针找到第一个大于等于该哈希值的 token 节点
上图就是一个一致性哈希的原理解析。
假设我们有 n1~n4 这四台机器, 我们对每一台机器分配一个唯一 token, 每次有数据(图中黄色代表数据) , 一致性哈希算法规定每次都顺时针漂移数据, 也就是图中黄色的数据都指向 n3。
这个时候我们需要增加一个节点 n5, 在 n2 和 n3 之间, 数据还是会发生漂移(会偏移到大于等于的节点) , 但是这个时候你是否注意到, 其实只有 n2~n3 这部分的数据被漂移, 其他的数据都是不会变的, 这种方式相比节点取余最大的好处在于加入和删除节点只影响哈希环中相邻的节点, 对其他节点无影响。
- 缺点: 每个节点的负载不相同, 因为每个节点的hash是根据key计算出来的,换句话说就是假设key足够多, 被hash算法打散得非常均匀, 但是节点过少, 导致每个节点处理的key个数不太一样, 甚至相差很大, 这就会导致某些节点压力很大
- 实践: 加减节点会造成哈希环中部分数据无法命中, 需要手动处理或者忽略这部分数据, 因此一致性哈希常用于缓存场景。
3、 虚拟槽分区
虚拟槽分区巧妙地使用了哈希空间, 使用分散度良好的哈希函数把所有数据映射到一个固定范围的整数集合中, 整数定义为槽(slot) 。 这个范围一般远远大于节点数, 比如 Redis Cluster 槽范围是 0~16383 (也就是说有16384个槽) 。 槽是集群内数据管理和迁移的基本单位。 采用大范围槽的主要目的是为了方便数据拆分和集群扩展。 每个节点会负责一定数量的槽。
当前集群有5个节点, 每个节点平均大约负责3276个槽。 由于采用高质量的哈希算法, 每个槽所映射的数据通常比较均匀, 将数据平均划分到5个节点进行数据分区。 Redis Cluster 就是采用虚拟槽分区, 下面就介绍 Redis 数据分区方法。
每当 key 访问过来, Redis Cluster 会计算哈希值是否在这个区间里。 它们彼此都知道对应的槽在哪台机器上, 这样就能做到平均分配了。
集群限制: Key批量操作, mget()
详情
1. 节点通信
1.1 通信流程
在分布式存储中需要提供维护节点元数据信息的机制, 所谓元数据是指: 节点负责哪些数据, 是否出现故障等状态信息, Redis 集群采用 Gossip(流言) 协议,Gossip 协议工作原理就是节点彼此不断通信交换信息, 一段时间后所有的节点都会知道集群完整的信息, 这种方式类似流言传播
- 集群中的每个节点都会单独开辟一个 TCP 通道, 用于节点之间彼此通信, 通信端口号在基础端口上加10000。
- 每个节点在固定周期内通过特定规则选择几个节点发送 ping 消息。
- 接收到 ping 消息的节点用 pong 消息作为响应。
集群中每个节点通过一定规则挑选要通信的节点, 每个节点可能知道全部节点, 也可能仅知道部分节点, 只要这些节点彼此可以正常通信, 最终它们会达到一致的状态。 当节点出故障、 新节点加入、 主从角色变化、 槽信息变更等事件发生时, 通过不断的 ping/pong 消息通信, 经过一段时间后所有的节点都会知道整个集群全部节点的最新状态, 从而达到集群状态同步的目的。
1.2 Gossip消息
Gossip协议的主要职责就是信息交换。 信息交换的载体就是节点彼此发送的Gossip消息, 了解这些消息有助于我们理解集群如何完成信息交换。
Gossip消息可分为: ping消息, pong消息, meet消息, fail消息等;
- meet消息:用于通知新节点加入。 消息发送者通知接收者加入到当前集群,meet消息通信正常完成后, 接收节点会加入到集群中并进行周期性的ping、 pong消自交换。
- ping消息: 集群内交换最频繁的消息, 集群内每个节点每秒向多个其他节点发送ping消息, 用于检测节点是否在线和交换彼此状态信息。 ping消息发送封装了自身节点和部分其他节点的状态数据。
- pong消息:当接收到ping、 meet消息时, 作为响应消息回复给发送方确认消息正常通信。 pong消息内部封装了自身状态数据。 节点也可以向集群内广播自身的pong消息来通知整个集群对自身状态进行更新。
- fail消息:当节点判定集群内另一个节点下线时, 会向集群内广播一个fail消息, 其他节点接收到fail消息之后把对应节点更新为下线状态。 .
1.3 为什么redis选择槽的个数为16384个
对于客户端请求的key, 根据公式HASH_SLOT=CRC16(key) mod 16384, 计算出映射到哪个分片上, 然后Redis会去相应的节点进行操作!
那大家思考过, 为什么有16384个槽么?
ps:CRC16算法产生的hash值有16bit, 该算法可以产生2^16-=65536个值。 换句话说, 值是分布在0~65535之间。 那作者在做mod运算的时候, 为什么不mod65536, 而选择mod16384?
这个问题, 作者是给出了回答的! 地址如下:https://github.com/antirez/redis/issues/2576
需知: char 1个字节字节也叫Byte, 是计算机数据的基本存储单位。 8bit(位)=1Byte(字节) 1024Byte(字节)=1KB 1024KB=1MB 1024MB=1GB 1024GB=1TB
cluster.h
#define CLUSTERMSG_TYPE_COUNT 10 /* 消息的总数量. */
typedef struct {
/* ... */
uint16_t type; /* 消息类型, 用于区分meet,ping,pong */
unsigned char myslots[CLUSTER_SLOTS/8]; /* 发送节点负责的槽信息 */
unsigned char mflags[3]; /* 至少有3个节点信息 */
/* ... */
} clusterMsg;
在消息头中, 最占空间的是 myslots[CLUSTER_SLOTS/8]。 这块的大小是:16384÷8÷1024=2kb
那在消息体中, 会携带一定数量的其他节点信息用于交换。 那这个其他节点的信息, 到底是几个节点的信息呢? 约为集群总节点数量的1/10, 至少携带3个节点的信息。
这里的重点是:节点数量越多, 消息体内容越大。 消息体大小是10个节点的状态信息约1kb
redis集群内节点, 每秒都在发ping消息。 规律如下
- 每秒会随机选取5个节点, 找出最久没有通信的节点发送ping消息
- 每100毫秒(1秒10次)都会扫描本地节点列表, 如果发现节点最近一次接受pong消息的时间大于cluster-node-timeout/2 则立刻发送ping消息
因此, 每秒单节点发出ping消息数量为
数量=1+10*num(node.pong_received>cluster_node_timeout/2)
当槽位为65536时, 这块的大小是: 65536 ÷ 8 ÷ 1024 = 8kb 因为每秒钟, redis节点需要发送一定数量的ping消息作为心跳包, 如果槽位为65536, 这个ping消息的消息头太大了, 浪费带宽
2. 故障转移与恢复
2.1 故障解释
Redis集群自身实现了高可用。 高可用首先需要解决集群部分失败的场景:当集群内节点出现故障时通过自动故障转移保证集群可以正常对外提供服务。 本节介绍故障转移的细节, 分析故障发现和替换故障节点的过程。
2.3.1 故障发现
当集群内某个节点出现问题时, 需要通过一种健壮的方式保证识别出节点是否发生了故障。 Redis 集群内节点通过ping/pong消息实现节点通信, 消息不但可以传播节点槽信息, 还可以传播其他状态如:主从状态、 节点故障等。 因此故障发现也是通过消息传播机制实现的, 主要环节包括:主观下线(pfail)和客观下线(fail)。
- 主观下线:指某个节点认为另一个节点不可用, 即下线状态, 这个状态并不是最终的故障判定, 只能代表一个节点的意见, 可能存在误判情况。
- 客观下线:指标记一个节点真正的下线, 集群内多个节点都认为该节点不可用, 从而达成共识的结果。 如果是持有槽的主节点故障, 需要为该节点进行故障转移。
- 主观下线
集群中每个节点都会定期向其他节点发送ping消息, 接收节点回复pong消息作为响应。 如果在cluster-node- timeout时间内通信一直失败, 则发送节点会认为接收节点存在故障, 把接收节点标记为主观下线(pfail)状态。 流程如图下
- 节点a发送ping消息给节点b,如果通信正常将接收到pong消息, 节点a更新最近一次与节点b的通信时间。
- 如果节点a与节点b通信出现问题则断开连接, 下次会进行重连。 如果直通信失败, 则节点a记录的与节点b最后通信时间将无法更新。
- 节点a内的定时任务检测到与节点b最后通信时间超高cluster-node-timeout时, 更新本地对节点b的状态为主观下线(pfail)。
主观下线简单来讲就是, 当cluster-note-timeout时间内某节点无法与另一个节点顺利完成ping消息通信时, 则将该节点标记为主观下线状态。 每个节点内的clusterState结构都需要保存其他节点信息, 用于从自身视角判断其他节点的状态。
Redis集群对于节点最终是否故障判断非常严谨, 只有一个节点认为主观下线并不能准确判断是否故障。 多个节点协作完成故障发现的过程叫做客观下线。
2.客观下线
当某个节点判断另一个节点主管下线后, 相应的节点状态会跟随消息在集群内传播。 ping/pong消息的消息体会携带集群1/10的其他节点状态数据, 当接收节点发现消息体中含有主观下线的节点状态时、 会在本地找到故障及节点clusterNode结构, 保存到下线报告连接中 clusterNode fail_reportsx
通过Gossip消息传播, 集群内节点不断收集到故障节点的下线报告。 当半数以上持有槽的主节点都标记某个节点是主观下线时。 触发客观下线流程。
假设节点a标记节点b为主观下线, 一段时间后节点a通过消息把节点b的状态发送到其他节点, 当节点c接受到消息并解析出消息体含有节点b的pfail状态时, 会触发客观下线流程
流程:
- 当消息体内含有其他节点的pfail状态会判断发送节点的状态, 如果发送节点是主节点则对报告的pfail状态处理, 从节点则忽略
- 找到pfail对应的节点结构, 更新clusterNode内部下线报告链表
- 根据更新后的下线报告链接表尝试进行客观下线。
流程说明:
- 首先统计有效的下线报告数量, 如果小于集群内持有槽的主节点总数的一半则退出。
- 当下线报告大于槽主机节点数量一半时, 标记对应故障节点为客观下线状态
- 向集群广播一条fail消息, 通知所有的节点讲故障接地那标记为客观下线, fail消息的消息体指包含故障节点的id
广播fail消息是客观下线的最后一步, 它承担着非常重要的职责:
- 通知集群内所有的节点标记故障节点为客观下线状态并立刻生效。
- 通知故障节点的从节点触发故障转移流程。
2.3.2 故障恢复
故障节点变为客观下线后, 如果下线节点是持有槽的主节点则需要在它的从节点中选出一个替换它, 从而保证集群的高可用。 下线主节点的所有从节点承担故障恢复的义务, 当从节点通过内部定时任务发现自身复制的主节点进入客观下线时, 将会触发故障恢复流程。
- 资格检查
- 准备选举时间
- 发起选举
- 选举投票
- 替换主节点
2.3.2.1. 资格检查
每个从节点都要检查最后与主节点断线时间, 判断是否有资格替换故障的主接地那。 如果从节点与主节点断线时间超过 cluster-node-time * cluster-slave-validityfactor, 则当前从节点不具备故障转移资格。
2.3.2.2. 准备选举时间
当从节点符合故障资格后, 更新触发故障选举的时间, 只有到达该时间后才能执行后续流程。
这里之所以采用延迟触发机制, 主要是通过对多个从节点使用不同的延迟选举时间来支持优先级问题。 复制偏移量越大说明从节点延迟越低, 那么它应该具有更高的优先级来替换故障主节点。
2.3.2.3. 发起选举
配置纪元是一个只增不减的整数, 每个主节点自身维护一个配置纪元( clusterNode.configEpoch)标示当前主节点的版本, 所有主节点的配置纪元都不相等, 从节点会复制主节点的配置纪元。 整个集群又维护一个全局的配置纪元( clusterState. currentEpoch),用于记录集群内所有主节点配置纪元的最大版本。
配置纪元会跟随ping/pong消息在集群内传播, 当发送方与接收方都是主节点且配置纪元相等时代表出现了冲突, nodeld 更大的一方会递增全局配置纪元并赋值给当前节点来区分冲突。
2.3.2.4. 选举投票
这个过程就类似于哨兵的操作, 会根据拥有槽的主节点进行投票, 而从节点必须要等到N/2 + 1的票数才可以。