基于版本:CDH5.4.2

上述版本较老,但是目前生产上是使用这个版本,所以以此为例。

1. 概要


说明:

  1. 客户端API发送的请求将会被RPCServer的Listener线程监听到。

  2. Listener线程将分配Reader给到此Channel用户后续请求的相应。

  3. Reader线程将请求包装成CallRunner实例,并将通过RpcScheduler线程根据请求属性分类dispatch到不同的Executor线程。

  4. Executor线程将会保存这个CallRunner实例到队列。

  5. 每一个Executor队列都被绑定了指定个数的Handler线程进行消费,消费很简单,即拿出队列的CallRunner实例,执行器run()方法。

  6. run()方法将会组装response到Responder线程中。

  7. Responder线程将会不断地将不同Channel的结果返回到客户端。

2. 代码梳理


总体来说服务端RPC处理机制是一个生产者消费者模型。

2.1 组件初始化

  • RpcServer是在master或者regionserver启动时候进行初始化的,关键代码如下:

public HRegionServer(Configuration conf, CoordinatedStateManager csm)
     throws IOException, InterruptedException {
   this.fsOk = true;
   this.conf = conf;
   checkCodecs(this.conf);
  .....
   rpcServices.start();
  .....
  }
  • rpcServeice声明RSRpcServices类型,为RpcServer类的实现接口。start()方法将会启动三个主要生产和消费 线程

      /** Starts the service.  Must be called before any calls will be handled. */
    @Override
    public synchronized void start() {
      if (started) return;
    ......
      responder.start();
      listener.start();
      scheduler.start();
      started = true;
    }
2.2 客户端API请求接收和包装

