公众号|松华说|一文带你搞懂RPC核心原理
  • 分享在京东工作的技术感悟,还有JAVA技术和业内最佳实践,大部分都是务实的、能看懂的、可复现的

扫一扫
关注公众号

一文带你搞懂RPC核心原理

博客首页文章列表 松花皮蛋me 2020-03-08 11:44

一、RPC的作用

  1. 屏蔽远程调用跟本地调用的区别,让我们感觉就是调用项目内的方法。
  2. 隐藏底层网络通信的复杂性,让我们更专注于业务逻辑。

二、完整的RPC涉及到的核心


编解码、序列化和反序列、请求协议、桩生成(动态代理、反射执行)。


三、RPC使用过程需要注意什么问题

  • 1、避免多米诺骨牌效应,所以要根据服务能力提前协商限流。
  • 2、调用服务异常时,要考虑降级、重试等措施。
  • 3、核心的服务不能强依赖非核心的服务,避免核心服务因为非核心服务异常而不可用。 

四、RPC协议


在传输过程中,RPC并不会把请求参数的所有二进制数据整体一下子发送到对端机器上,中间可能会拆分成多个数据包,也有可能合并成其他请求的数据包。RPC协议就是为了”正确进行装包和拆包”而生的,比如使用长度限制或者标识设定边界。
其中,协议包括固定部分、协议头内容和协议体内容,固定部分常包括协议长度、协议标识、序列化方式、业务消息ID、消息类型,协议头内容用来进行扩展的,保证协议可平滑升级,协议体通常包括请求接口方法、请求的业务参数值和业务扩展属性。其中,协议头内容单独定义出来,是为了避免将其信息放到协议体中,导致协议体负载太重。


五、序列化和反序列化需要注意什么


编解码组件应该考虑安全性、版本升级的兼容性、跨语言支持性、存储空间占用、网络传输效率、可读性。


复杂的接口定义可能会导致序列化异常,为了减少发生的概率,应该尽量使用原生类型,还有不要使用过深的继承关系或者依赖关系,最后是避免序列化对象过大。


六、动态代理


RPC自动给接口生成一个代理类,我们在项目中注入接口后,运行时实际绑定的是这个代理类。


JDK默认的代理功能是有一定的局限性的,它要求被代理的类只能是接口。因为生成的代理类会继承Proxy类,但Java是不支持多重继承的。


七、如何设计灵活的RPC框架


将变化部分封装在插件里面,才能达到快速灵活扩展的目的,本质就是微内核架构,比如规则引擎架构。


微内核的核心技术点有插件管理、插件连接、插件通信。插件管理也称为插件注册表,用来述插件模块信息、如何加载、加载时机等等;插件连接是指插件按照核心系统的规范实现后如何连接到系统上,比如使用依赖注入;插件通信则是用来协调没有关联关系的插件,比如请求上下文Pipline。

八、服务注册选型

  • 方案一:DNS轮询。解析缓存可能导致无法及时响应服务提供者的变更。
  • 方案二:DNS负载均衡。需要搭建四层负载均衡器,也就意味着它存在单点压力隐患。另外,支持负载均衡算法不够灵活,后端的实例节点还需要手动维护。
  • 方案三:Zookeeper节点服务。大量的服务提供者频繁变更会导致Zookeeper服务负载过高甚至宕机。
  • 方案四:基于消息总线的最终一致性的注册中心。某个注册中心节点推送消息到消息总线,消息总线生成新版本号的数据,再通知其他注册中心和服务提供者更新本地缓存。


九、如何识别服务节点存活情况


服务方的状态通常有三种,分别为健康状态、亚健康状态、死亡状态,其中,亚健康状态下连接是成功的但是心跳请求连续失败。不过,心跳探测通常还会结合业务可用率来判断状态,这样可以及时发现心跳探测间歇性失败的问题。


具体的健康检查分类有静态方法和动态方法。


静态方法:基于服务消费者本身调用来判断服务节点是否可用,更加实时更加准确,而且当注册中心或者网络出现问题时,基本不受影响。这种方式使用更加普遍。


动态方法:服务提供者向注册中心上报心跳信息,然后更新注册列表并同步到服务消费者,同时结合心跳开关保护和服务节点摘取保护机制(比如控制比例)进行存活判断优化。


十、路由策略


