Hadoop -- RPC通信

Hadoop -- RPC通信

前言

本篇文章浅显的介绍了Hadoop RPC的基础以及三个大类RPC、Server、Client一些较为重要的方法的源码剖析,目的在于理解Hadoop RPC核心的原理以提升自身知识储备。本篇博文参考了大量董西成老师的《Hadoop技术内幕-深入解析YARN架构设计与实现原理》,感谢老师的书籍在我学习Hadoop Yarn过程中给予的莫大帮助。

基础库

1. Protocol Buffers

Protocol Buffers是一种轻便高效的结构化数据存储格式,可以用于结构化数据序列化/反序列化。目前作用于Yarn PRC中参数的序列化/反序列化,具有平台无关、高性能(解析速度约是XML的20~100倍)、兼容性强、体积小(大小仅是XML文件的1/10~1/3)等优点

2. Apache Avro

Apache Avro是一个序列化框架,同时实现了PRC功能设计的最初动机是解决Yarn RPC兼容性和扩展性差等问题,但是在Yarn项目启启动时Apache Avro仍不成熟,考虑稳定性问题,Yarn暂时采用Protocol Buffers作为序列化库。目前用于MapReduce应用程序日志(用于故障后应用程序恢复),今后可能会替代Protocol Buffers。

Hadoop RPC基础介绍

一、通信模型

RPC通常采用客户机/服务器模型。请求程序是一个客户机,而服务提供程序则是一个服务器。一个典型的RPC框架包含以下几个部分:

① 通信模块:两个相互协作的通信模块实现请求-应答协议,它们在客户和服务器之间传递和应答消息,一般不会对数据包进行任何处理。

② Stub程序: 客户端和服务端均包含Stub程序,可看做一个代理程序。它使得远程函数调用表现的像本地调用一样

③调度程序:调用程序接收来自通信模块的请求消息,并根据其中的标识选择一个Stub程序进行处理。

④客户程序/服务过程:请求的发出者与处理者。

通常,一个RPC请求从发送到获取处理结果,有如下几个步骤:

  1. 客户程序以本地方式调用系统产生的Stub程序

  2. 该Stub程序将函数调用信息按照网络通信模块的要求封装成消息包,并交给通信模块发送到远程服务器端

  3. 远程服务器接收到此消息后,将此消息发送给相应的Stub程序

  4. Stub程序拆封消息,行程被调用过程要求的形式,并调用对应函数

  5. 被调用函数按照所获参数执行,并将结果返回给Stub程序

  6. Stub程序将此结果封装成消息,通过网络通信模块逐级地传送给客户程序

二、Hadoop RPC框架结构

①序列化层:主要作用是将结构化对象转为字节流以便于通过网络进行传输或者写入持久存储,在RPC框架中,它主要用于将用户请求中的参数或者应答转化成字节流以便跨机器传输

②函数调用层:函数调用层主要功能是定位要调用的函数并执行该函数,Hadoop RPC采用了Java反射机制与动态代理实现了函数调用

③网络传输层:描述了Client、Server之间的消息传输的方式,Hadoop RPC采用了基于TCP/IP的Socket机制

④服务器端处理框架。服务器端处理框架可被抽象为网络I/O模型,它描述了客户端与服务器端间信息交互方式,它的设计直接决定着服务器端的并发处理能力。

三、RPC重要类详解

Reactor架构图

在看上图时,我们需要先了解一些概念:

1)Listener线程:单线程,负责创建服务器监听,即负责处理SelectionKey.OP_ACCEPT事件,一旦对应事件发生,就调用doAccept()方法对事件进行处理,处理方法其实只是将对应的channel封装成Connection,Reader.getReader()负责选出一个Reader线程,然后把这个新的请求交付给这个Reader对象(添加到这个对象的pendingConnections队列)。getReader()选择Reader线程的方式为简单轮询。

2)Reader线程:多线程,由Listener线程创建并管理,通过doRunLoop()方法,反复从自己的pendingConnections 中取出连接通道,注册到自己的readSelector,处理SelectionKey.OP_READ事件,一旦对应事件发生,则调用doRead()进行处理。doRead()的实际工作,是从请求头中提取诸如callId、retry、rpcKind,生成对应的Call对象,放入callQueue中,callQueue 队列将由Handler进行处理。

3)Call对象:封装了RPC请求的信息,包括callId、retryCount、rpcKink(RPC.rpcKind)、clientId、connection信息。Reader线程创建了Call对象,封装了请求信息,交付给下面的Handler线程。此后,信息在Reator的不同角色之间的传递都封装在了Call对象中,包括请求、响应。

4)Handler线程:Handler的总体职责是取出Call对象中的用户请求,对请求进行处理并拿到response,然后将response封装在Call中,交付给Responder进行响应。

