解析高可用Redis服务架构分析与搭建方案


Posted in Redis onJune 20, 2021

基于内存的Redis应该是目前各种web开发业务中最为常用的key-value数据库了,我们经常在业务中用其存储用户登陆态(Session存储),加速一些热数据的查询(相比较mysql而言,速度有数量级的提升),做简单的消息队列(LPUSH和BRPOP)、订阅发布(PUB/SUB)系统等等。规模比较大的互联网公司,一般都会有专门的团队,将Redis存储以基础服务的形式提供给各个业务调用。

不过任何一个基础服务的提供方,都会被调用方问起的一个问题是:你的服务是否具有高可用性?最好不要因为你的服务经常出问题,导致我这边的业务跟着遭殃。最近我所在的项目中也自己搭了一套小型的“高可用”Redis服务,在此做一下自己的总结和思考。

首先我们要定义一下对于Redis服务来说怎样才算是高可用,即在各种出现异常的情况下,依然可以正常提供服务。或者宽松一些,出现异常的情况下,只经过很短暂的时间即可恢复正常服务。所谓异常,应该至少包含了以下几种可能性:

【异常1】某个节点服务器的某个进程突然down掉(例如某开发手残,把一台服务器的redis-server进程kill了)

【异常2】某台节点服务器down掉,相当于这个节点上所有进程都停了(例如某运维手残,把一个服务器的电源拔了;例如一些老旧机器出现硬件故障)

【异常3】任意两个节点服务器之间的通信中断了(例如某临时工手残,把用于两个机房通信的光缆挖断了)

其实以上任意一种异常都是小概率事件,而做到高可用性的基本指导思想就是:多个小概率事件同时发生的概率可以忽略不计。只要我们设计的系统可以容忍短时间内的单点故障,即可实现高可用性。

对于搭建高可用Redis服务,网上已有了很多方案,例如Keepalived,Codis,Twemproxy,Redis Sentinel。其中Codis和Twemproxy主要是用于大规模的Redis集群中,也是在Redis官方发布Redis Sentinel之前twitter和豌豆荚提供的开源解决方案。我的业务中数据量并不大,所以搞集群服务反而是浪费机器了。最终在Keepalived和Redis Sentinel之间做了个选择,选择了官方的解决方案Redis Sentinel。

Redis Sentinel可以理解为一个监控Redis Server服务是否正常的进程,并且一旦检测到不正常,可以自动地将备份(slave)Redis Server启用,使得外部用户对Redis服务内部出现的异常无感知。我们按照由简至繁的步骤,搭建一个最小型的高可用的Redis服务。

方案1:单机版Redis Server,无Sentinel

解析高可用Redis服务架构分析与搭建方案

一般情况下,我们搭的个人网站,或者平时做开发时,会起一个单实例的Redis Server。调用方直接连接Redis服务即可,甚至Client和Redis本身就处于同一台服务器上。这种搭配仅适合个人学习娱乐,毕竟这种配置总会有单点故障的问题无法解决。一旦Redis服务进程挂了,或者服务器1停机了,那么服务就不可用了。并且如果没有配置Redis数据持久化的话,Redis内部已经存储的数据也会丢失。

方案2:主从同步Redis Server,单实例Sentinel

解析高可用Redis服务架构分析与搭建方案

为了实现高可用,解决方案1中所述的单点故障问题,我们必须增加一个备份服务,即在两台服务器上分别各启动一个Redis Server进程,一般情况下由master提供服务,slave只负责同步和备份。与此同时,在额外启动一个Sentinel进程,监控两个Redis Server实例的可用性,以便在master挂掉的时候,及时把slave提升到master的角色继续提供服务,这样就实现了Redis Server的高可用。这基于一个高可用服务设计的依据,即单点故障本身就是个小概率事件,而多个单点同时故障(即master和slave同时挂掉),可以认为是(基本)不可能发生的事件。

对于Redis服务的调用方来说,现在要连接的是Redis Sentinel服务,而不是Redis Server了。常见的调用过程是,client先连接Redis Sentinel并询问目前Redis Server中哪个服务是master,哪些是slave,然后再去连接相应的Redis Server进行操作。当然目前的第三方库一般都已经实现了这一调用过程,不再需要我们手动去实现(例如Nodejs的ioredis,PHP的predis,Golang的go-redis/redis,JAVA的jedis等)。

然而,我们实现了Redis Server服务的主从切换之后,又引入了一个新的问题,即Redis Sentinel本身也是个单点服务,一旦Sentinel进程挂了,那么客户端就没办法链接Sentinel了。所以说,方案2的配置并无法实现高可用性。

方案3:主从同步Redis Server,双实例Sentinel

解析高可用Redis服务架构分析与搭建方案

