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 条评论
登录 后参与评论

相关文章

来自专栏廖念波的专栏

谈谈后台服务的 RPC 和路由管理

互联网服务的后台,硬件通常是由大量的廉价机器组成,软件架构通常采取大系统小做、分而治之的思想。这就决定了业务逻辑涉及到大量的网路IO,同时单机故障、网络局部故障...

2.5K0
来自专栏非著名程序员

重构才是写代码,需求只是干活。

462
来自专栏CSDN技术头条

微博广告推荐中有关Hadoop的那些事

一、背景 微博,一个DAU上亿、每日发博量几千万的社交性产品,拥有庞大的数据集。如何高效得从如此规模的数据集中挖掘出有价值的信息,以增强用户粘性,提高信息传播速...

1885
来自专栏斑斓

代码诊所

几年前,我有机会负责一个项目的咨询。团队很小,目标是对旧有系统的后端用Java改写,而团队的开发人员全为C程序员。我的工作职责是负责项目设计、开发,以及担任项目...

3176
来自专栏数据和云

月之初 数之慎

上周日,也就是9月1日,收到了一则用户的故障请求,系统中一个存储过程出错,紧急安排工程师响应处理,解决了故障。 我们经历过很多类似的故障,在月初、月尾、年初、年...

3179
来自专栏Java后端生活

UUID

1324
来自专栏大数据和云计算技术

MQTT协议

MQTT协议简介 MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)是IBM开发的一个即时通讯协议,该协议支持...

3014
来自专栏java一日一条

采用断路器设计模式来保护软件

程序员的人生就像在一个快车道上行驶。几周甚至几小时完成某些特性编码,打包测试没有问题,盖上QA认证,代码部署到生产环境。然而最坏的事情发生了,你所部署的软件在运...

352
来自专栏IT技术精选文摘

再论分布式事务:从理论到实践

本文补充一种分布式事务解决方法:Best Effort. Best Effort   best effort即尽最大努力交付,主要用于在这样一种场景:不同的服...

2156
来自专栏阮一峰的网络日志

如何降低软件的复杂性?

John Ousterhout 是斯坦福大学计算机系教授,也是 Tcl 语言的创造者。

693

扫码关注云+社区