Hadoop RPC框架解读

1. RPC概述

1.1 RPC简介

RPC(Remote Procedure Call Protocol)——远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC协议假定某些传输协议的存在,如TCP或UDP,为通信程序之间携带信息数据。在OSI网络通信模型中,RPC跨越了传输层应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。

RPC采用客户机/服务器模式。请求程序就是一个客户机,而服务提供程序就是一个服务器。首先,客户机调用进程发送一个进程参数的调用信息到服务进程,然后等待应答信息。在服务器端,进程保持睡眠状态直到调用信息的到达为止。当一个调用信息到达,服务器获得进程参数,计算结果,发送答复信息,然后等待下一个调用信息,最后,客户端调用进程接收答复信息,获得进程结果,然后调用执行继续执行。

目前,有多种 RPC 模式和执行。最初由 Sun 公司提出。IETF ONC 宪章重新修订了 Sun 版本,使得 ONC RPC 协议成为 IETF 标准协议。现在使用最普遍的模式和执行是开放式软件基础的分布式计算环境(DCE)。

1.2 RPC工作原理

运行时,一次客户机对服务器的RPC调用大致过程如下:

1.客户端发起请求,执行参数传递;

2.调用本地系统内核发送套接字信息;

3.服务器套接字监听器接收数据;

4.解析参数,执行远程过程;

5.执行过程将结果写回到套接字通道;

6.客户机在建立的套接字通道中接收到相应的应答信息;

7.客户机解析套接字信息,根据结果继续执行本地过程;

2. HDFS的RPC框架

2.1 HDFS的RPC客户机与服务器之间的协同工作

HDFS的RPC客户端和服务端启动流程如下:


客户端执行RPC请求的时候有如下几个过程:

1.       客户端使用创建RPC客户机时候返回的代理实例发出请求;

2.       由Invoker.invoke去执行,获得连接对象(如果未建立则建立连接,已建立则直接使用);

3.       发送请求参数,序列化数据,将数据写入到输出流中;

4.       RPC服务器的Listener接收数据,反序列化数据,构建connection对象;

5.       Reader从选择器中读取connection对象,构建call请求加入到callQueue;

6.       Handler线程从callQueue取出RPC请求;

7.       Handler通过反射和RPC服务器上的实例instance执行远程过程;

8.       Handler将执行结果通过Responder发给客户机;

9.       Responder序列化数据,将数据写入到连接中的套接字中,直到写完所有数据清楚连接对象;

客户机的connection线程接收到数据,反序列化执行结果,并将执行结果返回给HDFS客户端;

2.2 HDFS-RPC的Server分析

Server内部实现了Call、Listener、Responder、Connection、Handler共五个内部类,Call用来维护每个调用请求排队,Listener用来创建并监听套接字接收调用请求,Responder用来向客户端发送应答,Conenection维护每个连接请求对象,Handler用来处理每个RPC请求;


2.2.1 Call分析

每从套接字中读取到一个RPC请求,则解析请求数据的时候构建一个Call的对象;

Call主要包含4个参数,分别是:

1、id:每个Call包含一个id,区分不同的Call对象;

2、param:每个Call请求中包含的参数;

3、connection:每个RPC请求都必须包含的连接对象,作出应答;

4、response:是一个ByteBuffer对象,保存了应答的数据,new一个新的Call对象的时候,这个参数为空,直到对远程过程执行完成请求后,给客户机返回执行结果的将结果写入到response中;

部分代码如下(Server.Call的构造函数):

public Call(int id, Writable param, Connection connection) {

      this.id = id;

      this.param =param;

      this.connection =connection;

      this.timestamp =System.currentTimeMillis();

      this.response =null;

    }

2.2.2 Listener分析

Listener是在启动RPC服务端的时候创建的一个监听并接收连接请求数据的线程类,Listener对象中包含了一个绑定了服务端机器Socket地址的Socket通道,并且包含一个使用这个Socket通道注册的选择器selector,以及一个Reader线程;