为了解决方案2的问题,我们把Redis Sentinel进程也额外启动一份,两个Sentinel进程同时为客户端提供服务发现的功能。对于客户端来说,它可以连接任何一个Redis Sentinel服务,来获取当前Redis Server实例的基本信息。通常情况下,我们会在Client端配置多个Redis Sentinel的链接地址,Client一旦发现某个地址连接不上,会去试图连接其他的Sentinel实例,这当然也不需要我们手动实现,各个开发语言中比较热门的redis连接库都帮我们实现了这个功能。我们预期是:即使其中一个Redis Sentinel挂掉了,还有另外一个Sentinel可以提供服务。

然而,愿景是美好的,现实却是很残酷的。如此架构下,依然无法实现Redis服务的高可用。方案3示意图中,红线部分是两台服务器之间的通信,而我们所设想的异常场景(【异常2】)是,某台服务器整体down机,不妨假设服务器1停机,此时,只剩下服务器2上面的Redis Sentinel和slave Redis Server进程。这时,Sentinel其实是不会将仅剩的slave切换成master继续服务的,也就导致Redis服务不可用,因为Redis的设定是只有当超过50%的Sentinel进程可以连通并投票选取新的master时,才会真正发生主从切换。本例中两个Sentinel只有一个可以连通,等于50%并不在可以主从切换的场景中。

你可能会问,为什么Redis要有这个50%的设定?假设我们允许小于等于50%的Sentinel连通的场景下也可以进行主从切换。试想一下【异常3】,即服务器1和服务器2之间的网络中断,但是服务器本身是可以运行的。如下图所示:

解析高可用Redis服务架构分析与搭建方案

实际上对于服务器2来说,服务器1直接down掉和服务器1网络连不通是一样的效果,反正都是突然就无法进行任何通信了。假设网络中断时我们允许服务器2的Sentinel把slave切换为master,结果就是你现在拥有了两个可以对外提供服务的Redis Server。Client做任何的增删改操作,有可能落在服务器1的Redis上,也有可能落在服务器2的Redis上(取决于Client到底连通的是哪个Sentinel),造成数据混乱。即使后面服务器1和服务器2之间的网络又恢复了,那我们也无法把数据统一了(两份不一样的数据,到底该信任谁呢?),数据一致性完全被破坏。

方案4:主从同步Redis Server,三实例Sentinel

解析高可用Redis服务架构分析与搭建方案

鉴于方案3并没有办法做到高可用,我们最终的版本就是上图所示的方案4了。实际上这就是我们最终搭建的架构。我们引入了服务器3,并且在3上面又搭建起一个Redis Sentinel进程,现在由三个Sentinel进程来管理两个Redis Server实例。这种场景下,不管是单一进程故障、还是单个机器故障、还是某两个机器网络通信故障,都可以继续对外提供Redis服务。

实际上,如果你的机器比较空闲,当然也可以把服务器3上面也开启一个Redis Server,形成1 master + 2 slave的架构,每个数据都有两个备份,可用性会提升一些。当然也并不是slave越多越好,毕竟主从同步也是需要时间成本的。

在方案4中,一旦服务器1和其他服务器的通信完全中断,那么服务器2和3会将slave切换为master。对于客户端来说,在这么一瞬间会有2个master提供服务,并且一旦网络恢复了,那么所有在中断期间落在服务器1上的新数据都会丢失。如果想要部分解决这个问题,可以配置Redis Server进程,让其在检测到自己网络有问题的时候,立即停止服务,避免在网络故障期间还有新数据进来(可以参考Redis的min-slaves-to-write和min-slaves-max-lag这两个配置项)。

至此,我们就用3台机器搭建了一个高可用的Redis服务。其实网上还有更加节省机器的办法,就是把一个Sentinel进程放在Client机器上,而不是服务提供方的机器上。只不过在公司里面,一般服务的提供方和调用方并不来自同一个团队。两个团队共同操作同一个机器,很容易因为沟通问题导致一些误操作,所以出于这种人为因素的考虑,我们还是采用了方案4的架构。并且由于服务器3上面只跑了一个Sentinel进程,对服务器资源消耗并不多,还可以用服务器3来跑一些其他的服务。

易用性:像使用单机版Redis一样使用Redis Sentinel

作为服务的提供方,我们总是会讲到用户体验问题。在上述方案当中始终有一个让Client端用的不是那么舒服的地方。对于单机版Redis,Client端直接连接Redis Server,我们只需要给一个ip和port,Client就可以使用我们的服务了。而改造成Sentinel模式之后,Client不得不采用一些支持Sentinel模式的外部依赖包,并且还要修改自己的Redis连接配置,这对于“矫情”的用户来讲显然是不能接收的。有没有办法还是像在使用单机版的Redis那样,只给Client一个固定的ip和port就可以提供服务呢?

解析高可用Redis服务架构分析与搭建方案

