当前位置:首页 » 硬盘大全 » redis路由缓存
扩展阅读
webinf下怎么引入js 2023-08-31 21:54:13
堡垒机怎么打开web 2023-08-31 21:54:11

redis路由缓存

发布时间: 2023-06-05 09:48:55

‘壹’ redis内存满了怎么办

redis内存满了解决方法:

1,增加内存。

2,使用内存淘汰策略。

3,Redis集群。

重点介绍下2、3:

第二点:

我们知道,redis设置配置文件的maxmemory参数,可以控制其最大可用内存大小(字节)。

那么当所需内存,超过maxmemory怎么办?

这个时候就该配置文件中的maxmemory-policy出场了。

其默认值是noeviction。

下面我将列出当可用内存不足时,删除redis键具有的淘汰规则。

规则说明:

1、volatile-lru

使用LRU算法删除一个键(只对设置了生存时间的键)

2、allkeys-lru

使用LRU算法删除一个键

3、volatile-random

随机删除一个键(只对设置了生存时间的键)

4、allkeys-random

随机删除一个键

5、volatile-ttl

删除生存时间最近的一个键

6、noeviction

不删除键,只返回错误

LRU算法,least RecentlyUsed,最近最少使用算法。也就是说默认删除最近最少使用的键。

但是一定要注意一点!redis中并不会准确的删除所有键中最近最少使用的键,而是随机抽取3个键,删除这三个键中最近最少使用的键。

那么3这个数字也是可以设置的,对应位置是配置文件中的maxmeory-samples.

三、集群怎么做

Redis仅支持单实例,内存一般最多10~20GB。对于内存动辄100~200GB的系统,就需要通过集群来支持了。

Redis集群有三种方式:客户端分片、代理分片、RedisCluster(在之后一篇文章详细说一下。)

1、客户端分片

通过业务代码自己实现路由

优势:可以自己控制分片算法、性能比代理的好

劣势:维护成本高、扩容/缩容等运维操作都需要自己研发

2、代理分片

代理程序接收到来自业务程序的数据请求,根据路由规则,将这些请求分发给正确的Redis实例并返回给业务程序。使用类似Twemproxy、Codis等中间件实现。

优势:运维方便、程序不用关心如何链接Redis实例

劣势:会带来性能消耗(大概20%)、无法平滑扩容/缩容,需要执行脚本迁移数据,不方便(Codis在Twemproxy基础上优化并实现了预分片来达到Auto Rebalance)。

3、Redis Cluster

优势:官方集群解决方案、无中心节点,和客户端直连,性能较好

劣势:方案太重、无法平滑扩容/缩容,需要执行相应的脚本,不方便、太新,没有相应成熟的解决案例

‘贰’ redis架构模式(4) Codis

codis是在redis官方集群方案redis-cluster发布之前便已被业界广泛使用的redis集群解决方案。

codis server :这是进行了二次开发的 Redis 实例,其中增加了额外的数据结构,支持

数据迁移操作,主要负责处理具体的数据读写请求。

codis proxy :接收客户端请求,并把请求转发给 codis server。

Zookeeper 集群 :保存集群元数据,例如数据位置信息和 codis proxy 信息。

codis dashboard 和 codis fe :共同组成了集群管理工具。其中,codis dashboard 负

责执行集群管理工作,包括增删 codis server、codis proxy 和进行数据迁移。

而 codis fe 负责提供dashboard的Web 操作界面,便于我们直接在 Web 界面上进行集群管理

codis proxy本身支持 Redis 的 RESP 交互协议,所以只需和proxy建立连接,就相当于和

整个codis集群建立连接,codis proxy 接收到请求,就会查询请求数据和 codis server 的

映射关系,并把请求转发给相应的 codis server 进行处理。当 codis server 处理完请求后,

会把结果返回给codis proxy,proxy 再把数据返回给客户端。

codis集群共有1024个slot,编号从0-1023,可以自定义或者自动均匀分配到各个server上,

使用 CRC32 算法计算数据 key 的哈希值然后对1024取模,得到slot编号,然后通过slot

和server的映射关系请求到具体实例。

Slot 和 codis server 的映射关系称为数据路由表,我们在 codis dashboard 上分配好路由表后,

dashboard 会把路由表发送给 codis proxy,同时,dashboard 也会把路由表保存在

 Zookeeper 中。codis-proxy 会把路由表缓存在本地,当它接收到客户端请求后,直接查询

