赞
踩
在 redis3.0之前,redis使用的哨兵架构,它借助 sentinel 工具来监控 master 节点的状态;如果 master 节点异常,则会做主从切换,将一台 slave 作为 master。当master挂掉的时候,sentinel 会选举出来一个 master,选举的时候是没有办法去访问Redis的,会存在访问瞬断的情况;若是在电商网站大促的时候master给挂掉了,几秒钟损失好多订单数据;哨兵模式,对外只有master节点可以写,slave节点只能用于读。尽管Redis单节点最多支持10W的QPS,但是在电商大促的时候,写数据的压力全部在master上。Redis的单节点内存不能设置过大,若数据过大在主从同步将会很慢;在节点启动的时候,时间特别长;(从节点上有主节点的所有数据)。
Redis集群是一个由多个主从节点群组成的分布式服务集群,它具有复制、高可用和分片特性。Redis集群不需要sentinel哨兵也能完成节点移除和故障转移的功能。需要将每个节点设置成集群模式,这种集群模式没有中心节点,可水平扩展,据官方文档称可以线性扩展到上万个节点(官方推荐不超过1000个节点)。redis集群的性能和高可用性均优于之前版本的哨兵模式,且集群配置非常简单。Redis 集群是一种分布式数据库方案,集群通过分片(sharding)来进行数据管理(分治思想的一种实践),并提供复制和故障转移功能。将数据划分为 16384 的 slots,每个节点负责一部分槽位。槽位的信息存储于每个节点中。它是去中心化的,如图所示,该集群有三个 Redis 节点组成,每个节点负责整个集群的一部分数据,每个节点负责的数据多少可能不一样。三个节点相互连接组成一个对等的集群,它们之间通过 Gossip
协议相互交互集群信息,最后每个节点都保存着其他节点的 slots 分配情况。
Redis集群的优点:
使用 Redis Cluster 集群,主要解决了大数据量存储导致的各种慢问题,同时也便于横向拓展
两种方案对应着 Redis 数据增多的两种拓展方案:垂直扩展(scale up)、水平扩展(scale out)。
比如需要一个内存 24 GB 磁盘 150 GB 的服务器资源,有以下两种方案:
在面向百万、千万级别的用户规模时,横向扩展的 Redis 切片集群会是一个非常好的选择。
Redis Cluster 具有以下特点:
集群的整个数据库被分为 16384 个槽(slot),数据库中的每个键都属于这 16384 个槽的其中一个,集群中的每个节点可以处理 0 个或最多 16384 个槽。
Key 与哈希槽映射过程可以分为两大步骤:
Cluster 还允许用户强制某个 key 挂在特定槽位上,通过在 key 字符串里面嵌入 tag 标记,这就可以强制 key 所挂在的槽位等于 tag 所在的槽位。
客户端直连 Redis 服务,进行读写操作时,Key 对应的 Slot 可能并不在当前直连的节点上,经过“重定向”才能转发到正确的节点。
和普通的查询路由相比,Redis Cluster 借助客户端实现的请求路由是一种混合形式的查询路由,它并非从一个 Redis 节点到另外一个 Redis,而是借助客户端转发到正确的节点。实际应用中,可以在客户端缓存 Slot 与 Redis 节点的映射关系,当接收到 MOVED 响应时修改缓存中的映射关系。如此,基于保存的映射关系,请求时会直接发送到正确的节点上,从而减少一次交互,提升效率。
客户端又怎么确定访问的数据到底分布在哪个实例上呢?
Redis 实例会将自己的哈希槽信息通过 Gossip 协议发送给集群中其他的实例,实现了哈希槽分配信息的扩散。这样,集群中的每个实例都有所有哈希槽与实例之间的映射关系信息。
在切片数据的时候是将 key 通过 CRC16 计算出一个值再对 16384 取模得到对应的 Slot,这个计算任务可以在客户端上执行发送请求的时候执行。但是,定位到槽以后还需要进一步定位到该 Slot 所在 Redis 实例。当客户端连接任何一个实例,实例就将哈希槽与实例的映射关系响应给客户端,客户端就会将哈希槽与实例映射信息缓存在本地。当客户端请求时,会计算出键所对应的哈希槽,在通过本地缓存的哈希槽实例映射信息定位到数据所在实例上,再将请求发送给对应的实例。
哈希槽与实例之间的映射关系由于新增实例或者负载均衡重新分配导致改变了咋办?
集群中的实例通过 Gossip 协议互相传递消息获取最新的哈希槽分配信息,但是,客户端无法感知。Redis Cluster 提供了重定向机制:客户端将请求发送到实例上,这个实例没有相应的数据,该 Redis 实例会告诉客户端将请求发送到其他的实例上。
Redis 如何告知客户端重定向访问新实例呢?分为两种情况:MOVED 错误、ASK 错误。
MOVED 错误:
MOVED 错误(负载均衡,数据已经迁移到其他实例上):当客户端将一个键值对操作请求发送给某个实例,而这个键所在的槽并非由自己负责的时候,该实例会返回一个 MOVED 错误指引转向正在负责该槽的节点。
(error) MOVED 16330 172.17.18.2:6379
该响应表示客户端请求的键值对所在的哈希槽 16330 迁移到了 172.17.18.2 这个实例上,端口是 6379。这样客户端就与 172.17.18.2:6379 建立连接,并发送 GET 请求。同时,客户端还会更新本地缓存,将该 slot 与 Redis 实例对应关系更新正确。
ASK 错误:如果某个 slot 的数据比较多,部分迁移到新实例,还有一部分没有迁移咋办?
如果请求的 key 在当前节点找到就直接执行命令,否则时候就需要 ASK 错误响应了,槽部分迁移未完成的情况下,如果需要访问的 key 所在 Slot 正在从从 实例 1 迁移到 实例 2,实例 1 会返回客户端一条 ASK 报错信息:客户端请求的 key 所在的哈希槽正在迁移到实例 2 上,你先给实例 2 发送一个 ASKING 命令,接着发发送操作命令。
(error) ASK 16330 172.17.18.2:6379
比如客户端请求定位到 key的槽16330 在实例 172.17.18.1 上,节点1如果找得到就直接执行命令,否则响应 ASK 错误信息,并指引客户端转向正在迁移的目标节点 172.17.18.2。
注意:ASK 错误指令并不会更新客户端缓存的哈希槽分配信息。所以客户端再次请求 Slot 16330 的数据,还是会先给 172.17.18.1
实例发送请求,只不过节点会响应 ASK 命令让客户端给新实例发送一次请求。MOVED
指令则更新客户端本地缓存,让后续指令都发往新实例。
采用一致性哈希算法(consistent hashing),将key和节点name同时hashing,然后进行映射匹配,采用的算法是MURMUR_HASH。采用一致性哈希而不是采用简单类似哈希求模映射的主要原因是当增加或减少节点时,不会产生由于重新匹配造成的rehashing。一致性哈希只影响相邻节点key分配,影响量小。
为了避免一致性哈希只影响相邻节点造成节点分配压力,ShardedJedis会对每个Redis节点根据名字(没有,Jedis会赋予缺省名字)会虚拟化出160个虚拟节点进行散列。根据权重weight,也可虚拟化出160倍数的虚拟节点。用虚拟节点做映射匹配,可以在增加或减少Redis节点时,key在各Redis节点移动再分配更均匀,而不是只有相邻节点受影响。
Hash环的数据倾斜问题:一致性Hash算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜(被缓存的对象大部分集中缓存在某一台服务器上)问题,例如系统中只有两台服务器,其环分布如图所示,此时必然造成大量数据集中到Node A上,而只有极少量会定位到Node B上。为了解决这种数据倾斜问题,一致性Hash算法引入了虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。具体做法可以在服务器IP或主机名的后面增加编号来实现。例如上面的情况,可以为每台服务器计算三个虚拟节点,于是可以分别计算 “Node A#1”、“Node A#2”、“Node A#3”、“Node B#1”、“Node B#2”、“Node B#3”的哈希值,于是形成六个虚拟节点:
CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST
消息,要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票。CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK
消息,表示这个主节点支持从节点成为新的主节点。CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK
消息,如果收集到的票 >= (N/2) + 1 支持,那么这个从节点就被选举为新主节点。跟哨兵类似,两者都是基于 Raft 算法来实现的,流程如图所示:
Redis 自3.0版本起,支持 Redis Cluster,真正意义上实现了分布式。在分布式系统中,节点间的通信十分重要,是构建集群的基石。那么 Redis Cluster 中,节点间是如何通信的呢?又是如何保障一致性、可用性的呢?欲知答案,必先了解 Gossip 算法。Gossip 算法源自流行病学的研究,经过不断的发展演化,作为一种分布式一致性协议而得到广泛应用,如 Cassandra、Akka、Redis 都有用到。
Gossip 特点:在一个有界网络中,每个节点都随机地与其它节点通信,经过一番杂乱无章的通信,最终所有节点的状态都会达成一致。每个节点可能知道所有其它节点,也可能仅知道几个邻居节点,只要这些节可以通过网络连通,最终它们的状态都是一致的。要注意到的一点是,即使有的节点因宕机而重启,有新节点加入,但经过一段时间后,这些节点的状态也会与其他节点达成一致,也就是说,Gossip 天然具有分布式容错的优点。
Gossip 是一个带冗余的容错算法,更进一步,Gossip 是一个最终一致性算法。虽然无法保证在某个时刻所有节点状态一致,但可以保证在“最终”所有节点一致,“最终”是一个现实中存在,但理论上无法证明的时间点。因为 Gossip 不要求节点知道所有其它节点,因此又具有去中心化的特点,节点之间完全对等,不需要任何的中心节点。实际上 Gossip 可以用于众多能接受“最终一致性”的领域:失败检测、路由同步、Pub/Sub、动态负载均衡。但 Gossip 的缺点也很明显,冗余通信会对网路带宽、CUP 资源造成很大的负载,而这些负载又受限于通信频率,该频率又影响着算法收敛的速度,
Gossip 在 Redis Cluster 中的作用:
在分布式系统中,需要提供维护节点元数据信息的机制,所谓元数据是指节点负责哪些数据、主从属性、是否出现故障等状态信息。常见的元数据维护方式分为集中式和无中心式。Redis Cluster 采用 Gossip 协议实现了无中心式。Redis Cluster 中使用 Gossip 主要有两大作用:
发送的消息结构是 clusterMsgDataGossip
结构体组成:
- typedef struct {
- char nodename[CLUSTER_NAMELEN]; //40字节
- uint32_t ping_sent; //4字节
- uint32_t pong_received; //4字节
- char ip[NET_IP_STR_LEN]; //46字节
- uint16_t port; //2字节
- uint16_t cport; //2字节
- uint16_t flags; //2字节
- uint32_t notused1; //4字节
- } clusterMsgDataGossip;
所以每个实例发送一个 Gossip
消息,就需要发送 104 字节。如果集群是 1000 个实例,那么每个实例发送一个 PING
消息则会占用 大约 10KB。除此之外,实例间在传播 Slot 映射表的时候,每个消息还包含了 一个长度为 16384 bit 的 Bitmap
。每一位对应一个 Slot,如果值 = 1 则表示这个 Slot 属于当前实例,这个 Bitmap 占用 2KB,所以一个 PING
消息大约 12KB。PONG
与PING
消息一样,一发一回两个消息加起来就是 24 KB。集群规模的增加,心跳消息越来越多就会占据集群的网络通信带宽,降低了集群吞吐量。
Gossip 协议的主要职责就是信息交换。信息交换的载体就是节点彼此发送的Gossip 消息,常用的 Gossip 消息可分为:Ping 消息、Pong 消息、Meet 消息、Fail 消息。
由于集群内部需要频繁地进行节点信息交换,而 Ping/Pong 消息携带当前节点和部分其它节点的状态数据,势必会加重带宽和计算的负担。Redis 集群内节点通信采用固定频率(定时任务每秒执行10次),因此,节点每次选择需要通信的节点列表变得非常重要。通信节点选择过多虽然可以做到信息及时交换但成本过高。节点选择过少则会降低集群内所有节点彼此信息交换的频率,从而影响故障判定、新节点发现等需求的速度。因此 Redis 集群的 Gossip 协议需要兼顾信息交换实时性和成本开销。
发送 PING 消息的频率也会影响集群带宽吧?
Redis Cluster 的实例启动后,默认会每秒从本地的实例列表中随机选出 5 个实例,再从这 5 个实例中找出一个最久没有收到 PING 消息的实例,把 PING 消息发送给该实例。
随机选择 5 个,但是无法保证选中的是整个集群最久没有收到 PING 通信的实例,有的实例可能一直没有收到消息,导致他们维护的集群信息早就过期了,咋办呢?
这个问题问的好,Redis Cluster 的实例每 100 ms 就会扫描本地实例列表,当发现有实例最近一次收到 PONG
消息的时间 > cluster-node-timeout / 2
。那么就立刻给这个实例发送 PING
消息,更新这个节点的集群状态信息。当集群规模变大,就会进一步导致实例间网络通信延迟怎加。可能会引起更多的 PING 消息频繁发送。
PING
消息,降低这个频率可能会导致集群每个实例的状态信息无法及时传播。PONG
消息接收是否超过 cluster-node-timeout / 2
,这个是 Redis 实例默认的周期性检测任务频率,我们不会轻易修改。所以,只能修改 cluster-node-timeout
的值:集群中判断实例是否故障的心跳时间,默认 15 S。所以,为了避免过多的心跳消息占用集群宽带,将 cluster-node-timeout
调成 20 秒或者 30 秒,这样 PONG
消息接收超时的情况就会缓解。但是,也不能设置的太大。都则就会导致实例发生故障了,等待 cluster-node-timeout
时长才能检测出这个故障,影响集群正常服务、
如果某个主节点没有从节点,那么当它发生故障时,集群将完全处于不可用状态。
不过 Redis 也提供了一个参数cluster-require-full-coverage
可以允许部分节点故障,其它节点还可以继续提供对外访问。比如 7000 主节点宕机,作为 slave 的 7003 成为 Master 节点继续提供服务。当下线的节点 7000 重新上线,它将成为当前 70003 的从节点。
一个节点认为某个节点失联了并不代表所有的节点都认为它失联了。只有当大多数负责处理 slot 节点都认定了某个节点下线了,集群才认为该节点需要进行主从切换。Redis 集群节点采用 Gossip
协议来广播自己的状态以及自己对整个集群认知的改变。比如一个节点发现某个节点失联了 (PFail),它会将这条信息向整个集群广播,其它节点也就可以收到这点失联信息。
如果一个节点收到了某个节点失联的数量 (PFail Count) 已经达到了集群的大多数,就可以标记该节点为确定下线状态 (Fail),然后向整个集群广播,强迫其它节点也接收该节点已经下线的事实,并立即对该失联节点进行主从切换。
当一个 Slave 发现自己的主节点进入已下线状态后,从节点将开始对下线的主节点进行故障转移。
CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST
消息,要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票。CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK
消息,表示这个主节点支持从节点成为新的主节点。CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK
消息,如果收集到的票 >= (N/2) + 1 支持,那么这个从节点就被选举为新主节点。跟哨兵类似,两者都是基于 Raft 算法来实现的,流程如图所示:
随着应用场景的升级,缓存可能需要扩容,扩容的方式有两种:垂直扩容(Scale Up)和水平扩容(Scale Out)。垂直扩容无需详述。实际应用场景中,采用水平扩容更多一些,根据是否增加主节点数量,水平扩容方式有两种。
主节点数量不变
比如,当前有一台物理机 A,构建了一个包含3个 Redis 实例的集群;扩容时,我们新增一台物理机 B,拉起一个 Redis 实例并加入物理机 A 的集群;B 上 Redis 实例对 A 上的一个主节点进行复制,然后进行主备倒换;如此,Redis 集群还是3个主节点,只不过变成了 A2-B1 的结构,将一部分请求压力分担到了新增的节点上,同时物理容量上限也会增加,主要步骤如下:
增加主节点数量。
不增加主节点数量的方式扩容比较简单,但是,从负载均衡的角度来看,并不是很好的选择。例如,如果主节点数量较少,那么单个节点所负责的 Slot 的数量必然较多,很容易出现大量 Key 的读写集中于少数节点的现象,而增加主节点的数量,可以更有效的分摊访问压力,充分利用资源。主要步骤如下:
新节点刚开始都是master节点,但是由于没有负责的槽,所以不能接收任何读写操作,对新节点的后续操作,一般有两种选择:
- # 新节点加入集群
- redis-trib.rb add-node new_host:new_port old_host:old_port
- # 新节点加入集群并作为指定master的slave
- redis-trib.rb add-node new_host:new_port old_host:old_port --slave --master-id <master-id>
建议使用redis-trib.rb add-node
将新节点添加到集群中,该命令会检查新节点的状态,如果新节点已经加入了其他集群或者已经包含数据,则会报错,而使用cluster meet
命令则不会做这样的检查,假如新节点已经存在数据,则会合并到集群中,造成数据不一致。
slot迁移的其他说明
Gossip
协议传播集群实例信息,所以通信频率是限制集群大小的主要原因,主要可以通过修改 cluster-node-timeout
调整频率。云数据库 Redis 提供集群版实例,轻松突破 Redis 自身单线程瓶颈,可极大满足对于 Redis 大容量或高性能的业务需求。 云数据库 Redis 集群版内置数据分片及读取算法,整体过程对用户透明,免去用户开发及运维 Redis 集群的烦恼。
云数据库 Redis集群版实例由 Proxy 服务器(服务代理)、分片服务器(数据节点)和配置服务器(分区策略存储)三个组件组成。
双节点:每个分片服务器均是双副本高可用架构,主节点故障之后,系统会自动进行主备切换保证服务高可用。
单节点:每个分片服务器是单节点架构,没有备用节点实时同步数据,不提供数据持久化和备份策略,数据节点故障之后,系统会在30秒内重新拉起一个 Redis 进程保证服务高可用,但是该节点的数据将会丢失掉。
阿里云的redis集群版由3大组件构成:
redis-proxy 无状态,一个集群根据集群规格可挂多个proxy节点。
redis-config 双节点,支持容灾。
集群元数据存储在rds db上。
提供独立的组件HA负责集群的主备切换等。
阿里云的redis集群同样基于proxy,用户对路由信息无感知,同时提供vip给客户端访问,客户端只需一个连接地址即可,无须关心proxy访问的负载均衡等。
在3台物理机上分别搭建了以上3种redis集群。每台物理机千兆网卡,24核cpu,内存189G。3台物理机分别跑压测工具memtier_benchmark、codis proxy/阿里云proxy、redis server。redis server使用各种集群配套的redis内核。
固定key size 32个字节,set/get 操作比例为1:10。每个线程16个客户端。连续压测5分钟,分8个, 16个, 32个, 48个, 64个线程压测。因为redis4.0集群需要额外的客户端选择节点,而memtier_benchmark不支持,所以使用了hashtag 来压测redis4.0。每个集群有8个master db, 8个slave db, aof打开。aof rewrite的最小buffer为64MB。压测的对象分别为单个redis 4.0 节点, 单个阿里云redis-proxy, 单核的codis-proxy, 8核的codis-proxy。codis 使用的go版本为1.7.4。
可看出,单核的codis-proxy性能最弱。8核的codis-proxy压测没有对key使用hashtag,如此相当于将请求分散到后端8个db节点上, 也可以说相当于8个阿里云的redis-proxy。自然性能数据就比较高了。
单核的阿里云redis-proxy在压力够大的情况下性能逼近原生的redis db节点。在实际生产环境中,使用原生的redis cluster,客户端需要实现cluster protocol, 解析move, ask等指令并重定向节点,随意访问key可能需要两次访问操作才能完成,性能上并不能完全如单节点一样。
redis 4.0 | 阿里云redis | codis | |
事务 | 支持相同slot | 支持相同的slot | 不支持 |
sub/pub | 支持相同slot | 支持 | 不支持 |
flushall | 支持 | 支持 | 不支持 |
select | 不支持 | 支持 | 不支持 |
mset/mget | 支持相同slot | 支持相同的slot | 不支持 |
redis4.0 cluster,codis,阿里云redis 分布式集群均实现了面对slot的管理,扩展的最小单元是slot。分布式集群中水平扩展的本质是对集群节点的路由信息管理以及数据的迁移。这3种集群迁移数据的最小单位均是key。
redis4.0、codis、阿里云redis 3种redis集群对比分析-阿里云开发者社区
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。