Hadoop3.2.1 【 HDFS 】源码分析 : RPC原理 [八] Client端实现&源码

这篇文章主要写 Hadoop RPC Client 的设计 与实现 . 在讲解的时候, 以 ProtobufRpcEngine为实例, 然后分步进行叙述.

一.Client端架构

Client类只有一个入口, 就是call()方法。 代理类会调用Client.call()方法将RPC请求发送到远程服务器, 然后等待远程服务器的响应。 如果远程服务器响应请求时出现异常, 则在call()方法中抛出异常。 

  • 在 call 方法中先将远程调用信息封装成一个 Client.Call 对象(保存了完成标志、返回信息、异常信息等),然后得到 connection 对象用于管理 Client 与 Server 的 Socket 连接。

  • getConnection 方法中通过 setupIOstreams 建立与 Server 的 socket 连接,启动 Connection 线程,监听 socket 读取 server 响应。

  • call() 方法发送 RCP 请求。

  • call() 方法调用 Call.wait() 在 Call 对象上等待 Server 响应信息。

  • Connection 线程收到响应信息设置 Call 对象返回信息字段,并调用 Call.notify() 唤醒 call() 方法线程读取 Call 对象返回值。

扫描二维码关注公众号,回复: 11032019 查看本文章

二.Client端创建流程

下面是创建Client端的代码.协议采用proto, 所以产生的RpcEngine是 ProtobufRpcEngine. 所以接下来的文章是以ProtobufRpcEngine为蓝本进行源码分析.

我只放了Server端, 详细的代码请查看:

 Hadoop3.2.1 【 HDFS 】源码分析 : RPC原理 [六] ProtobufRpcEngine 使用

     public static void main(String[] args) throws Exception {

        //1. 构建配置对象
        Configuration conf = new Configuration();

        //2. 设置协议的RpcEngine为ProtobufRpcEngine .
        RPC.setProtocolEngine(conf, Server.MetaInfoProtocol.class,
                ProtobufRpcEngine.class);


        //3. 拿到代理对象
        Server.MetaInfoProtocol proxy = RPC.getProxy(Server.MetaInfoProtocol.class, 1L,
                new InetSocketAddress("localhost", 7777), conf);

        //4. 构建发送请求对象
        CustomProtos.GetMetaInfoRequestProto obj =  CustomProtos.GetMetaInfoRequestProto.newBuilder().setPath("/meta").build();

        //5. 将请求对象传入, 获取响应信息
        CustomProtos.GetMetaInfoResponseProto metaData = proxy.getMetaInfo(null, obj);

        //6. 输出数据
        System.out.println(metaData.getInfo());

    }    

上面的代码, 主要是分三部分. 

1.构建配置对象&设置RpcEngine引擎

2.获取代理对象

3.设置请求参数&通过代理对象请求.

4.处理结果

第一条和第二条,我就不细说了. 这个很简单,就是使用proto定义一个协议, 绑定到RPC.Builder的实现对象里面.

我们直接看这段, 获取代理对象.

 Server.MetaInfoProtocol proxy = RPC.getProxy(Server.MetaInfoProtocol.class, 1L,
                new InetSocketAddress("localhost", 7777), conf);

也是就是通过RPC.getProxy方法获取协议的代理对象.

   /**
    * Construct a client-side proxy object with the default SocketFactory
    * @param <T>
    * 
    * @param protocol 协议
    * @param clientVersion 客户端的版本
    * @param addr  请求地址
    * @param conf 配置文件
    * @return a proxy instance
    * @throws IOException
    */
   public static <T> T getProxy(Class<T> protocol,
                                 long clientVersion,
                                 InetSocketAddress addr, Configuration conf)
     throws IOException {

     return getProtocolProxy(protocol, clientVersion, addr, conf).getProxy();
   }

