RPC 解读

什么是RPC

RPC 全称 Remote Procedure Call —— 远程过程调用,它是一种进程间通信方式,通过网络从远程计算机程序上请求服务(通常是共享网络的另一台机器上的过程或函数),而不需要了解底层网络技术。简单一点就是:通过一定协议和方法使得调用远程计算机上的服务,就像调用本地服务一样 ,程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同。

互联网公司常用的RPC框架,阿里巴巴的hsf、dubbo(开源)、Facebook的thrift(开源)、Google grpc(开源)、Twitter的finagle(开源)等。

目标

RPC 的主要目标是让构建分布式计算(应用)更容易,在提供强大的远程调用能力时不损失本地调用的语义简洁性。 为实现该目标,RPC 框架需提供一种透明调用机制让使用者不必显式的区分本地调用和远程调用。

RPC 调用分类

从通信协议层面可以分为:

  • 基于 HTTP 协议的 RPC;
  • 基于二进制协议的 RPC;
  • 基于 TCP 协议的 RPC。

从是否跨平台可分为:

  • 单语言 RPC,如 RMI, Remoting;
  • 跨平台 RPC,如 google protobuffer, restful json,http XML。

从调用过程来看,可以分为同步通信RPC和异步通信RPC:

  • 同步 RPC:指的是客户端发起调用后,必须等待调用执行完成并返回结果;
  • 异步 RPC:指客户方调用后不关心执行结果返回,如果客户端需要结果,可用通过提供异步 callback 回调获取返回信息。大部分 RPC 框架都同时支持这两种方式的调用。

RPC 框架结构

RPC 框架的架构主要模块:

输入图片说明

RPC 服务方 的主要职责是提供服务,供客户端调用访问,服务端会通过一个接收器接受客户端的调用请求,根据相应的 RPC 协议进行解码获取调用方法以及相关参数,当调用完成后,服务器端通过后台处理模块处理完成并将结果返回给客户端。

对于客户端来说 ,服务调用完全透明,像调用本地服务一样调用远程方法,客户端调用服务时候通过一个远程连接和服务端建立通道,并通过相应的协议进行编码,将调用的方法和相关参数发送给服务方。

模块详解

  1. 服务端(Server):RPC 服务的提供者,负责将 RPC 服务导出
  2. 客户端 (Client):RPC 服务的消费者,负责调用 RPC 服务
  3. 代理(Proxy):通过动态代理,提供对远程接口的代理实现
  4. 执行器(Invoker):对于客户端:主要负责服务调用的编码,调用请求发送和等待结果返回;对于服务方:负责处理调用逻辑并返回调用结果
  5. 协议管理(Protocol):协议管理组件,负责整个 RPC 通信协议的编/解码
  6. 连接端口(Connector):负责维持客户方和服务方的长连接通道
  7. 后台处理(Processor):负责整个调用服务中的管理调度,包括线程池,分发,异常处理等
  8. 连接通道(Channel):客户端和服务器端的数据传输通道

具体到 JAVA 平台来说,其中的3,4通常使用动态代理实现,5,6,7,8使用 NIO 或者一些高性能 NIO 框架,如 mina,netty 实现。

更详细的分析

输入图片说明

分析:

  1. 服务消费方(client)调用以本地调用方式调用服务
  2. client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体
  3. client stub找到服务地址,并将消息发送到服务端
  4. server stub收到消息后进行解码
  5. server stub根据解码结果调用本地的服务
  6. 本地服务执行并将结果返回给server stub
  7. server stub将返回结果打包成消息并发送至消费方
  8. client stub接收到消息,并进行解码
  9. 服务消费方得到最终结果

RPC的目标就是要2~8这些步骤都封装起来,让用户对这些细节透明

复杂过程的封装

透明化服务

使用java代理的方式让用户像以本地调用方式调用远程服务。java代理有两种方式:

  • 字节码生成:实现高效,但维护不易
  • jdk 动态代理:推荐

消息编解码

确定消息数据结构

客户端和服务端互相通信需要先确定消息结构,客户端请求的消息结构一般包括:

  • 接口名称:告知服务端要调用哪个接口
  • 方法名:接口中会有多个方法,要具体到调用的那个方法
  • 参数类型&参数值:比如有bool、int、long、double、string、map、list,甚至如struct(class);以及相应的参数值
  • 超时时间
  • requestID:标识唯一请求id

服务器返回消息一般包括:

  • 返回值
  • 状态码
  • requestID

客户端以异步的方式发送二进制消息串到远程服务端,请求完远程接口后不会等待,而是继续执行当前线程,服务端处理完后再以消息的形式发送到客户端,这样就会出现两个问题:

  1. 怎么让当前线程“暂停”,等结果回来后,再向后执行?
  2. 如果有多个线程同时进行远程方法调用,这时建立在client server之间的socket连接上会有很多双方发送的消息传递,前后顺序也可能是随机的,server处理完结果后,将结果消息发送给client,client收到很多消息,怎么知道哪个消息结果是原先哪个线程调用的?

如图所示:

输入图片说明

