TAF 必修课(六):容错

作者:温昂展 导语:海量服务之道其一,有损服务; TAF特性其一,容错; Less is more

上一节简单提到了客户端在选取Invoker节点时,会对Invoker列表执行死活检查,屏蔽掉一定时间内异常的节点,从而达到容灾的目的。下面对TAF容错机制做具体探讨。

一、容错性

从概率学的角度,随着分布式系统规模不断扩大,即使是小概率的系统错误依然不可忽略不计,容错设计必不可少。

在分布式计算领域有一个公理即:CAP理论,分布式系统必然需要满足“P” 项,在遇到某个节点或网络分区故障时,仍然能对外提供满足一致性和可用性的服务,而一致性和可用性须有一方取舍,通常我们会选择系统高可用。(在这次实习做的项目中也有所体会,为保证系统高可用我容忍了一定判重状态的不一致,实际上很多业界优秀的NoSQL方案也是这么做的)。

说得再直白一点? 一句话: 任何一台服务节点down掉,都不影响业务的访问;

怎么保护? 简单点: 先屏蔽掉该异常节点,请求先发送到别的节点去,隔一段时间再试试异常的。

注意这里所说的容错性是站在系统层面上的,而业务上的容错是交给业务方自行根据需要做定制和实现的,如:根据服务端错误返回、捕获调用异常信息或是在错误回调中做相应重试处理。

二、容错保护

系统要做到容错,首先需要思考:系统会有哪些错误? 系统如何能发现这些错误?

而所谓的容错保护就是在发现这些错误节点后采取特定的容错策略来保证系统的可用性,最简单的方式就是将这些错误节点移除屏蔽掉,然后定期重试,若发现错误节点恢复正常则取消屏蔽。

1. 错误类型

根据前面对客户端向服务端发起请求过程的分析,为保证系统的高可用性,若出现建立连接失败,或是处理请求时出现大量超时(参考:过载保护),我们应将该节点判定为异常节点。

具体分析连接失败或处理超时的原因是比较复杂的,可能是网络线路中断引起,亦有可能是节点系统异常,或是服务节点宕机等等。既然异常情况可能性较多,我们则不去具体细化探讨到各种情况的异同,而是概括性地抽象出系统错误出现的表征,以此作为依据发现错误却是比较容易实现的。

2. 如何发现

针对这个问题,必然要从两个角度出发考虑:

  • 在服务端做监控
  • 客户端主动发现

对于节点连接失败,一方面可以让服务端保持心跳上报,告知当前服务正常运行;另一方面可以使客户端建立连接失败时返回错误信息,以此判定;

对于节点过载,一方面可以监控服务端的服务队列处理情况; 另一方面可以在客户端统计请求的超时响应情况,以此判定。

三、TAF实现

分析清楚问题,再考虑如何实现就比较简单了,TAF的实现同样是从以上两个角度做考虑的。回想前面在整体架构介绍中提到的,petsvr服务会定期上报心跳到node服务,由node服务统一将心跳上报registry,以此我们可以在registry端设计名字服务排除策略,移除故障节点;而对于节点过载情况,考虑到在Invoker上直接统计更为精确,直接更新可用节点列表更为及时,同时没有服务端Obj 复用问题,因此我们可以设计客户端主动屏蔽策略。

1. 名字服务排除策略:

业务服务 svr 主动上报心跳给名字服务,使名字服务知道服务部署的节点存活情况,当服务的某节点故障时,名字服务不再返回故障节点的地址给Client,达到排除故障节点的目标。名字服务排除故障需要通过服务心跳和Client地址列表拉取两个过程,默认故障排除时间在1分钟。

2. Client主动屏蔽策略:

为了更及时的屏蔽故障节点,Client根据调用被调服务的异常情况来判断是否有故障来更快进行故障屏蔽。具体策略是,当client调用某个svr出现调用连续超时,或者调用的超时比率超过一定百分比,client会对此svr进行屏蔽,让流量分发到正常的节点上去。对屏蔽的svr节点,每隔一定时间(默认30秒)进行重连,如果正常,则进行正常的流量分发。

代码实现放在ServantnvokerAliveChecker工具类中,每个服务URL会对应一个死活统计状态ServantInvokerAliveStat,每次Invoker执行请求结束后会检查更新该活性,

代码逻辑很简单,以下情况则屏蔽该服务节点:

  • 周期内超时次数超过MinTimeoutInvoke,且超时比率大于总数的frequenceFailRadio
  • 连续调用超时次数超过frequnceFailInvoke(5秒内)
  • 连接失败connectionTimeout错误

如下:

