Hadoop RPC分析 (二) -- Server

2 篇文章 0 订阅
[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返回给客户端。


Server模型比客户端模型负责,借用一张来自( http://langyu.iteye.com/blog/1183337 )的图来说明Server这边内部各个组成模块之间的关系




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))。在下一次处理返回结果时,继续发送这个处理结果。避免数据错乱。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值