本地的路由表进而完成请求的转发。

跟redis cluster的区别是,codis的把映射关系存在zookeeper和proxy中,redis cluster

是通过各个实例相互通信进而存在各个实例本地,当实例较多时,可能会受到网络状况

影响而且整个集群的网络资源消耗相较之下也比较大。

codis按照slot粒度进行数据迁移,比如从serverA迁移至serverB,会从A要迁移的slot中

随机选择一个数据,发送给B,A得到确认消息后,删除本地数据。重复这个过程直到

迁移完毕。

迁移方式有两种,一种是同步迁移,一种是异步迁移。

同步迁移时,源server是阻塞的,由于迁移过程中需要数据的序列化、网络传输、删除

等等操作,如果是bigkey则会阻塞较长时间。

所以通常采用异步迁移。异步迁移模式下,源server发送完迁移数据之后,就可以继续接受

处理请求,等到目标server返回ack之后,再执行删除操作,这个过程中,为了防止数据

不一致的情况,被迁移的数据是只读模式。

对于bigkey的数据,异步迁移采用拆分指令的方式,对bigkey中的每个元素用一条指令

进行迁移,而不是等待整个数据序列化完毕再迁移,进而避免了需要序列化大量数据而

阻塞的风险,因为整个bigkey的数据是被拆分的,如果迁移期间server宕机,便会破坏

迁移的原子性,所以codis对bigkey的数据设置了一个过期时间,如果原子性遭到破坏,

目标server的数据就会过期后删除以保证迁移的原子性。可以通过异步迁移命令 

SLOTSMGRTTAGSLOT-ASYNC 的参数numkeys 设置每次迁移的 key 数量,以提升

bigkey异步迁移的速度。

最后分享一下我的一些架构设计心得,通过redis cluster和codis的slot设计,把原本

数百上千万的key和实例的映射抽象成slot和实例的映射,大大减少了映射量,进而把

slot和key的映射分担到各个实例中,这种粒度的抽象,值得借鉴,另外codis与redis cluser

不同的是,codis通过zk作为第三方储存系统,保存了集群实例、状态和slot分配等信息,

这样避免了实例间的大量心跳来同步集群状态进而减少实例间的网络通信量,对于实现

大规模的集群是有益的设计,网络带宽可以集中在客户端请求上面,需要注意的是需要保证

zk的通信带宽,毕竟集群的信息同步都交给了zk

‘叁’ 京东面试官:Redis 这些我必问

缓存好处:高性能 + 高并发


数据库查询耗费了800ms,其他用户对同一个数据再次查询 ,假设该数据在10分钟以内没有变化过,并且 10 分钟之内有 1000 个用户 都查询了同一数据,10 分钟之内,那 1000 每个用户,每个人查询这个数据都感觉很慢 800ms
比如 :某个商品信息,在 一天之内都不会改变,但是这个商品每次查询一次都要耗费2s,一天之内被浏览 100W次
mysql 单机也就 2000qps,缓存单机轻松几万几十万qps,单机 承载并发量是 mysql 单机的几十倍。


在中午高峰期,有 100W 个用户访问系统 A,每秒有 4000 个请求去查询数据库,数据库承载每秒 4000 个请求会宕机,加上缓存后,可以 3000 个请求走缓存 ,1000 个请求走数据库。
缓存是走内存的,内存天然可以支撑4w/s的请求,数据库(基于磁盘)一般建议并发请求不要超过 2000/s

redis 单线程 ,memcached 多线程
redis 是单线程 nio 异步线程模型

一个线程+一个队列

redis 基于 reactor 模式开发了网络事件处理器,这个处理器叫做文件事件处理器,file event handler,这个文件事件处理器是单线程的,所以redis 是单线程的模型,采用 io多路复用机制同时监听多个 socket,根据socket上的事件来选择对应的事件处理器来处理这个事件。
文件事件处理器包含:多个 socket,io多路复用程序,文件事件分派器,事件处理器(命令请求处理器、命令恢复处理器、连接应答处理器)
文件事件处理器是单线程的,通过 io 多路复用机制监听多个 socket,实现高性能和线程模型简单性
被监听的 socket 准备好执行 accept,read,write,close等操作的时候,会产生对应的文件事件,调用之前关联好的时间处理器处理
多个 socket并发操作,产生不同的文件事件,i/o多路复用会监听多个socket,将这些 socket放入一个队列中排队。事件分派器从队列中取出socket给对应事件处理器。
一个socket时间处理完后,事件分派器才能从队列中拿到下一个socket,给对应事件处理器来处理。

