基于版本: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启动时候进行初始化的,关键代码如下:

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

    1. /** 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请求的接收:

  1.    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用于调度。

    1.    void doRead(SelectionKey key) throws InterruptedException {
      ......
           try {
             // 此时将调用connection的读取和处理方法
             count = c.readAndProcess();
            ......
          }
        }
    1.    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;
        }
    1.    private void process() throws IOException, InterruptedException {
      ......
             if (useSasl) {
                // Kerberos安全模式
               saslReadAndProcess(data.array());
            } else {
                // AuthMethod.SIMPLE模式
               processOneRpc(data.array());
            }
            .......
        }

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

    1.    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);
            }
          }
        }
    1.  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方法后)减去相应值。

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

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

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

  1.  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方法进行转发。

    具体代码为:

    1.  @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请求执行器

      1.  private final RpcExecutor callExecutor;
         private final RpcExecutor priorityExecutor;
         private final RpcExecutor replicationExecutor;

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

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

    1. 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 {

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

  1.    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绑定逻辑:

    1.  @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进行绑定:

    1.  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方法。

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

    接着看一下CallRunner的run方法:

    1.  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的返回队列中

  1.  // 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方法最终执行如下代码:

  1. 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. MySql数据库迁移图文展示

    MySql数据库的数据从一台服务器迁移到另外一台服务器需要将数据库导出,再从另外一台服务器导入.方法有很多,MySql配套的相关工具都有这个功能.phpMyAdmin就可以做,但是这个加载起来慢,推荐 ...

  2. Udp广播的发送与接收(C#+UdpClient) 上篇

    简介: Udp广播消息用在局域网的消息传递很方便.本文使用UdpClient类在WPF下实现Udp广播收发 发送: void MainWindow_Loaded(object sender, Rout ...

  3. 九省LNOI2018退役记

    立个flag不会退役. Day 0: 水一发. 大连大学的键盘敲起来就跟敲纸似的. 膜拜要进队的gqh,yxd,sjq. (都进啦2333) (高斯消元,高原反应,分麾下治……) 给我这只弱鸡烧根香. ...

  4. BZOJ 1031 [JSOI2007]字符加密Cipher | 后缀数组模板题

    BZOJ 1031 [JSOI2007]字符加密Cipher | 后缀数组模板题 将字符串复制一遍接在原串后面,然后后缀排序即可. #include <cmath> #include &l ...

  5. Alpha 冲刺 —— 十分之九

    队名 火箭少男100 组长博客 林燊大哥 作业博客 Alpha 冲鸭鸭鸭鸭鸭鸭鸭鸭鸭! 成员冲刺阶段情况 林燊(组长) 过去两天完成了哪些任务 协调各成员之间的工作 多次测试软件运行 学习OPENMP ...

  6. 【linux之文件查看,操作,权限管理】

    一.shell如何处理命令 1.shell会根据在命令中出现的空格字符,将命令划分为多个部分 2.判断第一个字段是内部命令还是外部命令 内部命令:内置于shell的命令(shell builtin) ...

  7. WEB入门.八 背景特效

    学习内容 background属性 CSS Sprite 技术 滑动门技术 能力目标 使用background设置网页背景 使用Sprites制作平滑投票特效 使用滑动门技术实现Tab菜单 本章简介 ...

  8. java 操作命令行

    目的:用java进行调用ab压测 window: cmd ab.exe linux: sh  ab.sh 命令: abs -n 500 -c 10 https://www.baidu.com/ > ...

  9. springcloud的分布式配置Config

    1.为什么要统一配置管理? 微服务由多个服务构成,多个服务多个配置,则对这些配置需要集中管理.不同环境不同配置,运行期间动态调整,自动刷新. 统一管理微服务的配置:分布式配置管理的一些组件: zook ...

  10. 「Vue」vue cli3中axios的基本用法

    1.安装axiosnpm i axios -S2.main.js中设置import axios from 'axios'Vue.prototype.$axios = axiosPS:这里有个小坑,ax ...