接续看,这里面就一句getProtocolProxy,加断点一直跟进

 /**
   * Get a protocol proxy that contains a proxy connection to a remote server
   * and a set of methods that are supported by the server
   *
   * @param protocol protocol
   * @param clientVersion client's version
   * @param addr server address
   * @param ticket security ticket
   * @param conf configuration
   * @param factory socket factory
   * @param rpcTimeout max time for each rpc; 0 means no timeout
   * @param connectionRetryPolicy retry policy
   * @param fallbackToSimpleAuth set to true or false during calls to indicate if
   *   a secure client falls back to simple auth
   * @return the proxy
   * @throws IOException if any error occurs
   */
   public static <T> ProtocolProxy<T> getProtocolProxy(Class<T> protocol,
                                long clientVersion,
                                InetSocketAddress addr,
                                UserGroupInformation ticket,
                                Configuration conf,
                                SocketFactory factory,
                                int rpcTimeout,
                                RetryPolicy connectionRetryPolicy,
                                AtomicBoolean fallbackToSimpleAuth)
       throws IOException {
    if (UserGroupInformation.isSecurityEnabled()) {
      SaslRpcServer.init(conf);
    }
    return getProtocolEngine(protocol, conf).getProxy(protocol, clientVersion,
        addr, ticket, conf, factory, rpcTimeout, connectionRetryPolicy,
        fallbackToSimpleAuth, null);
  }

debug界面是这样的:

核心的是这句 

return getProtocolEngine(protocol, conf).getProxy(protocol, clientVersion,
        addr, ticket, conf, factory, rpcTimeout, connectionRetryPolicy,
        fallbackToSimpleAuth, null);

首先通过 getProtocolEngine 获取RPC Engine [ ProtobufRpcEngine] ,

// return the RpcEngine configured to handle a protocol
  static synchronized RpcEngine getProtocolEngine(Class<?> protocol,
      Configuration conf) {
    //从缓存中获取RpcEngine ,
    // 这个是提前设置的
    // 通过 RPC.setProtocolEngine(conf, MetaInfoProtocol.class,ProtobufRpcEngine.class);

    RpcEngine engine = PROTOCOL_ENGINES.get(protocol);
    if (engine == null) {

      //通过这里 获取RpcEngine的实现类 , 这里我们获取的是 ProtobufRpcEngine.class
      Class<?> impl = conf.getClass(ENGINE_PROP+"."+protocol.getName(),
                                    WritableRpcEngine.class);

      // impl  : org.apache.hadoop.ipc.ProtobufRpcEngine
      engine = (RpcEngine)ReflectionUtils.newInstance(impl, conf);
      PROTOCOL_ENGINES.put(protocol, engine);
    }
    return engine;
  }

然后再调用 ProtobufRpcEngine的 getProxy方法.将协议,客户端的版本号. socket地址, ticket , 配置文件, socket 的创建工厂对象[ StandardSocketFactory ] , PRC 服务的超时时间, connetion的重试策略,以及权限等信息,传入.

@Override
  @SuppressWarnings("unchecked")
  public <T> ProtocolProxy<T> getProxy(Class<T> protocol, long clientVersion,
      InetSocketAddress addr, UserGroupInformation ticket, Configuration conf,
      SocketFactory factory, int rpcTimeout, RetryPolicy connectionRetryPolicy,
      AtomicBoolean fallbackToSimpleAuth, AlignmentContext alignmentContext)
      throws IOException {


    //构造一个实现了InvocationHandler接口的invoker 对象
    // (动态代理机制中的InvocationHandler对象会在invoke()方法中代理所有目标接口上的 调用,
    // 用户可以在invoke()方法中添加代理操作)
    final Invoker invoker = new Invoker(protocol, addr, ticket, conf, factory,
        rpcTimeout, connectionRetryPolicy, fallbackToSimpleAuth,
        alignmentContext);

    //然后调用Proxy.newProxylnstance()获取动态代理对象,并通过ProtocolProxy返回
    return new ProtocolProxy<T>(protocol, (T) Proxy.newProxyInstance(
        protocol.getClassLoader(), new Class[]{protocol}, invoker), false);
  }

在getProxy 这个方法中. 主要是分两步, 

