引言
高可用(High Available)是线上生产环境所必不可少的重要条件,阿里云数据库Redis版作为一款成熟稳定的数据库产品,针对Redis的特性也支持高可用,本文将介绍云Redis是如何实现这一方案。
架构
目前云Redis有主从版和集群版两种架构,本次主要针对主从版做HA的解析。
下图为主从版架构:
由图可知,云Redis实例有主备两个节点,平时只有Master提供服务,Slave只做热备不提供访问,Slave通过slaveof命令挂载到Master上,不断从Master接收数据,保证Master宕机时云Redis仍可提供服务。
每一个云Redis实例都会分配一个VIP并与DNS绑定,VIP经过SLB后直接访问Master不再有其他中间层,访问Redis的链路为DNS-->VIP-->SLB-->REDIS(MASTER)。
在底层原理上集群版HA与主从版基本相同,只是实现时稍有差异,因为链路上在VIP和后端REDIS之间多了一层proxy的路由转发,所以在做后端rs切换时无需更改VIP指向,而是去更新proxy的路由表。
HA模块
HA作为一个独立的系统模块,远程探测云Redis的健康状况,当发生实例不可用时及时主备切换以保证服务质量。
健康检查
健康检查的逻辑很简单,通过客户端连接Redis并发送PING命令,如果返回PONG则说明Redis健康,其他情况则说明Redis异常,检测逻辑用伪代码来说明:
try:
client = Redis(ip, port, connection_timeout, socket_timeout)
//指定要连接Redis的ip:port(这里的ip:port即可以是VIP:VPORT也可以是Master或Slave的物理ip:port,HA会有多维度的探测),并设置超时时间
client.connect()
//尝试连接Redis,如果连接失败或超时则会抛出异常
res = client.ping()
//向Redis发送ping命令,结果为PONG说明Redis健康,返回OK;结果非PONG或超时则会抛出异常
if res == PONG:
return OK
except:
//处理异常情况,若异常在预先定义的错误内,说明Redis真的异常,返回ERROR,HA会做下一步切换动作
if e.message in ERRORS:
return ERROR
else:
return OK
需要HA真正做切换的异常情况有以下几种:
/* 指定ip地址的机器不能找到(也就是说从当前机器不存在到指定ip路由),或者是该ip存在,但找不到指定的端口进行监听,这时Redis所在主机可能宕机或是进程挂掉 */
"Connection refused"
/* 服务器的并发连接数超过了其承载量,服务器会将其中一些连接主动Down掉 */
"Connection reset"
/* 连接超时 */
"connect timed out"
/* 读取数据超时 */
"Read timed out"
/* Redis正在加载数据 */
"LOADING Redis is loading the dataset in memory"
/* 访问的Redis是Slave */
"READONLY You can't write against a read only slave"
健康检查升级方案
Redis工作在单线程模式下,接收到的所有命令都会进入队列串行处理,当遇到一些耗时命令如flushall、keys * 等,很容易造成客户端等待超时。此时如果超时时间设置不准确的话,HA远程探测就容易误判Redis不健康,而且单单一个PING命令也不能完全获得Redis的进程状态。
Redis自带的HA方案Sentinel就是依赖PING来做健康检查,通过配置sentinel down-after-milliseconds mymaster,来决定当PING超时多久没有收到回复就认为Redis宕机,但是这样做有一定风险,比如一个64G的实例flushall清空数据大概为2分钟,如果超时时间设置为60s那么很容易造成误判。
为了解决这个问题,我们对Redis内核进行了改造,增加了一个状态线程来专门为HA提供健康检查的服务。同时也新增了一个状态端口,状态线程监听这个端口,HA探测时也通过这个端口与Redis进行交互,不会对Redis服务主线程造成影响。通过新增的状态线程也可以做很多额外的探测,例如读写性能、磁盘IO情况等。
我们把Redis主线程监听的端口叫做redis_port,状态线程监听的端口叫做status_port新的健康检查逻辑如下:
try:
client = Redis(ip, status_port, connection_timeout, socket_timeout)
//这里要连接的端口为状态端口status_port
client.connect()
//尝试连接Redis,如果连接失败或超时则会抛出异常
res = client.health_check()
//向Redis发送定制的健康检查命令,结果为True说明Redis健康,返回OK;否则抛出异常
if res == True:
return OK
except:
//处理异常情况,若异常在预先定义的错误内,说明Redis真的异常,返回ERROR,HA会做下一步切换动作
if e.message in ERRORS:
return ERROR
else:
return OK
主备切换前准备工作
当健康检查发现Redis出现不可用情况时就要准备进行主备切换,在主备切换真正执行前需要额外做一些工作:
通过VIP检查Redis健康状态
if VIP不健康:
检查Slave状态
if Slave健康:
再次检查VIP状态
if VIP健康:
无需主备切换
else:
执行主备切换
else:
Slave不健康无法切换
else:
无需主备切换
在执行切换前要检查Slave状态以确保切换后实例是可服务的,否则即使切换也是无效的,比如两台主机都宕机这种极端情况。
同时对VPC类型的实例做了特殊处理,因为我们是没有办法访问用户自定义网络的VIP的,这时需要把对VIP的健康检查换成对Master的健康检查。
执行主备切换
当Redis出现不可用且满足切换条件时,真正开始执行主备切换动作。同时切换动作也支持主动的任务切换和被动的故障切换,两者主要区别在是否需要Slave等待Master达到同步状态,以下对主备切换做详细说明:
1. 额外再次检查一次Master状态
if Master健康:
if 故障切换:
无需切换,返回成功
else:
a. 设置Master为readONLY只读状态
b. 通过info replication命令检查主备同步状态,也即master_repl_offset是否等于Slave的offset
if 若超时仍未达到一致状态:
重置Master为readwrite可读写状态,并返回异常
else:
日志记录此时Master无法连接
2. 切换VIP指向Slave
if 切换失败:
记录日志并返回异常
3. 更新主备元信息
4. 向Slave发送slaveof no one命令,使其升级为新的Master
5. 尝试向原Master发送slaveof命令,使其降级为新的Slave
此时原Master可能已经宕机,故不做失败处理,仅记录日志
通过以上方案,云数据库Redis版SLA可以达到99.99%,仅在主从都发生宕机的极端情况无法服务。
切换场景
宕机或是进程异常退出是最为严重的情况,此时HA远程探测到redis无法连接会立即切换,实际测试整个过程可以实现秒级切换。
在主备切换后原来的Master变成了Slave,为了保障云Redis依旧高可用,会有另外的组件来探测Slave的可用性,当备库发生宕机时触发备库重搭功能,时刻保证主从双节点热备。
与Redis-Sentinel对比
Redis2.8版本自带Redis-Sentinel的高可用解决方案,通过一个或多个Sentinel实例组成的Sentinel系统来监测Master和Slave,具体方案有很多资料可以查阅不再赘述。
我们在生产环境中并没有采用自带的Sentinel,主要是出于以下几点考虑:
1. 虽然Sentinel本身也是一个Redis进程,但是运行在特殊模式下,实际维护Sentinel就需要额外配置一个角色,增加了系统的复杂度。
2. Sentinel自身的高可用问题,如果是单点Sentinel其本身也具有故障无法转移的风险,那么就要搭建Sentinel集群,这无形中也增加了管理成本。
3. Sentinel本身是有状态的,需要在启动时加载配置文件来获取Master信息,如果搭建分布式的Sentinel集群,动态增删改也会带来一致性问题,这对云上弹性资源并不适用,无法应对实例的创建、删除、变配、迁移等动作。
4. Sentinel的健康检查并不够灵活,上一节已经讲过单纯通过PING来检查无法应对flushall、keys * 等耗时命令,我们的健康检查升级方案会更好的应对云上复杂环境。
由于上述问题Sentinel不能作为一个统一的HA系统来管理所有云Redis资源,而为每个云Redis单独搭建一套Sentinel系统又会造成资源浪费和管理复杂度,基于此我们开发了这一套适用于云Redis的HA系统。
结束
本文介绍了云数据Redis版HA方案,通过主从双机热备来保证服务高可用,健康检查出现异常时及时进行主备切换,有效保障业务运行。