HadoopRPC源码解析

来源:互联网 发布:迅龙数据恢复有用吗 编辑:程序博客网 时间:2024/05/28 05:14

HadoopRPC源码阅读学习
    
背景
    RPC在Hadoop生态中的作用

    HadoopRPC源码解析

          ipc.RPC源码分析

          ipc.Server源码分析

          ipc.Client源码分析

    与其他RPC框架对比分析来提炼通用模型

    参考资源

一、背景

        Hadoop是一个大数据处理平台,目前在大数据领域应用也非常广泛,刚好最近我们BI组在进行把底层数据仓库迁移到Hadoop平台,所以在工作之余开始去深入了解下hadoop内部实现以更好地应用它,在遇到问题的时候有更好的解决思路。本篇分享先介绍Hadoop领域中RPC框架的实现原理,后续会继续分析HDFS及MapReduce及Hive实现原理跟大家分享(hadoop版本1.0,JDK版本1.6)。

二、RPC在Hadoop生态中的作用
在整个Hadoop集群当中,RPC扮演者重要的角色,一切与数据传输相关的都是通过RPC来请求传输的,在分析RPC源码之前先简单介绍下RPC在Hadoop中的涉及到的地方,首先hadoop要提交任务的话,JobClient和JobTracker会有一个交互,JobClient作为Client角色,JobTracker作为Server角色,相同的在MR集群内部,TaskTracker去获取用户提交的任务,就会和JobTracker去交互,TaskTracker作为Client角色, JobTracker作为Server角色,最后在TaskTracker内部,执行具体的Task时,Task和TaskTracker也会有一个交互,Task作为Client角色, TaskTracker作为Server角色。


三、HadoopRPC源码解析
和其他RPC框架一样,HadoopRPC也是有Client和Server构成,主要的API主要集中在ipc.RPC,ipc.Client,ipc.Server等这几个类中,接下来就会围绕这三个主要API去详细解析HadoopRPC的实现细节,先来看下HadoopRPC完整的处理流程图如下:


1、ipc.RPC源码分析
ipc.RPC类中有一些内部类,为了大家对RPC类有个初步的印象,就先罗列几个我们感兴趣的分析一下吧
Invoker :是动态代理中的调用实现类,继承了InvocationHandler.
Invocation :用于封装方法名和参数,作为数据传输层.
Server :是ipc.Server的实现类.
从以上的分析可以知道,Invocation类仅作为调用传输类,而Server类用于服务端的处理,他们都和客户端的数据流和业务逻辑没有关系。现在就只剩下Invoker类了。如果你对动态代理比较了解的话,你一下就会想到,我们接下来主要去研究的就是RPC.Invoker类中的invoke()方法了,这个其实就是发起接口RPC调用时被动态代理调用的InvocationHandler,核心代码如下:
Java代码 
  1. public Object invoke(Object proxy, Method method, Object[] args)  
  2.   throws Throwable {  
  3.   final boolean logDebug = LOG.isDebugEnabled();  
  4.   long startTime = 0;  
  5.   if (logDebug) {  
  6.     startTime = System.currentTimeMillis();  
  7.   }  
  8.   ObjectWritable value = (ObjectWritable)  
  9.   // 一次远程RPC调用, 传入客户端调用的方法名,方法参数类型,方法参数值
  10.   client.call(new Invocation(method, args), remoteId);  
  11.   // 返回远程方法调用返回值
  12.   return value.get();  
  13. }  
2、ipc.Client源码分析
Client主要完成的功能是发送远程过程调用信息并接收执行结果。具体Client流程图如下:


同样,为了对Client类有个初步的了解,我们也先罗列几个我们感兴趣的内部类:

Call :该类封装了一个RPC请求,它包含五个成员变量,分别是唯一标识id,函数调用信息param、函数执行返回值value,异常信息error和执行完成标识done。由于HadoopRPCServer采用了异步方式处理客户端请求,这使得远程过程调用的发生顺序与结果返回顺序无直接关系,而Client端正是通过id识别不同的函数调用。当客户端向服务端发送请求时,只需要填充id和param这两个变量,而剩下的三个变量:value,error,done,则由服务端根据函数执行情况填充.
Connection :用Client与每个Server之间维护一个通信连接。该连接相关的基本信息及操作被封装到Connection类中,其中基本信息主要包括:通信连接唯一标识remoteId,与Server端通信的Socket,网络输入流in,网络输出流out,保存RPC请求的哈希表calls等.
ConnectionId :唯一确定一个连接