路由策略就是服务提供者集群列表筛选规则,比如根据来源IP或者请求参数控制请求,常用于灰度发布的风险控制,还可用于同机房调用优先调度、读写分离、黑白名单控制。


十一、异常重试


当业务具有幂等性保证时才能让RPC框架帮助我们进行重试,通常借助路由策略实现,当连接异常或者业务白名单异常时就进行重试,重新选择一个新的服务节点进行调用,直到重试最大次数门槛,或者已经达到超时时间设定。


十二、优雅关闭


服务对象在关闭过程中,会拒绝新的请求,同时根据引用计数器等待正在处理的请求全部结束之后才会真正关闭。另外,为了避免一直等待造成应用无法正常退出,还需要在整个ShutdownHook里面加上超时控制。


十三、优雅启动


优雅启动是指不要让应用刚启动成功就接收正常量级的请求,此时数据可能还未完全准备好,容易造成请求超时。常见的做法有启动预热和延迟暴露。启动预热的意思就是借助路由策略根据实例注册时间动态调整权重,刚启动的应用缓慢放大流量接收的占比。而延迟暴露则是应用启动完成后,先通过Hook钩子机制执行预热逻辑后再执行注册上报。


十四、服务依赖检查

启动时对引用的服务提供者进行存活检查,如果不存活快速失败,避免上线后才暴露问题。


十五、逻辑分组


稳定性保障中很重要的一点就是自适应保护,比如通过隔离失败保证提供给核心服务的接口可用,更具体的落地方案有路由分组,不同级别的系统调用不同的分组,从而达到隔离的目的。不过对于服务使用者来说,可调用的列表减少了,这种情况下RPC框架最好提供主、备分组的逻辑,当主分组全部不可用后,再使用备分组。


十六、动态分组


使用逻辑分组进行流量隔离时,如果某些分组的服务使用方流量突增,提供方紧急扩容不现实,一来及时性差二来麻烦。针对这种情况,我们可以通过注册中心的控制台动态修改已有分组配置,进行替换或者追加,曲线求国地增加了服务使用方的可使用实例列表。


十七、异步调用


客户端调用服务端后直接返回的不再是结果,而是CompletableFuture对象。当客户端收到服务端发送过来的响应之后,RPC框架自动地调用先前的CompletableFuture对象的complete方法,也就是将返回值注入到异步模型中,从而完成异步通知。


十八、RPC安全体系


RPC通信一般为内网服务间通信,所以它的安全问题可以简化为认证授权问题、伪造注册问题。针对前者,我们可以通过授权平台管理调用方列表,调用方申请调用权限通过后生成与其标识相对应的令牌。然后每次调用都通过上下文隐式传递这些认证参数,提供方接着进行Hash校验,通过才放行。而针对伪造注册隐患的解决方案是不允许多个应用同时注册相同的服务。


十九、快速定位问题


RPC 框架自身以及服务提供方的业务逻辑实现,都应该对异常进行合理地封装,包装排查问题所需要的重要信息,比如接口签名信息、客户端和服务端的IP、异常信息。同时还要对各种异常做好分类标识,比如业务线程池耗尽,Netty连接异常,Netty数据传输异常,限流异常等等。如果想更加高效排查链路问题,就得处理好埋点和传递。


二十、定时处理


我们可以利用时间轮机制优雅完成异步请求超时、启动超时、心跳探测等等功能。时间轮类似生活中的时钟,它只会轮循第一层时间槽的任务,当遍历完成后才将更高层的任务重新分布到第一层,然后重新遍历。这样遍历时就不会额外遍历其他暂时不会执行到的槽,避免浪费CPU能力。


二十一、流量回放


开源界支持流量回放的软件有很多,比如TcpCopy,但是都需要运维手工操作。有了RPC框架后,操作姿势就不一样了,RPC框架可以收集请求数据,然后伪装成一个服务调用者,请求改造后需要回归验证或者大促时需要压促的服务提供者。


二十二、泛化调用


在不同开发语言、测试平台和网关场景中,调用者无法强依赖服务提供者的接口私服JAR包信息,那此时调用就需要RPC框架进行兼容。调用者只需要指定接口、方法、参数类型、参数值,就可以完成一次调用,这种调用方式称为Generic泛化调用。


我们可以利用插件机制新增一种序列化和反序列化方式实现这个功能。但是在网关调用中,一般不要求指定参数类型,而是要求服务端的入参必须是集合Map对象。