解决方案:

  1. client线程每次通过socket调用一次远程接口前,生成一个唯一的ID,即requestID(requestID必需保证在一个Socket连接里面是唯一的),一般常常使用AtomicLong从0开始累计数字生成唯一ID
  1. 将处理结果的回调对象callback,存放到全局ConcurrentHashMap里面put(requestID, callback)
  2. 当线程调用channel.writeAndFlush()发送消息后,紧接着执行callback的get()方法试图获取远程返回的结果。在get()内部,则使用synchronized获取回调对象callback的锁,再先检测是否已经获取到结果,如果没有,然后调用callback的wait()方法,释放callback上的锁,让当前线程处于等待状态
  3. 服务端接收到请求并处理后,将response结果(此结果中包含了前面的requestID)发送给客户端,客户端socket连接上专门监听消息的线程收到消息,分析结果,取到requestID,再从前面的ConcurrentHashMap里面get(requestID),从而找到callback对象,再用synchronized获取callback上的锁,将方法调用结果设置到callback对象里,再调用callback.notifyAll()唤醒前面处于等待状态的线程

序列化

  • 序列化:将数据结构或对象转换成二进制串的过程,也就是编码的过程。(转换为二进制串后便于网络传输)
  • 反序列化:将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程。(将二进制转换为对象才好进行后续处理)

序列化方案选取,主要看三点:

  1. ** 通用性**,比如是否能支持Map等复杂的数据结构
  2. 性能, 包括时间复杂度和空间复杂度,由于RPC框架将会被公司几乎所有服务使用,如果序列化上能节约一点时间,对整个公司的收益都将非常可观,同理如果序列化上能节约一点内存,网络带宽也能省下不少
  3. 可扩展性, 对互联网公司而言,业务变化飞快,如果序列化协议具有良好的可扩展性,支持自动增加新的业务字段,而不影响老的服务,这将大大提供系统的灵活度

目前 JAVA 平台常用的序列化方式有:

  • xml:如 webservie SOAP
  • json:如 JSON-RPC
  • binary:如Protobuf;thrift; hession; kryo 等

通信

消息数据结构被序列化为二进制串后,下一步就要进行网络通信了。RPC 的应用场景实质是一种可靠的请求应答消息流,这点和 HTTP 类似。因此选择长连接 方式的 TCP 协议会更高效,与 HTTP 不同的是在协议层面我们定义了每个消息的唯一 id, 因此可以更容易的复用连接。

目前有两种常用IO通信模型:1)BIO;2)NIO。一般RPC框架需要支持这两种IO模型。

实现:

  • java的NIO
  • 基于mina,mina在早几年比较火热,不过这些年版本更新缓慢
  • 基于netty(推荐),现在很多RPC框架都直接基于netty这一IO通信框架,省力又省心,比如阿里巴巴的HSF、dubbo,Twitter的finagle等。

注意事项

服务端在处理客户端发来的请求时,要考虑很多东西

  • 并发控制:当多个请求并发处理的时候,如何管理和控制线程池和超时等待时间

  • 版本隔离:当服务有多个版本的时候,如何让不同的调用者能够调用正确的服务

  • 服务路由:当服务提供者有多台机器的时候,如何提高系统负载均衡,路由到正确的服务端

  • 服务降级:当多个服务重要性有不同的时候,如果保证核心业务的稳定性,适当的降低非核心业务优先级

  • 服务监控和报警:服务出现异常情况时候,运维和对应的系统负责人能够第一时间得到告警和错误信息

  • 网络问题:本地调用无需考虑是否能够执行问题,网络调用可能会因为各种外部网络环境,端口拦截,IP 受限等可能情况导致无法成功执行。所以 RPC 的服务端通常要考虑幂等性和容错性,接口需要较强的鲁棒性设计

  • 异常处理:RPC 和本地服务最大的不同就是 RPC 服务存在分布式一致性问题, 当服务没有调用成功情况下,本地和远程的服务可能处于一个不一致的状态,如何进行异常处理和事物的回滚机制也是一个需要考虑的问题,是需要保障强一致性和最终一致性通常取决于具体的业务需求

发布服务

  • 本办法:直接告诉调动方服务的IP和端口,如果增加机器的话,又得通知对方,对方只好手动修改。

  • 使用服务注册于发现中间件

    使用zookeeper

    zookeeper可以充当一个服务注册表(Service Registry),让多个服务提供者形成一个集群,让服务消费者通过服务注册表获取具体的服务访问地址(ip+端口)去访问具体的服务提供者

输入图片说明

具体来说,zookeeper就是个分布式文件系统,每当一个服务提供者部署后都要将自己的服务注册到zookeeper的某一路径上: /{service}/{version}/{ip:port}, 比如将服务HelloWorldService部署到两台机器,那么zookeeper上就会创建两条目录:分别为/HelloWorldService/1.0.0/100.19.20.01:16888 /HelloWorldService/1.0.0/100.19.20.02:16888。

zookeeper提供了“心跳检测”功能,它会定时向各个服务提供者发送一个请求(实际上建立的是一个 Socket 长连接),如果长期没有响应,服务中心就认为该服务提供者已经“挂了”,并将其剔除,比如100.19.20.02这台机器如果宕机了,那么zookeeper上的路径就会只剩/HelloWorldService/1.0.0/100.19.20.01:16888。

服务消费者会去监听相应路径(/HelloWorldService/1.0.0),一旦路径上的数据有任务变化(增加或减少),zookeeper都会通知服务消费方服务提供者地址列表已经发生改变,从而进行更新。

更为重要的是zookeeper与生俱来的容错容灾能力(比如leader选举),可以确保服务注册表的高可用性。

参考

转载于:https://my.oschina.net/zjm528el/blog/1556725

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值