RPC通信功能实现
HBase的RPC通信功能主要基于Protobuf和NIO这两个组件来实现。在通信管道上选择的是protobuf对外声明的BlockingRpcChannel(堵塞式),其callBlockingMethod方法决定了client与服务端的交互行为。比方採用什么样的方法进行通信以及通信报文的格式规则都是通过该方法来描写叙述的。
HBase对外声明了BlockingRpcChannelImplementation实现类用于实现BlockingRpcChannel接口的业务逻辑。其在通信方式的选择上採用的是Socket通信。在通信的服务端通过RpcServer来构建ServerSocket。而在client使用RpcClient来构建与服务端通信的Socket,依照功能职责的不同,RpcServer可划分成3大组件。当中:
-
Listener负责监听client的连接请求
在Listener的内部主要封装着一个ServerSocketChannel以及多个Reader线程。当中ServerSocketChannel主要负责接收client的连接请求,请求被响应前,会暂存于等待队列中,等待队列的长度通过hbase.ipc.server.listen.queue.size參数来设置(默觉得128)。针对每一个已建立的连接。系统还会实时检測其空暇时间,假设空暇时间超过2秒(即2秒内client没有再次通过该连接来发送请求,而且之前的请求操作已经处理完成),系统会将该连接进行关闭,时间阀值是通过hbase.ipc.client.connection.maxidletime參数来控制的。
当请求信息到达后,其会派遣合适的Reader进行读取(基于轮训的方式来使每一个Reader的负载可以均衡),Reader线程的数量是通过hbase.ipc.server.read.threadpool.size參数来指定的,默觉得10个。线程启动后会进入堵塞状态直至client请求操作的到来。client向服务端发送的通信报文是依照一定格式进行组织的,如图所看到的:
-
当client与服务端进行初次握手时,其会向服务端发送RPCHeader报文。以便服务端可以对client的连接请求做校验处理。
假设校验结果满足下面规则,说明该请求操作是合法的:
(1)前4个字节信息为HBas;
(2)第5个字节(VERSION信息)的值为0;
(3)在没有启用security的情况下(hbase.security.authentication属性值不为kerberos)。第6个字节的值为80。
-
接着。client会向服务端发送ConnectionHeader报文,通过它来封装client所请求的服务。
ConnectionHeader是通过使用protobuf来完毕序列化处理的,其protocol声明例如以下:
message ConnectionHeader { optional UserInformation user_info = 1; optional string service_name = 2; optional string cell_block_codec_class = 3; optional string cell_block_compressor_class = 4; }
服务端收到该请求消息之后,可通过其service_name属性来推断client所要訪问的服务名称,从而定位到详细的服务。
-
确定了详细的服务之后,client便可持续向服务端发送Request报文,通过它来定位将要运行服务的哪一个方法。
方法名称是通过RequestHeader来封装的。其属于Request报文的一部分,如图所看到的:
RequestHeader相同是採用protobuf进行序列化处理。其protocol声明例如以下:
message RequestHeader { optional uint32 call_id = 1; optional RPCTInfo trace_info = 2; optional string method_name = 3; optional bool request_param = 4; optional CellBlockMeta cell_block_meta = 5; optional uint32 priority = 6; }
其method_name属性用于定位将要运行的方法名称,方法參数是通过Param报文来封装的。除此之外,client还可向服务端传递一些KeyValue数据(比方Replication功能会用到这些数据)。这些数据会序列化到CellBlock报文里。
Reader线程读取到这些信息后開始构造CallRunner对象。并将其赋予空暇的Handler进行处理。
-
-
Handler负责处理client的请求操作
从服务端的角度观察,client的全部请求都可封装成CallRunner对象,假设把Reader看做是CallRunner的生产者,那么Handler便是消费者。为了加快服务端的响应效率,RpcServer是同意同一时候存在多个消费者的,以此来并发消费全部的CallRunner产品。然而CallRunner产品在全部的消费者之间应当怎样做到合理分配?这主要是通过RpcScheduler来调度的。HBase对外声明了两种RpcScheduler的功能实现类。当中HMaster使用的是FifoRpcScheduler,而HRegionServer使用的SimpleRpcScheduler。
-
FifoRpcScheduler
基于线程池来消费全部的CallRunner产品,CallRunner的消费顺序採用FIFO原则(依照产出的先后顺序依次进行消费)。针对每一个CallRunner产品,系统都会开启一个Handler线程负责对其进行消费处理,线程池所能同意的最大并发数是由详细的服务来对外进行声明的,如HMaster默认情况下同意25个并发Handler(通过hbase.master.handler.count參数进行设置)。
-
SimpleRpcScheduler
採用该策略进行调度处理以后,系统会依据不同的请求类型将全部的CallRunner产品划分成3组:
(1)假设其封装的请求是基于meta表格的操作,将其划分到priorityExecutor组里。
(2)假设其封装的请求是基于用户表格的操作,将其划分到callExecutor组里;
(3)假设其封装的是replication请求,将其划分到replicationExecutor组里。
然后为每个产品组分配数量不等的Handler,让Handler仅仅消费指定组中的产品。不同的产品组所分配的Handler数量相同是由详细的服务来对外声明的。拿HRegionServer举例:
分配给priorityExecutor组的Handler数量通过hbase.regionserver.metahandler.count參数来指定。默觉得10个;
分配给callExecutor组的Handler数量通过hbase.regionserver.handler.count參数来指定,默觉得30个;
分配给replicationExecutor组的Handler数量通过hbase.regionserver.replication.handler.count參数来指定。默觉得3个。
每个产品组还可细分成多个产品队列,默认情况下每个产品组仅仅包括一个产品队列。
这样产品组中的全部Handler都会去竞争该队列中的资源。为了防止竞争慘烈的情况发生,可将每个产品组划分成多个产品队列,让每个Handler仅仅去抢占指定队列中的资源。
在HRegionServer中,可通过例如以下方法来计算callExecutor组能够划分成多少个产品队列:
Math.max(1,hbase.regionserver.handler.count*hbase.ipc.server.callqueue.handler.factor)
当中hbase.ipc.server.callqueue.handler.factor属性值默觉得0。即在默认情况下仅仅将该产品组划分成一个产品队列。
单个产品队列的容量并非按需使用无限增长的,HBase对其长度及空间大小都做了对应的阀值控制,当中:
hbase.ipc.server.max.callqueue.length用于限制产品队列的长度(默觉得handler数乘以10)
hbase.ipc.server.max.callqueue.size用于限制产品队列的空间大小(默觉得1G)
成功将CallRunner产品分配给Handler之后,该Handler開始对其进行消费处理。消费过程主要是通过调用RpcServer的call方法来运行指定服务的对应方法。并通过Responder将方法的运行结果返回给client。
-
Responder负责将服务端的处理结果返回给client
服务端返回给client的通信报文是依照例如以下格式进行组织的:
当中ResponseHeader是採用protobuf进行序列化的。其protocol声明例如以下:
message ResponseHeader { optional uint32 call_id = 1; optional ExceptionResponse exception = 2; optional CellBlockMeta cell_block_meta = 3; }
其内部主要封装了服务端的运行异常信息,以及CellBlock的元数据信息;Result用于封装运行方法的返回结果。其序列化方法须要依据详细的返回值类型来做决定。CellBlock用于封装服务端所返回的KeyValue数据(如scan操作的查询结果)。
-
客户端发送请求消息之后,会进入循环等待状态,直至服务端返回运行结果,假设等待时间超过10秒。则系统会觉得该请求失败,将开启重试或关闭连接(假设hbase.ipc.client.connect.max.retries參数值为0)。
-
服务端相关配置例如以下:
-
hbase.ipc.server.listen.queue.size
存放连接请求的等待队列长度,默认与ipc.server.listen.queue.size參数值同样,为128个。
-
hbase.ipc.server.tcpnodelay
是否在TCP通信过程中启用Nagle算法,默认不启用。
-
hbase.ipc.server.tcpkeepalive
是否启用TCP的keepalive机制。通过心跳包来推断连接是否断开,默认启用。
-
hbase.ipc.server.read.threadpool.size
Reader线程数。默觉得10个。
-
hbase.ipc.server.max.callqueue.size
单个消费队列所同意的存储空间上限(默觉得1GB),超过该上限client会抛出下面异常:
Call queue is full, is ipc.server.max.callqueue.size too small?
-
hbase.ipc.server.max.callqueue.length
单个消费队列的长度限制,默认值为10倍的Handler数。
-
hbase.ipc.server.callqueue.handler.factor
该參数用于决定消费队列的个数。
-
hbase.ipc.server.callqueue.read.share
读Handler数占总Handler数的比例。
-
-
client相关配置例如以下:
-
hbase.ipc.ping.interval
client与服务端的心跳时间间隔,以及Socket的默认读写超时时间(HBase的其它一些參数会覆盖该值,如hbase.rpc.timeout)。
-
hbase.client.rpc.codec
CellBlock报文内容的编码/解码器,默认与hbase.client.default.rpc.codec的參数值同样,为org.apache.hadoop.hbase.codec.KeyValueCodec。
假设将hbase.client.default.rpc.codec设置成空字符串,而且不正确hbase.client.rpc.codec參数进行设置,则在rpc通信过程中将不在使用CellBlock报文对KeyValue进行序列化。而是将其序列化到protobuf的message里(Param或Result)。
-
hbase.client.rpc.compressor
CellBlock报文内容的压缩/解压缩算法,默认不採用压缩。
-
hbase.ipc.socket.timeout
client尝试与服务端建立连接的超时时间,默认与ipc.socket.timeout同样为20秒。
-
hbase.rpc.timeout
client对RegionServer的rpc请求超时时间。
-
hbase.client.pause
Socket连接失败后。会休眠一段时间。然后在又一次连接,该參数用于指定休眠多久。默觉得0.1秒。
-
hbase.ipc.client.connect.max.retries
当client与服务端的连接出现错误时。通过该參数来指定重试次数,默觉得0(不重试)。
-
hbase.ipc.client.connection.maxidletime
client与服务端的连接空暇时间超过该数值时(即指定时间范围内,client没有收到服务端的响应信息)。系统会将该连接关闭,默认的响应超时时间为10秒。
-
hadoop.rpc.socket.factory.class.default
SocketFactory实现类,默觉得org.apache.hadoop.net.StandardSocketFactory,其createSocket方法会创建SocketChannel用于NIO通信。
-
-
在client可通过例如以下代码来构建所需服务,拿ClientService举例:
-
在服务端可通过例如以下代码来公布服务,相同拿ClientService举例:
List<BlockingServiceAndInterface> services = new ArrayList<BlockingServiceAndInterface>(); services.add(new BlockingServiceAndInterface( ClientService.newReflectiveBlockingService(regionServer), ClientService.BlockingInterface.class)); RpcServer rpcServer = new RpcServer(serverInstance , name , services , isa , conf, scheduler ); rpcServer.start();