5)Responder线程:单线程,内部有一个Selector对象,负责监听writeSelector上的SelectionKey.OP_WRITE,将response通过对应的连接返回给客户端。后面我会详细介绍到,并不是所有的写都是Responder进行的,有一部分是Handler直接进行的:Handler在将响应交付给Responder之前,会检查当前连接上的响应是否只有当前一个,如果是,就会尝试在自己的当前线程中直接把响应发送出去,如果发现响应很多,或者这个响应无法完全发送给远程客户端,才会将剩余任务交付给Responder进行。

RPC

RPC类定义了一系列构建、销毁RPC客户端的方法,构造方法有getProxy和waitForProxy两大类,销毁则是stopProxy。而内部类Builder则是用于创建服务端时设置一些必要的参数。通过调动Builder.build()完成一个服务器对象的构建,最终调用start()实现Server的启动。

Client

使用call()方法来执行某个远程方法,涉及以下4个步骤:

  1. 创建一个Connection对象,并将远程方法调用信息封装成Call对象放到Connection对象的哈希表中

  2. 调用Connection类中的sendRpcRequest()方法将当前的Call对象发送给Server端;

  3. Server处理完RPC请求后,将结果通过网络返回给Client端,Client端通过receiveRpcResponse()函数获取结果

  4. Client检查结果处理状态(成功还是失败),将对应的Call对象从哈希表中删除

    public Writable call(RPC.RpcKind rpcKind, Writable rpcRequest,
        ConnectionId remoteId, int serviceClass,
        AtomicBoolean fallbackToSimpleAuth) throws IOException {
      final Call call = createCall(rpcKind, rpcRequest);
      //第一步创建Connection对象
      Connection connection = getConnection(remoteId, call, serviceClass,
        fallbackToSimpleAuth);
      try {
        connection.sendRpcRequest(call);                 // 第二步,将RPC请求发送到Server端
      } cathch(...)
    ​
      synchronized (call) {
        while (!call.done) {
          try {
            call.wait();                           // 第三步等待获取结果
          } catch (...)
          ...
          return call.getRpcResponse(); //返回结果
       
      }
    }

Server

Server采用了许多提高并发处理能力的技术,主要包括线程池、事件驱动和Reactor设计模式等,Reactor是并发变成中的一种基于事件驱动的设计模式,具有通过派发I/O分离I/O操作事件提高系统的并发性能;提供了粗粒度的并发控制,使用单线程实现,避免复杂的同步处理。

Server类中有三个重要的内部类,都有各自的run()方法,分别是Listener、Responder、Hadnler。下面就要结合Server处理请求的过程来介绍这三个类。

Server接受来自Client客户端的RPC请求。经过 调用相应的函数处理得到结果后返回给客户端,所以Server可以分为三个阶段:

①接收请求

1)先来看一下Server类的构造函数,我只取了重要部分的代码,可以看到内部去初始化几个内部类,但是我们没有看到Hadnler类,它是在Server类的start()方法中初始化并启动的。

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 {
  ...
  //起了一个监听,用于监听Client发过来的请求
  listener = new Listener();
 ...
  // Create the responder here
  responder = new Responder();
  
  if (secretManager != null || UserGroupInformation.isSecurityEnabled()) {
    SaslRpcServer.init(conf);
    saslPropsResolver = SaslPropertiesResolver.getInstance(conf);
  }
  
  this.exceptionsHandler.addTerseExceptions(StandbyException.class);
}

2)可以看到几个重要内部类的初始化和启动

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();
  }
}

3)那先来看一下Listener类的run方法,可以看到内部有一个doAcceept()

while (running) {
  SelectionKey key = null;
  try {
    getSelector().select();
    Iterator<SelectionKey> iter = getSelector().selectedKeys().iterator();
    while (iter.hasNext()) {
      key = iter.next();
      iter.remove();
      try {
        if (key.isValid()) {
          if (key.isAcceptable())
            doAccept(key);//执行ACCEPT对应的处理逻辑
        }
      } catch (IOException e) {
      }
      key = null;
    }

4) 跟踪doAcceept()内部分析,发现初始化了一个Reader

void doAccept(SelectionKey key) throws InterruptedException, IOException,  OutOfMemoryError {
  ServerSocketChannel server = (ServerSocketChannel) key.channel();
  SocketChannel channel;
  while ((channel = server.accept()) != null) {
    channel.configureBlocking(false);
    channel.socket().setTcpNoDelay(tcpNoDelay);
    channel.socket().setKeepAlive(true);
    
    Reader reader = getReader(); //使用轮询的方式选择一个Reader
    Connection c = connectionManager.register(channel);
    // If the connectionManager can't take it, close the connection.
    if (c == null) {
      if (channel.isOpen()) {
        IOUtils.cleanup(null, channel);
      }
      connectionManager.droppedConnections.getAndIncrement();
      continue;
    }
    key.attach(c);  // so closeCurrentConnection can get the object
    reader.addConnection(c);
  }
}

5)那来看一下Reader都做了些什么,再进入Reader内部的doRead()方法,Listener会将监听到的新的请求交给Reader去处理,

      Connection c = (Connection)key.attachment();
      if (c == null) {
        return;  
      }
      c.setLastContact(Time.now());
      
      try {
        count = c.readAndProcess(); //这里就是通过Listener监听得到的请求后交给内部Reader去处理
      } catch (InterruptedException ieo) {
        LOG.info(Thread.currentThread().getName() + ": readAndProcess caught InterruptedException", ieo);
        throw ieo;
​

6)看一下关键代码c.readAndProcess()

