作者:温昂展 导语:海量服务之道其一,有损服务; TAF特性其一,容错; Less is more
上一节简单提到了客户端在选取Invoker节点时,会对Invoker列表执行死活检查,屏蔽掉一定时间内异常的节点,从而达到容灾的目的。下面对TAF容错机制做具体探讨。
从概率学的角度,随着分布式系统规模不断扩大,即使是小概率的系统错误依然不可忽略不计,容错设计必不可少。
在分布式计算领域有一个公理即:CAP理论,分布式系统必然需要满足“P” 项,在遇到某个节点或网络分区故障时,仍然能对外提供满足一致性和可用性的服务,而一致性和可用性须有一方取舍,通常我们会选择系统高可用。(在这次实习做的项目中也有所体会,为保证系统高可用我容忍了一定判重状态的不一致,实际上很多业界优秀的NoSQL方案也是这么做的)。
说得再直白一点? 一句话: 任何一台服务节点down掉,都不影响业务的访问;
怎么保护? 简单点: 先屏蔽掉该异常节点,请求先发送到别的节点去,隔一段时间再试试异常的。
注意这里所说的容错性是站在系统层面上的,而业务上的容错是交给业务方自行根据需要做定制和实现的,如:根据服务端错误返回、捕获调用异常信息或是在错误回调中做相应重试处理。
系统要做到容错,首先需要思考:系统会有哪些错误? 系统如何能发现这些错误?
而所谓的容错保护就是在发现这些错误节点后采取特定的容错策略来保证系统的可用性,最简单的方式就是将这些错误节点移除屏蔽掉,然后定期重试,若发现错误节点恢复正常则取消屏蔽。
根据前面对客户端向服务端发起请求过程的分析,为保证系统的高可用性,若出现建立连接失败,或是处理请求时出现大量超时(参考:过载保护),我们应将该节点判定为异常节点。
具体分析连接失败或处理超时的原因是比较复杂的,可能是网络线路中断引起,亦有可能是节点系统异常,或是服务节点宕机等等。既然异常情况可能性较多,我们则不去具体细化探讨到各种情况的异同,而是概括性地抽象出系统错误出现的表征,以此作为依据发现错误却是比较容易实现的。
针对这个问题,必然要从两个角度出发考虑:
对于节点连接失败,一方面可以让服务端保持心跳上报,告知当前服务正常运行;另一方面可以使客户端建立连接失败时返回错误信息,以此判定;
对于节点过载,一方面可以监控服务端的服务队列处理情况; 另一方面可以在客户端统计请求的超时响应情况,以此判定。
分析清楚问题,再考虑如何实现就比较简单了,TAF的实现同样是从以上两个角度做考虑的。回想前面在整体架构介绍中提到的,petsvr服务会定期上报心跳到node服务,由node服务统一将心跳上报registry,以此我们可以在registry端设计名字服务排除策略,移除故障节点;而对于节点过载情况,考虑到在Invoker上直接统计更为精确,直接更新可用节点列表更为及时,同时没有服务端Obj 复用问题,因此我们可以设计客户端主动屏蔽策略。
业务服务 svr 主动上报心跳给名字服务,使名字服务知道服务部署的节点存活情况,当服务的某节点故障时,名字服务不再返回故障节点的地址给Client,达到排除故障节点的目标。名字服务排除故障需要通过服务心跳和Client地址列表拉取两个过程,默认故障排除时间在1分钟。
为了更及时的屏蔽故障节点,Client根据调用被调服务的异常情况来判断是否有故障来更快进行故障屏蔽。具体策略是,当client调用某个svr出现调用连续超时,或者调用的超时比率超过一定百分比,client会对此svr进行屏蔽,让流量分发到正常的节点上去。对屏蔽的svr节点,每隔一定时间(默认30秒)进行重连,如果正常,则进行正常的流量分发。
代码实现放在ServantnvokerAliveChecker工具类中,每个服务URL会对应一个死活统计状态ServantInvokerAliveStat,每次Invoker执行请求结束后会检查更新该活性,
代码逻辑很简单,以下情况则屏蔽该服务节点:
如下:
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;
}
}
}
感谢阅读,有错误之处还请不吝赐教。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有