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

相关文章

来自专栏Jaycekon

Python-WXPY实现微信监控报警

概述:   本文主要分享一下博主在学习wxpy 的过程中开发的一个小程序。博主在最近有一个监控报警的需求需要完成,然后刚好在学习wxpy 这个东西,因此很巧妙的...

1.7K8
来自专栏一只程序汪的自我修养

使用requirejs编写模块化代码

2265
来自专栏运维

ELK分析ngx_lua_waf软件防火墙日志

https://github.com/loveshell/ngx_lua_waf

2171
来自专栏lulianqi

超高性能管线式HTTP请求(实践·原理·实现)

这里的高性能指的就是网卡有多快请求发送就能有多快,基本上一般的服务器在一台客户端的压力下就会出现明显延时。

1351
来自专栏微服务生态

深入浅出Netflix Conductor使用

Netflix Conductor框架是典型的服务编排框架,通过Conductor还可以实现工作流和分布式调度,性能非常卓越。

4143
来自专栏SDNLAB

如何向OpenDaylight社区贡献代码

本文由两部分构成,第一部分简要介绍了Gerrit的工作原理以工作流程,第二部分结合自己成功提交代码的例子,向大家展示向OpenDaylight提交源码的过程。向...

3709
来自专栏后端技术探索

Restful 接口设计最佳事件

本小编这一年是在一家移动互联网公司做App后端接口设计开发工作,最近组内做了一次很大的重构,就是把接口完全根据restful规范进行设计重写。这么做的目的首先是...

1003
来自专栏我的小碗汤

6个最好的Go语言Web框架

原文:Top 6 web frameworks for Go as of 2017

1411
来自专栏腾讯云Elasticsearch Service

Elasticsearch调优实践

本文基于ES 5.6.4,从性能和稳定性两方面,从linux参数调优、ES节点配置和ES使用方式三个角度入手,介绍ES调优的基本方案。当然,ES的调优绝不能一概...

2681
来自专栏美团技术团队

MTDDL——美团点评分布式数据访问层中间件

背景 2016年Q3季度初,在美团外卖上单2.0项目上线后,商家和商品数量急速增长,预估商品库的容量和写峰值QPS会很快遇到巨大压力。随之而来也会影响线上服务的...

5185

扫码关注云+社区