Listener通过NIO机制进行端口监听,客户端API连接服务端指定端口将会被监听。

  • Listener对于API请求的接收:

    void doAccept(SelectionKey key) throws IOException, OutOfMemoryError {
     Connection c;
     ServerSocketChannel server = (ServerSocketChannel) key.channel();

     SocketChannel channel;
     while ((channel = server.accept()) != null) {
       try {
......
// 当一个API请求过来时候将会打开一个Channel,Listener将会分配一个Reader注册。
       // reader实例个数有限,采取顺序分配和复用,即一个reader可能为多个Channel服务。
       Reader reader = getReader();
       try {
         reader.startAdd();
         SelectionKey readKey = reader.registerChannel(channel);
         // 同时也将保存这个Channel,用于后续的结果返回等
         c = getConnection(channel, System.currentTimeMillis());
         readKey.attach(c);
         synchronized (connectionList) {
           connectionList.add(numConnections, c);
           numConnections++;
......
    }
  }

上述中Reader个数是有限的并且可以顺序复用的,个数可以通过如下参数进行设定,默认为10个。

this.readThreads = conf.getInt("hbase.ipc.server.read.threadpool.size", 10);

当生产能力不足时,可以考虑增加此配置值。

  • Reader读取请求并包装请求

    当Reader实例被分配到一个Channel后,它将读取此通道过来的请求,并包装成CallRunner用于调度。

        void doRead(SelectionKey key) throws InterruptedException {
    ......
         try {
           // 此时将调用connection的读取和处理方法
           count = c.readAndProcess();
          ......
        }
      }
        public int readAndProcess() throws IOException, InterruptedException {
    ......
         // 通过connectionPreambleRead标记为判断此链接是否为新连接,如果是新的那么需要读取
         // 头部报文信息,用于判断当前链接属性,比如是当前采取的是哪种安全模式?
         if (!connectionPreambleRead) {
           count = readPreamble();
           if (!connectionPreambleRead) {
             return count;
          }
          ......

         count = channelRead(channel, data);
         if (count >= 0 && data.remaining() == 0) { // count==0 if dataLength == 0
           // 实际处理请求,里面也会根据链接的头报文读取时候判断出的两种模式进行不同的处理。
           process();
        }

         return count;
      }
        private void process() throws IOException, InterruptedException {
    ......
           if (useSasl) {
              // Kerberos安全模式
             saslReadAndProcess(data.array());
          } else {
              // AuthMethod.SIMPLE模式
             processOneRpc(data.array());
          }
          .......
      }

    如下以AuthMethod.SIMPLE模式为例进行分析:

        private void processOneRpc(byte[] buf) throws IOException, InterruptedException {
         if (connectionHeaderRead) {
           // 处理具体请求
           processRequest(buf);
        } else {
           // 再次判断链接Header是否读取,未读取则取出头报文用以确定请求的服务和方法等。
           processConnectionHeader(buf);
           this.connectionHeaderRead = true;
           if (!authorizeConnection()) {
             throw new AccessDeniedException("Connection from " + this + " for service "
               connectionHeader.getServiceName() + " is unauthorized for user: " + user);
          }
        }
      }
      protected void processRequest(byte[] buf) throws IOException, InterruptedException {
         long totalRequestSize = buf.length;
    ......
         // 这里将会判断RpcServer做接收到的请求是否超过了maxQueueSize,注意这个值为
         // RpcServer级别的变量
         if ((totalRequestSize + callQueueSize.get()) > maxQueueSize) {
           final Call callTooBig =
             new Call(id, this.service, null, null, null, null, this,
               responder, totalRequestSize, null);
           ByteArrayOutputStream responseBuffer = new ByteArrayOutputStream();
           setupResponse(responseBuffer, callTooBig, new CallQueueTooBigException(),
             "Call queue is full on " + getListenerAddress() +
             ", is hbase.ipc.server.max.callqueue.size too small?");
           responder.doRespond(callTooBig);
           return;
        }
        ......
         Call call = new Call(id, this.service, md, header, param, cellScanner, this, responder,
                 totalRequestSize,
                 traceInfo);
         // 此时请求段处理结束,将请求包装成CallRunner后发送到不同的Executer的队列中去。
         scheduler.dispatch(new CallRunner(RpcServer.this, call, userProvider));
      }

    注意这个值为 RpcServer级别的变量,默认值为1G,超过此阈值将会出现Call queue is full错误。

    callQueueSize的大小会在请求接收的时候增加,在请求处理结束(调用完毕CallRunner的run方法后)减去相应值。

    this.maxQueueSize =this.conf.getInt("hbase.ipc.server.max.callqueue.size",DEFAULT_MAX_CALLQUEUE_SIZE);
2.3 请求转发与调度

客户端请求在经过接收和包装为CallRunner后将会被具体的Scheduler进行dispatch,master和regionserver

调度器并不相同,这里以regionserver的调度器进行讲解。具体为:SimpleRpcScheduler。

  public RSRpcServices(HRegionServer rs) throws IOException {
    ......
   RpcSchedulerFactory rpcSchedulerFactory;
   try {
     Class<?> rpcSchedulerFactoryClass = rs.conf.getClass(
         REGION_SERVER_RPC_SCHEDULER_FACTORY_CLASS,
         SimpleRpcSchedulerFactory.class);
     rpcSchedulerFactory = ((RpcSchedulerFactory) rpcSchedulerFactoryClass.newInstance());
  • 请求转发

    前面已经提到请求包装完CallRunner后由具体的RpcScheduler实现类的dispacth方法进行转发。

    具体代码为:

      @Override
     public void dispatch(CallRunner callTask) throws InterruptedException {
       RpcServer.Call call = callTask.getCall();
        // 取得优先级,一般也是根据请求的内容事先定义好的一些操作作为高优先级
       int level = priority.getPriority(call.getHeader(), call.param);
       if (priorityExecutor != null && level > highPriorityLevel) {
         // 高优先级则进入高优先级执行器内
         priorityExecutor.dispatch(callTask);
      } else if (replicationExecutor != null && level == HConstants.REPLICATION_QOS) {
         // replication级别的进入相应的replication执行器内
         replicationExecutor.dispatch(callTask);
      } else {
         // 其他的一般请求为一般执行器内,大部分的请求都将落入此执行器
         callExecutor.dispatch(callTask);
      }
    }
  • 执行器介绍-队列初始化

    在此调度器中共分为三个级别的调度执行器:

    1. 高优先请求级执行器

    2. 一般请求执行器

    3. replication请求执行器

        private final RpcExecutor callExecutor;
       private final RpcExecutor priorityExecutor;
       private final RpcExecutor replicationExecutor;

    上述中callExecutor为最主要一般请求执行器,在当前版本中此执行器中可以将读取和写入初始化为不同比例的队列,并将handler也分成不同比例进行队列的绑定。即一个队列上面只有被绑定的handler具体处理权限。默认的不划分读写分离的场景下就只有一个队列,所有请求都进入其中,所有的handler也将去处理这个队列。

    具体我们以读写分离队列为例进行代码分析:

    float callQueuesHandlersFactor = conf.getFloat(CALL_QUEUE_HANDLER_FACTOR_CONF_KEY, 0);
    int numCallQueues = Math.max(1, (int)Math.round(handlerCount * callQueuesHandlersFactor));

    LOG.info("Using " + callQueueType + " as user call queue, count=" + numCallQueues);

    if (numCallQueues > 1 && callqReadShare > 0) {
    // multiple read/write queues
    if (callQueueType.equals(CALL_QUEUE_TYPE_DEADLINE_CONF_VALUE)) {
      CallPriorityComparator callPriority = new CallPriorityComparator(conf, this.priority);
        // 实例化RW读取执行器,构造参数中的为读写比例,其中读取又分为一般读取和scan读取比例
        // 后续将会调用重载的其他构造方法,最终将会计算出各个读取队列的个数和handler的比例数
      callExecutor = new RWQueueRpcExecutor("RW.default", handlerCount, numCallQueues,
          callqReadShare, callqScanShare, maxQueueLength, conf, abortable,
          BoundedPriorityBlockingQueue.class, callPriority);
    } else {

如下为最终调用的重载构造方法:

    public RWQueueRpcExecutor(final String name, int writeHandlers, int readHandlers,
       int numWriteQueues, int numReadQueues, float scanShare,
       final Class<? extends BlockingQueue> writeQueueClass, Object[] writeQueueInitArgs,
       final Class<? extends BlockingQueue> readQueueClass, Object[] readQueueInitArgs) {
     super(name, Math.max(writeHandlers, numWriteQueues) + Math.max(readHandlers, numReadQueues));
 
     int numScanQueues = Math.max(0, (int)Math.floor(numReadQueues * scanShare));
     int scanHandlers = Math.max(0, (int)Math.floor(readHandlers * scanShare));
     if ((numReadQueues - numScanQueues) > 0) {
       numReadQueues -= numScanQueues;
       readHandlers -= scanHandlers;
    } else {
       numScanQueues = 0;
       scanHandlers = 0;
    }
// 确定各个主要队列参数
     this.writeHandlersCount = Math.max(writeHandlers, numWriteQueues);
     this.readHandlersCount = Math.max(readHandlers, numReadQueues);
     this.scanHandlersCount = Math.max(scanHandlers, numScanQueues);
     this.numWriteQueues = numWriteQueues;
     this.numReadQueues = numReadQueues;
     this.numScanQueues = numScanQueues;
     this.writeBalancer = getBalancer(numWriteQueues);
     this.readBalancer = getBalancer(numReadQueues);
     this.scanBalancer = getBalancer(numScanQueues);
 
     queues = new ArrayList<BlockingQueue<CallRunner>>(writeHandlersCount + readHandlersCount);
     LOG.debug(name + " writeQueues=" + numWriteQueues + " writeHandlers=" + writeHandlersCount +
               " readQueues=" + numReadQueues + " readHandlers=" + readHandlersCount +
              ((numScanQueues == 0) ? "" : " scanQueues=" + numScanQueues +
                 " scanHandlers=" + scanHandlersCount));
// 初始化队列列表,注意queues为有序列表,如下队列位置初始化后不会变动,在后续按照具体的请求
     // 通过具体的getBalancer方法进行查找
     for (int i = 0; i < numWriteQueues; ++i) {
       queues.add((BlockingQueue<CallRunner>)
         ReflectionUtils.newInstance(writeQueueClass, writeQueueInitArgs));
    }
 
     for (int i = 0; i < (numReadQueues + numScanQueues); ++i) {
       queues.add((BlockingQueue<CallRunner>)
         ReflectionUtils.newInstance(readQueueClass, readQueueInitArgs));
    }
  }
  • 执行器介绍--handler绑定

    当请求被分类放入不同的执行器队列后,将有此队列上被绑定的handler进行处理,handler是请求的消费者。

    如下为RWQueueRpcExecutor类中handler绑定逻辑:

      @Override
     protected void startHandlers(final int port) {
       startHandlers(".write", writeHandlersCount, queues, 0, numWriteQueues, port);
       startHandlers(".read", readHandlersCount, queues, numWriteQueues, numReadQueues, port);
       startHandlers(".scan", scanHandlersCount, queues,
                     numWriteQueues + numReadQueues, numScanQueues, port);
    }

    具体startHandlers方法,此方法中将根据参数指定的index和size进行绑定:

      protected void startHandlers(final String nameSuffix, final int numHandlers,
         final List<BlockingQueue<CallRunner>> callQueues,
         final int qindex, final int qsize, final int port) {
       final String threadPrefix = name + Strings.nullToEmpty(nameSuffix);
       for (int i = 0; i < numHandlers; i++) {
         final int index = qindex + (i % qsize);
         Thread t = new Thread(new Runnable() {
           @Override
           public void run() {
             // 值处理指定队列的请求
             consumerLoop(callQueues.get(index));
          }
        });
         t.setDaemon(true);
         t.setName(threadPrefix + "RpcServer.handler=" + handlers.size() +
           ",queue=" + index + ",port=" + port);
         t.start();
         LOG.debug(threadPrefix + " Start Handler index=" + handlers.size() + " queue=" + index);
         handlers.add(t);
      }
    }
  • 执行器介绍--handler消费

    handler的消费很简单,不断的读取指定队列的CallRunner实例,并执行CallRunner实例的run方法。

      protected void consumerLoop(final BlockingQueue<CallRunner> myQueue) {
        .......
         while (running) {
           try {
             // 请求取得
             CallRunner task = myQueue.take();
             try {
               activeHandlerCount.incrementAndGet();
               // 指定callrunner的run方法
               task.run();
            .......
    }

    接着看一下CallRunner的run方法:

      public void run() {
        .......
           // 执行具体操作
           // make the call
           resultPair = this.rpcServer.call(call.service, call.md, call.param, call.cellScanner,
      .......
         // Set the response for undelayed calls and delayed calls with
         // undelayed responses.
         // 将response放入实例中
         if (!call.isDelayed() || !call.isReturnValueDelayed()) {
           Message param = resultPair != null ? resultPair.getFirst() : null;
           CellScanner cells = resultPair != null ? resultPair.getSecond() : null;
           call.setResponse(param, cells, errorThrowable, error);
        }
        ........
         // call中有connection的句柄,将response放入具体connection的返回队列中
         call.sendResponseIfReady();
    .....

call中有connection的句柄,将response放入具体connection的返回队列中

  // If there is already a write in progress, we don't wait. This allows to free the handlers
 // immediately for other tasks.
 if (call.connection.responseQueue.isEmpty() && call.connection.responseWriteLock.tryLock()) {
   try {
     if (call.connection.responseQueue.isEmpty()) {
       // If we're alone, we can try to do a direct call to the socket. It's
       // an optimisation to save on context switches and data transfer between cores..
       if (processResponse(call)) {
         return; // we're done.
      }
       // Too big to fit, putting ahead.
       call.connection.responseQueue.addFirst(call);
       added = true; // We will register to the selector later, outside of the lock.
    }
  } finally {
     call.connection.responseWriteLock.unlock();
  }
}

 if (!added) {
   call.connection.responseQueue.addLast(call);
}
 call.responder.registerForWrite(call.connection);

 // set the serve time when the response has to be sent later
 call.timestamp = System.currentTimeMillis();
2.4 Response返回

CallRunner的run方法将会具体执行请求操作,并将response放入Responder实例的对应的connection的返回队列中用于后续返回

具体为Responder实例也是一个线程实例,它的run方法最终执行如下代码:

 private void doAsyncWrite(SelectionKey key) throws IOException {
     Connection connection = (Connection) key.attachment();
     if (connection == null) {
       throw new IOException("doAsyncWrite: no connection");
    }
     if (key.channel() != connection.channel) {
       throw new IOException("doAsyncWrite: bad channel");
    }

     if (processAllResponses(connection)) {
       try {
         // We wrote everything, so we don't need to be told when the socket is ready for
         // write anymore.
        key.interestOps(0);
      } catch (CancelledKeyException e) {
         /* The Listener/reader might have closed the socket.
          * We don't explicitly cancel the key, so not sure if this will
          * ever fire.
          * This warning could be removed.
          */
         LOG.warn("Exception while changing ops : " + e);
      }
    }
  }

   /**

3. 结束语


上述介绍服务端HRegionserver端的RPC接受与处理的过程,粗粒度的介绍了代码的结构,希望后续遇到这方面的问题时能够帮助进行代码级别的问题定位和解决。

[HBase] 服务端RPC机制及代码梳理的更多相关文章

  1. react服务端/客户端,同构代码心得

    FKP-REST是一套全栈javascript框架   react服务端/客户端,同构代码心得 作者:webkixi react服务端/客户端,同构代码心得 服务端,客户端同构一套代码,大前端的梦想, ...

  2. Photon Server 实现注册与登录(五) --- 服务端、客户端完整代码

    客户端代码:https://github.com/fotocj007/PhotonDemo_Client 服务端代码:https://github.com/fotocj007/PhotonDemo_s ...

  3. linux epoll机制对TCP 客户端和服务端的监听C代码通用框架实现

    1 TCP简介 tcp是一种基于流的应用层协议,其“可靠的数据传输”实现的原理就是,“拥塞控制”的滑动窗口机制,该机制包含的算法主要有“慢启动”,“拥塞避免”,“快速重传”. 2 TCP socket ...

  4. Hadoop RPC源码阅读-服务端Server

    Hadoop版本Hadoop2.6 RPC主要分为3个部分:(1)交互协议 (2)客户端(3)服务端 (3)服务端 RPC服务端的实例代码: public class Starter { public ...

  5. 根据服务端生成的WSDL文件创建客户端支持代码的三种方式

    第一种:使用wsimport是JDK自带的工具,来生成 生成java客户端代码常使用的命令参数说明: 参数 说明 -p 定义客户端生成类的包名称 -s 指定客户端执行类的源文件存放目录 -d 指定客户 ...

  6. IOS IAP APP内支付 Java服务端代码

    IOS IAP APP内支付 Java服务端代码   场景:作为后台需要为app提供服务,在ios中,app内进行支付购买时需要进行二次验证. 基础:可以参考上一篇转载的博文In-App Purcha ...

  7. Java 断点下载(下载续传)服务端及客户端(Android)代码

    原文: Java 断点下载(下载续传)服务端及客户端(Android)代码 - Stars-One的杂货小窝 最近在研究断点下载(下载续传)的功能,此功能需要服务端和客户端进行对接编写,本篇也是记录一 ...

  8. app开发中如何利用sessionId来实现服务端与客户端保持回话

    app开发中如何利用sessionId来实现服务端与客户端保持回话 这个问题太过于常见,也过于简单,以至于大部分开发者根本没有关注过这个问题,我根据和我沟通的开发者中,总结出来常用的方法有以下几种: ...

  9. Netty 服务端创建

    参考:http://blog.csdn.net/suifeng3051/article/details/28861883?utm_source=tuicool&utm_medium=refer ...

随机推荐

  1. 【ActiveMQ】- 发布/订阅模式

    publish/subscribe 特点:A发送的消息可以被所有监听A的对象的接收,就好比学校的广播,所有的学生都可以收听校园广播信息. 消息生产者: package com.zhiwei.advan ...

  2. Vue 中使用UEditor富文本编辑器-亲测可用-vue-ueditor-wrap

    其中UEditor中也存在不少错误,再引用过程中. 但是UEditor相对还是比较好用的一个富文本编辑器. vue-ueditor-wrap说明 Vue + UEditor + v-model 双向绑 ...

  3. Mysql索引机制B+Tree

    1.问题引入 有一个用户表,为了查询的效率,需要基于id去构建索引.构建索引我们需要考虑两个方面的问题,1个是查询的效率,1个是索引数据的存储问题.该表的记录需要支持百万.千万.甚至上亿的数据量,如果 ...

  4. CF993E Nikita and Order Statistics 【fft】

    题目链接 CF993E 题解 我们记小于\(x\)的位置为\(1\),否则为\(0\) 区间由端点决定,转为两点前缀和相减 我们统计出每一种前缀和个数,记为\(A[i]\)表示值为\(i\)的位置出现 ...

  5. 2018java面试集合

    作者:刘成链接:https://www.zhihu.com/question/266822548/answer/317700943来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注 ...

  6. PCA主成分分析 R语言

    1. PCA优缺点 利用PCA达到降维目的,避免高维灾难. PCA把所有样本当作一个整体处理,忽略了类别属性,所以其丢掉的某些属性可能正好包含了重要的分类信息 2. PCA原理 条件1:给定一个m*n ...

  7. Kubernetes--kubectl

    一.Kubectl命令行说明 类型 命令 描述 基础命令 create  通过文件名或标准输入创建资源 expose  将一个资源公开为一个新的kubernetes服务 run 创建并运行一个特定的镜 ...

  8. 区间DP的思路(摘自NewErA)及自己的心得

    以下为摘要 区间dp能解决的问题就是通过小区间更新大区间,最后得出指定区间的最优解 个人认为,想要用区间dp解决问题,首先要确定一个大问题能够剖分成几个相同较小问题,且小问题很容易组合成大问题,从而从 ...

  9. word2vec原理CBOW与Skip-Gram模型基础

    转自http://www.cnblogs.com/pinard/p/7160330.html刘建平Pinard word2vec是google在2013年推出的一个NLP工具,它的特点是将所有的词向量 ...

  10. 科学计算三维可视化---Mayavi入门(Mayavi管线)

    一:Mayavi管线 mlab.show_pipeline() #显示管线层级,来打开管线对话框 (一)管线中的对象scene Mayavi Scene:处于树的最顶层的对象,他表示场景,配置界面中可 ...