文件事件:
AE_READABLE 对应 socket变得可读(客户端对redis执行 write操作)
AE_WRITABLE 对应 socket 变得可写(客户端对 redis执行 read操作)
I/O 多路复用可以同时监听AE_REABLE和 AE_WRITABLE ,如果同时达到则优先处理 AE_REABLE 时间
文件事件处理器:
连接应答处理器 对应 客户端要连接 redis
命令请求处理器 对应 客户端写数据到 redis
命令回复处理器 对应 客户端从 redis 读数据

流程:

一秒钟可以处理几万个请求

普通的 set,get kv缓存

类型 map结构,比如一个对象(没有嵌套对象)缓存到 redis里面,然后读写缓存的时候,可以直接操作hash的字段(比如把 age 改成 21,其他的不变)
key=150
value = {

}

有序列表 ,元素可以重复
可以通过 list 存储一些列表型数据结构,类似粉丝列表,文章评论列表。
例如:微信大 V的粉丝,可以以 list 的格式放在 redis 里去缓存
key=某大 V value=[zhangsan,lisi,wangwu]
比如 lrange 可以从某个元素开始读取多少个元素,可以基于 list 实现分页查询功能,基于 redis实现高性能分页,类似微博下来不断分页东西。
可以搞个简单的消息队列,从 list头怼进去(lpush),list尾巴出来 (brpop)

无序集合,自动去重
需要对一些数据快速全局去重,(当然也可以基于 HashSet,但是单机)
基于 set 玩差集、并集、交集的操作。比如:2 个人的粉丝列表整一个交集,看看 2 个人的共同好友是谁?
把 2 个大 V 的粉丝都放在 2 个 set中,对 2 个 set做交集(sinter)

排序的 set,去重但是可以排序,写进去的时候给一个分数,自动根据分数排序

排行榜:

zadd board score username

例如:
zadd board 85 zhangsan
zadd board 72 wangwu
zadd board 96 lis
zadd board 62 zhaoliu

自动排序为:
96 lisi
85 zhangsan
72 wangwu
62 zhaoliu

获取排名前 3 的用户 : zrevrange board 0 3
96 lisi
85 zhangsan
72 wangwu

查看zhaoliu的排行 :zrank board zhaoliu 返回 4

内存是宝贵的,磁盘是廉价的
给key设置过期时间后,redis对这批key是定期删除+惰性删除
定期删除:
redis 默认每隔 100ms随机抽取一些设置了过期时间的 key,检查其是否过期了,如果过期就删除。
注意:redis是每隔100ms随机抽取一些 key来检查和删除,而不是遍历所有的设置过期时间的key(否则CPU 负载会很高,消耗在检查过期 key 上)
惰性删除:
获取某个key的时候, redis 会检查一下,这个key如果设置了过期时间那么是否过期,如果过期了则删除。
如果定期删除漏掉了许多过期key,然后你也没及时去查,也没走惰性删除,如果大量过期的key堆积在内存里,导致 redis 内存块耗尽,则走内存淘汰机制。

内存淘汰策略:

LRU 算法:

缓存架构(多级缓存架构、热点缓存)
redis 高并发瓶颈在单机,读写分离,一般是支撑读高并发,写请求少,也就 一秒一两千,大量请求读,一秒钟二十万次。


一主多从,主负责写,将数据同步复制到其他 slave节点,从节点负责读,所有读的请求全部走从节点。主要是解决读高并发。、
主从架构->读写分离->支撑10W+读QPS架构


master->slave 复制,是异步的
核心机制:

master持久化对主从架构的意义:
如果开启了主从架构,一定要开启 master node的持久化,不然 master宕机重启数据是空的,一经复制,slave的数据也丢了

主从复制原理:


第一次启动或者断开重连情况:

正常情况下:
master 来一条数据,就异步给 slave

全年 99.99%的时间,都是出于可用的状态,那么就可以称为高可用性
redis 高可用架构叫故障转移,failover,也可以叫做主备切换,切换的时间不可用,但是整体高可用。
sentinal node(哨兵)

作用:


