RPC框架实现之容灾策略

RPC(Remote Procedure Call,远程过程调用)框架是分布式服务的基石,实现RPC框架需要考虑方方面面。其对业务隐藏了底层通信过程(TCP/UDP、打包/解包、序列化/反序列化),使上层专注于功能实现;框架层面,提供各类可选架构(多进程/多线程/协程);应对设备故障(高负载/死机)、网络故障(拥塞/网络分化),提供相应容灾措施。

分布式服务后台Server通常基于普通的x86_64服务器,单机设备故障是常态,同时网络也不能确保100%服务可用,交换机重启引起下联设备断连,流量挤占导致链路拥塞,甚至整个机房通信 光缆断开都有一定概率发生, RPC框架在实现时需要考虑以上情况,在框架层面具备容灾措施。

超时与重试

大家知道,RPC调用结果分成功、失败、超时三种状态,其中调用失败的一般原因是Server设备故障(connect失败,我们又称这类调用失败为系统失败),这时Client可以通过挑选其他Server,进行请求重试。超时的原因可能有多种,如网络拥塞、所选Server设备过载等,Client也可以通过重试尽量让请求得到处理。超时和重试的配置形式如下:

[ClientTimeout] 
ConnRetryCount = 3 
ConnTimeoutMS = 50 
SockTimeoutMS = 500

以上配置表示如果调用失败或超时,Client将选不同Server重试3次,连接(connect调用)的超时时间是50MS,请求的超时时间(包括发包->Server处理->收包的时间总和)是500MS。

以上连接超时(ConnTimeoutMS)可以根据网络情况进行设定,但请求超时(SockTimeoutMS)包含了Server的处理时长,情况就变得复杂。单从一层调用看,请求超时应该略大于Server处理时长,从一条调用链看,上一层的超时设定应大于下一层的超时设定,以下图为例:

对于 user -> A -> C -> E 这条调用链,AC间超时设定需大于CE间超时设定;从调用网的角度看,AC间超时设定不仅需大于CE间超时设定,还需要大于CD间超时设定。 联级超时的设定是RPC框架实现中的经典问题,解决超时配置和维护问题可借鉴 Google Dapper、 Twitter zipkin等分布式跟踪系统的思路。

以上提到系统失败、超时都可以通过重试尽量让请求得到处理,但重试次数并非越大越好,考虑Server整体服务过载的情形,过多重试可能引发后端Server 雪崩

Client端屏蔽策略

重试仅针对单次请求,如果一台Server异常,一次请求踩过的坑后续请求还要再踩一遍。为解决重复踩坑的问题,RPC框架需实现Client对异常Server的自动屏蔽。

屏蔽策略可以这样实现,Client对每个后端Server(IP/port)维护一个评分,每次请求失败(系统失败或超时)则将分数减一,当分数为0时,将IP/port、当前时间戳(timestamp)信息写入一块共享内存(block_shm),Client上各进程访问Server前,先跳过block_shm中记录的IP/port,在一段时间后(如10分钟)再放开该IP/port的访问。

以上屏蔽策略,做到了单机内进程/线程间信息共享,但在模块内甚至全网,Server异常信息依然没有共享,一台机器趟过的坑其他机器也会趟一遍。为解决机器间Server异常信息共享问题,我们可以对上面的实现做一些修改:将屏蔽信息上报到 Zookeeper中心节点,各台机监听相应路径节点,当有新增节点时拉取IP/port、timestamp信息到本机,存入block_shm,从而实现机器间屏蔽信息共享。我们把Client端的这种屏蔽策略称为 漏桶屏蔽

Server端反馈机制

Client端屏蔽策略是从调用方的角度保证服务质量,被调方Server也可以从自己的角度尽量保证服务可用。

设想一台Server服务过载,落到这台Server上的请求有较大概率得不到处理或处理超时,Server可以提前预判自己的服务状况,当Server认为自身服务能力达到瓶颈时,在Accept或读取请求包的阶段即丢弃请求并给Client一个约定的返回码。

预判的条件可以是历史请求在请求队列中的滞留时长、本机CPU占用率、Server系统失败率等。我们把Server端的这种反馈机制称为 快速拒绝,快速拒绝在Server服务能力达到瓶颈时生效,但明明是Server拒绝了请求,怎么能说是保证了服务可用呢?正如快速拒绝这个名称所指,在Accept或读取请求包这个阶段即把请求拒绝,减轻了Server的负担,同时更快地通知到Client,让其更换Server进行请求重试。

同一个Server可能为归属不同业务类型的Client提供接口,不同业务的重要性不同,快速拒绝也可以分业务的重要性进行拒绝。Server优先拒绝重要性低的请求,过载时尽量保证重要业务的服务质量。

雪崩

最后我们聊聊分布式服务出灾的一种极端情况——雪崩。 