1. 构造一个实现了InvocationHandler接口的invoker 对象 (动态代理机制中的InvocationHandler对象会在invoke()方法中代理所有目标接口上的 调用, 用户可以在invoke()方法中添加代理操作

2.调用Proxy.newProxylnstance()获取动态代理对象,并通过ProtocolProxy返回

我们先看Invoker的创建.

    private Invoker(Class<?> protocol, InetSocketAddress addr,
        UserGroupInformation ticket, Configuration conf, SocketFactory factory,
        int rpcTimeout, RetryPolicy connectionRetryPolicy,
        AtomicBoolean fallbackToSimpleAuth, AlignmentContext alignmentContext)
        throws IOException {
      this(protocol, 
            Client.ConnectionId.getConnectionId(addr, protocol, ticket, rpcTimeout, connectionRetryPolicy, conf), conf, factory);
      this.fallbackToSimpleAuth = fallbackToSimpleAuth;
      this.alignmentContext = alignmentContext;
    }

主要是: 

this(protocol,  Client.ConnectionId.getConnectionId(addr, protocol, ticket, rpcTimeout, connectionRetryPolicy, conf), conf, factory);

我们先看

 Client.ConnectionId.getConnectionId(addr, protocol, ticket, rpcTimeout, connectionRetryPolicy, conf), conf, factory)

这里会调用getConnectionId方法 构建一个Client.ConnectionId对象.

ConnectionId :  这个类 持有 请求地址 和 用户的ticketclient 连接 server 的唯一凭证 :  [remoteAddress, protocol, ticket]

/**
     * Returns a ConnectionId object. 
     * @param addr Remote address for the connection.
     * @param protocol Protocol for RPC.
     * @param ticket UGI
     * @param rpcTimeout timeout
     * @param conf Configuration object
     * @return A ConnectionId instance
     * @throws IOException
     */
    static ConnectionId getConnectionId(InetSocketAddress addr,
        Class<?> protocol, UserGroupInformation ticket, int rpcTimeout,
        RetryPolicy connectionRetryPolicy, Configuration conf) throws IOException {


      //构建重试策略
      if (connectionRetryPolicy == null) {
        //设置最大重试次数 默认值: 10
        final int max = conf.getInt(
            CommonConfigurationKeysPublic.IPC_CLIENT_CONNECT_MAX_RETRIES_KEY,
            CommonConfigurationKeysPublic.IPC_CLIENT_CONNECT_MAX_RETRIES_DEFAULT);

        // 设置重试间隔: 1 秒
        final int retryInterval = conf.getInt(
            CommonConfigurationKeysPublic.IPC_CLIENT_CONNECT_RETRY_INTERVAL_KEY,
            CommonConfigurationKeysPublic
                .IPC_CLIENT_CONNECT_RETRY_INTERVAL_DEFAULT);

        //创建重试策略实例 RetryUpToMaximumCountWithFixedSleep
        //              重试10次, 每次间隔1秒
        connectionRetryPolicy = RetryPolicies.retryUpToMaximumCountWithFixedSleep(
            max, retryInterval, TimeUnit.MILLISECONDS);
      }

      //创建ConnectionId :
      //  这个类 持有 请求地址 和 用户的ticket
      //  client 连接 server 的唯一凭证 :  [remoteAddress, protocol, ticket]

      return new ConnectionId(addr, protocol, ticket, rpcTimeout,
          connectionRetryPolicy, conf);
    }
    

在getConnectionId这个方法里面会干两个事, 创一个重试策略 [RetryUpToMaximumCountWithFixedSleep]. 然后构建一个ConnectionId对象.