(1)请求建立连接

下面开始分析客户端是怎么跟服务端建立连接的,客户端向服务端发起RPC请求时,首先会请求服务端建立连接,核心代码如下:

Java代码 
  1.  public Writable call(Writable param, ConnectionId remoteId)  
  2.                       throws InterruptedException, IOException {  
  3. Call call = new Call(param); // 创建一个可序列化的Call对象,封装了调用方法名、方法参数类型、方法参数值, 并且随机生成一个标识调用Call的唯一ID  
  4. Connection connection = getConnection(remoteId, call); // 获取此次调用的连接对象, 并把调用Call放入此连接的calls调用队列中  
  5. connection.sendParam(call); // 发起一个远程RPC调用  
  6. boolean interrupted = false;  
  7.    synchronized (call) {  
  8.      while (!call.done) {  
  9.        try {  
  10.          call.wait();  // 阻塞, 等待远程服务器返回结果 【这里注意下,mysqlJDBC驱动的Connection数据库库连接因为是线程安全的,所以可以直接阻塞线程等待mysql的响应,但是这里hadoop rpc是可以再多线层环境下调用rpc接口的, 所以才以这种方式处理】  
  11.        } catch (InterruptedException ie) {  
  12.          interrupted = true;  
  13.        }  
  14.      }  
  15.      if (call.error != null) { // 如果RPC服务端异常则直接抛出异常  
  16.        if (call.error instanceof RemoteException) {  
  17.          throw call.error; // 直接抛出服务端异常  
  18.        } else {
  19.          throw wrapException(connection.getRemoteAddress(), call.error);  
  20.        }  
  21.      } else {  
  22.        return call.value; // 返回远程服务器返回值  
  23.      }  
  24.    }  
  25.  }  
