Hadoop RPC 简介
数新网络,让每个人享受数据的价值
前 言
RPC(Remote Procedure Call)远程过程调用协议,一种通过网络从远程计算机上请求服务,而不需要了解底层网络技术的协议。RPC 它假定某些协议的存在,例如 TPC/UDP 等,为通信程序之间携带信息数据。在 OSI 网络七层模型中,RPC 跨越了传输层和应用层,RPC 使得开发,包括网络分布式多程序在内的应用程序更加容易。
01 RPC 调用流程

·服务消费方(client)调用,以本地调用方式调用服务
·client stub 接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体
·client stub 找到服务地址,并将消息发送到服务端
·server stub 收到消息后进行解码
·server stub 根据解码结果调用本地的服务
·本地服务执行并将结果返回给 server stub
·server stub 将返回结果打包成消息并发送至消费方
·client stub 接收到消息,并进行解码
·服务消费方得到最终结果
02 RPC 的特点
2-1 透明性
远程调用其他机器上的程序,对用户来说就像是调用本地方法一样
2-2 高性能
RPC server 能够并发处理多个来自 Client 的请求(请求队列)
2-3 可控性
jdk 中已经提供了一个 RPC 框架-RMI,但是该 RPC 框架过于重量级并且可控之处比较少,因此 Hadoop RPC 实现了自定义的 RPC 框架
03 Hadoop RPC
与其他 RPC 框架一样,Hadoop RPC 主要分为四个部分,分别是序列化层、函数调用层、 网络传输层和服务器端处理框架,具体实现机制如下:
序列化层:序列化层的主要作用是将结构化对象转为字节流以便于通过网络进行传输或 写入持久存储。在 RPC 框架中,它主要用于将用户请求中的参数或者应答转化成字节流 以便跨机器传输。Hadoop 自己实现了序列化框架,一个类只要实现 Writable 接口,即 可支持对象序列化与反序列化。
函数调用层:函数调用层的主要功能是定位要调用的函数并执行该函数。HadoopRPC 采 用 Java 反射机制与动态代理实现了函数调用。
网络传输层:网络传输层描述了 Client 与 Server 之间消息传输的方式,Hadoop RPC 采用了基于 TCP/IP 的 Socket 机制。
服务器端处理框架:服务器端处理框架可被抽象为网络 I/O 模型。它描述了客户端与服 务器端间信息交互的方式。它的设计直接决定着服务器端的并发处理能力。常见的网络 I/O 模型有阻塞式 I/O、非阻塞式 I/O、事件驱动 I/O 等,而 Hadoop RPC 采用了基于 Reactor 设计模式的非阻塞式 I/O 模型。

由上图可知 Hadoop RPC 实现主要包括三部分 Client 类、Server 类、RPC 类。
3-1 客户端代理的创建
由 RPC.getProxy 获取客户端代理,一个代理处理用户到具体服务器的具体协议对应的连接,同时连接属于一个 Client,而 Client 一般由 SocketFactory 决定,不同 SocketFactory 对应不同 Client。
因此 getProxy 需指定包括 SocketFactory,用户,服务器地址,协议这 4 个信息,另外还需指定连接读操作超时时间。对应 getProxy 重要的 5 个信息,最少需给定服务器地址和使用的协议,其他的都可以默认。
SocketFactory 默认为默认 SocketFactory,用户默认为当前用户,读操作超时时间默认为 0,此时会设置为 pingInterval 获取客户端代理的类为 RPCEngine 类,默认实现为 ProtobufRpcEngine 在 ProtobufRpcEngine 中对 getProxy 方法进行了重载。

最终调用的为参数最多的 getProxy 方法

由方法实现可知创建代理最主要的就是通过 getProxy 方法传入的参数创建 invoker 对象,然后通过 Java 动态代理创建动态代理,因此创建的代理核心信息保存在调用处理器 invoker 中,下面是 invoker 的构造函数。

因此,就是通过传入的用户、服务器地址、协议、超时时间等信息构建 Invoker。ConnectionId 用来确定该 invoker 负责处理的链接,SocketFactory 用来在缓存中查找所属客户端,若存在则使用该 factory 的客户端,否则构建一个 Client 对象。
获取 client 的方法:

