良好调优过的主从复制架构的Redis集群能够支撑绝大部分体量(10万tps以下)的系统。

要满足这个要求,主从架构的Redis集群应具备几个能力:

  • 自动故障转移
  • 读写分离(主写,从读)
  • 读负载均衡(在多个从节点之间)

Redis Sentinel为我们实现了主从架构下的故障转移能力,但后两项能力Redis并未提供支持,需要自己实现。各种语言的众多Redis客户端中,只有少数重量级的库实现了这两个能力(比如Java的Redisson和Lettuce)。被迫和某些库绑死是一件让人不太舒服的事情(被迫换库就更难受了),本文介绍的方法将读写分离和读负载均衡的能力实现在服务端。

先决条件

本文使用HAProxy做负载均衡,或者其他任何能够自定义TCP健康检查策略的负载均衡服务都行(比如Nginx,F5等等),本文将以HAProxy为例。

实现思路

在Redis服务前面搭建L4负载均衡服务,做下面两件事:

  • 把写请求定向到当前的Master节点上
  • 把读请求在当前的Slave节点间负载均衡

这就要求负载均衡服务不仅要知道当前哪些Redis节点是可用的,还要知道这些Redis节点的身份。
通过自定义TCP健康检查策略可以很容易实现:

backend redis-legacy
    mode tcp
    balance first
    option tcp-check
    tcp-check send info\ replication\r\n     #向Redis发送'INFO replication'命令
    tcp-check expect string role:master      #如Redis返回的内容中包含'role:master',即说明此节点为Master,即认为其健康
    tcp-check send QUIT\r\n
    tcp-check expect string +OK
    server redis-1 <ip>:<port> maxconn 5000 check inter 2s
    server redis-2 <ip>:<port> maxconn 5000 check inter 2s
    server redis-3 <ip>:<port> maxconn 5000 check inter 2s

也就是说,只要对命令'INFO replication'的返回内容中包含'role:master'的Redis节点,就被认为是有效节点,所有写请求都被定向到这个节点上去。

看上去似乎很OK,但其实这里存在双Master的风险
上面的配置只依靠每个Redis节点本身的反馈来决定其是否为Master,是不够严谨的。错误的配置和巧合的异常网络状态都有可能导致集群中一个以上的Redis节点认为自己是Master。
一旦出现双Master,那么写到错误的Master节点上的数据实际上就等同于丢失了,一旦出现会造成很严重的问题。
要解决这一问题,负载均衡服务就不能仅询问Redis节点本身,还要综合Sentinel们的意见。

下面这段配置,是在询问redis-1本身是否认为自己是Master:

 

backend redis-master-1
    mode tcp
    balance first
    option tcp-check
    tcp-check send info\ replication\r\n
    tcp-check expect string role:master
    tcp-check send QUIT\r\n
    tcp-check expect string +OK
    server redis <ip>:<port> maxconn 5000 check inter 2s

而下面这段配置会询问三个Sentinel节点当前redis-1是否为Master: 

backend mastercheck-redis-1
    mode tcp
    option tcp-check
    tcp-check send SENTINEL\ master\ <your-master-name>\r\n  ##向Sentinel服务发送'SENTINEL master <name>'命令
    tcp-check expect string <ip-of-redis-server-1>           ##如Sentinel返回的内容中包含redis-1的地址,那么说明Sentinel们认为redis-1为Master
    tcp-check send QUIT\r\n
    tcp-check expect string +OK
    server sentinel-1 <ip>:<port> check inter 2s
    server sentinel-2 <ip>:<port> check inter 2s
    server sentinel-3 <ip>:<port> check inter 2s

综合上面两个信息,就能得知当前redis-1节点的准确状态:

frontend redis-write
    mode tcp
    option tcplog
    bind *:16379
    #如果redis-1认为自己是Master,同时至少有两个Sentinel也认为redis-1是Master,那么就将写请求定向至redis-1
    use_backend redis-master-1 if { srv_is_up(redis-master-1/redis) } { nbsrv(mastercheck-redis-1) ge 2 }
    #以此类推,对redis-2和redis-3的策略
    use_backend redis-master-2 if { srv_is_up(redis-master-2/redis) } { nbsrv(mastercheck-redis-2) ge 2 }
    use_backend redis-master-3 if { srv_is_up(redis-master-3/redis) } { nbsrv(mastercheck-redis-3) ge 2 }
    #如果redis-1/2/3都不满足,那么就使用最上面提到的原始办法,即仅询问Redis节点本身
    #实际上此时集群肯定是有问题的,所以下面这句只是挣扎一下,并不是什么保险措施
    default_backend redis-legacy

读写分离和读负载均衡就简单了。如果一个Redis节点认为自己是Slave,那它就肯定是Slave,不可能存在节点认为自身是Slave而Sentinel认为其是Master的情况。
配置如下:

listen redis-read
    bind *:16479
    mode tcp
    balance roundrobin
    option tcp-check
    tcp-check send info\ replication\r\n
    tcp-check expect string role:slave
    tcp-check send QUIT\r\n
    tcp-check expect string +OK
    server redis-1 <ip>:<port> maxconn 5000 check inter 2s
    server redis-2 <ip>:<port> maxconn 5000 check inter 2s
    server redis-3 <ip>:<port> maxconn 5000 check inter 2s

不过,使用上述配置,假设在极端情况下某个Slave由于配置错误正在从错误的Master上同步数据,也会出现问题,因为HAProxy只询问了Redis节点“是否为Slave”,而没有询问“是谁的Slave”。
因为这种情况太极端了,所以我也就没有管,不过也并不代表没有办法,INFO replication命令的返回中同样会包含Master的host和port,如果一定要的话,HAProxy利用这些信息也能做出判断。

配置完成后,让客户端访问HAProxy的16379端口执行写操作,访问16479端口执行读操作就OK了。