当某个Server服务过载,大量请求处理失败时,用户行为带来的重试、客户端/后台Server的重试带来翻倍的请求,进一步加重该Server的负担。调用量翻倍上涨,但Server有效输出接近零,这是雪崩的标志现象。主要有两种措施应对雪崩,一是通过扩容提升Server服务能力,二是通过柔性措施,丢弃一部分请求,并且应该尽量在调用链的前端丢弃。

小结

本文讨论了RPC框架中的几种容灾措施,包括超时设置、重试、Client端屏蔽策略与Server端反馈机制,最后谈到分布式服务的雪崩。雪崩的处理涉及柔性,对用户体验有损,需要人为决策参与实施。

配置中心(ConfigServer)

alibaba有好几个分布式框架,主要有:进行远程调用(类似于RMI的这种远程调用)的(dubbo、hsf),jms消息服务(napoli、notify),KV数据库(tair)等。这个框架/工具/产品在实现的时候,都考虑到了容灾,扩展,负载均衡,于是出现一个配置中心(ConfigServer)的东西来解决这些问题。

基本原理如图:

在我们的系统中,经常会有一些跨系统的调用,如在A系统中要调用B系统的一个服务,我们可能会使用RMI直接来进行,B系统发布一个RMI接口服务,然后A系统就来通过RMI调用这个接口,为了解决容灾,扩展,负载均衡的问题,我们可能会想很多办法,alibaba的这个办法感觉不错。

本文只说dubbo,原理如下:

  • ConfigServer

配置中心,和每个Server/Client之间会作一个实时的心跳检测(因为它们都是建立的Socket长连接),比如几秒钟检测一次。收集每个Server提供的服务的信息,每个Client的信息,整理出一个服务列表,如:

 serviceName serverAddressList clientAddressList
 UserService 192.168.0.1,192.168.0.2,192.168.0.3,192.168.0.4  172.16.0.1,172.16.0.2
 ProductService 192.168.0.3,192.168.0.4,192.168.0.5,192.168.0.6 172.16.0.2,172.16.0.3
 OrderService 192.168.0.10,192.168.0.12,192.168.0.5,192.168.0.6  172.16.0.3,172.16.0.4

当某个Server不可用,那么就更新受影响的服务对应的serverAddressList,即把这个Server从serverAddressList中踢出去(从地址列表中删除),同时将推送serverAddressList给这些受影响的服务的clientAddressList里面的所有Client。如:192.168.0.3挂了,那么UserService和ProductService的serverAddressList都要把192.168.0.3删除掉,同时把新的列表告诉对应的Client 172.16.0.1,172.16.0.2,172.16.0.3;

当某个Client挂了,那么更新受影响的服务对应的clientAddressList

ConfigServer根据服务列表,就能提供一个web管理界面,来查看管理服务的提供者和使用者。

新加一个Server时,由于它会主动与ConfigServer取得联系,而ConfigServer又会将这个信息主动发送给Client,所以新加一个Server时,只需要启动Server,然后几秒钟内,Client就会使用上它提供的服务

  • Client

调用服务的机器,每个Client启动时,主动与ConfigServer建立Socket长连接,并将自己的IP等相应信息发送给ConfigServer。

Client在使用服务的时候根据服务名称去ConfigServer中获取服务提供者信息(这样ConfigServer就知道某个服务是当前哪几个Client在使用),Client拿到这些服务提供者信息后,与它们都建立连接,后面就可以直接调用服务了,当有多个服务提供者的时候,Client根据一定的规则来进行负载均衡,如轮询,随机,按权重等。

一旦Client使用的服务它对应的服务提供者有变化(服务提供者有新增,删除的情况),ConfigServer就会把最新的服务提供者列表推送给Client,Client就会依据最新的服务提供者列表重新建立连接,新增的提供者建立连接,删除的提供者丢弃连接

  • Server

真正提供服务的机器,每个Server启动时,主动与ConfigServer建立Scoket长连接,并将自己的IP,提供的服务名称,端口等信息直接发送给ConfigServer,ConfigServer就会收集到每个Server提供的服务的信息。

优点:

1,只要在Client和Server启动的时候,ConfigServer是好的,服务就可调用了,如果后面ConfigServer挂了,那只影响ConfigServer挂了以后服务提供者有变化,而Client还无法感知这一变化。

2,Client每次调用服务是不经过ConfigServer的,Client只是与它建立联系,从它那里获取提供服务者列表而已

3,调用服务-负载均衡:Client调用服务时,可以根据规则在多个服务提供者之间轮流调用服务。

4,服务提供者-容灾:某一个Server挂了,Client依然是可以正确的调用服务的,当前提是这个服务有至少2个服务提供者,Client能很快的感知到服务提供者的变化,并作出相应反应。

5,服务提供者-扩展:添加一个服务提供者很容易,而且Client会很快的感知到它的存在并使用它。

猜你喜欢

转载自blog.csdn.net/shaderdx/article/details/88303334