Listener的构造函数部分代码如下:

bind(acceptChannel.socket(), address, backlogLength);

for (int i = 0; i < readThreads; i++) {

Selector readSelector = Selector.open();

Reader reader = newReader("Socket Reader #" + (i + 1) + " for port " + port,readSelector);

        readers[i] =reader;

        reader.start();

      }

acceptChannel.register(selector, SelectionKey.OP_ACCEPT);

Listener线程启动后,使用selector去读取Socket通道,在Socket通道读出每一个RPC请求,将读到的请求注册到Reader线程的选择器中,同时构建一个Connection对象,构建Connection对象的时候会先读RPC连接头部,将客户端请求的协议从字符串加载为Class对象,然后将Connection对象附加到Reader中选择器增加的键中,供Reader线程去读取,并且将这个Connection对象加入到全局的一个连接对象List中;如果发生异常,则将当前正在进行读取连接对象关闭,如果是OutOfMemoryError还会清楚cnnectionList对象,将一些无意义的连接清楚,不如一些超时的连接,清楚的时候会关闭每个connection的Socket通道;如果停止Listener线程则会清楚cnnectionList中的所有连接对象;

Listener线程的run方法主要代码如下:

public void run() {

     SERVER.set(Server.this);

      while (running) {

        SelectionKeykey = null;

        try {selector.select();

         Iterator<SelectionKey> iter = selector.selectedKeys().iterator();

          while(iter.hasNext()) {

            key =iter.next();

           iter.remove();

            try {

              if(key.isValid()) {

                if(key.isAcceptable())

                 doAccept(key); }

            } catch(IOException e) {}

            key = null;}

        } catch(OutOfMemoryError e) {

         closeCurrentConnection(key, e);

         cleanupConnections(true);

          try {Thread.sleep(60000); } catch (Exception ie) {}

        } catch(Exception e) {

         closeCurrentConnection(key, e); }

       cleanupConnections(false); }}

run方法中调用的doAccept方法构建每一个connection对象,然后将该对象附加到Reader中的readSelector选择器中,代码片段如下:

while ((channel = server.accept()) != null) {

        Reader reader =getReader();

        try {

         reader.startAdd();

          SelectionKeyreadKey = reader.registerChannel(channel);

          c=new Connection(readKey, channel,System.currentTimeMillis());

         readKey.attach(c);

          synchronized(connectionList) {

           connectionList.add(numConnections, c);

           numConnections++;}} finally {

         reader.finishAdd();}}

Reader是创建Listener对象的时候就会启动的一个线程,作用是处理Listener监听并并接收到的RPC连接请求,将这些请求加入到callQueue队列中;

Reader包含两个重要的参数:

1.           adding:默认初始值为false,通过startAdd和finishAdd来改变adding的值,分别设置true和false;

2.           readSelector:读选择器,用来解析数据的时候从与这个选择器绑定的通道中读取请求,Listener接收连接的时候将每一个连接与这个选择器进行绑定;

Reader的私有变量申明如下:

private volatile boolean adding = false;

private Selector readSelector = null;

Reader从readSelector选择器中读取出来的每一个key都是附加了一个连接对象的,这里由SelectionKey的attach(将给定的对象附加到此键)和attachment(获取当前键的附加对象)实现的;Reader读取出connection对象后,读取这个连接对象的数据,从相应的connection维护的Socket通道读取读取数据,这里会做一次版本匹配以及安全信息的验证,版本匹配失败则关闭这个连接;数据读取完成后就是对数据进行处理,即创建Call对象,并且将Call对象加入到callQueue中,等待Handler去处理,主要的代码如下:

Reader的run方法:

while (running) {

           SelectionKey key = null;

            try {

             readSelector.select();

              while(adding) {

               this.wait(1000); }

Iterator<SelectionKey> iter =readSelector.selectedKeys().iterator();

              while(iter.hasNext()) {

                key =iter.next();

               iter.remove();

                if(key.isValid()) {

                  if(key.isReadable()) {

                   doRead(key);

                  }}key= null;

              }}}