这样,客户端的代理构建完成了,其实主要是构建了代理关联的调用处理器中的 connectionId(对应一个 Connection),以及 client(将代理处理的连接注册到相应客户端)。
3-2 创建服务器对象
因为 Server 端涉及到多个客户端的调用,所以使用了 Reactor 的设计模式。Reactor 主要是基于多路复用的非阻塞 IO 实现的基于事件驱动的 IO 框架。Hadoop RPC 底 层使用的是 Java NIO,而 Java NIO 正好就是一种多路复用的非阻塞 IO,其中最重要的就是 Selector 选择器。
RPC Server 处理流程:

其中有几个比较重要的组件:
Client: 客户端
Listener: Server 端只存在一个 Listener,主要功能就是分发,在 Selector 中注册了 ACCEPT 事件,每当有新的 Client 连接,便会为 Client 指定一个 Reader 线程。创建服务器对象,通过 RPC.getServer 完成
Reader: Reader 线程有多个,主要任务是读取请求,并将请求封装成一个 Call,放入 callQueue 中
CallQueue: reader handler 之间的缓冲队列,生产消费者模型
Responder:read request 和 write response 采用不同的 selector 实现读写分离
connectionManager: 定时清理 idle 时间过长的 Connection

上图是 RPCEngine 获取 Server 服务器对象的方法,该方法最后会调用 Server 类中的构造方法创建 Server 对象。
在构造方法中除了对端口地址等属性初始化外,还构建了 Listener 和 Responder,一般通过 RPC.getServer 创建服务器后,会调用服务器的 start 方法启动服务器。当服务端启动时同时会启动 Listener,Responder 及 Handler。

Listener 构造:

Listener 类是一个线程类,主要任务就是为连入的 Socket 分配 Reader。详细代码逻辑在启动线程后的 run 方法的 doAccept 方法中。
Reader:
将 Request 中的属性提取出来封装成一个 RpcCall 对象,并将 Call 对象放入 CallQueue 中。

Call:

Call 类中封装了 Request 对象和 Response 对象,Call 类被 Reader 存放在 CallQueue 中,等待 Handler 的处理。
Handler:
Handler 的主要任务就是从 callQueue 拿出 Call,并通过 Request 找到真实的实现方法,并通过方法名和参数进行执行。
3-3 建立连接
Client 与每个 Server 之间维护一个通信连接。该连接相关的基本信息及操作被封装到 Connection 类中。其中,基本信息主要包括:通信连接唯一标识(remoteId),与 Server 端通信的 Socket(socket),网络输入数据流(in),网络输出数据流(out),保存 RPC 请求的哈希表(calls)等。
当调用 call 函数执行某个远程方法时,Client 端需要进行如下几个步骤:
步骤 1
创建一个 Connection 对象,并将远程方法调用信息封装成 Call 对象,放到 Connection 对象中的哈希表 calls 中;
步骤 2
调用 Connetion 类中的 sendParam()方法将当前 Call 对象发送给 Server 端;
步骤 3
Server 端处理完 RPC 请求后,将结果通过网络返回给 Client 端,Client 端通过 receiveResponse()函数获取结果;
步骤 4
Client 端检查结果处理状态(成功还是失败),并将对应的 Call 对象从哈希表中删除。

在建立连接完成后就可以处理客户端的请求了,主要任务是从共享队列中获取 call 对象,执行对应的函数调用,并将结果返回给客户端,这全部由 Handler 线程完成。
Server 端可同时存在多个 Handler 线程,它们并行从共享队列中读取 Call 对象,经执行对应的函数调用后,将尝试着直接将结果返回给对应的客户端。
但考虑到某些函数调用返回结果很大或者网络速度很慢,可能难以将结果一次性发送给客户端,此时 Handler 将尝试着将后续发送任务交给 Responder 线程。
版权声明: 本文为 InfoQ 作者【数新网络官方账号】的原创文章。
原文链接:【http://xie.infoq.cn/article/db2dbf840f65e8a4fbc2bafa1】。文章转载请联系作者。
评论