前言分库分表中间件经过一年多的锤炼,基本解决了可用性和高性能的问题(只能说基本,还有一定是隐藏的坑要填),问题自然就集中在了高可用上。本文介绍了我们在这方面所做的一些工作。什么是高可用性问题?作为一个无状态的中间件,高可用问题并没有那么难。但是,它仍然需要做一些工作来尽量减少不可用期间的流量损失。这些流量损失主要分布在:(1)某个中间件所在的物理机突然宕机。(2)中间件升级发布。由于我们的中间件是作为数据库的代理提供给应用程序的,即应用程序把我们的中间件当成了数据库,如下图所示:所以出现上述问题后,业务很难再通过重试等操作屏蔽这些影响。这就必然需要我们在底层做一些操作,能够自动感知中间件的状态,从而有效避免流量的流失。中间件所在的物理机宕机其实是一个很普遍的现象。此时,应用程序不会立即响应。那么它上面运行的sql肯定是失败了(准确的说是未知状态,除非重新查询后端数据库,应用无法知道确切的状态)。这部分流量我们是肯定救不了的。我们做的是在客户端(Druid数据源)快速发现和淘汰宕机的中间件节点。发现和移除不可用的节点通过心跳发现不可用的节点自然地,我们使用心跳来检测后端中间件的存活状态。我们定时新建一个连接ping(mysql的ping)然后马上关闭做心跳(这个方法方便我们区分正常流量和心跳流量,如果保持连接发送sqllikeselect'1'allthetime以不同的方式区分流量会有点麻烦)。为了防止网络抖动导致的偶发连接失败,我们判断某个中间件在3次连接失败后不可用。但是这3次检测延长了错误感知时间,所以我们3次connect的时间间隔呈指数级衰减,如下图所示:为什么在第一次connect失败后不连续发送两次connect?也许考虑网络的抖动可能有一个时间窗口。如果在时间窗口内连续发送了3次,并且过了这个时间窗口网络还好,那么就会错误的发现后端某个节点不可用,所以我们做了一个指标级别的衰减权衡。使用错误计数来查找不可用的节点。上面的心跳感知总是有一个时间窗的。当流量大的时候,在这个时间窗口内使用这个不可用的节点会失败,所以我们可以用错误计数来辅助不可用的节点。感知(当然这个方法的实现还在计划中)。这里要注意一点,只能通过创建连接异常来统计,不能通过读取超时之类的来统计。原因是读超时异常可能是SQL慢或者后端数据库问题导致的。只有连接异常才能确定是中间件问题(connectionclosed也可能是后端关闭了连接,并不代表整体不可用)。如下图所示:一个请求使用多个连接带来的问题因为我们要保证事务尽可能小,所以一个请求中的多个SQL不会使用同一个连接。在非事务(auto-commit)的情况下,从连接池中取出并放回多少个SQL连接。保证事务小是非常重要的,但是这会在中间件宕机的时候造成一些问题,如下图:如上图所示,在故障发现窗口期(即当某些中间件未确定不可用),随机选择数据源进行连接。而这个连接有一定的1/N(N为中间件个数)概率命中一个不可用的中间件,导致一条sql失败,导致整个请求失败。我们来做个计算:假设N为8,一个请求有20条sql,那么这段时间内每个请求失败的概率为(1-(7/8)的20次方)=0.93,即93%的概率会失败!更何况,整个应用集群都会经历这个阶段,即每个应用都有93%的失败概率。一个中间件的宕机导致整个服务的请求在十秒内几乎全部失败,这是无法容忍的。使用粘性数据源解决问题由于我们无法即时发现并确认中间件不可用,这个故障发现窗口肯定是存在的(当然错误计数的方式会大大缩短发现时间)。但理想情况下,如果一台服务器出现故障,只会丢失1/N的流量。我们使用粘性数据源来解决这个问题,这样丢失的概率只有流量的1/N,如下图:并且有了错误计数,总流量的丢失会更小(因为故障窗口是短的)如上图所示,只有在失败时间内随机选择到中间件2(不可用)的请求才会失败,让我们看看整个应用集群的情况。只有从粘性到中间件2的请求流会丢失。由于它是随机选择的,因此该流的损失按1/N应用。中间件升级发布过程中高可用分库分表中间件的升级发布是不可避免的。例如,错误修复和新功能添加需要重新启动中间件。重启时间也会导致不可用。相对于物理机宕机的情况,其不可用的时间点是已知的,重启动作也是可控的。那么我们就可以利用这些信息来实现流量控制。流畅无损。让客户端感知到即将下线。在笔者知道的众多做法中,要让客户端意识到自己下线了,就是引入第三方协调器(比如zookeeper/etcd)。而我们也不想引入第三方组件来做这个操作,因为这会引入zookeeper的高可用问题,并且会让客户端的配置变得更加复杂。平滑无损(状态机)的总体思路如下图所示:让心跳流量感知下线,保持正常流量。我们可以复用之前客户端检测不可用的逻辑,即让心跳的新建连接失败,正常请求连接的新建连接成功。这样client端就会认为Server不可用,在内部移除server。由于我们只是模拟不可用,所以无论是建立的连接还是正常的新建连接(非心跳)都是正常可用的,如下图:心跳连接的创建可以通过第一个在服务器端执行mysqlping正常流量的第一行是执行一个sql来区分(当然我们使用的Druid连接池在新建连接成功后也会ping,所以我们用另一种方式来区分,这个细节不会在这里详细说明)。3次心跳失败后,客户端确定Server1出现故障,需要销毁与server1的连接。思路是当业务层用完连接返回连接池时,直接关闭(当然这是简单的描述,实际对Druid数据源的操作也略有不同)。由于配置了一个连接的最大保留时间,所以过了这个时间后Server1的连接数肯定为0。由于在线流量不低,所以这个收敛时间比较快(进一步的做法是主动销毁,不过我们还没有这样做)。如何判断下线的服务器已经没有流量了经过以上仔细的操作,Server1下线的过程中不会有流量丢失。但是我们仍然需要确定服务器端什么时候没有新的流量。此标准是Server1没有任何客户端连接。这就是为什么我们执行完上面的sql之后销毁连接,这样连接数就可以变成0了,如下图:当连接数为0时,我们可以重新发布Server1(分库分表中间件)).为此,我们写了一个脚本,其伪代码如下:while(true){count=`netstat-anp|grepport|grepESTABLISHED|wc-l`if(0==count){//流量一直为0,转offtheserverkillServer//发布并升级服务器publicServerbreak}else{Sleep(30s)}}将此脚本连接到发布平台,即可滚动上线下线。现在我们可以解释为什么recover_time要长了,因为新的连接也会增加脚本计算的连接数,所以需要一个时间窗口不建立心跳,这样脚本才能顺利运行。recover_time实际上是不必要的。如果我们把heartbeat创建的端口号和正常流量的端口号分开,就不需要recover_time了,如下图:图中如果采用这种方案,会大大降低我们客户端代码的复杂度。但是这样无疑会在客户端增加一个新的配置,给用户增加一个负担,网络上多一个开墙的操作,所以我们采用了recover_time的方案。中间件启动顺序问题之前的流程是一个优雅的离线流程,但是我们发现我们的中间件在在线的时候有些情况下并不优雅。即在中间件启动时,如果新建立的与后端数据库的连接由于某些原因断开,中间件的reactor线程会卡住一分钟左右,这段时间无法服务,导致交通损失。所以,在所有后端数据库连接创建成功后,我们启动reactor的accept线程接收新的流量,从而解决这个问题,如下图:比高性能复杂。因为性能高,可以多次去线下压测,利用压测的数据分析瓶颈,提升性能。但是高可用需要应对线上各种奇怪的现象。本篇博客所描述的高可用方案只是我们工作的一小部分,我们很大一部分精力都在处理中间件本身。但只要不遗漏一点,问题都能分析清楚,解决好,系统就会越来越好。本文转载自微信公众号《Bug解决之路》,可通过以下二维码关注。转载本文请联系BUG解决公众号。