在Reader线程中调用的doRead方法最后会调用processOneRpc处理RPC请求,首先是处理头部,然后是处理数据:

if (headerRead) {

       processData(buf);

      } else {

       processHeader(buf);

        headerRead = true;

     if(!authorizeConnection()) {

     throw newAccessControlException("Connection from " + this

              + "for protocol " + header.getProtocol()

              + "is unauthorized for user " + user); }}

在做processData的时候会生成Call对象,加入到callQueue中:

Call call = new Call(id, param, this);

callQueue.put(call);  

incRpcCount();

在做processHeader的时候先将头部读取出来,然后读取协议,获取协议名字对应的类:

String protocolClassName = header.getProtocol();

        if(protocolClassName != null) {

          protocol =getProtocolClass(header.getProtocol(), conf);

        }



Listener与Reader之间关系如下:


2.2.3 Handler分析

Handler是用来调用远程过程的,在上述RPC调用过程中属于第4步的执行远程过程部分,Handler在启动是在创建完成RPC的Server后使用server.start()创建并启动的,如果默认启动10个Handler线程执行远程过程;

Handler处理的数据是callQueue记录的数据,即上述Reader处理后加入到这个队列中的Call对象,callQueue是一个BlockingQueue类型的对象,这是一个阻塞队列,即获取队列元素的期间是阻塞,保证一个取队列元素的调用未完成之前,不能有其他取队列元素的操作,这样保证所有的Handler线程不会因为并发而取到相同的请求而发生错误;

Handler获取callQueue元素的代码如下:

final Call call = callQueue.take();

取出Call对象后将它加入到一个ThreadLocal类型的CurCall中,这是所有Handler共享的,使用ThreadLocal能够很好的保证线程并发的安全,保证个线程之间的数据不会互相影响;

Handler将获取到的Call对象加入到curCall中,处理完后设置当前线程为空:

CurCall.set(call);

……

CurCall.set(null);

执行请求调用的call是一个抽象方法,具体实现在RPC的Server内部类中,下面是调用call方法执行远程过程。首先是根据参数及参数对应的类型利用反射机制从协议对象中得到客户端请求的方法名,然后利用反射的invoke方法以及启动的RPC Server时候的远程实例执行远程过程,返回执行结果;Handler将执行结果写入到输出流对象response中,交给Responder线程给客户机返回执行结果;

调用call的代码如下:

value =call(call.connection.protocol, call.param,call.timestamp);

call方法实现在RPC.Server中,部分代码如下:

Invocation call =(Invocation)param;

if (verbose) log("Call:" + call);

Method method =

protocol.getMethod(call.getMethodName(),call.getParameterClasses());

method.setAccessible(true);

long startTime =System.currentTimeMillis();

Object value =method.invoke(instance, call.getParameters());

其中的Invocation类是继承自Writable、Configurable的,记录方法名、参数、参数类型等;invoke执行的时候,instance是构建RPC.Server对象的时候namenode的实例。

执行远程过程完成后,则由Handler线程将执行结果返回给Server中的Call对象,然后使用Responder的方法给RPC客户机返回执行结果:

setupResponse(buf, call,

 (error == null) ?Status.SUCCESS : Status.ERROR,

 value, errorClass,error);

……

responder.doRespond(call);

Handler线程之间获取请求关系图如下:

CallQueue是使用BlockingQueue声明的,保证Handler之间不会取到相同的call去处理;


2.2.4 Responder分析


Responder是一个发送应答给客户机的线程类,在Handler执行远程过程返回后,会使用Responder的方法将执行结果写入到connection对象中的Socket通道中,供客户机读取,写入的时候是尽可能的将数据写入到Socket通道,也可能存在一次没有将所有的执行结果写入到Socket通道,那么此时将这个请求重新加入到应答队列中,同此将connection与Responder的writeSelector选择器绑定,附加到writeSelector的键中;