Java代码 
  1. private Connection getConnection(ConnectionId remoteId,  Call call)  
  2.                                 throws IOException, InterruptedException {  
  3.  if (!running.get()) {  
  4.    throw new IOException("The client is stopped");
  5.  }  
  6.  Connection connection;  
  7.  do {  
  8.    synchronized (connections) {  
  9.      connection = connections.get(remoteId); // 根据ConnectionId获取Connection对象  
  10.      if (connection == null) {  
  11.        connection = new Connection(remoteId); // 创建Connection连接对象, 这个连接对象就代表一个远程服务器一个服务的连接, 可能包含多个RPC方法  
  12.        connections.put(remoteId, connection); // 往客户端Client对象中添加一个Connection对象  
  13.      }  
  14.    }  
  15.  } while (!connection.addCall(call)); // 往连接Connection中添加Call, 并唤醒Connection线程接收远程服务器响应   
  16.  connection.setupIOstreams(); // 建立与远程服务器的连接, 创建Socket对象, 只初始化一次, 并且做一些验证工作及初始化写一次ConnectionHeader头信息, 并且启动Connectin线程获取该连接上的响应  
  17.  return connection; // 返回连接对象  

在Connection的setupIOstreams方法中会去建立和服务端的连接,本质会去创建一个Socket对象,建立一个TCP长连接,并且封装相关输入输出流,核心代码如下:

Java代码 
  1. private synchronized void setupIOstreams() throws InterruptedException {  
  2.   if (socket != null || shouldCloseConnection.get()) { // 已经建立连接直接返回
  3.     return;  
  4.   }  
  5.   try {
  6.     short numRetries = 0;  
  7.     final short maxRetries = 15;  
  8.     Random rand = null;  
  9.     while (true) {  
  10.       // 与远程服务器建立连接, 创建一个Socket对象  
  11.       setupConnection();  
  12.       // 获取输入流  
  13.       InputStream inStream = NetUtils.getInputStream(socket);  
  14.       // 获取输出流  
  15.       OutputStream outStream = NetUtils.getOutputStream(socket);  
  16.   
  17.       // 发送RPC Header信息给RPC服务器, 这里RPC服务器正常接收后不会响应, 因为只会验证客户端和服务端RPC程序版本是否匹配, 但是验证没通过后会响应失败状态, 并且服务端会关闭连接  
  18.       // 这里RPC服务器正常接收后不会响应  
  19.       writeRpcHeader(outStream);  
  20.   
  21.       // 包装输入流  
  22.       this.in = new DataInputStream(new BufferedInputStream  
  23.           (new PingInputStream(inStream)));  
  24.       // 包装输出流  
  25.       this.out = new DataOutputStream  
  26.       (new BufferedOutputStream(outStream));  
  27.   
  28.       // 写入Header信息, 在该连接的第一次RPC请求时一起随参数发送, 主要发送的是接口名  
  29.       writeHeader();  
  30.       // 启动Connection线程, 用于接收远程服务器的响应  
  31.       start();  
  32.       return;  
  33.     }  
  34.   } catch (Throwable t) {  
  35.     close();  
  36.   }  
  37. }  

(2)发送请求数据

建立好和服务端的连接之后,就可以和服务端通信了,客户端发起RPC请求时,会先去把请求相关的调用方法参数等序列化成字节流发送给服务端,核心代码如下:

Java代码 
  1. public void sendParam(Call call) {  
  2.   if (shouldCloseConnection.get()) { // 如果连接已经关闭则直接返回  
  3.     return;  
  4.   }  
  5.   DataOutputBuffer d = null;  
  6.   try {  
  7.     synchronized (this.out) { // 对于同一个OutputStream必须同步发送RPC调用, 因为在同一个连接上的多个调用Call必须在同步下进行RPC请求  
  8.       d = new DataOutputBuffer();  
  9.       d.writeInt(call.id); // callId  
  10.       call.param.write(d); // Invocation对象, 实现了Writable接口, 把Invocation对象序列化成二进制数据, 在服务端又会把二进制数据反序列化成Invocation对象  
  11.       byte[] data = d.getData(); // 获取字节数据
  12.       int dataLength = d.getLength();  
  13.       out.writeInt(dataLength);      // 1.写入CallId和调用参数(方法名、方法参数类型、方法参数值)的长度, 4个字节  
  14.       out.write(data, 0, dataLength);// 2.写入CallId和序列化后的调用参数(方法名、方法参数类型、方法参数值)  
  15.       /** (数据长度4字节  + callId4个字节 + Invocation对象字节流) */  
  16.       out.flush(); // 刷新发送, 在该连接上的RPC调用第一次肯定会发送(完整ConnectionHeader头信息+调用参数), 之后都只会发送调用参数  
  17.     }  
  18.   } catch(IOException e) {  
  19.     markClosed(e);  
  20.   } finally {  
  21.     IOUtils.closeStream(d);  
  22.   }  
  23. }  

(3)获取服务端响应

发起请求时候,客户端会阻塞等待服务端响应数据,响应完之后业务线程会被唤醒,核心代码如下:

Java代码 
  1. public void run() {
  2.   if (LOG.isDebugEnabled())
  3.   // 这里Hadoop-RPC通过一个线程去接收响应, 因为是在同一个连接上只能这样处理  
  4.   while (waitForWork()) { 
  5.     receiveResponse(); // 单线程方式接收远程RPC服务器的响应, 并不能保证每个调用是按序返回的,因为这得取决于Server的响应  
  6.   }  
  7.   close(); // 关闭连接及连接上的Call    
  8. }  
Java代码 
  1. private void receiveResponse() {  
  2.   if (shouldCloseConnection.get()) {  
  3.     return;  
  4.   }  
  5.   touch();  
  6.   try {  
  7.     int id = in.readInt(); // 获取CallId 这里可能会阻塞, 如果服务端验证Header时抛异常断开连接则这里会直接抛异常  

  8.     Call call = calls.get(id); // 从此连接的调用队列中取出对于CallId的Call对象    
  9.     int state = in.readInt();     // read call status 获取RPC服务端状态status  
  10.     if (state == Status.SUCCESS.state) { // 1.RPC调用成功  
  11.       Writable value = ReflectionUtils.newInstance(valueClass, conf); // 创建一个ObjectWritable对象, 用来反序列化服务端返回值  
  12.       value.readFields(in);                 // read value 读取RPC远程调用返回值  
  13.       call.setValue(value); // 设置此次远程RPC调用的返回值到Call, 并且唤醒此个RPC业务线程, 返回调用结果  
  14.       calls.remove(id); // 响应成功则从连接的调用队列中把Call对象移除  
  15.     } else if (state == Status.ERROR.state) { // 2.如果当前调用在服务端出现调用异常, 则此调用业务线程抛出异常即可, 不必关闭连接  
  16.       call.setException(new RemoteException(WritableUtils.readString(in),  
  17.                                             WritableUtils.readString(in))); // 设置异常和完成标志,最后唤醒业务线程抛出异常  
  18.       calls.remove(id); // 把当前调用从此连接的调用队列中移除  
  19.     } else if (state == Status.FATAL.state) { //3.第一次服务端RPC-Header验证失败, 则此连接要被关闭  
  20.         // 验证没通过直接关闭连接  
  21.         markClosed(new RemoteException(WritableUtils.readString(in), // 设置shouldCloseConnection为true, 并且设置异常  
  22.                                      WritableUtils.readString(in)));  
  23.     }  
  24.   } catch (IOException e) { // 4.第二次服务端Header验证失败, 则此连接要被关闭  
  25.     markClosed(e); // 第二次服务端Header验证失败, 则此连接要被关闭  
  26.   }  
  27. }  
Java代码 
  1. public synchronized void setValue(Writable value) {  
  2.   this.value = value; // 设置响应  
  3.   callComplete(); // 唤醒业务线程  
  4. }  
Java代码 
  1. protected synchronized void callComplete() {  
  2.   this.done = true// 完成RPC调用  
  3.   this.notify();      // 唤醒业务线程  
  4. }  

HadoopRPC-Client端请求处理流程具体序列图如下:


3、ipc.Server源码分析
        Hadoop采用了Master/Slave结构,其中Master是整个系统的单点,如NameNode或JobTracker,这是制约系统性能和可扩展性的最关键因素之一,而Master通过ipc.Server接收并处理所有Salve发送的请求,这久要求ipc.Server将高并发和可扩展性作为设计目标,为此,ipc.Server采用了很多具有提高高并发处理能力的技术,主要包括线程池、事件驱动和Reactor设计模式等。ipc.Server的主要功能是接收来自客户端的RPC请求,经过调用相应的函数获取结果后,返回给相应的客户端,为此,ipc.Server被划分为三个阶段:请求接收,请求处理和请求响应,具体流程如下图所示。


接下来通过源码详细分析各阶段实现细节,同样,为了让大家对ipc.Server有个初步的了解,我们先分析一下它的几个内部类吧: 

 Listener : 请求监听类,用于监听客户端发来的请求.

 Connection :连接类,真正的客户端请求读取逻辑在这个类中.

 Reader : 当监听器监听到用户请求,便让Reader读取用户请求数据.

 Call :用于封装客户端发来的请求.

 Handler :请求处理类,会循环阻塞读取callQueue中的call对象,并对其进行操作.

 Responder :响应RPC请求类,请求处理完毕,由Responder发送给请求客户端.

(1)请求接收

该阶段的主要任务是接收来自各个客户端的RPC请求,并将它们封装成固定的格式(Call对象)放到一个共享阻塞队列callQueue中,以便进行后续处理。该阶段内部又分为两个子阶段:请求接收和请求读取,分别有两种线程完成:Listener和Reader请求接收线程Listener初始化源码如下,整个Server只有一个Listener线程,统一负责监听来自客户端的连接请求,一旦有新的请求到达,它会采用轮训的方式从线程池中选择一个Reader线程进行处理。Listener的run() 方法中会阻塞等待客户端请求建立连接,Listener的run()方法的核心代码如下: 

Java代码 
  1. public void run() {  
  2.   LOG.info(getName() + ": starting");  
  3.   SERVER.set(Server.this);  
  4.   while (running) {  
  5.     SelectionKey key = null;  
  6.     try {  
  7.       selector.select(); // 如果Selector中注册的ServerSocketChannel没有新的Socket请求的话, 就阻塞在这里  
  8.       Iterator<SelectionKey> iter = selector.selectedKeys().iterator();  
  9.       while (iter.hasNext()) {  
  10.         key = iter.next();  
  11.         iter.remove();  
  12.         try {  
  13.           if (key.isValid()) {  
  14.             if (key.isAcceptable()) // 连接事件  
  15.               doAccept(key); // 处理连接事件  
  16.           }  
  17.         } catch (IOException e) {  
  18.         }  
  19.         key = null;  
  20.       }  
  21.     } catch (Exception e) {  
  22.       closeCurrentConnection(key, e);  
  23.     }  
  24.     cleanupConnections(false);  
  25.   }  
  26. }  

紧接着具体的请求接收处理是在Listener的doAccept()方法中处理的,获取连接后会往Reader线程中的多路复用器Selector注册连接,Listener的doAccept方法的核心代码如下:

Java代码 
  1. void doAccept(SelectionKey key) throws IOException,  OutOfMemoryError {  
  2.   Connection c = null;  
  3.   ServerSocketChannel server = (ServerSocketChannel) key.channel(); // 拿到ServerSocketchannel  
  4.   SocketChannel channel; // 拿到Socketchannel  
  5.   while ((channel = server.accept()) != null) { // 非阻塞的拿到SocketChannel  
  6.     channel.configureBlocking(false); // 把SocketChannel设置为非阻塞模式  
  7.     channel.socket().setTcpNoDelay(tcpNoDelay);  
  8.     Reader reader = getReader(); // 随机轮询获取一个Rearder线程  
  9.     try {  
  10.       reader.startAdd(); // 开始添加SocketChannel,先阻塞Reader线程,唤醒Reader线程中的Selector  
  11.   
  12.       SelectionKey readKey = reader.registerChannel(channel); // 往reader线程中的Selector注册刚刚连接的SocketChannel的OP_READ事件  
  13.   
  14.       c = new Connection(readKey, channel, System.currentTimeMillis()); // 传入SocketChannel创建Connection连接, 每个SocketChannel都有一个Connection对象  
  15.   
  16.       readKey.attach(c); // 为每个SelectionKey(每个SocketChannel)绑定一个Connection,当做attachment  
  17.   
  18.       synchronized (connectionList) {  
  19.         connectionList.add(numConnections, c); // 往Server端的连接队列connectionList中添加一个Connection  
  20.         numConnections++;  
  21.       }  
  22.     } finally {  
  23.         reader.finishAdd(); // 完成添加SocketChannel并唤醒Reader线程  
  24.     }  
  25.   }  
  26. }  

客户端和服务端连接建立成功之后,服务端的Reader线程中维护了连接,有了连接就可以传输数据,Reader线程的run方法中就是阻塞去等待客户端的请求数据,一旦该连接上有可读数据,该Reader线程就会被唤醒,紧接着会去解析字节流序列化请求数据,封装成Call对象,塞到callQueue阻塞队列,Reader的run()方法的核心代码如下:

Java代码 
  1. public void run() {  
  2.   LOG.info("Starting SocketReader");  
  3.   synchronized (this) {  
  4.     while (running) {  
  5.       SelectionKey key = null;  
  6.       try {  
  7.         readSelector.select(); // 如果Selector中注册的SocketChannel中都没有可读数据的话, 就阻塞在这里  
  8.   
  9.         while (adding) { // 如果Selector需要注册新的Channel,则这里需阻塞掉,等待Channel注册完被唤醒  
  10.           this.wait(1000);  
  11.         }  
  12.   
  13.         Iterator<SelectionKey> iter = readSelector.selectedKeys().iterator(); // 获取OP_READ事件的SocketChannel的SelectionKey  
  14.         while (iter.hasNext()) {  
  15.           key = iter.next();  
  16.           iter.remove();  
  17.           if (key.isValid()) {  
  18.             if (key.isReadable()) { // SocketChannel有可读数据  
  19.               doRead(key);  
  20.             }  
  21.           }  
  22.           key = null;  
  23.         }  
  24.       } catch (IOException ex) {  
  25.         LOG.error("Error in Reader", ex);  
  26.       }  
  27.     }  
  28.   }  
  29. }
具体的读取及解析请求数据交给Connection来处理,Reader的doRead()方法的核心代码如下:
Java代码 
  1. public void doRead(SelectionKey key) throws InterruptedException {  
  2.   Connection c = (Connection)key.attachment(); // 拿到之前SocketChannel关联绑定的Connection连接  
  3.   if (c == null) {  
  4.     return;  
  5.   }  
  6.   c.setLastContact(System.currentTimeMillis()); // 最后被访问时间  
  7.   try {  
  8.     count = c.readAndProcess(); // 从SocketChannel中读取数据并处理
  9.   } catch (InterruptedException ieo) {  
  10.     throw ieo;  
  11.   } catch (Exception e) { // 这里一般是服务端验证RPCHeader时或验证调用Header时抛的异常, 则需要解绑SelectionKey并且关闭连接  
  12.     count = -1//so that the (count < 0) block is executed  
  13.   }  
  14.   if (count < 0) {  
  15.     closeConnection(c); // Reader线程读取PRC-Header协议验证失败后或请求Header验证失败后会关闭连接  
  16.     c = null;  
  17.   }  
  18.   else {  
  19.     c.setLastContact(System.currentTimeMillis());  
  20.   }  
  21. }  
接着来看Connection类的readAndProcess()方法,主要从连接中读取请求数据,核心代码如下:
Java代码 
  1. public int readAndProcess() throws IOException, InterruptedException {  
  2.   while (true) {  
  3.     int count = -1;  
  4.     if (dataLengthBuffer.remaining() > 0) {  
  5.       count = channelRead(channel, dataLengthBuffer); // 读取协议的第一个字段,协议的长度, 4个字节(可能是hrpc或协议长度或数据长度)
  6.       if (count < 0 || dataLengthBuffer.remaining() > 0)  
  7.         return count;  
  8.     }  
  9.     // 首先收到无需响应的客户单RPC-Header信息
  10.     if (!rpcHeaderRead) { // 第一次RPC-Header验证
  11.       if (rpcHeaderBuffer == null) {
  12.         rpcHeaderBuffer = ByteBuffer.allocate(2);  
  13.       }  
  14.       count = channelRead(channel, rpcHeaderBuffer); // 读取客户端程序版本4及授权方式80, 共2个字节
  15.       if (count < 0 || rpcHeaderBuffer.remaining() > 0) {  
  16.         return count;  
  17.       }  
  18.       int version = rpcHeaderBuffer.get(0); // 读取客户端程序版本4, 共1个字节
  19.       byte[] method = new byte[] {rpcHeaderBuffer.get(1)}; // 读取授权方式80, 共1个字节
  20.       authMethod = AuthMethod.read(new DataInputStream(  
  21.           new ByteArrayInputStream(method)));  
  22.       dataLengthBuffer.flip();  
  23.       dataLengthBuffer.clear(); // 清空dataLengthBuffer  
  24.       if (authMethod == null) {  
  25.         throw new IOException("Unable to read authentication method");  
  26.       }
  27.       rpcHeaderBuffer = null;  
  28.       rpcHeaderRead = true// 头信息已经读取标识  
  29.       continue// RPC-Header正常读取后直接continue, 可能直接返回, 也可能读取第一次RPC请求  
  30.     }  
  31.     if (data == null) {  
  32.       dataLengthBuffer.flip();  
  33.       dataLength = dataLengthBuffer.getInt(); // 获取协议长度或数据长度  
  34.       if (dataLength == Client.PING_CALL_ID) { // 如果没有信息了则直接返回  
  35.         if(!useWrap) { //covers the !useSasl too  
  36.           dataLengthBuffer.clear();  
  37.           return 0;  //ping message // 返回  
  38.         }  
  39.       }  
  40.       data = ByteBuffer.allocate(dataLength); // 根据dataLength创建一个dataLength大小的缓冲区, 用来读数据  
  41.     }  
  42.     count = channelRead(channel, data); // 读取第一次请求Header信息或请求数据  
  43.     if (data.remaining() == 0) {  
  44.       dataLengthBuffer.clear(); // 清空dataLengthBuffer  
  45.       data.flip();  
  46.       if (skipInitialSaslHandshake) {  
  47.         data = null;  
  48.         skipInitialSaslHandshake = false;  
  49.         continue;  
  50.       }  
  51.       boolean isHeaderRead = headerRead; // 是否已经读取第一次请求Header信息  
  52.       if (useSasl) {  
  53.         saslReadAndProcess(data.array());  
  54.       } else {  
  55.         processOneRpc(data.array()); // 处理rpc请求,把封装好的请求信息Call塞到callQueue阻塞队列  
  56.       }  
  57.       data = null;  
  58.       if (!isHeaderRead) { // 读取第一次RPC请求Header之后会再continue, 继续读取请求数据  
  59.         continue;  
  60.       }  
  61.     }  
  62.     return count; // 一个正常RPC请求读取完后会直接返回  
  63.   }  
  64. }  
获取到请求字节流后就可以处理数据了,核心代码如下:
Java代码 
  1. private void processOneRpc(byte[] buf) throws IOException,  
  2.     InterruptedException {  
  3.   if (headerRead) {  
  4.     processData(buf); // 处理RPC请求  
  5.   } else {  
  6.     processHeader(buf); // 读取头信息,随第一次RPC请求时带过来,解析出此连接上的客户端请求接口的完全限定名  
  7.     headerRead = true;  
  8.   }  
  9. }  
最终会在Connection的processDate方法中反序列化字节流,反序列化字节流成Invocation对象,再封装成Call对象(代表一次客户端调用),塞入阻塞队列callQueue中,供请求处理线程Handler消费,Connection的processData()方法如下:
Java代码 
  1. private void processData(byte[] buf) throws  IOException, InterruptedException {  
  2.   DataInputStream dis =  
  3.     new DataInputStream(new ByteArrayInputStream(buf)); // 获取Invocation对象的字节流封装成DataInputStream  
  4.   int id = dis.readInt();                    // try to read an id 前四个字节是客户端调用Call ID  
  5.   
  6.   Writable param = ReflectionUtils.newInstance(paramClass, conf);//read param  创建一个Invocation对象, 实现了Writable接口, 用来反序列化客户端调用参数  
  7.   param.readFields(dis); // 读取二进制流反序列化成客户端调用参数封装对象Invocation(方法名+方法参数类型+方法参数值)  
  8.   
  9.   Call call = new Call(id, param, this); // 把请求封装成Call(调用id+请求参数+此连接Connection对象), 塞到阻塞队列中  
  10.   callQueue.put(call); // 把Call塞到callQueue阻塞队列中, queue the call; maybe blocked here  
  11.   incRpcCount();  // Increment the rpc count  RPC请求计数加1  
  12. }  
(2)请求处理
该阶段的主要任务是从共享队列callQueue中获取Call对象,执行相应的函数调用,并将结果返回给客户端,这全部由Handler线程完成的。Server端可同时存在多个Handler线程。它们并行从共享队列中读取Call对象,经执行对应的韩式调用后,将尝试着直接将结果返回给对应的客户端。但考虑到某些函数调用返回的结果很大或者网络速度过慢,可能难以将结果一次性发送到客户端,此时Handler将尝试着将后续发送任务交给Responder线程。Handler的run方法中会阻塞等待callQueue队列中有请求数据,Handler的run()核心代码如下:
Java代码 
  1. public void run() {  
  2.       LOG.info(getName() + ": starting");  
  3.       SERVER.set(Server.this);  
  4.       ByteArrayOutputStream buf =  
  5.         new ByteArrayOutputStream(INITIAL_RESP_BUF_SIZE);  
  6.       while (running) {  
  7.         try {  
  8.           // 从阻塞队列中获取Call,如果阻塞队列为空,则当前线程阻塞  
  9.           final Call call = callQueue.take();
  10.           String errorClass = null;  
  11.           String error = null;  
  12.           Writable value = null// Server端RPC方法的返回结果值ObjectWritable  
  13.  
  14.           CurCall.set(call);  
  15.           try {
  16.             if (call.connection.user == null) {  
  17.               value = call(call.connection.protocol, call.param,  
  18.                            call.timestamp);  
  19.             } else {  
  20.               value =  
  21.                 call.connection.user.doAs  
  22.                   (new PrivilegedExceptionAction<Writable>() {  
  23.                      @Override  
  24.                      public Writable run() throws Exception {  
  25.                        // 反射调用对应服务,返回结果ObjectWritable, 传入Connection中接口的Class对象, 是在建立连接之后第一次客户端请求带过来的  
  26.                        return call(call.connection.protocol,call.param, call.timestamp);  
  27.   
  28.                      }  
  29.                    }  
  30.                   );  
  31.             }
  32.           } catch (Throwable e) {  
  33.             errorClass = e.getClass().getName(); // 服务端RPC方法抛出的异常类信息, java.lang.IOException  
  34.             error = StringUtils.stringifyException(e); // 服务端RPC方法抛出的异常堆栈信息,封装成字符串的形式传送给客户端  
  35.           }
  36.           CurCall.set(null);
  37.           synchronized (call.connection.responseQueue) { // 同一个连接上的多个响应必须在同步下进行  
  38.             setupResponse(buf, call,  
  39.                         (error == null) ? Status.SUCCESS : Status.ERROR,  
  40.                         value, errorClass, error); // 生成返回给客户端的数据包,包含(客户端调用ID+状态status+RPC方法返回值),设置到Call对象中  
  41.             if (buf.size() > maxRespSize) {  
  42.               LOG.warn("Large response size " + buf.size() + " for call " +  
  43.                 call.toString());  
  44.               buf = new ByteArrayOutputStream(INITIAL_RESP_BUF_SIZE);  
  45.             }  
  46.             responder.doRespond(call); // 响应请求,调用Responder的doRespond方法, 同一个连接上的多个响应必须在同步下进行RPC响应  
  47.           }  
  48.         }
  49.       }  
  50.     }
服务端拿到调用参数之后,会反射调用对应服务,返回方法返回值
Java代码 
  1. public Writable call(Class<?> protocol, Writable param, long receivedTime)  
  2. throws IOException {  
  3.   try {  
  4.     // 获取客户端封装的调用参数Invocation, 是在Reader线程中反序列化成Invocation的  
  5.     Invocation call = (Invocation)param;  
  6.     if (verbose) log("Call: " + call);  
  7.   
  8.     // 根据反射获取Method对象
  9.     Method method = protocol.getMethod(call.getMethodName(),call.getParameterClasses());  
  10.     // 设置可见性  
  11.     method.setAccessible(true);  
  12.   
  13.     long startTime = System.currentTimeMillis();  
  14.   
  15.     // 通过反射调用服务端方法
  16.     Object value = method.invoke(instance, call.getParameters());  
  17.   
  18.     rpcMetrics.addRpcQueueTime(qTime);
  19.     rpcMetrics.addRpcProcessingTime(processingTime);  
  20.     rpcMetrics.addRpcProcessingTime(call.getMethodName(), processingTime);
  21.     if (verbose) log("Return: "+value);
  22.     // 返回服务端方法返回值, 把返回值value和返回值类型包装成ObjectWritable进行序列化
  23.     // 服务端返回值包装类ObjectWritable
  24.     return new ObjectWritable(method.getReturnType(), value);  
  25.   } 
  26. }  
(3)请求响应
每个Handler线程执行完函数调用后,会尝试着将执行结果返回给客户端,但由于特殊情况,比如函数调用返回的结果过大或者网络异常情况,会将发送任务交给Responder线程,Server端仅存在一个Responder线程,它的内部包含一个多路复用器Selector对象,用于监听SelectionKey.OP_WRITE事件,当Handler没能够将结果一次性发送到客户端时,会向该Selector对象注册SelectorKey.OP_WRITE事件,进而由Responder线程采用异步方式继续发送未发送完成的结果,具体的核心代码如下:
Java代码 
  1. public void run() {  
  2.   LOG.info(getName() + ": starting");  
  3.   SERVER.set(Server.this);  
  4.   long lastPurgeTime = 0;
  5.   
  6.   while (running) {  
  7.     try {  
  8.       // Responder线程阻塞在这里,需要在Handler线程中唤醒  
  9.       // 如果Selector需要注册新的Channel,则这里需阻塞掉,等待Channel注册完被唤醒  
  10.       waitPending();
  11.       writeSelector.select(PURGE_INTERVAL);  
  12.       Iterator<SelectionKey> iter = writeSelector.selectedKeys().iterator();  
  13.       while (iter.hasNext()) {  
  14.         SelectionKey key = iter.next();  
  15.         iter.remove();  
  16.         try {  
  17.           if (key.isValid() && key.isWritable()) {  
  18.               doAsyncWrite(key); // 写数据  
  19.           }  
  20.         } catch (IOException e) {  
  21.           LOG.info(getName() + ": doAsyncWrite threw exception " + e);  
  22.         }  
  23.       }  
  24.       long now = System.currentTimeMillis();  
  25.       if (now < lastPurgeTime + PURGE_INTERVAL) {  
  26.         continue;  
  27.       }  
  28.     } 
  29. }  
具体的异步写入是在Responder的doAsyncWrite方法中处理的,核心代码如下:
Java代码 
  1. private void doAsyncWrite(SelectionKey key) throws IOException {  
  2.   Call call = (Call)key.attachment(); // 从SelectionKey中获取Call对象, 【其实用的是Call对象中的Connection对象的responseQueue响应队列】  
  3.   if (call == null) {  
  4.     return;  
  5.   }  
  6.   if (key.channel() != call.connection.channel) {  
  7.     throw new IOException("doAsyncWrite: bad channel");  
  8.   }  
  9.   synchronized(call.connection.responseQueue) {  
  10.     if (processResponse(call.connection.responseQueue, false)) { // 往SocketChannel中写数据, 如果此连接上还有响应没发送完则返回false, 继续监听此SocketChannel上的写事件  
  11.       try {  
  12.         key.interestOps(0); // 此连接上的响应如果已经全部发送完,则从Responder线程中的Selector解绑SelectionKey  
  13.       } catch (CancelledKeyException e) {
  14.         LOG.warn("Exception while changing ops : " + e);  
  15.       }  
  16.     }  
  17.   }  
  18. }  
HadoopRPC-Server端请求处理流程具体序列图如下:



四、与其他RPC框架对比分析来提炼通用模型
1、Tomcat的NIO线程模型


2、Jetty的NIO线程模型


3、Mina的NIO线程模型


4、通用的NIO网络服务器模型一


5、通用的NIO网络服务器模型二



0 0
原创粉丝点击