前言
RPC(Remote Procedure Call,远程过程调用)框架是分布式服务的基石,实现RPC框架需要考虑方方面面。其对业务隐藏了底层通信过程(TCP/UDP、打包/解包、序列化/反序列化),使上层专注于功能实现;
框架层面,提供各类可选架构(多进程/多线程/协程);应对设备故障(高负载/死机)、网络故障(拥塞/网络分化),提供相应容灾措施。
分布式服务后台Server通常基于普通的x86_64服务器,单机设备故障是常态,同时网络也不能确保100%服务可用,交换机重启引起下联设备断连,流量挤占导致链路拥塞,甚至整个机房通信光缆断开都有一定概率发生, RPC框架在实现时需要考虑以上情况,在框架层面具备容灾措施。
一、超时与重试
大家知道,RPC调用结果分成功、失败、超时三种状态,其中调用失败的一般原因是Server设备故障(connect失败,我们又称这类调用失败为系统失败),这时Client可以通过挑选其他Server,进行请求重试。
超时的原因可能有多种,如网络拥塞、所选Server设备过载(steal-time过高,load-average过高,io读写飚高等几个指标)等,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端反馈机制,最后谈到分布式服务的雪崩。雪崩的处理涉及柔性,对用户体验有损,需要人为决策参与实施。