public int readAndProcess()
    throws WrappedRpcServerException, IOException, InterruptedException {
  while (true) {
    ...
    //读取数据长度字段
    if (data == null) {
      dataLengthBuffer.flip();
      dataLength = dataLengthBuffer.getInt();
      checkDataLength(dataLength);
      data = ByteBuffer.allocate(dataLength);
    }
    //读取数据
    count = channelRead(channel, data);
    
    if (data.remaining() == 0) {
      dataLengthBuffer.clear();
      data.flip();
      boolean isHeaderRead = connectionContextRead;
      processOneRpc(data.array()); //解析RPC请求,交付给具体的处理器类
      data = null;
      if (!isHeaderRead) {
        continue;
      }
    } 
    return count;
  }
}

7)那进入 processOneRpc(data.array())看一下,这个方法内部调用了processRpcRequest(header, dis),这个方法则是将请求封装成了call对象放在了callQueue中等待Handler来处理这些信息。

try {
...
  
  if (callId < 0) { // callIds typically used during connection setup
    processRpcOutOfBandRequest(header, dis);
  } else if (!connectionContextRead) {
    throw new WrappedRpcServerException(
        RpcErrorCodeProto.FATAL_INVALID_RPC_HEADER,
        "Connection context not established");
  } else {
    processRpcRequest(header, dis);//真正处理RPC请求将请求封装成call对象放在callQueue中
  }

②处理请求

1)接下来看一下Handler类似如何处理请求的,首先可以看到将之前放入的call从callQueue队列中取出来

try {
  final Call call = callQueue.take(); // pop the queue; maybe blocked here
  if (LOG.isDebugEnabled()) {
    LOG.debug(Thread.currentThread().getName() + ": " + call + " for RpcKind " + call.rpcKind);
  }
  if (!call.connection.channel.isOpen()) {
    LOG.info(Thread.currentThread().getName() + ": skipped " + call);
    continue;
  }

2)然后交给对应的call函数去处理请求

if (call.connection.user == null) {
  value = call(call.rpcKind, call.connection.protocolName, call.rpcRequest, 

3)最终去设置Response,返回给对应的客户端,内部就是通过Responsder去响应客户端的,sendResponse()方法其实就是将Response放在了responseQueue队列当中

  synchronized (call.connection.responseQueue) {
    setupResponse(buf, call, returnStatus, detailedErr,
        value, errorClass, error);
​
    // Discard the large buf and reset it back to smaller size
    // to free up heap.
    if (buf.size() > maxRespSize) {
      LOG.warn("Large response size " + buf.size() + " for call "
          + call.toString());
      buf = new ByteArrayOutputStream(INITIAL_RESP_BUF_SIZE);
    }
    //内部就是通过Responsder去响应客户端的
    call.sendResponse();
  }
} catch (InterruptedException e) {

③返回结果

1)最终来看一下Responsder类中的run()方法,内部是如何处理responseQueue

while (iter.hasNext()) {
  SelectionKey key = iter.next();
  iter.remove();
  try {
    if (key.isWritable()) {
      doAsyncWrite(key);
    }
    }

2)进入doAsyncWritee(),可以看到同步去处理responseQueue,接下来去看一下processResponse(call.connection.responseQueue, false)

synchronized(call.connection.responseQueue) {
  if (processResponse(call.connection.responseQueue, false)) {
    try {
      key.interestOps(0);
    } catch (CancelledKeyException e) {

3)这里则是最终在responseQueue里添加返回的信息。以及内部有一个Selector对象,用于监听SelectionKey.OP_WRITE。如果Handler没能将结果一次性发送到客户端时,回向该对象注册SelectionKey.OP_WRITE事件,进而由Responder线程采用异步方式继续发送未发送完成的结果

call.connection.responseQueue.addFirst(call);//添加返回
if (inHandler) {
  // set the serve time when the response has to be sent later
  call.timestamp = Time.now();
  
  incPending();
  try {
    // Wakeup the thread blocked on select, only then can the call 
    // to channel.register() complete.
    writeSelector.wakeup();
    channel.register(writeSelector, SelectionKey.OP_WRITE, call);
  } catch (ClosedChannelException e) {
    //Its ok. channel might be closed else where.
    done = true;
  } finally {
    decPending();
  }
} 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章