使用Responder的processResponse去往客户机写返回结果,尽可能的写入,如果还是未写完,则将未写入的绑定到Responder的writeSelector中,最后由Responder线程去给客户机作出应答:

int numBytes = channelWrite(channel, call.response);

          if (numBytes< 0) {

            returntrue;

          }

……

call.connection.responseQueue.addFirst(call);

if (inHandler) {

incPending();

try {

writeSelector.wakeup();

channel.register(writeSelector, SelectionKey.OP_WRITE, call);}}

Responder线程通过writeSelector不断的提取需要应答的call,直到应答发送完成;同时Responder线程会清理长时间没有将执行结果发送出去的connection对象,Responder线程run方法如下:

writeSelector.select(PURGE_INTERVAL);

Iterator<SelectionKey> iter =writeSelector.selectedKeys().iterator();

          while(iter.hasNext()) {

           SelectionKey key = iter.next();

           iter.remove();

            try {

              if(key.isValid() && key.isWritable()) {

                 doAsyncWrite(key); }}}

……

synchronized (writeSelector.keys()) {

calls = new ArrayList<Call>(writeSelector.keys().size());

            iter =writeSelector.keys().iterator();

            while(iter.hasNext()) {

             SelectionKey key = iter.next();

              Call call= (Call)key.attachment();

if (call != null && key.channel() == call.connection.channel){

               calls.add(call); }}}

          for(Call call: calls) {

            try {

             doPurge(call, now); }}

2.2.5 Connection分析

每一个RPC请求都维护了Connection对象,这个对象中有如下信息:

1.       rpcHeaderRead:标明当前连接对象rpc头部是否已经读取;

2.       headerRead:标明当前连接对象头部是否已经读取;

3.       channel:Socket通道,通道客户端IP+端口,应答的时候只需将数据写入到这个channel中;

4.       responseQueue:当前连接对象的应答队列;

5.       socket:关闭连接时候关闭socket;

6.       hostAddress+remotePort+addr:远程即客户机的hostname、端口、和Socket地址;

7.       protocol:客户端协议;

8.       authFailedCall:执行验证的时候验证失败的请求;

在上面的Listener分析中指出,Connection是由Listener线程在读取套接字的时候创建的,每一个请求对应一个Connection对象,维护了连接相应的信息;


2.3 HDFS-RPC的Client分析

建立客户端与服务器的连接是使用RPC.getProxy实现的,即创建一个RPC代理;

Proxy:使用newProxyInstance创建代理实例,这是一个动态代理类(以下简称为代理类)是一个实现在创建类时在运行时指定的接口列表的类,该类具有下面描述的行为。代理接口:是代理类实现的一个接口。 代理实例:是代理类的一个实例。每个代理实例都有一个关联的调用处理程序对象,它可以实现接口InvocationHandler。通过其中一个代理接口的代理实例上的方法调用将被指派到实例的调用处理程序的Invoke方法,并传递代理实例、识别调用方法的java.lang.reflect.Method对象以及包含参数的Object类型的数组。调用处理程序以适当的方式处理编码的方法调用,并且它返回的结果将作为代理实例上方法调用的结果返回。

创建proxy实例的代码如下:

VersionedProtocol proxy =

       (VersionedProtocol) Proxy.newProxyInstance(

       protocol.getClassLoader(), new Class[] { protocol },

        new Invoker(protocol, addr, ticket,conf, factory, rpcTimeout));

HDFS的关联调用处理程序对象是由RPC类里面实现的Invoker内部类声明的,它继承自InvocationHandler,这个对象标明当前客户端的一个remoteId,以及Client的一个实例;这个类实现了invoke方法,这意味着使用Proxy实例对象的调用都将经由invoke去执行。

Invoker中的invoke执行RPC请求如下:

ObjectWritable value = (ObjectWritable)

        client.call(newInvocation(method, args), remoteId);

Client类下面主要有Call、Connection、ConnectionID,用来维护客户机的RPC调用;

2.3.1 Call分析