quorum = 1 (代表哨兵最低个数可以尝试故障转移,选举执行的哨兵)
master 宕机,只有 S2 存活,因为 quorum =1 可以尝试故障转移,但是没达到 majority =2 (最低允许执行故障转移的哨兵存活数)的标准,无法执行故障转移


如果 M1 宕机了,S2,S3 认为 master宕机,选举一个执行故障转移,因为 3 个哨兵的 majority = 2,所以可以执行故障转移

丢数据:

解决方案:

sdown 主观宕机,哨兵觉得一个 master 宕机(ping 超过了 is-master-down-after-milliseconds毫秒数)
odown 客观宕机,quorum数量的哨兵都觉得 master宕机
哨兵互相感知通过 redis的 pub/sub系统,每隔 2 秒往同一个 channel里发消息(自己的 host,ip,runid),其他哨兵可以消费这个消息
以及同步交换master的监控信息。
哨兵确保其他slave修改master信息为新选举的master
当一个 master被认为 odown && marjority哨兵都同意,那么某个哨兵会执行主备切换,选举一个slave成为master(考虑 1. 跟master断开连接的时长 2. slave 优先级 3.复制 offset 4. runid)
选举算法:

quorum 数量哨兵认为odown->选举一个哨兵切换->获得 majority哨兵的授权(quorum majority 需要 majority个哨兵授权,quorum >= majority 需要 quorum 哨兵授权)
第一个选举出来的哨兵切换失败了,其他哨兵等待 failover-time之后,重新拿confiuration epoch做为新的version 切换,保证拿到最新配置,用于 configuration传播(通过 pu/sub消息机制,其他哨兵对比 version 新旧更新 master配置)

高并发:主从架构
高容量:Redis集群,支持每秒几十万的读写并发
高可用:主从+哨兵

持久化的意义在于故障恢复数据备份(到其他服务器)+故障恢复(遇到灾难,机房断电,电缆被切)

AOF 只有一个,Redis 中的数据是有一定限量的,内存大小是一定的,AOF 是存放写命令的,当大到一定的时候,AOF 做 rewrite 操作,就会基于当时 redis 内存中的数据,来重新构造一个更小的 AOF 文件,然后将旧的膨胀很大的文件给删掉,AOF 文件一直会被限制在和Redis内存中一样的数据。AOF同步间隔比 RDB 小,数据更完整

优点:

缺点:

AOF 存放的指令日志,数据恢复的时候,需要回放执行所有指令日志,RDB 就是一份数据文件,直接加载到内存中。

优点:

缺点:

AOF 来保证数据不丢失,RDB 做不同时间的冷备


支持 N 个 Redis master node,每个 master node挂载多个 slave node
多master + 读写分离 + 高可用

数据量很少,高并发 -> replication + sentinal 集群
海量数据 + 高并发 + 高可用 -> redis cluster

hash算法->一致性 hash 算法-> redis cluster->hash slot算法

redis cluster :自动对数据进行分片,每个 master 上放一部分数据,提供内置的高可用支持,部分master不可用时,还是可以继续工作
cluster bus 通过 16379进行通信,故障检测,配置更新,故障转移授权,另外一种二进制协议,主要用于节点间进行高效数据交换,占用更少的网络带宽和处理时间

key进行hash,然后对节点数量取模,最大问题只有任意一个 master 宕机,大量数据就要根据新的节点数取模,会导致大量缓存失效。


key进行hash,对应圆环上一个点,顺时针寻找距离最近的一个点。保证任何一个 master 宕机,只受 master 宕机那台影响,其他节点不受影响,此时会瞬间去查数据库。
缓存热点问题:
可能集中在某个 hash区间内的值特别多,那么会导致大量的数据都涌入同一个 master 内,造成 master的热点问题,性能出现瓶颈。
解决方法:
给每个 master 都做了均匀分布的虚拟节点,这样每个区间内大量数据都会均匀的分布到不同节点内,而不是顺时针全部涌入到同一个节点中。

redis cluster 有固定 16384 个 hash slot,对每个key计算 CRC16 值,然后对16384取模,可以获取 key对应的 hash slot
redis cluster 中每个 master 都会持有部分 slot ,当一台 master 宕机时候,会最快速度迁移 hash slot到可用的机器上(只会短暂的访问不到)
走同一个 hash slot 通过 hash tag实现


