当前位置: 首页 > 科技观察

搭建一个高可用的Redis服务,需要注意这几个方面!

时间:2023-03-16 16:06:16 科技观察

内存式Redis应该是各种web开发业务中最常用的key-value数据库。我们在业务中经常用它来存储用户登录状态(session存储)来加速一些热点数据的查询(相对于mysql而言,速度有一个数量级的提升),简单的消息队列(LPUSH和BRPOP)、订阅和发布(PUB/SUB)系统等。大型互联网公司一般都有专门的团队,以基础服务的形式为各种业务调用提供Redis存储。但是,任何基础服务的提供者都会被调用者问一个问题:你们的服务是否高可用?***不要因为您的服务经常出现问题而使我的业务受到影响。最近项目中也搭建了一套小型的“高可用”Redis服务,在此做一下自己的总结和思考。首先需要定义什么是Redis服务的高可用,即在各种异常情况下仍然可以正常提供服务。或者更宽松一点,在出现异常的情况下,只需要很短的时间就可以恢复正常服务。所谓异常,至少应该包括以下几种可能:【异常一】某节点服务器进程突然宕机(比如开发者禁用了某服务器的redis-server进程并kill掉)【异常二】某节点服务器宕机,表示该节点上的所有进程都停止了(比如某个运维手被禁用,某个服务器的电源被拔掉;比如一些老机器出现硬件故障)【异常3】任意两个节点服务器之间的通信中断(比如临时工断手,剪断了两个机房通信的光缆)。其实以上任何一种异常都是小概率事件,实现高可用的基本指导思想就是:多个小概率事件同时发生的概率可以忽略不计。只要我们将系统设计为在短时间内容忍单点故障,就可以实现高可用性。对于构建高可用的Redis服务,网上已经有很多解决方案,比如Keepalived、Codis、Twemproxy、RedisSentinel等。其中Codis和Twemproxy主要用于大型Redis集群,也是Redis官方发布RedisSentinel之前twitter和豌豆荚提供的开源解决方案。我的业务数据量不大,搞集群服务很浪费机器。最终我在Keepalived和RedisSentinel之间做出了选择,选择了官方的方案RedisSentinel。RedisSentinel可以理解为一个监控RedisServer服务是否正常的进程,一旦检测到异常,会自动启用备份(slave)RedisServer,让外部用户察觉不到内部发生的异常服务。我们按照从简单到复杂的步骤来构建一个最小的高可用性Redis服务。方案一:没有Sentinel的单机版RedisServer一般情况下,我们在搭建个人网站,或者平时做开发的时候,都会搭建一个RedisServer的单实例。调用者可以直接连接Redis服务,甚至Client和Redis在同一台服务器上。这种组合只适合个人学习和娱乐。毕竟这个配置总会存在无法解决的单点故障。一旦Redis服务进程挂掉,或者服务器1宕机,服务不可用。而如果不配置Redis数据持久化,Redis中已经存在的数据也会丢失。方案二:RedisServer主从同步,单实例Sentinel为了实现高可用,我们必须针对方案一中描述的单点故障问题添加一个备份服务,即在每一个上启动一个RedisServer进程两台服务器,一般情况下由master提供服务,slave只负责同步和备份。同时启动一个额外的Sentinel进程来监控两个RedisServer实例的可用性,以便在master挂掉时,及时将slave提升为master的角色继续提供服务,从而实现Redis服务器的高可用性。这是基于一个高可用的服务设计基础,即单点故障本身是一个小概率事件,多个单点同时故障(即master和slave同时挂掉)同时)可以被认为是(基本上)不可能的事件。对于Redis服务的调用者,现在需要连接的是RedisSentinel服务,而不是RedisServer。常见的调用流程是客户端先连接RedisSentinel,询问当前RedisServer中哪些服务是master,哪些是slave,然后连接到对应的RedisServer进行操作。当然,目前的第三方库已经普遍实现了这个调用过程,我们不再需要手动实现(比如Nodejs的ioredis,PHP的predis,Golang的go-redis/redis,JAVA的jedis等。).但是,在我们实现了RedisServer服务的主从切换之后,又引入了一个新的问题,那就是RedisSentinel本身也是一个单点服务。一旦Sentinel进程挂掉,客户端就无法连接到Sentinel。所以方案2的配置无法实现高可用。方案三:主从同步RedisServer,双实例Sentinel为了解决方案二的问题,我们还额外启动了一个RedisSentinel进程,两个Sentinel进程同时为客户端提供服务发现功能。对于客户端来说,可以连接任意一个RedisSentinel服务,获取当前RedisServer实例的基本信息。通常,我们会在客户端配置多个RedisSentinel链接地址。一旦客户端发现某个地址无法连接,就会尝试连接其他的Sentinel实例。当然,这并不需要我们手动去实现。在各个开发语言中比较流行的redis连接库都帮助我们实现了这个功能。我们的期望是:即使其中一个RedisSentinel挂了,还有另一个Sentinel可以提供服务。然而,愿景是美好的,现实却是残酷的。在这样的架构下,仍然无法实现Redis服务的高可用。在方案三的示意图中,红线部分是两台服务器之间的通信,我们预想的异常场景(【异常2】)是某台服务器整体宕机。可以假定服务器1已关闭。此时服务器2上只有RedisSentinel和slaveRedisServer进程,此时Sentinel不会将剩下的slave切换到master继续服务,会导致Redis服务不可用,因为Redis的设置是only当超过50%的Sentinel进程可以连接并投票选出新的master时,才会发生真正的主从切换。在这个例子中,两个Sentinels只能连接一个,等于50%,不属于可以主从切换的场景。你可能会问,为什么Redis会有这个50%的设置?假设我们允许在少于或等于50%的Sentinels连接的场景下进行主从切换。试想【异常3】,即服务器1和服务器2之间的网络中断了,但是服务器本身可以运行。如下图所示:其实对于服务器2来说,直接关闭服务器1和服务器1无法联网的效果是一样的。反正就是突然间无法进行任何通讯了。假设我们让服务器2的Sentinel在网络中断的时候将slave切换为master。这样一来,你现在就有了两台可以对外提供服务的RedisServer。Client的任何增删改查都可能落在Server1的Redis上,也可能落在Server2的Redis上(取决于Client连接的是哪个Sentinel),造成数据混乱。即使后面服务器1和服务器2之间的网络恢复了,我们也无法统一数据(两个不同的数据,我们该相信谁?),数据的一致性就会被彻底破坏。方案四:主从同步RedisServer,三个Sentinel实例由于方案三无法做到高可用,我们最终的版本是上图方案四。事实上,这就是我们最终构建的架构。我们引入了server3,在3上设置了一个RedisSentinel进程,现在三个Sentinel进程管理着两个RedisServer实例。在这种场景下,无论是单进程故障,单机故障,还是两台机器之间的网络通信故障,都可以继续对外提供Redis服务。其实如果你的机器比较空闲,当然你也可以在Server3上开一个RedisServer,组成1主+2从的架构。每个数据都有两个备份,可用性会提高。当然也不是说slave越多越好,毕竟主从同步也是需要时间成本的。在场景4中,一旦服务器1和其他服务器的通信完全中断,那么服务器2和3就会从slave切换到master。对于client来说,此时会有2个master在服务,一旦网络恢复,宕机期间落在server1上的所有新数据都会丢失。如果想部分解决这个问题,可以配置RedisServer进程在检测到自身网络出现问题时立即停止服务,避免在网络故障时有新的数据进来(参考Redis的min-slaves-to-write和min-slaves-max-lag这两个配置项)。至此,我们用3台机器搭建了一个高可用的Redis服务。其实网上有一个比较省机器的办法,就是在Client机器上放一个Sentinel进程,而不是服务提供者的机器。只是在公司里,一般服务的提供者和调用者不是来自同一个团队。两个团队一起操作同一台机器,很容易因为通信问题造成一些误操作,所以出于人为因素的考虑,我们还是采用方案4的架构。而且由于服务器3上只有一个Sentinel进程运行,所以不会占用太多的服务器资源,服务器3还可以用来运行一些其他的服务。易用性:使用RedisSentinel作为服务提供者就像使用单机版的Redis一样,我们总是谈论用户体验问题。在以上方案中,总有一个地方让客户端用起来不太舒服。对于单机版的Redis,客户端直接连接RedisServer,我们只需要给一个ip和端口,客户端就可以使用我们的服务了。Client改造为Sentinel模式后,不得不采用一些支持Sentinel模式的外部依赖包,还要修改自己的Redis连接配置,这对于“矫情”的用户来说显然是不能接受的。有没有办法像单机版的Redis一样,只给Client一个固定的ip和端口就可以提供服务?答案当然是肯定的。这可能需要引入虚拟IP(VirtualIP,VIP),如上图所示。我们可以将虚拟IP指向RedisServermaster所在的服务器。当Redis发生主从切换时,会触发回调脚本。在回调脚本中,VIP会被切换到slave所在的服务器。这样一来,对于client端来说,他好像还是在使用单机版的高可用Redis服务。结论构建任何服务并使其“可用”实际上非常简单,就像我们运行单机版的Redis一样。但是一旦要实现“高可用”,事情就变得复杂了。业务中使用了额外的两台服务器,3个Sentinel进程+1个Slave进程,只是为了保证在小概率的意外情况下服务依然可用。在实际业务中,我们还启用了supervisor来监控流程。一旦进程意外退出,它会自动尝试重启。