这个Call与Server类实现的Call有不同,这个Call仅仅维护一次RPC调用的数据,包括客户端调用传递的数据以及远程调用返回的数据,用id来区分每一个不同的Call,id是自增的。次Call中还维护了这一次调用是否完成以及是否调用发生IO异常的信息;


2.3.2 Connection分析

这里的Connection与Server中的Connection也是不相同的,这个Connection是一个线程类,它主要包含如下信息:

1.       server:RPC服务端的套接字对象;

2.       remoteId:连接Id;

3.       socket:连接套接字;

4.       maxIdleTime:用来标明一个无效的连接需要被剔除的时间;

5.       maxRetries:最大重试次数;

6.       calls:当前有效的请求;

Connection内部类维护了RPC当前客户端的所有连接,此后所有由这个RPC客户机发起的RPC请求都将使用这个Connection建立的连接对象与RPC服务器进行通信,代码片段如下:

synchronized (connections) {

        connection =connections.get(remoteId);

        if (connection== null) {

          connection =new Connection(remoteId);

         connections.put(remoteId, connection); }}

具体执行是在Client.Invoker的invoke中,通过Client的call去调用,这里会首先获得连接对象,获取连接对象后会判断connection对象的socket是否为空,为空则开启IO流,创建socket,同时启动connection线程用以接收每个请求的远程执行结果;不为空说明连接是可直接使用的,则直接发送参数,同时等待请求执行完成,即call.done的值置为true。

开启IO流是在getConnection中使用connection.setupIOstreams执行,这里面首先是判断socket是否为空和是否应该关闭连接(只有在停止或清除无效连接或返回执行结果后才会关闭连接),然后决定是否与RPC服务器建立连接和启动connection线程:

if (socket != null || shouldCloseConnection.get()) {

        return; }

……

start();

每一个RPC调用在执行了call以后会等待RPC服务器返回执行结果:

while (!call.done) {

        try {

          call.wait();

        }}


3. Hadoop-2.0.2-RPC

Hadoop-2.0.2的版本引进了Google Protocol Buffer(简称Protobuf)结构化数据工具,这是Google公司内部的混合语言数据标准,是一种轻便高效的结构化数据存储格式,可用于结构化数据串行化,或者说序列化。它很适合做数据存储或者RPC数据交换格式,可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。

Hadoop-2.0.2的RPC通信机制使用了Protobuf,用于RPC通信产生数据交换的时候进行序列化与反序列化,提高RPC通信效率。CDH3中RPC通信机制的序列化与反序列化是自定义实现的。

3.1 Hadoop-2.0.2的RPC与CDH3的区别之Server端

在Namenode启动的时候分别启动了针对Client的RPC服务器和针对Datanode的RPC服务器,当然每个RPC服务器都注册了Namenode实现的所有协议。以Client为例,在Namenode封装了一层服务端传输PB,即ClientNamenodeProtocolServerSideTranslatorPB声明的与客户端通信的Protobuf服务,包括调用参数和发起远程过程调用,即下面代码中德clientNNPbService:

this.clientRpcServer =RPC.getServer(org.apache.hadoop.hdfs.protocolPB.ClientNamenodeProtocolPB.class,

        clientNNPbService,socAddr.getHostName(),

           socAddr.getPort(), handlerCount, false, conf,

           namesystem.getDelegationTokenSecretManager());

clientNNPbService是一个包含了NameNodeRpcServer实例的对象,在该对象中NameNodeRpcServer为server参数,而NameNodeRpcServer包含了当前NameNode的实例,即由clientNNPbService获取客户端请求数据,并使用server执行远程过程。NameNodeRpcServer实现了所有元数据操作,执行的时候使用NameNode的fsnamesystem实例。启动时候会将clientNNPbService对象写入到内存中,调用的时候从内存中获取。

启动RPC服务器的时候注册的协议是诸如ClientNamenodeProtocolPB这类接口,这类接口的implements实现都分别由客户端和服务端两部分,服务端的用于获取请求数据,并执行远程过程,然后将执行返回,客户端的用于序列化请求数据,并通过代理发起RPC请求。