集群元数据:包括 hashslot->node之间的映射表关系,master->slave之间的关系,故障的信息
集群元数据集中式存储(storm),底层基于zookeeper(分布式协调中间件)集群所有元数据的维护。好处:元数据的更新和读取,时效性好,一旦变更,其他节点立刻可以感知。缺点:所有元数据的更新压力全部集中在一个地方,可能会导致元数据的存储有压力。
goosip: 好处:元数据的更新比较分散,有一定的延时,降低了压力。缺点:更新有延时,集群的一些操作会滞后。(reshared操作时configuration error)

自己提供服务的端口号+ 10000 ,每隔一段时间就会往另外几个节点发送ping消息,同时其他几点接收到ping之后返回pong

故障信息,节点的增加和移除, hash slot 信息

meet:某个节点发送 meet给新加入的节点,让新节点加入集群中,然后新节点就会开始于其他节点进行通信
ping:每个节点都会频繁给其他节点发送ping,其中包含自己的状态还有自己维护的集群元数据,互相通过ping交换元数据
ping:返回ping和meet,包含自己的状态和其他信息
fail:某个节点判断另一个节点fail之后,就发送 fail 给其他节点,通知其他节点,指定的节点宕机了

ping 很频繁,且携带元数据,会加重网络负担
每个节点每秒会执行 10 次 ping,每次选择 5 个最久没有通信的其他节点
当如果发现某个节点通信延迟达到了 cluster_node_timeout /2 ,那么立即发送 ping, 避免数据交换延迟过长,落后时间太长(2 个节点之间 10 分钟没有交换数据,整个集群处于严重的元数据不一致的情况)。
每次ping,一个是带上自己的节点信息,还有就是带上1/10其他节点的信息,发送出去,进行数据交换
至少包含 3 个其他节点信息,最多包含总节点-2 个其他节点的信息

客户端发送到任意一个redis实例发送命令,每个redis实例接受到命令后,都会计算key对应的hash slot,如果在本地就本地处理,否则返回moved给客户端,让客户端进行重定向 (redis-cli -c)

通过tag指定key对应的slot,同一个 tag 下的 key,都会在一个 hash slot中,比如 set key1:{100} 和 set key2:{100}

本地维护一份hashslot->node的映射表。
JedisCluster 初始化的时候,随机选择一个 node,初始化 hashslot->node 映射表,同时为每个节点创建一个JedisPool连接池,每次基于JedisCluster执行操作,首先JedisCluster都会在本地计算key的hashslot,然后再本地映射表中找到对应的节点,如果发现对应的节点返回moved,那么利用该节点的元数据,更新 hashslot->node映射表(重试超过 5 次报错)

hash slot正在迁移,那么会返回ask 重定向给jedis,jedis 接受到ask重定向之后,,会重定向到目标节点去执行

判断节点宕机:
如果一个节点认为另外一个节点宕机了, 就是pfail,主观宕机
如果多个节点都认为另外一个节点宕机了,那么就是fail,客观宕机(跟哨兵原理一样)
在cluster-node-timeout内,某个节点一直没有返回 pong,那么就被认为是 pfail
如果一个节点认为某个节点pfail了,那么会在gossip消息中,ping给其他节点,如果超过半数的节点认为pfail了,那么就会变成fail。
从节点过滤:
对宕机的 mster node ,从其所有的 slave node中,选择一个切换成 master node
检查每个 slave node与master node断开连接的时间,如果超过了cluster-node-timeout * cluster-slave-validity-factor,那么就没资格切换成 master(和哨兵一致)
从节点选举:
每个从节点,根据自己对 master 复制数据的 offset,设置一个选举时间,offset越大(复制数据越多)的从节点,选举时间越靠前,所有的 master node 开始投票,给要进行选举的 slave进行投票,如果大部分 master node(N/2 +1) 都投票给某个从节点,那么选举通过,从节点执行主备切换,从节点切换成主节点
总结:和哨兵很像,直接集成了 replication 和 sentinal

方案:
事前:保证 redis 集群高可用性 (主从+哨兵或 redis cluster),避免全盘崩溃
事中:本地 ehcache 缓存 + hystrix 限流(保护数据库) & 降级,避免 MySQL被打死
事后: redis持久化,快速恢复缓存数据,继续分流高并发请求