ConnectionId(InetSocketAddress address, Class<?> protocol, 
                 UserGroupInformation ticket, int rpcTimeout,
                 RetryPolicy connectionRetryPolicy, Configuration conf) {

      // 协议
      this.protocol = protocol;

      // 请求地址
      this.address = address;

      //用户 ticket
      this.ticket = ticket;

      //设置超时时间
      this.rpcTimeout = rpcTimeout;

      //设置重试策略 默认: 重试10次, 每次间隔1秒
      this.connectionRetryPolicy = connectionRetryPolicy;

      // 单位 10秒
      this.maxIdleTime = conf.getInt(
          CommonConfigurationKeysPublic.IPC_CLIENT_CONNECTION_MAXIDLETIME_KEY,
          CommonConfigurationKeysPublic.IPC_CLIENT_CONNECTION_MAXIDLETIME_DEFAULT);

      // sasl client最大重试次数 5 次
      this.maxRetriesOnSasl = conf.getInt(
          CommonConfigurationKeys.IPC_CLIENT_CONNECT_MAX_RETRIES_ON_SASL_KEY,
          CommonConfigurationKeys.IPC_CLIENT_CONNECT_MAX_RETRIES_ON_SASL_DEFAULT);

      //指示客户端将在套接字超时时进行重试的次数,以建立服务器连接。 默认值: 45
      this.maxRetriesOnSocketTimeouts = conf.getInt(
          CommonConfigurationKeysPublic.IPC_CLIENT_CONNECT_MAX_RETRIES_ON_SOCKET_TIMEOUTS_KEY,
          CommonConfigurationKeysPublic.IPC_CLIENT_CONNECT_MAX_RETRIES_ON_SOCKET_TIMEOUTS_DEFAULT);

      //使用TCP_NODELAY标志绕过Nagle的算法传输延迟。 默认值: true
      this.tcpNoDelay = conf.getBoolean(
          CommonConfigurationKeysPublic.IPC_CLIENT_TCPNODELAY_KEY,
          CommonConfigurationKeysPublic.IPC_CLIENT_TCPNODELAY_DEFAULT);

      // 从客户端启用低延迟连接 默认 false
      this.tcpLowLatency = conf.getBoolean(
          CommonConfigurationKeysPublic.IPC_CLIENT_LOW_LATENCY,
          CommonConfigurationKeysPublic.IPC_CLIENT_LOW_LATENCY_DEFAULT
          );

      // 启用从RPC客户端到服务器的ping操作 默认值: true
      this.doPing = conf.getBoolean(
          CommonConfigurationKeys.IPC_CLIENT_PING_KEY,
          CommonConfigurationKeys.IPC_CLIENT_PING_DEFAULT);

      // 设置ping 操作的间隔, 默认值 : 1分钟
      this.pingInterval = (doPing ? Client.getPingInterval(conf) : 0);
      this.conf = conf;
    }


 

回到之前的调用Invoker 另一个构造方法,但是入参会变.

    /**
     * This constructor takes a connectionId, instead of creating a new one.
     */
    private Invoker(Class<?> protocol, Client.ConnectionId connId,
        Configuration conf, SocketFactory factory) {
      this.remoteId = connId;
      this.client = CLIENTS.getClient(conf, factory, RpcWritable.Buffer.class);
      this.protocolName = RPC.getProtocolName(protocol);
      this.clientProtocolVersion = RPC
          .getProtocolVersion(protocol);
    }

这个是Invoker真正的构建方法,这里面会将刚刚构建好的ConnectionId 赋值给remoteId 字段.

并且创建一个Client 对象.

// 获取/创建  客户端
this.client = CLIENTS.getClient(conf, factory, RpcWritable.Buffer.class);

我们看下getClient 方法. 这里面会先尝试从缓存中获取client对象, 如果没有的话,在自己创建一个,并且加到缓存中.

为什么会放到缓存中呢?? 

当client和server再次通讯的时候,可以复用这个client . 

/**
   * 如果没有缓存的client存在的话
   * 根据用户提供的SocketFactory 构造 或者 缓存一个IPC 客户端
   *
   * Construct & cache an IPC client with the user-provided SocketFactory 
   * if no cached client exists.
   * 
   * @param conf Configuration
   * @param factory SocketFactory for client socket
   * @param valueClass Class of the expected response
   * @return an IPC client
   */
  public synchronized Client getClient(Configuration conf,
      SocketFactory factory, Class<? extends Writable> valueClass) {
    // Construct & cache client.
    //
    // The configuration is only used for timeout,
    // and Clients have connection pools.  So we can either
    // (a) lose some connection pooling and leak sockets, or
    // (b) use the same timeout for all configurations.
    //
    // Since the IPC is usually intended globally, notper-job, we choose (a).

    //从缓存中获取Client
    Client client = clients.get(factory);

    if (client == null) {
      //client在缓存中不存在, 创建一个.
      client = new Client(valueClass, conf, factory);
      //缓存创建的client
      clients.put(factory, client);
    } else {
      //client的引用计数+1
      client.incCount();
    }


    if (Client.LOG.isDebugEnabled()) {
      Client.LOG.debug("getting client out of cache: " + client);
    }
    // 返回client
    return client;
  }

到这里 Invoker 对象就创建完了. 

回到 ProtobufRpcEngine 的getProxy 方法 .

//然后调用Proxy.newProxylnstance()获取动态代理对象,并通过ProtocolProxy返回
    return new ProtocolProxy<T>(protocol, (T) Proxy.newProxyInstance(
        protocol.getClassLoader(), new Class[]{protocol}, invoker), false);

