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

相关文章

来自专栏技术博文

SPDY初探

原文链接:http://blog.chinaunix.net/uid-22312037-id-4865410.html 现有的HTTP协议存在如下几个问题: ...

3417
来自专栏上善若水

016 进程内缓存和进程外缓存的对比

在java应用中,对于访问频率比较高,又不怎么变化的数据,常用的解决方案是把这些数据加入缓存。相比DB,缓存的读取效率快好不少。java应用缓存一般分两种,一是...

1303
来自专栏PHP在线

浏览器缓存机制浅析

原文出处: 韩子迟 浏览器缓存机制,其实主要就是HTTP协议定义的缓存机制(如: Expires; Cache-control等)。但是也有非HTTP协议...

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

HTTP 协议入门

HTTP 协议是互联网的基础协议,也是网页开发的必备知识,最新版本 HTTP/2 更是让它成为技术热点。 本文介绍 HTTP 协议的历史演变和设计思路。 ? 一...

33211
来自专栏Linyb极客之路

深入浅出Nginx

Nginx是一款轻量级的Web服务器、反向代理服务器,由于它的内存占用少,启动极快,高并发能力强,在互联网项目中广泛应用。

1416
来自专栏MelonTeam专栏

关于Android进程,你需要知道的

导语 Android系统是怎样杀进程的,native进程是怎么管理的?本文为你解密 一、Android进程管理 Android是基于组件工作的,...

21510
来自专栏Java Edge

SpringMVC的@ResponseBody注解说明

@ResponseBody 注解与 @RequestBody 注解类似。 @ResponseBody 注解可被应用于方法上,标志该方法的返回值将被直接写回到HT...

2725
来自专栏coder修行路

Go实现海量日志收集系统(一)

项目背景 每个系统都有日志,当系统出现问题时,需要通过日志解决问题 当系统机器比较少时,登陆到服务器上查看即可满足 当系统机器规模巨大,登陆到机器上查看几乎不现...

5817
来自专栏陈树义

亿级PV请求的三种负载均衡技术

在互联网+不断渗透到生活中的今天,各种各样的网络服务存在在我们身边,他们的访问流量也是大得惊人。一个大型网站(百万PV以上)想要正常访问,单单靠一台服务器是不可...

3334
来自专栏州的先生

Python爬虫实战入门二:从一个简单的HTTP请求开始

972

扫码关注云+社区