限制组件每秒就 2000 个请求通过限流组件进入数据库,剩余的 3000 个请求走降级,返回一些默认 的值,或者友情提示
好处 :


4000 个请求黑客攻击请求数据库里没有的数据
解决方案:把黑客查数据库中不存在的数据的值,写到缓存中,比如: set -999 UNKNOWN


读的时候,先读缓存,缓存没有,就读数据库,然后取出数据后放入缓存,同时返回响应
更新的时候,删除缓存,更新数据库
为什么不更新缓存:
更新缓存代价太高(更新 20 次,只读 1 次),lazy思想,需要的时候再计算,不需要的时候不计算

方案:先删除缓存,再修改数据库


方案:写,读路由到相同的一个内存队列(唯一标识,hash,取模)里,更新和读操作进行串行化(后台线程异步执行队列串行化操作),(队列里只放一个更新查询操作即可,多余的过滤掉,内存队列里没有该数据更新操作,直接返回 )有该数据更新操作则轮询取缓存值,超时取不到缓存值,直接取一次数据库的旧值


TP 99 意思是99%的请求可以在200ms内返回
注意点:多个商品的更新操作都积压在一个队列里面(太多操作积压只能增加机器),导致读请求发生大量的超时,导致大量的读请求走数据库
一秒 500 写操作,每200ms,100 个写操作,20 个内存队列,每个队列积压 5 个写操作,一般在20ms完成


方案:分布式锁 + 时间戳比较

10台机器,5 主 5 从,每个节点QPS 5W ,一共 25W QPS(Redis cluster 32G + 8 核 ,Redis 进程不超过 10G)总内存 50g,每条数据10kb,10W 条数据1g,200W 条数据 20G,占用总内存不到50%,目前高峰期 3500 QPS

作者: mousycoder

‘肆’ 如何实现高可用的 redis 集群

Redis 因具有丰富的数据结构和超高的性能以及简单的协议,使其能够很好的作为数据库的上游缓存层。但在大规模的 Redis 使用过程中,会受限于多个方面:单机内存有限、带宽压力、单点问题、不能动态扩容等。

基于以上, Redis 集群方案显得尤为重要。通常有 3 个途径:官方 Redis Cluster ;通过 Proxy 分片;客户端分片 (Smart Client) 。以上三种方案各有利弊。

Redis Cluster( 官方 ) :虽然正式版发布已经有一年多的时间,但还缺乏最佳实践;对协议进行了较大修改,导致主流客户端也并非都已支持,部分支持的客户端也没有经过大规模生产环境的验证;无中心化设计使整个系统高度耦合,导致很难对业务进行无痛的升级。

Proxy :现在很多主流的 Redis 集群都会使用 Proxy 方式,例如早已开源的 Codis 。这种方案有很多优点,因为支持原声 redis 协议,所以客户端不需要升级,对业务比较友好。并且升级相对平滑,可以起多个 Proxy 后,逐个进行升级。但是缺点是,因为会多一次跳转,平均会有 30% 左右的性能开销。而且因为原生客户端是无法一次绑定多个 Proxy ,连接的 Proxy 如果挂了还是需要人工参与。除非类似 Smart Client 一样封装原有客户端,支持重连到其他 Proxy ,但这也就带来了客户端分片方式的一些缺点。并且虽然 Proxy 可以使用多个,并且可以动态增加 proxy 增加性能,但是所有客户端都是共用所有 proxy ,那么一些异常的服务有可能影响到其他服务。为每个服务独立搭建 proxy ,也会给部署带来额外的工作。

而我们选择了第三种方案,客户端分片 (Smart Client) 。客户端分片相比 Proxy 拥有更好的性能,及更低的延迟。当然也有缺点,就是升级需要重启客户端,而且我们需要维护多个语言的版本,但我们更爱高性能。

下面我们来介绍一下我们的Redis集群:

概貌:

如图0所示,

我们的 Redis 集群一共由四个角色组成:

Zookeeper :保存所有 redis 集群的实例地址, redis 实例按照约定在特定路径写入自身地址,客户端根据这个约定查找 redis 实例地址,进行读写。

Redis 实例:我们修改了 redis 源码,当 redis 启动或主从切换时,按照约定自动把地址写到 zookeeper 特定路径上。

Sentinel : redis 自带的主从切换工具,我们通过 sentinel 实现集群高可用。