构建一个ProtocolProxy 对象返回

  /**
   * Constructor
   * 
   * @param protocol protocol class
   * @param proxy its proxy
   * @param supportServerMethodCheck If false proxy will never fetch server
   *        methods and isMethodSupported will always return true. If true,
   *        server methods will be fetched for the first call to 
   *        isMethodSupported. 
   */
  public ProtocolProxy(Class<T> protocol, T proxy,
      boolean supportServerMethodCheck) {
    this.protocol = protocol;
    this.proxy = proxy;
    this.supportServerMethodCheck = supportServerMethodCheck;
  }

然后我们看Client端的第四步 , 根据proto协议,构建一个请求对象.

这个没啥可说的.是proto自动生成的,我们只是创建了一下而已.

//4. 构建发送请求对象
 CustomProtos.GetMetaInfoRequestProto obj =  CustomProtos.GetMetaInfoRequestProto.newBuilder().setPath("/meta").build();

然后就是Client端的第5步了将请求对象传入, 获取响应信息

//5. 将请求对象传入, 获取响应信息
CustomProtos.GetMetaInfoResponseProto metaData = proxy.getMetaInfo(null, obj);

Client端的最后一步输出响应信息.

//6. 输出数据
System.out.println(metaData.getInfo());

------------------华丽的分割线-------------------------------------------------------------------

咦,到这里有点懵, 请求server端的代码呢??? 请求怎么发出去的??? 怎么拿到响应信息的呢????

嗯嗯,是动态代理. ProtobufRpcEngine 的getProxy 方法 .

return new ProtocolProxy<T>(protocol, (T) Proxy.newProxyInstance(
        protocol.getClassLoader(), new Class[]{protocol}, invoker), false);

主要是这个

(T) Proxy.newProxyInstance(
        protocol.getClassLoader(), new Class[]{protocol}, invoker)

public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h) {

  ........................

}

这个方法的作用就是创建一个代理类对象,

它接收三个参数,我们来看下几个参数的含义:

loader :       一个classloader对象,定义了由哪个classloader对象对生成的代理类进行加载


interfaces: 一个interface对象数组,表示我们将要给我们的代理对象提供一组什么样的接口,如果我们提供了这样一个接口对象数组,那么也就是声明了代理类实现了这些接口,代理类就可以调用接口中声明的所有方法。


h:                一个InvocationHandler对象,表示的是当动态代理对象调用方法的时候会关联到哪一个InvocationHandler对象上,并最终由其调用。

我们直接看 ProtobufRpcEngine#Invoker中的 invoke方法

