最近在自己的项目中搭建了一小套“高可用”的Redis服务,在此总结和思考。基于内存的Redis应该是各种web开发业务中最常用的key-value数据库。我们在业务中经常用它来存储用户登录状态(session存储),加速一些热点数据的查询(相对于MySQL,速度有一个数量级的提升),做简单的消息队列(LPUSH和BRPOP),到订阅和发布(PUB/SUB)系统等等。大型互联网公司一般都有专门的团队,以基础服务的形式为各种业务调用提供Redis存储。但是,任何基础服务的提供者都会被调用者问一个问题:你们的服务是否高可用?***不要因为你频繁的服务故障而让我的生意受到影响。最近在自己的项目中搭建了一小套“高可用”的Redis服务,在此做一下自己的总结和思考。首先,我们需要定义什么是Redis服务的高可用,即在各种异常情况下仍然可以正常提供服务;恢复正常服务。所谓异常,至少应该包括以下三种可能:某节点服务器的某个进程突然宕机,比如某开发手被禁用,某服务器的redis-server进程被kill掉。当一个节点服务器宕机时,意味着这个节点上的所有进程都停止了。比如某个运维手被禁用,某个服务器的电源被拔掉;例如,一些旧机器有硬件故障。任意两个节点服务器之间的通信中断。例如,一名手有残疾的临时工剪断了两个机房之间用于通信的光缆。其实以上任何一种异常都是小概率事件,高可用的基本指导原则是:多个小概率事件同时发生的概率可以忽略不计,只要我们设计的系统能够容忍单个短时间点故障,实现高可用。对于构建高可用的Redis服务,网上已经有很多解决方案,比如Keepalived、Codis、Twemproxy、RedisSentinel等。其中Codis和Twemproxy主要用于大型Redis集群,也是Redis官方发布RedisSentinel之前Twitter和PeaPods提供的开源解决方案。我的业务数据量不大,搞集群服务很浪费机器。最后在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宕机,此时只剩下server2上面的RedisSentinel和slaveRedisServer进程。这个时候Sentinel不会把剩下的slave切换到master继续服务,会导致Redis服务不可用,因为Redis的设置是只有50%以上的Sentinel进程可以连接并投给一个newmaster才会发生真正的主从切换。在这个例子中,两个Sentinels只能连接一个,等于50%,不属于可以主从切换的场景。你可能会问,为什么Redis会有这个50%的设置?假设我们允许在Sentinel连通性小于等于50%的场景下进行主从切换?想象一下异常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进程在检测到自身网络出现问题时立即停止服务,避免在网络故障时有新的数据进来(参考min-slaves-Redis-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来监控流程。一旦进程意外退出,它会自动尝试重启。