客户端( Smart Client ):客户端通过约定查找 redis 实例在 ZooKeeper 中写入的地址。并且根据集群的 group 数,进行一致性哈希计算,确定 key 唯一落入的 group ,随后对这个 group 的主库进行操作。客户端会在Z ooKeeper 设置监视,当某个 group 的主库发生变化时,Z ooKeeper 会主动通知客户端,客户端会更新对应 group 的最新主库。

我们的Redis 集群是以业务为单位进行划分的,不同业务使用不同集群(即业务和集群是一对一关系)。一个 Redis 集群会由多个 group 组成 ( 一个 group 由一个主从对 redis 实例组成 ) 。即 group 越多,可以部署在更多的机器上,可利用的内存、带宽也会更多。在图0中,这个业务使用的 redis 集群由 2 个 group 组成,每个 group 由一对主从实例组成。

Failover

如图1所示,

当 redis 启动时,会 把自己的 IP:Port 写入到 ZooKeeper 中。其中的 主实例模式启动时会在 /redis/ 业务名 / 组名 永久节点写入自己的 IP:Port (如果节点不存在则创建)。由 主模式 变成 从模式 时,会创建 /redis/ 业务名 / 组名 /slaves/ip:port 临时节 点,并写入自己的 IP:Port (如果相同节点已经存在,则先删除,再创建)。而从实例 模式 启动时会创建 /redis/ 业务名 / 组名 /slaves/ip:port 临时节点,并写入自己的 ip:port (如果相同节点已经存在,则先删除,再创建)。由 从模式 变成 主模式 时,先删除 /redis/ 业务名 / 组名 /slaves/ip:port 临时节点,并在 /redis/ 业务名 / 组名 永久节点写入自己的 IP:Port 。

ZooKeeper 会一直保存当前有效的 主从实例 IP:Port 信息。至于主从自动切换过程,使用 redis 自带的 sentinel 实现,现设置为超过 30s 主 server 无响应,则由 sentinel 进行主从实例的切换,切换后就会触发以主、从实例通过以上提到的一系列动作,从而完成最终的切换。

而客户端侧通过给定业务名下的所有 groupName 进行一致性哈希计算,确定 key 落入哪个组。 客户端启动时,会从 ZooKeeper 获取指定业务名下所有 group 的 主从 IP:Port ,并在 ZooKeeper 中设置监视(监视的作用是当 ZooKeeper 的节点发生变化时,会主动通知客户端)。若客户端从 Zookeeper 收到节点变化通知,会重新获取最新的 主从 I:Port ,并重新设置监视( ZooKeeper 监视是一次性的)。通过此方法,客户端可以实时获知当前可访问最新的 主从 IP:Port 信息。

因为我们的所有 redis 实例信息都按照约定保存在 ZooKeeper 上,所以不需要针对每个实例部署监控,我们编写了一个可以自动通过 ZooKeeper 获取所有 redis 实例信息,并且监控 cpu 、 qps 、内存、主从延迟、主从切换、连接数等的工具。

发展:

现在 redis 集群在某些业务内存需求超过预期很多后,无法通过动态扩容进行扩展。所以我们正在做动态扩容的支持。原先的客户端我们是通过一致性哈希进行 key 的
路由策略,但这种方式在动态扩容时会略显复杂,所以我们决定采用实现起来相对简单的预分片方式。一致性哈希的好处是可以无限扩容,而预分片则不是。预分片
时我们会在初始化阶段指定一个集群的所有分片数量,这个数量一旦指定就不能再做改变,这个预分片数量就是后续可以扩容到最大的 redis 实例数。假设预分片 128 个 slot ,每个实例 10G 也可以达到 TB 级别的集群,对于未来数据增长很大的集群我们可以预分片 1024 ,基本可以满足所有大容量内存需求了。

原先我们的 redis 集群有四种角色, Smart Client, redis , sentinel , ZooKeeper 。为了支持动态扩容,我们增加了一个角色, redis_cluster_manager (以下简称 manager ),用于管理 redis 集群。主要工作是初始化集群(即预分片),增加实例后负责修改Z ooKeeper 状态,待客户端做好准备后迁移数据到新增实例上。为了尽量减少数据迁移期间对现性能带来的影响,我们每次只会迁移一个分片的数据,待迁移完成,再进行下一个分片的迁移。

如图2所示

相比原先的方案,多了 slots 、M anager Lock 、 clients 、M igrating Clients 节点。

