
导读
在分布式存储系统中,节点间的通信是很重要的组成部分。如果不了解系统中的通信机制,就无法真正理解一套完整的读写流程。由于HBase年代久远(1.2.0版本),暂时使用的还是java原生的NIO,并没有使用Netty之类的框架。
本篇就来介绍下HBase中基于NIO的RPC通信。
1 RPC=protobuf+nio
HBase基于protobuf+nio实现rpc:基于protobuf预定义消息格式,基于nio做消息的传输以及服务器编写。

protobuf可以简单的理解成一种格式定义,定义好可以自动生成对应的接口类。开发者需要在客户端和服务端生成对应的实现类,客户端负责封装接口参数,通过socket发送到远程服务提供方;服务端负责实现接口的具体执行过程,在服务器上执行真正的操作,返回结果。这样调用者就可以像调用本地方法一样调用远程方法了。

HBase的服务端采用的是标准的Reactor模型:
Listener相当于acceptor,封装了Reader线程池,用于接收客户端的连接;
Scheduler中有不同的线程池用于专门处理任务;
Responder负责向客户端返回执行结果。
2 客户端详解
HBase的客户端与传统的数据库连接池类似,也是在内存中维护客户端与各个存储节点的连接,并配置对应的存活时间,存活时间范围内连接是可以复用的(这里的复用是指,不需要单独进行TCP的三次握手;通用的header,协议格式等都可以直接复用;直接调用对应的OutPutStream.write方法执行写操作就可以了)。因此如果想复用连接,就需要在逻辑业务上区分相同RegionServer的不同请求类型,HBase在protobuf中定义了不同的请求类型,如:Get,Mutate,Scan,Bulk,multi等,具体的可以参考protobuf中的定义。
总结来说,相同的RegionServer、相同的请求类型、存活时间范围内都可以共享连接,不需要重新建立连接。
整体的流程如下:当客户端需要发送请求时,先判断是否已经存在有效连接,如果不存在则重新建立。获得连接后执行发送操作,并阻塞等待。如果开启了异步发送,则会发送到另一个发送列表中,会有专门的线程执行发送操作。

客户端中比较重要的组件主要有下面几个:
RpcClientImpl:客户端主要实现类,内部维护了Connection的Map引用,其中Map的key由用户、方法服务名、目标地址等组成。
Connection:代表一次连接,内部封装了Socket。

整体流程如下:
1 当调用Callable时,获得RPC实例,并获得RpcClient实现类
2 调用Rpc的get方法
3 调用RpcClientImpl的call方法
4 获得连接Connection
5 执行发送
6 阻塞等待结果

3 服务端详解
服务端的设计明显就要复杂很多,首先是Rpc的服务器都封装在RpcServer中,当HRegionServer启动时,会执行初始化。

Server中包含三个主要的部分:
Listener,负责监听消息,内部封装Reader线程池异步接收处理、
Responder,负责回复、
RpcScheduler,负责根据不同的策略分发任务

整体流程如下:
1 HRegionServer启动,开启RsRpcServerices服务
2 初始化RpcServer,初始化内部组件:Listener、scheduler、responder等
3 listener接收请求,内部Reader线程池轮训处理,执行readprocess,交给scheduler进行分发
4 scheduler分发执行任务,调用rpcserver的call方法
5 调用rsrpcservices中的实现方法,执行处理
6 结果放入返回队列中,返回

4 总结
第一次阅读NIO相关的服务器编程,关于NIO的技术细节还不是很熟练,因此有很多Selector互相调用触发的代码看的有点蒙。不过总体的流程梳理的差不多,这些细节也就不过多花精力了。
总结一下:对于HBase的Client端,连接使用NIO的Socket实现,相同主机相同类型的请求复用连接,阻塞等待连接返回结果;对于HBase的Server端,使用Reader线程池异步接收消息执行读取操作;使用Scheduler进行消息的分发处理;使用Responder合并处理结果,批量返回。
往期推荐:
HBase源码:如何设计高效的分布式客户端mp.weixin.qq.com

