分库分表中间件在咱们一年多的锤炼下,基本解决了可用性和高性能的问题(只能说基本,确定还有隐藏的坑要填),问题天然而然的就聚焦于高可用。本文就阐述了咱们在这方面作出的一些工做。mysql
做为一个无状态的中间件,高可用问题并无那么困难。可是尽可能减小不可用期间的流量损失,仍是须要必定的工做的。这些流量损失主要分布在:react
(1)某台中间件所在的物理机忽然宕机。 (2)中间件的升级和发布。
因为咱们的中间件是做为数据库的代理提供给应用的,即应用把咱们的中间件当作数据库,以下图所示:
因此出现上述问题后,业务上很难经过重试等操做去屏蔽这些影响。这就势必须要咱们在底层作一些操做,可以自动的感知中间件的状态从而有效避免流量的损失。sql
物理机宕机实际上是一种常见现象,这时候应用一瞬间就没了响应。那么跑在上面的sql确定也是失败了的(准确来讲是未知状态,除非从新查询后端数据库,应用没法得知准确的状态)。这部分流量咱们确定是没法挽救。咱们所作的是在client端(Druid数据源)可以快速的发现并剔除宕机的中间件节点。数据库
天然而然的咱们经过心跳来探查后端中间件的存活状态。咱们经过定时建立一个新链接ping(mysql的ping)一下而后立马关闭来作心跳(这种作法便于咱们区分正常流量和心跳流量,若是经过保持一个链接而后一直发送相似select '1'的sql这种方式的话区分流量会稍微麻烦点)。
为了防止网络抖动形成的偶发性connect失败,咱们在三次connect都失败后才断定某台中间件处于不可用状态。而这三次的探活却延长了错误感知时间,因此咱们三次connect的时间间隔是指数级衰减的,以下图所示:
为什么不在第一次connect失败后,连续发送两次connect呢?可能考虑到网络的抖动可能会有一个时间窗口,若是在时间窗口内连续发了3次,出了这个时间窗口网络又okay了,那么会错误的发现后端某节点不可用了,因此咱们就作了指数级衰减的折衷。后端
上述的心跳感知始终有一个时间窗口,当流量很大的时候,在这个时间窗口内使用这个不可用节点的都会失败,因此咱们可使用错误计数去辅助不可用节点的感知(固然这个手段的实现还在计划中)。
这边有一个注意的点是,只能经过建立链接异常来计数,并不能经过read timeout之类的来计算。缘由是,read timeout异常多是慢sql或者后端数据库的问题致使,只有建立链接异常才能肯定是中间件的问题(connection closed也多是后端关闭了这个链接,并不表明总体不可用),以下图所示:服务器
因为咱们须要保证事务尽量小,因此在一个请求里面多条sql并不使用同一个链接。在非事务(auto-commit)状况下,运行多少条sql就从链接池里面取出多少链接,并放回。保证事务小是很是重要的,可是这在中间件宕机的时候会致使一些问题,以下图所示:
如上图所示,在故障发现窗口期中(即尚未肯定某台中间件不可用时),数据源是随机选择链接的。而这个链接就有必定1/N(N为中间件个数)的几率命中不可用中间件致使一条sql失败进而致使整个请求失败。咱们作一个计算:网络
假设N为8,一个请求有20条sql, 那么在这个期间每一个请求失败的几率就为(1-(7/8)的20次方)=0.93, 即有93%的几率会失败!
更为甚者,整个应用集群都会经历这个阶段,即每台应用都有93%的几率失败。
一台中间件宕机致使整个服务在十几秒内基本全部请求基本都失败,这是不可忍受的。ide
因为咱们不能瞬间发现并确认中间件不可用,因此这个故障发现窗口确定存在(固然,错误计数法会在很大程度上缩短发现时间)。但理想情况下,宕机一台,只损失1/N的流量就行了。咱们采用了sticky数据源解决了这个问题,使得在几率上大体只损失1/N的流量,以下图所示:
而配合错误计数的话,总流量的损失会更小(由于故障窗口短)
如上图所示,只有在故障时间内随机选择到中间件2(不可用)的请求才会失败,再让咱们看下整个应用集群的状况。
只有sticky到中间件2的请求流量才有损失,因为是随机选择,因此这个流量的损失应用在1/N。性能
分库分表中间件的升级发布不可避免。例如bug fix以及新功能添加等都须要重启中间件。而重启的时间也会致使不可用,与物理机宕机的状况相比是其不可用的时间点是可知的,重启的动做也是可控的,那么咱们就能够利用这些信息去作到流量的平滑无损。ui
在笔者所知的不少作法中,让client端感知下线是引入一个第三方协调者(例如zookeeper/etcd)。而咱们并不想引入第三方的组件去作这个操做,由于这又会引入zookeeper的高可用问题,并且会让client端的配置更加复杂。平滑无损的大体思路(状态机)以下图所示:
咱们能够复用以前client端检测不可用的逻辑,即让心跳的新建链接失败,而正常请求的新建链接成功。这样,client端就会认为Server不可用,而在内部剔除调这个server。因为咱们只是模拟不可用,因此已经创建的链接和正常新建的链接(非心跳)都是正常可用的,以下图所示:
心跳链接的建立在server端能够经过其第一条执行的是mysql的ping而正常流量第一条执行的是一条sql来区分(固然咱们采用的Druid链接池在新建链接成功之后也会ping一下,因此采用了另外一种方式区分,这个细节在这里就不阐述了)。
三次心跳失败后,client端断定Server1失败,须要将链接到server1的链接销毁。其思路是,业务层用完链接返回链接池的时候,直接给close掉(固然这个是简单的描述,实际操做到Druid数据源也是有细微的差异的)。
因为配置了一个connection最长保持时间,因此在这个时间以后确定会对Server1的链接数为0
因为线上流量也不低,这个收敛时间是比较快的(进一步的作法,实际上是主动去销毁,不过咱们还没有作这个操做)。
在上述当心翼翼的操做以后,在Server1下线的过程当中,是不会有流量损失的。可是咱们在Server端还须要断定什么时候不会再有新的流量,这个断定标准便是Server1没有任何一个client端的链接。
这也是上面咱们在执行完sql后销毁链接从而可让链接数变为0的缘由,以下图所示:
当链接数为0后,咱们就能够从新发布Server1(分库分表中间件)了。对于这一点,咱们写了个脚本,其伪代码以下所示:
while(true){ count =`netstat -anp | grep port | grep ESTABLISHED | wc -l` if(0 == count){ // 流量已经为0,关掉服务器 kill Server // 发布升级服务器 public Server break }else{ Sleep(30s) } }
将这个脚本接入发布平台,便可进行滚动式上下线了。
如今能够解释下recover_time为什么要较长了,由于新建链接也会致使脚本计算出来的 connection count数量增长,因此须要一个时间窗口不去创建心跳,从而能让这个脚本顺利运行。
若是咱们将心跳建立的端口号和正常流量的端口号分开,是不须要recover_time的,以下图所示:
采用这种方案的话,会在很大程度上下降咱们client端代码的复杂度。
可是这样无疑又给client端增长了一个新的配置,对使用人员就又多了一个负担,还得在网络上多一次开墙的操做,因此咱们采起了recover_time的方案。
前面的过程是一个优雅下线的过程,但咱们发现咱们的中间件才上线的时候在某些状况下也不会优雅。即在中间件启动时候,若是对后端数据库刚创建的链接创建上去后因为某些缘由断开了,会致使中间件的reactor线程卡住一分钟左右,这段时间没法服务,形成流量损失。因此咱们在后端数据库链接所有建立成功后,再启动reactor的accept线程从而接收新的流量,从而解决这一问题,以下图所示:
笔者我的感受高可用比高性能还要复杂。由于高性能能够在线下反复的去压测,经过压测的数据去分析瓶颈,提升性能。而高可用须要应付线上各类千奇百怪的现象,本篇博客讲述的高可用方案只是咱们工做的一小部分,还有很大一部分精力是处理中间件自己的问题上。但只要不放过任何一个点,将问题都可以分析清楚并解决,就会让系统愈来愈好。