不依赖客户端库,实现Redis主从架构的高可用/读写分离/负载均
在《Redis基础、高级特性与性能调优》这篇文章中,我曾比对过Redis主从复制架构和分片集群架构,并提到了避免过度设计架构的观点。我始终认为良好调优过的主从复制架构的Redis集群能够支撑绝大部分体量(10万tps以下)的系统。
要满足这个要求,主从架构的Redis集群应具备几个能力:
- 自动故障转移
- 读写分离(主写,从读)
- 读负载均衡(在多个从节点之间)
Redis Sentinel为我们实现了主从架构下的故障转移能力,但后两项能力Redis并未提供支持,需要自己实现。各种语言的众多Redis客户端中,只有少数重量级的库实现了这两个能力(比如Java的Redisson和Lettuce)。被迫和某些库绑死是一件让人不太舒服的事情(被迫换库就更难受了),本文介绍的方法将读写分离和读负载均衡的能力实现在服务端。
(不想看废话的大佬们可以直接看这里:https://github.com/kelgon/redisHA)
先决条件
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了。
完整的HAProxy配置,请见https://github.com/kelgon/redisHA/blob/master/haproxy.cfg
写在最后
- 关于主从复制的Redis集群如何搭建,Sentinel怎么建,官网上有详细的文档,中文资源也相当丰富,本文就不做赘述了
- 其他负载均衡软件的配置,可以参考HAProxy,理论上只要能够支持自定义TCP健康检查,就没有问题
- 注意负载均衡服务也不能是单点,否则Redis的故障转移做得再好也都没有意义。我的另一篇文章《HAProxy从零开始到掌握》的最后一部分中,有如何使用Keepalived搭建自动故障转移的主备负载均衡服务的描述,供参考