在Hadoop-2.0.2的版本中,Server端与CDH3的Server端一样,但是在Reader线程中执行数据读取与处理的时候会从RPC连接头部读取序列化类型,这里序列化类型只有PROTOBUF一种:

public enum IpcSerializationType{

PROTOBUF;

……

 }

如果不是这种序列化类型,则使用Responder向客户端返回“不支持的序列化类型”,同时处理客户端请求协议名字以及ugi的信息的时候也是使用protobuf解析数据。

Handler线程之间从callQueue队列中获取call是一样的,在真实调用请求的存在差异,CDH3是通过反射执行,Hadoop-2.0.2调用的时候在ProtoBufRpcInvoker.call中执行,首先是获取上述写入到内存的clientNNPbService对象:

ProtoNameVer pv = newProtoNameVer(protoName, version);

        ProtoClassProtoImpl impl =

server.getProtocolImplMap(RPC.RpcKind.RPC_PROTOCOL_BUFFER).get(pv);

以上是在call调用中执行的getProtocolImpl(…)方法中,获取到clientNNPbService后通过反射获取到方法描述,然后将请求交由clientNNPbService去执行。

ProtoClassProtoImpl protocolImpl= getProtocolImpl(server, protoName, clientVersion);

BlockingService service = (BlockingService)protocolImpl.protocolImpl;

……

Message prototype =service.getRequestPrototype(methodDescriptor);

Message param =prototype.newBuilderForType()

           .mergeFrom(rpcRequest.getRequest()).build();

……

server.rpcDetailedMetrics.init(protocolImpl.protocolClass);

result =service.callBlockingMethod(methodDescriptor,null, param);

调用callBlockingMethod的时候是由google probuf内部实现,找到对应执行的方法后,只有clientNNPbService执行,然后返回执行结果。

3.2 Hadoop-2.0.2的RPC与CDH3的区别之Client端

以HDFS客户端为例,创建客户机RPC代理是以ClientNamenodeProtocolPB接口为协议获取代理的,获取到的代理以ClientNamenodeProtocolTranslatorPB封装,即客户机发起调用都将经过ClientNamenodeProtocolTranslatorPB对象,然后再以该对象中的rpcProxy代理去调用RPC远程过程。

代码实现在NameNodeProxies.createNNProxyWithClientProtocol中:

ClientNamenodeProtocolPB proxy = RPC.getProtocolProxy(

ClientNamenodeProtocolPB.class,version, address, ugi, conf,

NetUtils.getDefaultSocketFactory(conf), 0,defaultPolicy).getProxy();

……

return new ClientNamenodeProtocolTranslatorPB(proxy);

上面这个是给客户端发起调用的代理,这个代理同时实现了客户端的failover机制,在上述的代码中执行的getProtocolProxy才会以ClientNamenodeProtocolPB协议去创建RPC的代理,上述代码中返回的proxy才是RPC层的代理。

RPC层代理的Invoker是在ProbufRpcEngine中定义的,Invoker中实现了invoke方法,客户端与NameNode之间的通信都会通过这个invoke方法。同时Invoker定义了一个constructRpcRequest方法,用来序列化客户端请求,然后调用client的call发送RPC请求,建立连接的时候会写连接头部,类型如下:

     +----------------------------------+

     | "hrpc" 4 bytes                  |      hadoop rpc

     +----------------------------------+

     | Version (1 bytes)              |      版本

    +----------------------------------+

     |  Authmethod(1 byte)             |      身份验证方法

     +----------------------------------+

     | IpcSerializationType (1 byte)  |      序列化类型


以上是本人对Hadoop中的RPC框架的一个源码的分析,其中还有很多细节方面的东西分析没有详述,上面的分析是基于cloud3u3版本和hadoop-2.0.2版本的,是一年多以前完成,最近才发到博客上来。
有不正确的地方请指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值