public synchronized void onCallFinished(int ret, ServantProxyConfig config) {
    if (ret == Constants.INVOKE_STATUS_SUCC) {
        frequenceFailInvoke = 0;
        frequenceFailInvoke_startTime = 0;
        lastCallSucess.set(true);
        netConnectTimeout = false;
        succCount++;
    } else if (ret == Constants.INVOKE_STATUS_TIMEOUT) {
        if (!lastCallSucess.get()) {
            frequenceFailInvoke++;
        } else {
            lastCallSucess.set(false);
            frequenceFailInvoke = 1;
            frequenceFailInvoke_startTime = System.currentTimeMillis();
        }
        netConnectTimeout = false;
        timeoutCount++;
    } else if (ret == Constants.INVOKE_STATUS_EXEC) {
        if (!lastCallSucess.get()) {
            frequenceFailInvoke++;
        } else {
            lastCallSucess.set(false);
            frequenceFailInvoke = 1;
            frequenceFailInvoke_startTime = System.currentTimeMillis();
        }
        netConnectTimeout = false;
        failedCount++;
    } else if (ret == Constants.INVOKE_STATUS_NETCONNECTTIMEOUT) {
        netConnectTimeout = true;
    }
    //周期重置
    if ((timeout_startTime + config.getCheckInterval()) < System.currentTimeMillis()) {
        timeoutCount = 0;
        failedCount = 0;
        succCount = 0;
        timeout_startTime = System.currentTimeMillis();
    }

    if (alive) {
        // 周期内超时次数超过MinTimeoutInvoke,且超时比率大于总数的frequenceFailRadio
        long totalCount = timeoutCount + failedCount + succCount;
        if (timeoutCount >= config.getMinTimeoutInvoke()) {
            double radio = div(timeoutCount, totalCount, 2);
            if (radio > config.getFrequenceFailRadio()) {
                alive = false;
                ClientLogger.getLogger().info(identity + "|alive=false|radio=" + radio + "|" + toString());
            }
        }

        if (alive) {
            // 5秒内连续失败n次
            if (frequenceFailInvoke >= config.getFrequenceFailInvoke() && (frequenceFailInvoke_startTime + 5000) > System.currentTimeMillis()) {
                alive = false;
                ClientLogger.getLogger().info(identity + "|alive=false|frequenceFailInvoke=" + frequenceFailInvoke + "|" + toString());
            }
        }
        if (alive) {
            //连接失败
            if (netConnectTimeout) {
                alive = false;
                ClientLogger.getLogger().info(identity + "|alive=false|netConnectTimeout" + "|" + toString());
            }
        }
    } else {
        if (ret == Constants.INVOKE_STATUS_SUCC) {
            alive = true;
        }
    }
}

感谢阅读,有错误之处还请不吝赐教。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏一个会写诗的程序员的博客

Spring Reactor 项目核心库Reactor Core

Non-Blocking Reactive Streams Foundation for the JVM both implementing a Reactiv...

2902
来自专栏魂祭心

原 canvas绘制clock

5214
来自专栏Golang语言社区

【Golang语言社区】GO1.9 map并发安全测试

var m sync.Map //全局 func maintest() { // 第一个 YongHuomap := make(map[st...

5568
来自专栏一个爱瞎折腾的程序猿

sqlserver使用存储过程跟踪SQL

USE [master] GO /****** Object: StoredProcedure [dbo].[sp_perfworkload_trace_s...

3070
来自专栏java 成神之路

使用 NIO 实现 echo 服务器

5737
来自专栏pangguoming

Spring Boot集成JasperReports生成PDF文档

由于工作需要,要实现后端根据模板动态填充数据生成PDF文档,通过技术选型,使用Ireport5.6来设计模板,结合JasperReports5.6工具库来调用渲...

1.5K7
来自专栏张善友的专栏

Miguel de Icaza 细说 Mix 07大会上的Silverlight和DLR

Mono之父Miguel de Icaza 详细报道微软Mix 07大会上的Silverlight和DLR ,上面还谈到了Mono and Silverligh...

3037
来自专栏C#

DotNet加密方式解析--非对称加密

    新年新气象,也希望新年可以挣大钱。不管今年年底会不会跟去年一样,满怀抱负却又壮志未酬。(不过没事,我已为各位卜上一卦,卦象显示各位都能挣钱...)...

6088
来自专栏张善友的专栏

Mix 10 上的asp.net mvc 2的相关Session

Beyond File | New Company: From Cheesy Sample to Social Platform Scott Hansel...

2797
来自专栏张善友的专栏

Silverlight + Model-View-ViewModel (MVVM)

     早在2005年,John Gossman写了一篇关于Model-View-ViewModel模式的博文,这种模式被他所在的微软的项目组用来创建Expr...

3358

扫码关注云+社区