codis3.0高可用方案分析

codis3.0内置了ha方案(在3.0以前ha是独立项目)

codis HA架构图

本文大致分析一下他的代码,试图找出他这样设计的目的,是否还存在什么缺点以及分析缺点的解决方案

本文的对象是对redis集群有一定经验,基本了解过codis方案的读者


HA代码

在cmd/ha下有ha的代码入口main.go

其中最重要的就是下面一段

1
2
3
4
5
6
7
8
9
10
11
var lasthc *HealthyChecker
for {
hc := newHealthyChecker(client)
hc.LogProxyStats()
hc.LogGroupStats()
hc.Maintains(client, lasthc)

lasthc = hc

time.Sleep(time.Second * 5)
}

newHealthyChecker的内容是通过dashboard的stat接口获取到HA对应集群的状态

LogProxyStats将记录下proxy的当前状态

LogGroupStats将记录下dashboard管理的所有codis-server的状态

Maintains就是实际的高可用策略了:

1 如果有proxy存在异常状态,那么不执行接下来的操作

2 遍历每个组的master,如果不处于SYNCED或者UNSYNC状态的(MISSING,ERROR,TIMEOUT,ALIVE),选取组中一台SYNCED状态的slave,对其执行promote指令,将其切换成主

分析:

1 灵活

HA组件完全利用dashboard提供的API进行高可用管理,这种方式非常灵活。可以自己选用是否需要HA支持。

2 出错的概率较小

以前参与过的redis集群HA方案是利用zookeeper做节点发现,每个redis-server都有一个watchdog组件来PING,并且将其对应的redis IP注册到zookeeper上响应group的路径上。

proxy会选择zk每个group路径下,序号最小的节点作为master(序号由zk api生成)来读写,因此master节点消失后,proxy会读写slave,同时该slave的watchdog会向slave发出SLAVE OF NO ONE来切换成master

这种方案看起来很美好,然而只要存在网络抖动,zk和watchdog的会话超时,就会发生误切。

redis集群方案对比

如图,老方案存在四个角色,节点发现方:zookeeper,监控方watchdog,被监控方redis,实际的通信客户端proxy。

而codis方案中,监控方和节点发行方是同一个(dashboard)。

可以看到,每一条箭头代表的网络不稳定都会出现问题

其中老方案存在四种较大的错误:

  1. zookeeper和watchdog断线,其他线路正常,于是主从切换,属于误切

  2. watchdog和redis断线,redis和proxy正常,于是主从切换,属于误切

  3. watchdog和redis正常,redis和proxy断线,血崩

4)proxy和zookeeper断线,高可用方案失灵

codis方案避免了第一种错误,但是其他错误依然是存在的

3 分析下codis方案错误的解决方案

错误3基本是无法解决的。

对于错误2,4,如果把节点发现方,监控方和客户端同时集成于一体,是可以完全消除这些错误的,这样proxy就非常复杂了。

从实际部署上考虑的话

例如将dashboard和proxy尽量部署在同一个网段,同一个交换机甚至同一个宿主机。这样能尽量规避错误2


Promote指令

Promote指令由两个指令组成,Promote和PromoteCommit

promote需要指定组ID和被promote的ip地址,promoteCommit只需要指定组ID,这两个指令共同组成了promote指令

promote指令主要是一些边界条件的检测,例如组内正在执行promote或者正在进行slot迁移的话就直接返回了。检测完边界条件将进入Preparing状态

promoteCommit才是实际的重点,这部分代码在Topom结构体的GroupPromoteCommit方法中,将推动状态机,从Preparing状态往Prepared走

如图:

promote状态转换

Prepared做的第一件事情是把该组相关的slot都锁死(fillslot函数来实现),假如存在失败,那么回滚到Preparing状态

而后把老的master从集群里删除,在组内标记master为被promote的slave,最后向该slave发出SLAVEOF NO ONE来真正提升为master

Prepared执行完毕后就变为Finished,这一步重新指定该组相关的slot的BackEnd为新的master,并且将slot全部解锁

这里说明下slot锁的作用

在proxy收到请求后会调用Route结构体的Dispatch方法,来指派请求分发到具体哪个slot

而后会调用Slot结构体的forward方法,来发送到具体哪个后端redis

在forward方法中会请求slot的读锁,而Promote指令调用的fillslot函数会请求slot的写锁

因此slot锁住以后,该slot不能进行任何的读写操作,请求该slot中的key将一直阻塞(可能有超时时间,这部分还没细看,但是根据测试30多秒还一直阻塞着)

分析

1 Promote指令分两段提交

这个我也没搞明白。。

2 Promote时为何要锁死redis对应的slot

在Promote指令发生的瞬间,其实就确认要放弃原来的master(不管是不是因为它挂了),所以不允许继续向原来的master读写,直到新的master准备完毕,而后才能继续进行请求


SyncCreateAction指令

将新的redis加入group是GroupAddServer指令,对codis-admin来说是以下指令

1
codis-admin [-v] --dashboard=ADDR            --group-add      --gid=ID --addr=ADDR

这之后如何和master建立主从链从而成为slave显然也是高可用的一部分

这部分用codis-admin或者在fe上可以完成

1
codis-admin [-v] --dashboard=ADDR            --sync-action    --create --addr=ADDR

这部分是Topom结构体的SyncCreateAction方法来开始的

SyncCreateAction只需要新的redis addr即可,最终目的是让该redis与他所在组的master建立主从关系

SyncCreateAction方法主要是检测一些边界条件,例如该组正在promote中或者发起过的SyncCreateAction方法正在后台被执行中

SyncCreateAction状态转换

如图,通过SyncCreateAction进入ActionPending状态后,有个协程启动的后台函数ProcessSyncAction将推动状态机继续演化

进入Syncing状态后将对该redis发起SLAVE OF MASTER_ADDR的命令来创建主从连接

创建成功将进入ActionSynced状态,失败将进入SyncedFailed状态

分析

1 主从链建立要用状态机机制的原因

即使等待SLAVE OF响应时间很长,也可以丢到协程来做这个事情,为何要用状态机+后台进程的方式

我猜测是因为多个从同时进行主从同步将对同一个主造成较大压力

因此需要丢到一个队列中去依次进行

2 这一部分是否可以优化

基于上一个问题的猜测,因为每个组之间是互相不影响的,所以我认为可以给每个组一个状态机+后台进程

在很多组同时进行SyncCreateAction的时候将节省很多时间

特别是如果限定了一个组只有一个主和一个从的时候,完全可以干掉这个状态机和后台进程

在这种条件下,甚至可以把SyncCreateAction和GroupAddServer指令集成起来


Codis高可用方案的优化

结合k8s做到进一步的自动化,有几个细节可以优化

1 Promote指令里Prepared阶段,将master从组里剔除,此时应当尝试重启master。

实现方式是向该master发出SHUTDOWN NOSAVE命令,使redis进程退出,进程退出将导致pod退出,k8s感知到pod退出后将使master重启

由于docker容器启动只能使用一条阻塞指令

因此重启后如何重新加入该组(包括加入组和建立主从链),这个方案还在完善中

2 HA指令只管理了master的状态异常,如果slave状态异常应该尝试重启

实现方式是用命令SHUTDOWN该slave,而后就同上了