答案当然是肯定的。这可能就要引入虚拟IP(Virtual IP,VIP),如上图所示。我们可以把虚拟IP指向Redis Server master所在的服务器,在发生Redis主从切换的时候,会触发一个回调脚本,回调脚本中将VIP切换至slave所在的服务器。这样对于Client端来说,他仿佛在使用的依然是一个单机版的高可用Redis服务。

结语

搭建任何一个服务,做到“能用”其实是非常简单的,就像我们运行一个单机版的Redis。不过一旦要做到“高可用”,事情就会变得复杂起来。业务中使用了额外的两台服务器,3个Sentinel进程+1个Slave进程,只是为了保证在那小概率的事故中依然做到服务可用。在实际业务中我们还启用了supervisor做进程监控,一旦进程意外退出,会自动尝试重新启动。

以上就是高可用Redis服务架构分析与搭建的详细内容,更多关于高可用Redis服务架构的资料请关注三水点靠木其它相关文章!

Redis 相关文章推荐
Redis IP地址的绑定的实现
May 08 Redis
分布式锁为什么要选择Zookeeper而不是Redis?看完这篇你就明白了
May 21 Redis
比较几种Redis集群方案
Jun 21 Redis
压缩Redis里的字符串大对象操作
Jun 23 Redis
Redis Cluster集群动态扩容的实现
Jul 15 Redis
Redis Stream类型的使用详解
Nov 11 Redis
分布式Redis Cluster集群搭建与Redis基本用法
Feb 24 Redis
redis调用二维码时的不断刷新排查分析
Apr 01 Redis
sentinel支持的redis高可用集群配置详解
Apr 01 Redis
Redis特殊数据类型HyperLogLog基数统计算法讲解
Jun 01 Redis
Redis特殊数据类型Geospatial地理空间
Jun 01 Redis
浅谈Redis变慢的原因及排查方法
Jun 21 Redis
Redis基于Bitmap实现用户签到功能
redis实现的四种常见限流策略
Redis 哨兵集群的实现
Redis可视化客户端小结
Windows中Redis安装配置流程并实现远程访问功能
详解Redis复制原理
Windows下redis下载、redis安装及使用教程
You might like
PHP脚本数据库功能详解(上)
2006/10/09 PHP
基于Windows下Apache PHP5.3.1安装教程
2010/01/08 PHP
php算开始时间到过期时间的相隔的天数
2011/01/12 PHP
理解和运用PHP中的多态性[译]
2011/08/02 PHP
php 实现Hash表功能实例详解
2016/11/29 PHP
php开发最强大的IDE编辑的phpstorm 2020.2配置Xdebug调试的详细教程
2020/08/17 PHP
使用基于jquery的gamequery插件做JS乒乓球游戏
2011/07/31 Javascript
原生JavaScript生成GUID的实现示例
2014/09/05 Javascript
JavaScript实现表格点击排序的方法
2015/05/11 Javascript
基于javascript实现窗口抖动效果
2016/01/03 Javascript
javascript 实现动态侧边栏实例详解
2016/11/11 Javascript
Vue.js组件tree实现省市多级联动
2016/12/02 Javascript
JavaScript正则获取地址栏中参数的方法
2017/03/02 Javascript
Node.js使用Express创建Web项目详细教程
2017/03/31 Javascript
JavaScript自执行函数和jQuery扩展方法详解
2017/10/27 jQuery
深入浅析var,let,const的异同点
2018/08/07 Javascript
vue-cli整合vuex的时候,修改actions和mutations,实现热部署的方法
2018/09/19 Javascript
详解react native页面间传递数据的几种方式
2018/11/07 Javascript
JS数组进阶示例【数组的几种函数用法】
2020/01/16 Javascript
JavaScript缺少insertAfter解决方案
2020/07/03 Javascript
Element Badge标记的使用方法
2020/07/27 Javascript
Python实现的下载网页源码功能示例
2017/06/13 Python
深入理解Django的自定义过滤器
2017/10/17 Python
python2.7实现FTP文件下载功能
2018/04/15 Python
python3应用windows api对后台程序窗口及桌面截图并保存的方法
2019/08/27 Python
opencv python如何实现图像二值化
2020/02/03 Python
在keras中实现查看其训练loss值
2020/06/16 Python
澳大利亚家具和家居用品在线:BROSA
2017/11/02 全球购物
实习医生自我评价
2013/09/22 职场文书
黄河象教学反思
2014/02/10 职场文书
签约仪式主持词
2014/03/19 职场文书
司机岗位职责
2015/02/04 职场文书
创业计划书之香辣虾火锅
2019/09/23 职场文书
用python批量解压带密码的压缩包
2021/05/31 Python
eclipse创建项目没有dynamic web的解决方法
2021/06/24 Java/Android
基于Python实现一个春节倒计时脚本
2022/01/22 Python