Slots: 所有分片会把自身信息写入到 slots 节点下面。 Manager 在初始化集群时,根据设置的分片数,以及集群下的 group 数,进行预分片操作,把所有分片均匀分配给已有 group 。分片的信息由一个 json 串组成,记录有分片的状态 (stats) ,当前拥有此分片的 group(src) ,需要迁移到的 group(dst) 。分片的状态一共有三种: online 、 pre_migrate 、 migrating 。

Online 指这个分片处于正常状态,这时 dst 是空值,客户端根据 src 的 group 进行读写。

Pre_migrate 是指这个分片被 manager 标记为需要迁移,此时 dst 仍然为空, manager 在等所有 client 都已经准备就绪,因为 ZooKeeper 回掉所有客户端有时间差,所以如果某些 client 没有准备就绪的时候 manager 进行了数据迁移,那么就会有数据丢失。

Migrating 是 manager 确认了所有客户端都已经做好迁移准备后,在 dst 写入此分片需要迁移的目标 group 。待迁移完成,会在 src 写入目标 group_name , dst 设为空, stats 设为 online 。

Manager Lock: 因为我们是每次只允许迁移一个 slot ,所以不允许超过一个 manager 操作一个集群。所以 manager 在操作集群前,会在M anager Lock 下注册临时节点,代表这个集群已经有 manager 在操作了,这样其他 manager 想要操作这个集群时就会自动退出。

Clients 和M igrating Clients 是为了让 manager 知道客户端是否已经准备就绪的节点。客户端通过 uid 代表自己,格式是 客户端语言 _ 主机名 _pid 。当集群没有进行迁移,即所有分片都是 online 的时候,客户端会在 clients 下创建 uid 的临时节点。

当某个 slot 从 online 变成 pre_migrate 后,客户端会删除 clients 下的 uid 临时节点,然后在M igrating Clients 创建 uid 临时节点。注意,因为需要保证数据不丢失,从 pre_migrate 到 migrating 期间,这个 slot 是被锁定的,即所有对这个 slot 的读写都会被阻塞。所以 mananger 会最多等待 10s ,确认所有客户端都已经切换到准备就绪状态,如果发现某个客户端一直未准备就绪,那么 mananger 会放弃此次迁移,把 slot 状态由 pre_migrate 改为 online 。如果客户端发现 slot 状态由 pre_migrate 变成 online 了,那么会删除 migrating_clients 下的 uid 节点,在 clients 下重新创建 uid 节点。还需要注意的一点是,有可能一个客户刚启动,并且正在往 clients 下创建 uid 节点,但是因为网络延迟还没创建完成,导致 manager 未确认到这个 client 是否准备就绪,所以 mananger 把 slot 改为 pre_migrate 后会等待 1s 再确认所有客户端是否准备就绪。

如果 Manager 看到 clients 下已经没有客户端的话(都已经准备就绪),会把 slot 状态改为 migrating 。 Slot 变成 migrating 后,锁定也随之解除, manager 会遍历 src group 的数据,把对应 slot 的数据迁移到 dst group 里。客户端在 migrating 期间如果有读写 migrating slot 的 key ,那么客户端会先把这个 key 从 src group 迁移到 dst group ,然后再做读写操作。即这期间客户端性能会有所下降。这也是为什么每次只迁移一个 slot 的原因。这样即使只有 128 个分片的集群,在迁移期间受到性能影响的 key 也只有 1/128 ,是可以接受的。

Manager 发现已经把 slot 已经迁移完毕了,会在 src 写入目标 group_name , dst 设为空, stats 设为 online 。客户端也删除 migrating_clients 下的 uid ,在 clients 下创建 uid 节点。

‘伍’ 面试问题redis有哪些集群方案

P2P模式,无中心化
把key分成16384个slot
每个实例负责一部分slot
客户端请求若不在连接的实例,该实例会转发给对应的实例。
通过Gossip协议同步节点信息

优点:
- 组件all-in-box,部署简单,节约机器资源
- 性能比proxy模式好
- 自动故障转移、Slot迁移中数据可用
- 官方原生集群方案,更新与支持有保障

缺点:
- 架构比较新,最佳实践较少
- 多键操作支持有限(驱动可以曲线救国)
- 为了性能提升,客户端需要缓存路由表信息
- 节点发现、reshard操作不够自动化