[Hadoop RPC服务端:Server]
服务端的基本思路可以简述如下:通过网络获取到远程调用相关的信息,找到对应的方法,执行完成后,将结果通过网络发送给客户端。
对于服务端,我们将主要关心内部的实现机制,找到服务端处理一次远程调用的整个流程。
与Server相关的类主要为下面几个(都是Server的内部类)
Server.Listener &
Server.Listener.Reader
将Listener和Reader放在一起,主要是因为Listener和Reader在一起合作,使用Java NIO来实现的网络请求的监听和接收。简单说来,就是Listener线程负责监听来自客户端的socket连接请求,做accept请求操作。然后将建立连接的channel注册到Reader上,由Reader来负责处理数据到达的事件。这里主要使用基于Java NIO实现的网络事件的处理,因此需要对Java NIO有基本的了解,才能读懂这里的代码逻辑。
Server.Connection
代表一个客户端和服务端的连接的访问通道。每个连接到服务端的channel都有一个附加Connection对象,来作为业务上的连接。服务端每次在accept客户端的连接请求的时候,会创建一个Connection对象,将之绑定在这个channel上。
在Connection中会读取数据,将数据反序列化成对应的对象。
Server.Call
表示一次远程调用业务,包含了本次远程过程调用的一些请求参数信息,以及执行完过程调用之后的返回结果信息。
Server.Responder
Responder在server模型中,主要负责远程调用结果的返回,将远程调用的结果通过socket发送给对应的客户端。
Server.Handler
Handler主要负责任务调度。从Server的Call队列中消费远程调用业务,交给Responder返回给客户端。
1、Server的入口
Server的入口可以认为是两部分,一个是构造函数生成一个Server对象,一个是start方法启动Server服务
protected
Server(String bindAddress,
int
port,
Class<?
extends
Writable> rpcRequestClass,
int
handlerCount,
int
numReaders,
int
queueSizePerHandler, Configuration conf,
String serverName, SecretManager<?
extends
TokenIdentifier> secretManager,
String portRangeConfig)
throws
IOException {
this
.
bindAddress
= bindAddress;
this
.
conf
= conf;
this
.
portRangeConfig
= portRangeConfig;
this
.
port
= port;
this
.
rpcRequestClass
= rpcRequestClass;
this
.
handlerCount
= handlerCount;
this
.
socketSendBufferSize
= 0;
this
.
maxDataLength
= conf.getInt(CommonConfigurationKeys.
IPC_MAXIMUM_DATA_LENGTH
,
CommonConfigurationKeys.
IPC_MAXIMUM_DATA_LENGTH_DEFAULT
);
if
(queueSizePerHandler != -1) {
this
.
maxQueueSize
= queueSizePerHandler;
}
else
{
this
.
maxQueueSize
= handlerCount * conf.getInt(
CommonConfigurationKeys.
IPC_SERVER_HANDLER_QUEUE_SIZE_KEY
,
CommonConfigurationKeys.
IPC_SERVER_HANDLER_QUEUE_SIZE_DEFAULT
);
}
this
.
maxRespSize
= conf.getInt(
CommonConfigurationKeys.
IPC_SERVER_RPC_MAX_RESPONSE_SIZE_KEY
,
CommonConfigurationKeys.
IPC_SERVER_RPC_MAX_RESPONSE_SIZE_DEFAULT
);
if
(numReaders != -1) {
this
.
readThreads
= numReaders;
}
else
{
this
.
readThreads
= conf.getInt(
CommonConfigurationKeys.
IPC_SERVER_RPC_READ_THREADS_KEY
,
CommonConfigurationKeys.
IPC_SERVER_RPC_READ_THREADS_DEFAULT
);
}
this. callQueue = new LinkedBlockingQueue<Call>(maxQueueSize
);
this
.
maxIdleTime
= 2 * conf.getInt(
CommonConfigurationKeysPublic.
IPC_CLIENT_CONNECTION_MAXIDLETIME_KEY
,
CommonConfigurationKeysPublic.
IPC_CLIENT_CONNECTION_MAXIDLETIME_DEFAULT
);
this
.
maxConnectionsToNuke
= conf.getInt(
CommonConfigurationKeysPublic.
IPC_CLIENT_KILL_MAX_KEY
,
CommonConfigurationKeysPublic.
IPC_CLIENT_KILL_MAX_DEFAULT
);
this
.
thresholdIdleConnections
= conf.getInt(
CommonConfigurationKeysPublic.
IPC_CLIENT_IDLETHRESHOLD_KEY
,
CommonConfigurationKeysPublic.
IPC_CLIENT_IDLETHRESHOLD_DEFAULT
);
this
.
secretManager
= (SecretManager<TokenIdentifier>) secretManager;
this
.
authorize
=
conf.getBoolean(CommonConfigurationKeys.
HADOOP_SECURITY_AUTHORIZATION
,
false
);
// configure supported authentications
this
.
enabledAuthMethods
= getAuthMethods(secretManager, conf);
this
.
negotiateResponse
= buildNegotiateResponse(
enabledAuthMethods
);
// Start the listener here and let it bind to the port
listener
= new Listener();
this
.
port
=
listener
.getAddress().getPort();
this
.
rpcMetrics
= RpcMetrics.create(
this
);
this
.
rpcDetailedMetrics
= RpcDetailedMetrics.create(
this
.
port
);
this
.
tcpNoDelay
= conf.getBoolean(
CommonConfigurationKeysPublic.
IPC_SERVER_TCPNODELAY_KEY
,
CommonConfigurationKeysPublic.
IPC_SERVER_TCPNODELAY_DEFAULT
);
// Create the responder here
responder
= new Responder();
if
(secretManager !=
null
) {
SaslRpcServer. init(conf);
}
this
.
exceptionsHandler
.addTerseExceptions(StandbyException.
class
);
}
初始化了Server的内部Call队列,这个队列将会由Handler线程来消费。初始化了Listener对象,初始化了Responder对象。
下面是启动Server的方法,启动Server的内部各个功能模块。
public
synchronized
void
start () {
responder
.start();
listener
.start();
handlers
=
new
Handler[
handlerCount
];
for
(
int
i = 0; i <
handlerCount
; i++) {
handlers
[i] =
new
Handler(i);
handlers
[i].start();
}
}
根据数据的流向,从Listener开始来做流程跟进。Listener和Reader合作,来完成网络连接的建立。
2、Listener
Listener的属性如下。
private
ServerSocketChannel
acceptChannel
=
null
;
//the accept channel
private
Selector
selector
=
null
;
//the selector that we use for the server
private
Reader[]
readers
=
null
;
private
int
currentReader
= 0;
private
InetSocketAddress
address
;
//the address we bind at
private
Random
rand
=
new
Random();
private
long
lastCleanupRunTime
= 0;
//the last time when a cleanup connec-
//-tion (for idle connections) ran
private
long
cleanupInterval
= 10000;
//the minimum interval between
//two cleanup runs
private
int
backlogLength
=
conf
.getInt(
CommonConfigurationKeysPublic.
IPC_SERVER_LISTEN_QUEUE_SIZE_KEY
,
CommonConfigurationKeysPublic.
IPC_SERVER_LISTEN_QUEUE_SIZE_DEFAULT
);
构造Listener对象时的操作,在Listener的构造函数中有体现
public
Listener()
throws
IOException {
address
=
new
InetSocketAddress(
bindAddress
,
port
);
// Create a new server socket and set to non blocking mode
acceptChannel
= ServerSocketChannel. open();
acceptChannel
.configureBlocking(
false
);
// Bind the server socket to the local host and port
bind(
acceptChannel .socket(), address, backlogLength, conf, portRangeConfig);
port
=
acceptChannel
.socket().getLocalPort();
//Could be an ephemeral port
// create a selector;
selector
= Selector. open();
readers
=
new
Reader[
readThreads
];
for
(
int
i = 0; i <
readThreads
; i++) {
Reader reader =
new
Reader(
"Socket Reader #"
+ (i + 1) +
" for port "
+
port
);
readers
[i] = reader;
reader.start();
}
// Register accepts on the server socket with the selector.
acceptChannel
.register( selector, SelectionKey. OP_ACCEPT);
this
.setName(
"IPC Server listener on "
+
port
);
this
.setDaemon(
true
);
}
打开一个服务端的channel,绑定服务端口,为服务端channel在Selector对象上注册接受事件。同时,启动了多个Reader线程。在Listener的构造方法中完成了对建立连接端口在selector的监听,这样当有连接请求到达时,就能够做相应的处理。
在Listener的run方法中,会完成处理建立连接请求的事件。以及将channel注册到reader上(在listener上只注册了建立连接的动作,没有注册数据读取动作)。
Listener线程启动之后的主体run方法,主要是进行selector的select操作来获取建立连接的事件,然后调用doAccept方法来建立连接。来看doAccept方法的实现,代码如下
void
doAccept(SelectionKey key)
throws
IOException, OutOfMemoryError {
Connection c =
null
;
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel channel;
while
((channel = server.accept()) !=
null
) {
channel.configureBlocking(
false
);
channel.socket().setTcpNoDelay(
tcpNoDelay
);
Reader reader = getReader();
try
{
reader.startAdd();
SelectionKey readKey = reader.registerChannel(channel);
c =
new Connection(readKey, channel, Time.now());
readKey.attach(c);
synchronized
(
connectionList
) {
connectionList
.add(
numConnections
, c);
numConnections
++;
}
if
(
LOG
.isDebugEnabled())
LOG
.debug(
"Server connection from "
+ c.toString() +
"; # active connections: "
+
numConnections
+
"; # queued calls: "
+
callQueue
.size());
}
finally
{
reader.finishAdd();
}
}
}
通过代码中底色标注的部分来看,接收客户端的建立连接的请求,然后将这个建立连接的channel注册到一个Reader线程上。注意这里,在注册时,为每个channel附件了一个Connection对象,这个对象在后面部分会用到。
到这里,客户端和服务端的网络连接已经建立。接着,就是客户端数据请求到达服务端时的处理逻辑,来跟进Reader的代码。Reader线程在Listener构造函数中已经启动,这里直接来看Reader的主体run方法(源码略),只做了调用doRunLoop方法一件事情。跟进doRunLoop方法,其实现也很简单,做一个selector的select操作,然后调用doRead做数据读取。而doRead方法也只是取出和这个channel对应的Connection对象,在这个Connection上对象上调用readAndProcess方法进行处理。
Connection并不是一个线程,它只是表示客户端和服务端连接的业务通道,
直接跟进它的readAndProcess方法,这个方法较长,删除了其中一些异常处理分支之后的代码如下
public
int
readAndProcess ()
throws
WrappedRpcServerException, IOException, InterruptedException {
while
(
true
) {
count = channelRead(
channel
,
dataLengthBuffer
);
if
(!
connectionHeaderRead
) {
count = channelRead(
channel
,
connectionHeaderBuf
);
}
count = channelRead(
channel
,
data
);
processOneRpc(
data
.array());
return
count;
}
基本上的逻辑就很简单了,从这个Connection对象对应的channel上读数据,然后进行处理。在processOneRpc方法中,做了数据的处理。来跟进这个方法
部分源代码如下
private
void
processOneRpc (
byte
[] buf)
throws
IOException, WrappedRpcServerException, InterruptedException {
final
DataInputStream dis =
new
DataInputStream(
new
ByteArrayInputStream(buf));
final
RpcRequestHeaderProto header =
decodeProtobufFromStream(RpcRequestHeaderProto. newBuilder(), dis);
processRpcRequest(header, dis);
}
去除掉一些异常分支之后,便是处理RPC请求的逻辑:对数据进行反序列化得到请求对象,然后进行处理。来看processRpcRequest的实现,去除一些异常分支之后的代码如下
private
void
processRpcRequest(RpcRequestHeaderProto header,
DataInputStream dis)
throws
WrappedRpcServerException,
InterruptedException {
Class<?
extends
Writable> rpcRequestClass =
getRpcRequestWrapper(header.getRpcKind());
Writable rpcRequest;
rpcRequest = ReflectionUtils. newInstance(rpcRequestClass,
conf
);
rpcRequest.readFields(dis);
Call call =
new
Call(header.getCallId(), header.getRetryCount(),
rpcRequest,
this
, ProtoUtil.convert(header.getRpcKind()), header
.getClientId().toByteArray());
callQueue
.put(call);
// queue the call; maybe blocked here
incRpcCount();
// Increment the rpc count
}
processRpcRequest方法算是一个起到中间作用的比较重要的方法,将数据流中的二进制数据转成对应的远程调用业务Call对象,然后将Call对象放入Server远程调用业务队列(callQueue,在Server的构造函数中完成的初始化)中.
到这里,我们以Listener开头做的数据追踪,便算到了尽头。要走后续的追踪,就需要回到Server的start方法,去看它的另一个内部组件Handler的实现了(在start方法中,同时启动了Listener和Handler)。
3、Handler
Handler本身继承了Thread类,所以在Server启动的时候,将Handler作为线程来启动,我们关注其主体的run方法。去除一些异常分支之后的代码如下
private
class
Handler
extends
Thread {
@Override
public
void
run() {
ByteArrayOutputStream buf =
new
ByteArrayOutputStream(
INITIAL_RESP_BUF_SIZE
);
while
(
running
) {
try
{
final
Call call =
callQueue
.take();
// pop the queue; maybe blocked here
if
(call.
connection
.
user
==
null
) {
value = call( call.
rpcKind
, call .
connection
.
protocolName
, call.
rpcRequest
,
call.
timestamp
);
}
else
{
value =
call.
connection
.
user
.doAs
(
new
PrivilegedExceptionAction<Writable>() {
@Override
public
Writable run()
throws
Exception {
// make the call
return
call(call.
rpcKind
, call.
connection
.
protocolName
,
call.
rpcRequest
, call .
timestamp
);
}
}
);
}
synchronized
(call.
connection
.
responseQueue
) {
// setupResponse() needs to be sync'ed together with
// responder.doResponse() since setupResponse may use
// SASL to encrypt response data and SASL enforces
// its own message ordering.
setupResponse(buf, call, returnStatus, detailedErr,
value, errorClass, error);
responder
.doRespond(call);
}
}
Handler在执行时候,从Server的远程调用业务队列中获取一个Call业务,执行这个远程调用业务(value=call(...)方法实现),设置远程调用业务的返回,然后进行结果返回。
4、Responder
在进行结果返回时,调用了Responder的doRespond,其实只是把要返回的数据添加到了对应的Connection的返回队列中。由于和之前的比较类似,所以,这里就不对Responder做过多的探讨。
主要说明一点,在processResponse方法中,是按照FIFO来从队列中取数据的。在进行数据发送时,如果数据没有一次发送完成,会将调用业务对象重新塞会队列中的第一个位置,用于下一次的继续发送。相关的代码如下所示
// Processes one response. Returns true if there are no more pending
// data for this channel.
//
private
boolean
processResponse(LinkedList<Call> responseQueue,
boolean
inHandler)
throws
IOException {
call = responseQueue.removeFirst();
SocketChannel channel = call.
connection
.
channel
;
//
// Send as much data as we can in the non-blocking fashion
//
int
numBytes = channelWrite(channel, call.
rpcResponse
);
if
(numBytes < 0) {
return
true
;
}
if
(!call.
rpcResponse
.hasRemaining()) {
}
else
{
//
// If we were unable to write the entire response out, then
// insert in Selector queue.
//
call.
connection
.
responseQueue
.addFirst(call);
}
}
每次都是取队列的第一个元素(removeFirst),执行写操作后,会检查数据是否已经全部发送完成。如果数据还未发送完成,则将对象继续放回队列(call.connection.responseQueue.addFirst(call))。在下一次处理返回结果时,继续发送这个处理结果。避免数据错乱。