上一篇博客中结合DN的应用简单的介绍了一下Hadoop RPC中client端的基本执行流程,算是给大家阅读源代码的一点提示吧。本文将对RPC的Server端的机制做一下简单的分析,但愿对大家的理解有所帮助,也算是对自己工作的一点总结。
按照惯例先来看一下Server类主要的组成部分Listener、Handler、Responser,通过名字很容易看出Server功能由三部分构成,负责获取用户请求的Listener,处理请求的Handler和响应用户请求的Responser。
首先来看下程序的入口Server中的start函数
1576 public synchronized void start() {
1577 responder.start();
1578 listener.start();
1579 handlers = new Handler[handlerCount];
1580
1581 for (int i = 0; i < handlerCount; i++) {
1582 handlers[i] = new Handler(i);
1583 handlers[i].start();
1584 }
1585 }
很明显函数分别启动了上边提到的基本的线程,当然我们也可以看出处理用户请求的handler线程的数目比较多,而Listener和Responser都是单线程。
上面提到Listener是一个单线程,有经验的程序猿就会想到这会不会成为RPC的一个瓶颈,因为如果请求的参数很长,那么读取一个call的就会花费比较长的时间,Listener性能可能不能满足高并发的需求。所以有必要提一下Listener两个很重要的辅助类Reader和Connection。
Reader是一个Runnable类,是实际进行监听的线程,Listener初始化时会创建一个readPool,一个readPool其实就是一坨Reader线程的集合,每个Reader线程都包含一个Selector对象(NIO的选择器,具体可以google一下)因为一个Reader线程可能监听多个Connection连接。
305 readers = new Reader[readThreads];
306 readPool = Executors.newFixedThreadPool(readThreads);
307 for (int i = 0; i < readThreads; i++) {
308 Selector readSelector = Selector.open();
309 Reader reader = new Reader(readSelector);
310 readers[i] = reader;
311 readPool.execute(reader);
312 }
Connection类每个客户端请求都会生成一个对应的Connection对象,并且该对象 被attach到reader的selector对象上。
按照一般的理解顺序我们先来看一下Listener线程的处理流程:
先看一个Listener的构造函数做了哪些事情
294 public Listener() throws IOException {
295 address = new InetSocketAddress(bindAddress, port);
296 // Create a new server socket and set to non blocking mode
297 acceptChannel = ServerSocketChannel.open();
298 acceptChannel.configureBlocking(false);
299
300 // Bind the server socket to the local host and port
301 bind(acceptChannel.socket(), address, backlogLength);
302 port = acceptChannel.socket().getLocalPort(); //Could be an ephemeral port
303 // create a selector;
304 selector= Selector.open();
305 readers = new Reader[readThreads];
306 readPool = Executors.newFixedThreadPool(readThreads);
307 for (int i = 0; i < readThreads; i++) {
308 Selector readSelector = Selector.open();
309 Reader reader = new Reader(readSelector);
310 readers[i] = reader;
311 readPool.execute(reader);
312 }
313
314 // Register accepts on the server socket with the selector.
315 acceptChannel.register(selector, SelectionKey.OP_ACCEPT);
316 this.setName("IPC Server listener on " + port);
317 this.setDaemon(true);
318 }
首选是创建一个ServerSocketChannel对象,创建一个选择器,初始化一个readpool,将创建的ServerSocketChannel注册到选择器上,将自己声明为一个后台线程。
接着来看线程的主方法run:
436 while (running) {
437 SelectionKey key = null;
438 try {
439 selector.select();
440 Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
441 while (iter.hasNext()) {
442 key = iter.next();
443 iter.remove();
444 try {
445 if (key.isValid()) {
446 if (key.isAcceptable())
447 doAccept(key);
448 }
不断的检查选择器的状态判断是不是有新的客户端请求,如果有且满足指定的类型调用doAccept方法。我们来看doAccept有哪些操作
504 while ((channel = server.accept()) != null) {
505 channel.configureBlocking(false);
506 channel.socket().setTcpNoDelay(tcpNoDelay);
507 Reader reader = getReader();
508 try {
509 reader.startAdd();
510 SelectionKey readKey = reader.registerChannel(channel);
511 c = new Connection(readKey, channel, System.currentTimeMillis());
512 readKey.attach(c);
513 synchronized (connectionList) {
514 connectionList.add(numConnections, c);
515 numConnections++;
516 }
首先得到客户端的连接,对该链接进行一个设置,从上文提到的readpool中获取一个reader,唤醒该reader包含的selector,将获取的connection channel注册到相应的selector上。针对这个客户端请求在创建一个connection对象,并将该对象attach到相应的selector上会调用这个connection中方法接受客户端发送的查询信息。
到这里我们可以看出Listener线程的角色只是为readpool分配任务,具体执行任务是每个reader线程。
Reader线程的执行流程:
330 while (running) {
331 SelectionKey key = null;
332 try {
333 readSelector.select();
334 while (adding) {
335 this.wait(1000);
336 }
337
338 Iterator<SelectionKey> iter = readSelector.selectedKeys().iterator();
339 while (iter.hasNext()) {
340 key = iter.next();
341 iter.remove();
342 if (key.isValid()) {
343 if (key.isReadable()) {
344 doRead(key);
345 }
346 }
347 key = null;
348 }
主要功能就是判断reader监听的channel是否有数据写入,如果有将进行数据的读取。接着来看读取的doRead方法:
530 Connection c = (Connection)key.attachment();
531 if (c == null) {
532 return;
533 }
534 c.setLastContact(System.currentTimeMillis());
535
536 try {
537 count = c.readAndProcess();
这里我们看到了前边提到的attach到selector上的connection对象,调用connection对象的readAndProcess方法读取客户端写入的数据。c.readAndProcess的主要功能是读取客户端写入的信息,包括一开始的头信息,后便RPC调用的函数和参数的信息。接受完数据之后调用方法processOneRpc进行处理,核心代码如下:
1279 private void processOneRpc(byte[] buf) throws IOException,
1280 InterruptedException {
1281 if (headerRead) {
1282 processData(buf);
1283 } else {
1284 processHeader(buf);
1285 headerRead = true;
首先判断头信息是否已经接收,如果接受通过processData处理获取的RPC调用的函数和参数。processData的具体处理流程如下:
1294 private void processData(byte[] buf) throws IOException, InterruptedException {
1295 DataInputStream dis =
1296 new DataInputStream(new ByteArrayInputStream(buf));
1297 int id = dis.readInt(); // try to read an id
1298
1299 if (LOG.isDebugEnabled())
1300 LOG.debug(" got #" + id);
1301
1302 Writable param = ReflectionUtils.newInstance(paramClass, conf);//read param
1303 param.readFields(dis);
1304
1305 Call call = new Call(id, param, this);
1306 callQueue.put(call); // queue the call; maybe blocked here
1307 incRpcCount(); // Increment the rpc count
1308 }
首选获取call的id(上边blog中提过传递的调用信息的具体结构),接受函数和参数信息存储在param中,同样创建一个Call对象,将整个Call对象添加到 BlockingQueue<Call> callQueue; 这个队列是Listener和Handerler交互的区域,Listener不断将请求添加到这个Queue中,handler不断检查这个Queue是否有元素,有就进行处理。
到此Listener部分的工作介绍完毕,总结一下就是Listener主要读取客户端的请求信息,将信息存储到一个Queue中,供Handler处理使用。
开始Handler线程的介绍:
1362 while (running) {
1363 try {
1364 final Call call = callQueue.take(); // pop the queue; maybe blocked here
1379 value = call(call.connection.protocol, call.param,
1380 call.timestamp);
1406 setupResponse(buf, call,
1407 (error == null) ? Status.SUCCESS : Status.ERROR,
1408 value, errorClass, error);
1416 responder.doRespond(call);
正如前面介绍的Hander首先从callQueue获取一个call请求,在调用Server的call方法具体执行call指明的函数和相应的参数。这里需要说明的一点是这个call方法具体实现在RPC.Server中,Server其实是一个abstract类而RPC.Server是Server的一个子类,这就再一次显示了RPC类作为对外交互的接口类的作用。
setupResponse函数的主要作用就是将执行结果的Status添加到call对象的response中,要将查询执行结果的状态通知客户端,客户端会据此进行相应的动作。最后调用的Responser的方法。
800 void doRespond(Call call) throws IOException {
801 synchronized (call.connection.responseQueue) {
802 call.connection.responseQueue.addLast(call);
803 if (call.connection.responseQueue.size() == 1) {
804 processResponse(call.connection.responseQueue, true);
805 }
806 }
807 }
主要就是将返回的结果添加到call.connection.responseQueue中,这是handler与respr交互的区域,handler负责把结果放进去,responser负责把结果返回客户端。这里需要解释下call.connection.responseQueue这个看上去很诡异的变量名,因为每个connection对象对应一个客户端请求,一个客户端可能发送n多请求,所以每个connection需要维护自己的Queue,而每个查询被封装成call对象,每个call对象记录了自己属于哪个connection,所以就出现了这样一个比较奇葩的变量名。
Responser线程介绍:
首先还是Responser的主函数run方法:
607 while (iter.hasNext()) {
608 SelectionKey key = iter.next();
609 iter.remove();
610 try {
611 if (key.isValid() && key.isWritable()) {
612 doAsyncWrite(key);
613 }
其中主要的工作是在doAsyncWrite中完成。其中大家可能注意到有一个writeSelector,这个selector的作用就是监听客户端连接是否可以写入数据了,具体绑定是在processResponse方法中。那么一开始绑定的调用在哪呢,我们在看一下上边提到的doRespond方法当call.connection.responseQueue的大小为1时会调用processResponse,也就是说当第一个call查询结果加入时,会唤醒writeSelector,并将对应的channel绑定上。
767 try {
768 // Wakeup the thread blocked on select, only then can the call
769 // to channel.register() complete.
770 writeSelector.wakeup();
771 channel.register(writeSelector, SelectionKey.OP_WRITE, call);
772 } catch (ClosedChannelException e) {
下面看一下doAsyncWrite方法都完成了那些工作:
675 synchronized(call.connection.responseQueue) {
676 if (processResponse(call.connection.responseQueue, false)) {
677 try {
678 key.interestOps(0);
679 } catch (CancelledKeyException e) {
正如函数名字一样这是一个同步操作的过程,主要的实现还是在上边提到的processResponse完成。
最后我们来看一下这个完成工作比较多的processResponse方法。
718 try {
719 synchronized (responseQueue) {
720 //
721 // If there are no items for this channel, then we are done
722 //
723 numElements = responseQueue.size();
724 if (numElements == 0) {
725 error = false;
726 return true; // no more data for this channel.
727 }
728 //
729 // Extract the first call
730 //
731 call = responseQueue.removeFirst();
732 SocketChannel channel = call.connection.channel;
733 if (LOG.isDebugEnabled()) {
734 LOG.debug(getName() + ": responding to #" + call.id + " from " +
735 call.connection);
736 }
737 //
738 // Send as much data as we can in the non-blocking fashion
739 //
740 int numBytes = channelWrite(channel, call.response);
741 if (numBytes < 0) {
742 return true;
743 }
744 if (!call.response.hasRemaining()) {
745 call.connection.decRpcCount();
746 if (numElements == 1) { // last call fully processes.
747 done = true; // no more data for this channel.
主要的工作还是做一些判断处理,真正写入实在方法channWrite中完成。
到此把RPC框架上的东西算是总结一了一遍,但是RPC机制远没有我介绍的这么简单,里边有很多细节还需要大家仔细思考和研究,我的工作也就是能让大家看代码不至于迷失方向,仅此而已。最后欢迎大家留言讨论交流,谢谢!