/**
     *
     * ProtobufRpcEngine.Invoker.invoker() 方法主要做了三件事情:
     *  1.构造请求头域,
     *    使用protobuf将请求头序列化,这个请求头域 记录了当前RPC调用是什么接口的什么方法上的调用;
     *  2.通过RPC.Client类发送请求头以 及序列化好的请求参数。
     *    请求参数是在ClientNamenodeProtocolPB调用时就已经序列化好的,
     *    调用Client.call()方法时,
     *    需要将请求头以及请求参数使用一个RpcRequestWrapper对象封装;
     *  3.获取响应信息,序列化响应信息并返回。
     *
     *
     * This is the client side invoker of RPC method. It only throws
     * ServiceException, since the invocation proxy expects only
     * ServiceException to be thrown by the method in case protobuf service.
     * 
     * ServiceException has the following causes:
     * <ol>
     * <li>Exceptions encountered on the client side in this method are 
     * set as cause in ServiceException as is.</li>
     * <li>Exceptions from the server are wrapped in RemoteException and are
     * set as cause in ServiceException</li>
     * </ol>
     * 
     * Note that the client calling protobuf RPC methods, must handle
     * ServiceException by getting the cause from the ServiceException. If the
     * cause is RemoteException, then unwrap it to get the exception thrown by
     * the server.
     */
    @Override
    public Message invoke(Object proxy, final Method method, Object[] args)
        throws ServiceException {
      long startTime = 0;
      if (LOG.isDebugEnabled()) {
        startTime = Time.now();
      }

      // pb接口的参数只有两个,即RpcController + Message
      if (args.length != 2) { // RpcController + Message
        throw new ServiceException(
            "Too many or few parameters for request. Method: ["
            + method.getName() + "]" + ", Expected: 2, Actual: "
            + args.length);
      }
      if (args[1] == null) {
        throw new ServiceException("null param while calling Method: ["
            + method.getName() + "]");
      }

      // if Tracing is on then start a new span for this rpc.
      // guard it in the if statement to make sure there isn't
      // any extra string manipulation.
      // todo 这个是啥
      Tracer tracer = Tracer.curThreadTracer();
      TraceScope traceScope = null;
      if (tracer != null) {
        traceScope = tracer.newScope(RpcClientUtil.methodToTraceString(method));
      }

      //构造请求头域,标明在什么接口上调用什么方法
      RequestHeaderProto rpcRequestHeader = constructRpcRequestHeader(method);
      
      if (LOG.isTraceEnabled()) {
        LOG.trace(Thread.currentThread().getId() + ": Call -> " +
            remoteId + ": " + method.getName() +
            " {" + TextFormat.shortDebugString((Message) args[1]) + "}");
      }


      //获取请求调用的参数,例如RenameRequestProto
      final Message theRequest = (Message) args[1];
      final RpcWritable.Buffer val;
      try {

        //调用RPC.Client发送请求
        val = (RpcWritable.Buffer) client.call(RPC.RpcKind.RPC_PROTOCOL_BUFFER,
            new RpcProtobufRequest(rpcRequestHeader, theRequest), remoteId,
            fallbackToSimpleAuth, alignmentContext);

      } catch (Throwable e) {
        if (LOG.isTraceEnabled()) {
          LOG.trace(Thread.currentThread().getId() + ": Exception <- " +
              remoteId + ": " + method.getName() +
                " {" + e + "}");
        }
        if (traceScope != null) {
          traceScope.addTimelineAnnotation("Call got exception: " +
              e.toString());
        }
        throw new ServiceException(e);
      } finally {
        if (traceScope != null) traceScope.close();
      }

      if (LOG.isDebugEnabled()) {
        long callTime = Time.now() - startTime;
        LOG.debug("Call: " + method.getName() + " took " + callTime + "ms");
      }
      
      if (Client.isAsynchronousMode()) {
        final AsyncGet<RpcWritable.Buffer, IOException> arr
            = Client.getAsyncRpcResponse();
        final AsyncGet<Message, Exception> asyncGet
            = new AsyncGet<Message, Exception>() {
          @Override
          public Message get(long timeout, TimeUnit unit) throws Exception {
            return getReturnMessage(method, arr.get(timeout, unit));
          }

          @Override
          public boolean isDone() {
            return arr.isDone();
          }
        };
        ASYNC_RETURN_MESSAGE.set(asyncGet);
        return null;
      } else {
        return getReturnMessage(method, val);
      }
    }

艾玛,这个有点长啊.

挑几点重要的说.

  1. 构造请求头域,标明在什么接口上调用什么方法
  2. 获取请求调用的参数
  3. 调用RPC.Client发送请求
  4. 获取响应信息

下面分别来说:

1.构造请求头域,标明在什么接口上调用什么方法

//构造请求头域,标明在什么接口上调用什么方法
RequestHeaderProto rpcRequestHeader = constructRpcRequestHeader(method);
      

2.获取请求调用的参数

      //获取请求调用的参数,这个是才client端代码就创建好的,通过参数传进来的.
      // 例如 GetMetaInfoRequestProto
      final Message theRequest = (Message) args[1];

3.调用RPC.Client发送请求

         //调用RPC.Client发送请求
        val = (RpcWritable.Buffer) client.call(RPC.RpcKind.RPC_PROTOCOL_BUFFER,
            new RpcProtobufRequest(rpcRequestHeader, theRequest), remoteId,
            fallbackToSimpleAuth, alignmentContext);

4. 获取响应信息

Client 获取响应信息, 不过是同步还是异步获取响应信息,都会调用这个方法:  getReturnMessage(method, val);

getReturnMessage(method, val);


 

困死了,先发出来, invoker  后面的我再补充. .........

发布了336 篇原创文章 · 获赞 846 · 访问量 41万+

猜你喜欢

转载自blog.csdn.net/zhanglong_4444/article/details/105669375
今日推荐