Bootstrap

Spark内置的RPC框架

在Spark中很多地方都涉及网络通信,比如各个组件间的消息互通、用户文件与Jar包的上传、节点间的Shuffle过程、Block数据的复制与备份等。在Spark 0.x.x与Spark 1.6.0前的版本中,组件间的消息通信主要借助于Akka,使用Akka可以轻松地构建强有力的高并发与分布式应用。虽然Akka作为一款优秀的分布式通信框架,但在Spark 2.0.0 版本中被移除了,Spark官网文档对此的描述为:“Akka的依赖被移除了,因此用户可以使用任何版本的Akka来编程了。” 在Spark 1.x.x 版本中,用户文件与Jar包的上传采用了由Jetty实现的HttpFileServer,但在Spark 2.0.0 版本中它也被废弃了,现在使用的是基于Spark内置RPC 框架的NettyStreamManager。节点间的Shuffle过程和Block数据的复制与备份在Spark 2.0.0 版本中依然没用了Netty,通过对接口和程序进行重新设计,将各个组件间的消息互通、用户文件与Jar包的上传等内容统一纳入Spark的RPC框架体系中。

Spark 内置 RPC 框架的基本架构:

TransportContext 内部包含传输上下文的配置信息 TransportConf 和对客户端请求消息进行处理的 RpcHandler。TransportConf 在创建TransportClientFactory 和 TransportServer 时都是必需的,而RpcHandler 只用于创建 TransportServer。TransportClientFactory 是 RPC客户端的工厂类,TransportServer 是RPC服务端的实现。图中记号的含义如下:

  • 记号①表示通过调用TransportContext的createClientFactory方法创建客户端工厂TransportClientFactory的实例。在构造TransportClientFactory的实例时,还会传递客户端引导程序TransportClientBootstrap的列表。
/**
   * Initializes a ClientFactory which runs the given TransportClientBootstraps prior to returning
   * a new Client. Bootstraps will be executed synchronously, and must run successfully in order
   * to create a Client.
   */
  public TransportClientFactory createClientFactory(List<TransportClientBootstrap> bootstraps) {
    return new TransportClientFactory(this, bootstraps);
  }
  • 此外,TransportClientFactory内部还存在针对每个Socket地址的连接池ClientPool。
private final ConcurrentHashMap<SocketAddress, ClientPool> connectionPool;

/** A simple data structure to track the pool of clients between two peer nodes. */
private static class ClientPool {
  TransportClient[] clients;
  Object[] locks;

  ClientPool(int size) {
    clients = new TransportClient[size];
    locks = new Object[size];
    for (int i = 0; i < size; i++) {
      locks[i] = new Object();
    }
  }
}

由此可见,ClientPool实际是由TransportClient的数组构成的,而locks数组中的Object与clients数组中的TransportClient按照数组索引一一对应,通过对每个TransportClient分别采用不同的锁,降低并发情况下线程间对锁的争用,进而减少阻塞,提高并发度。

  • 记号②表示通过调用TransportContext的createServer方法创建传输服务端TransportServer的实例。在构造TransportServer的实例时,需要传递TransportContext、host、port、RpcHandler及服务端引导程序TransportServerBootstrap的列表。
/** Create a server which will attempt to bind to a specific host and port. */
  public TransportServer createServer(
      String host, int port, List<TransportServerBootstrap> bootstraps) {
    return new TransportServer(this, host, port, rpcHandler, bootstraps);
  } //this为TransportContext对象本身

下面初步介绍Spark内置RPC框架的组件:

  • TransportContext:传输上下文,包含了用于创建传输服务端(TransportServer)和传输客户端工厂(TransportClientFactory)的上下文信息,并支持使用TransportChannelHandler设置Netty提供的SocketChannel的PipeLine的实现。
  • TransportConf:传输上下文的配置信息。
  • RpcHandler:对调用传输客户端(TransportClient)的sendRPC方法发送的消息进行处理的程序。
  • MessageEncoder:在将消息放入管道前,先对消息内容进行编码,防止管道另一端读取时丢包和解析错误。
  • MessageDecoder:对从管道中读取的ByteBuf进行解析,防止丢包和解析错误。
  • TransportFrameDecoder:对从管道中读取的ByteBuf按照数据帧进行解析。
  • RpcResponseCallback:RpcHandler对请求的消息处理完毕后进行回调的接口。
  • TransportClientFactory:创建TransportClient的传输客户端工厂类。
  • ClientPool:在两个对等节点间维护的关于TransportClient的池子。ClientPool是TransportClientFactory的内部组件。
  • TransportClient:RPC框架的客户端,用于获取预先协商好的流中的连续块。TransportClient旨在允许有效传输大量数据,这些数据将被拆分成几百KB到几MB的块。当TransportClient处理从流中获取的块时,实际的设置是在传输层之外完成的。sendRPC方法能够在客户端和服务端的同一水平的通信进行这些设置。
  • TransportClientBootstrap:当服务端响应客户端连接时在客户端执行一次的引导程序。
  • TransportServerBootstrap:当客户端连接到服务端时服务端执行一次的引导程序。
  • TransportRequestHandler:用于处理客户端的请求并在写完块数据后返回的处理程序。
  • TransportResponseHandler:用于处理服务端的响应,并且对发出请求的客户端进行响应的处理程序。
  • TransportChannelHandler:代理由TransportRequestHandler处理的请求和由TransportResponseHandler处理的响应,并加入传输层的处理。
  • TransportServer:RPC框架的服务端,提供高效的、低级别的流服务。

1 TransportConf

Spark的RPC框架由组件TransportConf提供配置信息,它由配置提供者conf、模块名称module两个属性组成:

private final ConfigProvider conf;//配置提供者
private final String module;//配置的模块名称

其中conf是真正的配置提供者,其类型ConfigProvider是一个抽象类,代码如下:

/**
 * Provides a mechanism for constructing a {@link TransportConf} using some sort of configuration.
 */
public abstract class ConfigProvider {
  /** Obtains the value of the given config, throws NoSuchElementException if it doesn't exist. */
  public abstract String get(String name);

  public String get(String name, String defaultValue) {
    try {
      return get(name);
    } catch (NoSuchElementException e) {
      return defaultValue;
    }
  }

  public int getInt(String name, int defaultValue) {
    return Integer.parseInt(get(name, Integer.toString(defaultValue)));
  }

  public long getLong(String name, long defaultValue) {
    return Long.parseLong(get(name, Long.toString(defaultValue)));
  }

  public double getDouble(String name, double defaultValue) {
    return Double.parseDouble(get(name, Double.toString(defaultValue)));
  }

  public boolean getBoolean(String name, boolean defaultValue) {
    return Boolean.parseBoolean(get(name, Boolean.toString(defaultValue)));
  }
}

ConfigProvider提供了包括get、getInt、getLong、getDouble、getBoolean等方法,这些方法都是基于抽象方法get获取值,经过一次类型转换而实现。这个抽象的get方法需要子类去实现。

object SparkTransportConf {
  private val MAX_DEFAULT_NETTY_THREADS = 8
  def fromSparkConf(_conf: SparkConf, module: String, numUsableCores: Int = 0): TransportConf = {
    val conf = _conf.clone
    val numThreads = defaultNumThreads(numUsableCores)
    conf.setIfMissing(s"spark.$module.io.serverThreads", numThreads.toString)
    conf.setIfMissing(s"spark.$module.io.clientThreads", numThreads.toString)
    new TransportConf(module, new ConfigProvider {
      override def get(name: String): String = conf.get(name)//实际是代理了SparkConf的get方法
    })
  }
  private def defaultNumThreads(numUsableCores: Int): Int = {
    val availableCores =
      if (numUsableCores > 0) numUsableCores else Runtime.getRuntime.availableProcessors()
    math.min(availableCores, MAX_DEFAULT_NETTY_THREADS)
  }
}

使用SparkTransportConf的fromSparkConf方法来构造TransportConf。传递的三个参数分别为SparkConf、模块名module及可用的内核数numUsableCores。如果numUsableCores小于等于0,那么线程数是系统可用处理器的数量,不过系统的内核数不可能全部用于网络传输,所以这里将分配给网络传输的内核数量最多限制在8个。最终确定的线程数将用于设置客户端传输线程数(spark.$module.io.clientTreads属性)和服务端传输线程数(spark.$module.io.serverThreads属性)。from-SparkConf最终构造TransportConf对象时传递的ConfigProvider为实现get方法的匿名内部类,get的实现实际是代理了SparkConf的get方法

2 客户端工厂TransportClientFactory

TransportClientFactory是创建TransportClient的工厂类。在TransportContext中,createClientFactory方法可以创建TransportClientFactory实例:

/**
 * Initializes a ClientFactory which runs the given TransportClientBootstraps prior to returning
 * a new Client. Bootstraps will be executed synchronously, and must run successfully in order
 * to create a Client.
 */
public TransportClientFactory createClientFactory(List<TransportClientBootstrap> bootstraps) {
  return new TransportClientFactory(this, bootstraps);
}

public TransportClientFactory createClientFactory() {
  return createClientFactory(Lists.<TransportClientBootstrap>newArrayList());//方法重载
}

TransportContext中有两个重载的createClientFactory方法,它们最终在构造TransportClientFactory时都会传递两个参数:TransportContext和TransportClientBootstrap列表,代码如下:

public TransportClientFactory(
    TransportContext context,
    List<TransportClientBootstrap> clientBootstraps) {
  this.context = Preconditions.checkNotNull(context);
  this.conf = context.getConf();
  this.clientBootstraps = Lists.newArrayList(Preconditions.checkNotNull(clientBootstraps));
  this.connectionPool = new ConcurrentHashMap<>();
  this.numConnectionsPerPeer = conf.numConnectionsPerPeer();
  this.rand = new Random();

  IOMode ioMode = IOMode.valueOf(conf.ioMode());
  this.socketChannelClass = NettyUtils.getClientChannelClass(ioMode);
  // TODO: Make thread pool name configurable.
  this.workerGroup = NettyUtils.createEventLoop(ioMode, conf.clientThreads(), "shuffle-client");
  this.pooledAllocator = NettyUtils.createPooledByteBufAllocator(
    conf.preferDirectBufs(), false /* allowCache */, conf.clientThreads());
}

相关变量如下:

  • context:参数传递的TransportContext的引用
  • conf:指的是TransportConf,通过调用TransportContext的getConf获取。
  • clientBootstraps:参数传递的TransportClientBootstrap列表。
  • connectionPool:针对每个Socket地址的连接池ClientPool的缓存。connectionPool的数据结构如下图:

  • numbConnectionsPerPeer:从TransportConf获取的key为“spark.+模块名+.io.numConnectionsPerPeer”的属性值。此属性值用于指定对等节点间的连接数。这里的模块名实际为TransportConf的module字段。Spark的很多组件都利用RPC框架构建,它们之间按照模块名区分,例如RPC模块的key为“spark.rpc.io.numConnectionsPerPeer”。
  • rand:对Socket地址对应的连接池ClientPool中缓存的TransportClient进行随机选择,对每个连接做负载均衡。
  • ioMode:IO模式,即从TransportConf获取key为“spark.+模块名+io.mode”的属性值。默认值为NIO,Spark还支持EPOLL。
  • socketChannelClass:客户端Channel被创建时使用的类,通过ioMode来匹配,默认为NioSocketChannel,Spark还支持EpollEventLoopGroup。
  • workerGroup:根据Netty的规范,客户端只有worker组,所以此处创建workerGroup。workerGroup的实际类型是NioEventLoopGroup。
  • pooledAllocator:汇集ByteBuf但对本地线程缓存禁用的分配器。

2.1 客户端引导程序TransportClientBootstrap

TransportClientFactory的clientBootstraps属性是TransportClientBootstrap的列表。TransportClientBootstrap是在TranportClient上执行的客户端引导程序,主要对连接建立时进行一些初始化的准备(例如验证、加密)。TransportClientBootstrap所做的操作往往是昂贵的,好在建立的连接可以重用。

public interface TransportClientBootstrap {
  void doBootstrap(TransportClient client, Channel channel) throws RuntimeException;
}

2.2 创建RPC客户端TransportClient

有了TransportClientFactory,Spark的各个模块就可以使用它创建RPC客户端TransportClient。每个TransportClient实例只能和一个无端的RPC服务通信,所以Spark中的组件如果想要和多个RPC服务通信,就需要持有多个TransportClient实例。创建TransportClient的方法如下(实际为从缓存中获取TransportClient):

public TransportClient createClient(String remoteHost, int remotePort) throws IOException {
  //创建InetSocketAddress
  final InetSocketAddress unresolvedAddress =
    InetSocketAddress.createUnresolved(remoteHost, remotePort);

  // Create the ClientPool if we don't have it yet.
  ClientPool clientPool = connectionPool.get(unresolvedAddress);
  if (clientPool == null) {
    connectionPool.putIfAbsent(unresolvedAddress, new ClientPool(numConnectionsPerPeer));
    clientPool = connectionPool.get(unresolvedAddress);
  }
  //随机选择一个TranportClient
  int clientIndex = rand.nextInt(numConnectionsPerPeer);
  TransportClient cachedClient = clientPool.clients[clientIndex];//从缓存中获取

  if (cachedClient != null && cachedClient.isActive()) {//获取并返回激活的TransportClient
    TransportChannelHandler handler = cachedClient.getChannel().pipeline()
      .get(TransportChannelHandler.class);
    synchronized (handler) {
      handler.getResponseHandler().updateTimeOfLastRequest();
    }

    if (cachedClient.isActive()) {
      logger.trace("Returning cached connection to {}: {}",
        cachedClient.getSocketAddress(), cachedClient);
      return cachedClient;
    }
  }

  final long preResolveHost = System.nanoTime();
  final InetSocketAddress resolvedAddress = new InetSocketAddress(remoteHost, remotePort);
  final long hostResolveTimeMs = (System.nanoTime() - preResolveHost) / 1000000;
  if (hostResolveTimeMs > 2000) {
    logger.warn("DNS resolution for {} took {} ms", resolvedAddress, hostResolveTimeMs);
  } else {
    logger.trace("DNS resolution for {} took {} ms", resolvedAddress, hostResolveTimeMs);
  }
  //创建并返回TranportClient对象
  synchronized (clientPool.locks[clientIndex]) {
    cachedClient = clientPool.clients[clientIndex];

    if (cachedClient != null) {
      if (cachedClient.isActive()) {
        logger.trace("Returning cached connection to {}: {}", resolvedAddress, cachedClient);
        return cachedClient;
      } else {
        logger.info("Found inactive connection to {}, creating a new one.", resolvedAddress);
      }
    }
    clientPool.clients[clientIndex] = createClient(resolvedAddress);
    return clientPool.clients[clientIndex];
  }
}

从上述代码中得知TransportClient创建步骤如下:

  • 1)调用InetSocketAddress的静态方法createUnresolved构建InetSocketAddress(这种方式创建InetSocketAddress,可以在缓存中已经有TransportClient时避免不必要的域名解析),然后从connectionPool中获取与此地址对应的ClientPool,如果没有则需要新建ClientPool,并放入缓存connectionPool中。
  • 2)根据numConnectionsPerPeer的大小(使用“spark.+模块名+.io.numConnectionsPerPeer”属性配置),从ClientPool中随机选择一个TransportClient
  • 3)如果ClientPool的clients数组中在随机产生的索引位置不存在TransportClient或者TransportClient没有激活,则进入第5步,否则对此TransportClient进行第4步的检查。
  • 4)更新TransportClient的channel中配置的TransportChannelHandler的最后一次使用时间,确保channel没有超时,然后检查TransportClient是否是激活状态,最后返回此TransportClient给调用方。
  • 5)由于缓存中没有TransportClient可用,于是调用InetSocketAddress的构造器创建InetSocketAddress对象(直接使用InetSocketAddress的构造器创建InetSocketAddress会进行域解析),在这一步骤多个线程可能会产生竞态条件(由于没有同步处理,所以多个线程极有可能同时执行到此处,都发现缓存中没有TransportClient可用,于是都使用InetSocketAddress的构造器创建InetSocketAddress)
  • 6)第5步创建InetSocketAddress的过程中产生的竞态条件如果不妥善处理,会产生线程安全问题,所以到了ClientPool的locks数组发挥作用的时候了。按照随机产生的数组索引,locks数组中的锁对象可以对clients数组中的TransportClient一对一进行同步。即使之前产生了竞态条件,但是这一步只能有一个线程进入临界区。在临界区内,先进入的线程调用重载的createClient方法创建TransportClient对象并放入ClientPool的clients数组中。当率先进入临界区的线程退出临界区后,其它线程才能进入,此时发现ClientPool的clients数组中已经存在了TransportClient对象,那么将不再创建TransportClient,而是直接使用它。

上述代码整个执行过程实际解决了TransportClient缓存的使用及createClient方法的线程安全问题,并没有涉及创建TransportClient的实现。TransportClient的创建过程在重载的createClient方法中实现:

private TransportClient createClient(InetSocketAddress address) throws IOException {
  logger.debug("Creating new connection to {}", address);
  //构建根引导程序Bootstrap并对其进行配置
  Bootstrap bootstrap = new Bootstrap();
  bootstrap.group(workerGroup)
    .channel(socketChannelClass)
    .option(ChannelOption.TCP_NODELAY, true)
    .option(ChannelOption.SO_KEEPALIVE, true)
    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, conf.connectionTimeoutMs())
    .option(ChannelOption.ALLOCATOR, pooledAllocator);

  final AtomicReference<TransportClient> clientRef = new AtomicReference<>();
  final AtomicReference<Channel> channelRef = new AtomicReference<>();
  //为根引导程序设置管道初始化回调函数
  bootstrap.handler(new ChannelInitializer<SocketChannel>() {
    @Override
    public void initChannel(SocketChannel ch) {
      TransportChannelHandler clientHandler = context.initializePipeline(ch);
      clientRef.set(clientHandler.getClient());
      channelRef.set(ch);
    }
  });

  // Connect to the remote server
  long preConnect = System.nanoTime();
  ChannelFuture cf = bootstrap.connect(address);
  if (!cf.awaitUninterruptibly(conf.connectionTimeoutMs())) {
    throw new IOException(
      String.format("Connecting to %s timed out (%s ms)", address, conf.connectionTimeoutMs()));
  } else if (cf.cause() != null) {
    throw new IOException(String.format("Failed to connect to %s", address), cf.cause());
  }

  TransportClient client = clientRef.get();
  Channel channel = channelRef.get();
  assert client != null : "Channel future completed successfully with null client";

  long preBootstrap = System.nanoTime();
  logger.debug("Connection to {} successful, running bootstraps...", address);
  try {
    for (TransportClientBootstrap clientBootstrap : clientBootstraps) {
      clientBootstrap.doBootstrap(client, channel);//给TransportClient设置客户端引导程序
    }
  } catch (Exception e) { // catch non-RuntimeExceptions too as bootstrap may be written in Scala
    long bootstrapTimeMs = (System.nanoTime() - preBootstrap) / 1000000;
    logger.error("Exception while bootstrapping client after " + bootstrapTimeMs + " ms", e);
    client.close();
    throw Throwables.propagate(e);
  }
  long postBootstrap = System.nanoTime();

  logger.info("Successfully created connection to {} after {} ms ({} ms spent in bootstraps)",
    address, (postBootstrap - preConnect) / 1000000, (postBootstrap - preBootstrap) / 1000000);

  return client;
}

根据上述代码得知创建TransportClient步骤如下:

  • 1)构建根引导程序Bootstrap并对其进行配置
  • 2)为根引导程序设置管道初始化回调函数,此回调函数将调用TransportContext的initializePipeLine方法初始化Channel的pipeline。
  • 3)使用根引导程序连接远程服务器,当连接成功对管道初始化时会回调初始化回调函数,将TransportClient和Channel对象分别设置到原子引用clientRef与channelRef中
  • 4)给TransportClient设置客户端引导程序,即设置TransportClientFactory中的TransportClientBootstrap列表
  • 5)返回此TransportClient对象

3 RPC服务端TransportServer

TransportServer是RPC框架的服务端,可提供高效、低级别的流服务,TransportContext的createServer方法用于创建TransportServer,其实现如下:

public TransportServer createServer(int port, List<TransportServerBootstrap> bootstraps) {
  return new TransportServer(this, null, port, rpcHandler, bootstraps);
}

TransportContext中有4个名为createServer的重载方法,但是它们最终调用了TransportServer的构造器来创建TransportServer实现。

public TransportServer(
    TransportContext context,
    String hostToBind,
    int portToBind,
    RpcHandler appRpcHandler,
    List<TransportServerBootstrap> bootstraps) {
  this.context = context;
  this.conf = context.getConf();
  this.appRpcHandler = appRpcHandler;
  this.bootstraps = Lists.newArrayList(Preconditions.checkNotNull(bootstraps));

  try {
    init(hostToBind, portToBind);
  } catch (RuntimeException e) {
    JavaUtils.closeQuietly(this);
    throw e;
  }
}

TransportServer的构造器中的各个变量如下:

  • context:参数传递的TransportContext的引用
  • conf:指TransportConf,这里通过调用TransportContext的getConf获取。
  • appRpcHandler:RPC请求处理器RpcHandler。
  • bootstraps:参数传递的TransportServerbootstrap列表

TransportServer的构造器中调用 了init方法,init方法用于对TransportServer进行初始化,代码如下:

private void init(String hostToBind, int portToBind) {
  //根据Netty的API文档,Netty服务端需同时创建bossGroup和workerGroup
  IOMode ioMode = IOMode.valueOf(conf.ioMode());
  EventLoopGroup bossGroup =
    NettyUtils.createEventLoop(ioMode, conf.serverThreads(), "shuffle-server");
  EventLoopGroup workerGroup = bossGroup;
  //创建一个汇集ByteBuf但对本地线程缓存禁用的分配器
  PooledByteBufAllocator allocator = NettyUtils.createPooledByteBufAllocator(
    conf.preferDirectBufs(), true /* allowCache */, conf.serverThreads());
  //创建Netty的服务端根引导程序并对其进行配置
  bootstrap = new ServerBootstrap()
    .group(bossGroup, workerGroup)
    .channel(NettyUtils.getServerChannelClass(ioMode))
    .option(ChannelOption.ALLOCATOR, allocator)
    .childOption(ChannelOption.ALLOCATOR, allocator);

  if (conf.backLog() > 0) {
    bootstrap.option(ChannelOption.SO_BACKLOG, conf.backLog());
  }
  if (conf.receiveBuf() > 0) {
    bootstrap.childOption(ChannelOption.SO_RCVBUF, conf.receiveBuf());
  }
  if (conf.sendBuf() > 0) {
    bootstrap.childOption(ChannelOption.SO_SNDBUF, conf.sendBuf());
  }
  //为根引导程序设置管道初始化回调函数
  bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
      RpcHandler rpcHandler = appRpcHandler;
      for (TransportServerBootstrap bootstrap : bootstraps) {
        rpcHandler = bootstrap.doBootstrap(ch, rpcHandler);
      }
      context.initializePipeline(ch, rpcHandler);
    }
  });
  //给根引导程序绑定Socket的监听端口
  InetSocketAddress address = hostToBind == null ?
      new InetSocketAddress(portToBind): new InetSocketAddress(hostToBind, portToBind);
  channelFuture = bootstrap.bind(address);
  channelFuture.syncUninterruptibly();
  port = ((InetSocketAddress) channelFuture.channel().localAddress()).getPort();
  logger.debug("Shuffle server started on port: {}", port);
}

根据上述代码可知TransportServer初始化的步骤如下:

  • 1)创建bossGroup和workerGroup
  • 2)创建一个汇集ByteBuf但对本地线程缓存禁用的分配器
  • 3)调用Netty的API创建Netty的服务端根引导程序并对其进行配置
  • 4)为根引导程序设置管道初始化回调函数,此回调函数首先设置TransportServerBootstrap到根引导程序中,然后调用TransportContext的initializePipeline方法初始化Channel的pipeline。
  • 5)给根引导程序绑定Socket的监听端口,最后返回监听的端口。

4 管道初始化

在创建TransportClient和对TranportServer初始化的实现中,都在管道初始化回调函数中调用了TranportContext的initializePipeline方法,initializePipeline方法将调用Netty的API对管道初始化。

public TransportChannelHandler initializePipeline(
    SocketChannel channel,
    RpcHandler channelRpcHandler) {
  try {
    TransportChannelHandler channelHandler = createChannelHandler(channel, channelRpcHandler);
    channel.pipeline()
      .addLast("encoder", encoder)
      .addLast(TransportFrameDecoder.HANDLER_NAME, NettyUtils.createFrameDecoder())
      .addLast("decoder", decoder)
      .addLast("idleStateHandler", new IdleStateHandler(0, 0, conf.connectionTimeoutMs() / 1000))
      .addLast("handler", channelHandler);
    return channelHandler;
  } catch (RuntimeException e) {
    logger.error("Error while initializing Netty pipeline", e);
    throw e;
  }
}

根据上述代码,可知initialPipeline方法的执行步骤如下:

  • 1)调用createChannelHandler方法创建TranportChannelHandler,从createChannelHandler的实现中可以看到,真正创建TransportClient是在这里发生的。通过TranportClient的构造过程看到RpcHandler与TransportClient毫无关系,TransportClient只使用了TransportResponseHandler。TransportChannelHandler在服务端将代理TransportRequestHandler对请求消息进行处理,并在客户端代理TransportResponseHandler对响应消息进行处理。
private TransportChannelHandler createChannelHandler(Channel channel, RpcHandler rpcHandler) {
  TransportResponseHandler responseHandler = new TransportResponseHandler(channel);
  TransportClient client = new TransportClient(channel, responseHandler);
  TransportRequestHandler requestHandler = new TransportRequestHandler(channel, client,
    rpcHandler);
  return new TransportChannelHandler(client, responseHandler, requestHandler,
    conf.connectionTimeoutMs(), closeIdleConnections);
}
  • 2)对管道进行设置,这里的ENCODER(即MessageEncoder)派生自Netty的ChannelOutboundHandler接口;DECODER(即MessageDecoder)、TransportChannelHandler及TransportFrameDecoder(由工具类NettyUtils的静态方法createFrameDecoder创建)派生自Netty的ChannelInboundHandler接口;IdleStateHandler同时实现了ChannelOutBoundHandler和ChannelInboundHandler接口。根据Netty的API行为,通过addLast方法注册多个Handler时,ChannelInboundHandler按照注册的先后顺序执行,ChannelOutboundHandler按照注册的先后顺序逆序执行,因此在管道两端(无论是服务端还是客户端)处理请求和响应的流程如下:

      

5 TransportChannelHandler详解

TransportChannelHandler实现了Netty的ChannelInboundHandler,以便对Netty管道中的消息进行处理。上图中的Handler(除了MessageEncoder)由于都实现了ChannelInboundHandler接口,作为自定义的ChannelInboundHandler,所以都要重写channelRead方法。Netty框架使用工作链模式来对每个ChannelInboundHandler的实现类的channelRead方法进行链式调用。TransportChannelHandler实现的channelRead方法如代码下:

public void channelRead0(ChannelHandlerContext ctx, Message request) throws Exception {
  if (request instanceof RequestMessage) {
    requestHandler.handle((RequestMessage) request);
  } else {
    responseHandler.handle((ResponseMessage) request);
  }
}

从上述代码可知,当TransportChannelHandler读取的request是RequestMessage时,则将此消息的处理进一步交给TransportRequestHandler,当读取的request是ResponseMessage时,则将此消息的处理进一步交给TransportResponseHandler。

5.1 MessageHandler的继承体系

TransportRequestHandler与TransportResponseHandler继承自抽象类MessageHandler,MessageHandler定义了子类的规范,详细定义如下:

public abstract class MessageHandler<T extends Message> {
  //用于对接收到的单个消息进行处理
  public abstract void handle(T message) throws Exception;
  //当channel激活时调用
  public abstract void channelActive();
  //当捕获到channel发生异常时调用
  public abstract void exceptionCaught(Throwable cause);
  //当channel非激活时调用
  public abstract void channelInactive();
}

5.2 Message的继承体系

根据MessageHandler代码可知,MessageHandler同时也是一个Java泛型类,其子类能处理的消息都派生自接口Message。Message的定义如代码下:

public interface Message extends Encodable {
  //返回消息的类型
  Type type();
  //返回消息中可选的内容体
  ManagedBuffer body();
  //用于判断消息的主体是否包含在消息的同一帧中
  boolean isBodyInFrame();

Message接口继承了Encodable接口,Encodable的定义如下:

public interface Encodable {
  int encodedLength();
  void encode(ByteBuf buf);
}

实现Encodable接口的类将可以转换到一个ByteBuf中,多个对象将被存储到预先分配的单个ByteBuf;encodedLenth用于返回转换的对象数量。

从上图中看到,最终的消息实现类都直接或间接地实现了RequestMessage或ResponseMessage接口,其中RequestMessage的具体实现有4种:

  • ChunkFetchRequest:请求获取流的单个块的序列
  • RpcRequest:此消息类型由远程的RPC服务端进行处理,是一种需要服务端向客户端回复的RPC请求信息类型
  • OneWayMessage:此消息也需要由远程的RPC服务端进行处理,与RpcRequest不同的是,不需要服务端向客户端回复。
  • StreamRequest:此消息表示向远程的服务发起请求,以获取流式数据

由于OneWayMessage不需要响应,所以ResponseMessage对于成功或失败状态的实现各有3种,分别如下:

  • ChunkFetchSuccess:处理ChunkFetchRequest成功后返回的消息
  • ChunkFetchFailure:处理ChunkFetchRequest失败后返回的消息
  • RpcResponse:处理RpcRequest成功后返回的消息
  • RpcFailure:处理RpcRequest失败后返回的消息
  • StreamResponse:处理StreamRequest成功后返回的消息
  • StreamFailure:处理StreamRequest失败后返回的消息

5.3 ManagerBuffer的继承体系

查看接口Message中对body的定义,其返回内容体的类型为ManagedBuffer。ManagedBuffer提供了由字节构成数据的不可变视图(也就是ManagedBuffer并不存储数据,也不是数据的实际来源,这与关系型数据库的视图类似)。抽象类ManagedBuffer代码如下:

public abstract class ManagedBuffer {
  //返回数据的字节数
  public abstract long size();
  //将数据按照NIO的ByteBuffer类型返回
  public abstract ByteBuffer nioByteBuffer() throws IOException;
  //将数据按照InputStream返回
  public abstract InputStream createInputStream() throws IOException;
  //当有新的使用者使用此视图时,增加引用此视图的引用数
  public abstract ManagedBuffer retain();
  //当有使用者不再使用此视图时,减少引用此视图的引用数;当引用数为0时释放缓冲区
  public abstract ManagedBuffer release();
  //将缓冲区的数据转换为Netty的对象,用来将数据写到外部。此方法返回的数据类型要么是io.netty.buffer.ByteBuf,要么是io.netty.channel.FileRegion
  public abstract Object convertToNetty() throws IOException;
}

6 服务端RpcHandler详解

由于TransportRequestHandler实际是把请求消息交给RpcHandler进行处理,RpcHandler是一个抽象类,定义了一些RPC处理器的规范,代码下:

public abstract class RpcHandler {
  private static final RpcResponseCallback ONE_WAY_CALLBACK = new OneWayRpcCallback();
  //抽象方法,用来接收单一的RPC消息,具体处理逻辑需要子类去实现
  public abstract void receive(
      TransportClient client,
      ByteBuffer message,
      RpcResponseCallback callback);
  //获取Streammanager,StreamManager可以从流中获取单个的块,因此它也包含着当前正在被TransportClient获取的流的状态
  public abstract StreamManager getStreamManager();
  //重载receive方法,RpcResponseCallback为默认的ONE_WAY_CALLBACK
  public void receive(TransportClient client, ByteBuffer message) {
    receive(client, message, ONE_WAY_CALLBACK);
  }
  //当与给定客户端相关联的channel处于活动状态时调用
  public void channelActive(TransportClient client) { }
  //当与给定客户端相关联的channel处于非活动状态时调用
  public void channelInactive(TransportClient client) { }
  //当channel产生异常时调用
  public void exceptionCaught(Throwable cause, TransportClient client) { }
}

介绍完RpcHandler,重新梳理TransportRequestHandler的处理过程。TransportRequestHandler处理以下4咱RequestMessage:

@Override
public void handle(RequestMessage request) {
  if (request instanceof ChunkFetchRequest) {
    processFetchRequest((ChunkFetchRequest) request);
  } else if (request instanceof RpcRequest) {
    processRpcRequest((RpcRequest) request);
  } else if (request instanceof OneWayMessage) {
    processOneWayMessage((OneWayMessage) request);
  } else if (request instanceof StreamRequest) {
    processStreamRequest((StreamRequest) request);
  } else {
    throw new IllegalArgumentException("Unknown request type: " + request);
  }
}

处理块获取请求

processFetchRequest方法用于处理ChunkFetchRequest类型的消息,其实现代码如下:

private void processFetchRequest(final ChunkFetchRequest req) {
  if (logger.isTraceEnabled()) {
    logger.trace("Received req from {} to fetch block {}", getRemoteAddress(channel),
      req.streamChunkId);
  }
  ManagedBuffer buf;
  try {
    streamManager.checkAuthorization(reverseClient, req.streamChunkId.streamId);
    streamManager.registerChannel(channel, req.streamChunkId.streamId);
    buf = streamManager.getChunk(req.streamChunkId.streamId, req.streamChunkId.chunkIndex);
  } catch (Exception e) {
    logger.error(String.format("Error opening block %s for request from %s", req.streamChunkId,
      getRemoteAddress(channel)), e);
    respond(new ChunkFetchFailure(req.streamChunkId, Throwables.getStackTraceAsString(e)));
    return;
  }
  respond(new ChunkFetchSuccess(req.streamChunkId, buf));
}

streamManager是通过调用RpcHandler的getStreamManager方法获取的StreamManager。processFetchRequest的处理都依托于RpcHandler的StreamManager,其处理步骤如下:

  • 1)调用StreamManager的checkAuthorization方法,校验客户端是否有权限从给定的流中读取
  • 2)调用StreamManager的registerChannel方法,将一个流和一条(只能是一条)客户端的TCP连接关联起来,这可以保证对于单个的流一个客户端读取。流关闭之后就永远不远够重用了。
  • 3)调用StreamManager的getChunk方法,获取单个的块;由于单个的流只能与单个的TCP连接相关联,因此getChunk方法不能为了某个特殊的流而并行调用
  • 4)将ManagedBuffer和流的块Id封装为ChunkFetchSuccess后,调用respond方法返回给客户端。

处理RPC、流以及无需回复的RPC请求分析方法差不多,这里不做重复累赘复述。

7 服务端引导程序TransportServerBootstrap

TransportServer的构造器中的bootstraps是TranportServerBootstrap的列表。接口TransportServerBootstrap定义了服务端引导程序的规范,服务端引导程序旨在当客户端与服务端建立连接之后,在服务端持有的客户端管道上执行的引导程序。TransportServerBootstrap的定义如下:

public interface TransportServerBootstrap {
  RpcHandler doBootstrap(Channel channel, RpcHandler rpcHandler);
}

TransportServerBootstrap的doBootstrap方法将对服务端的RpcHandler进行代理,接收客户端的请求。TransportServerBootstrap的实现类SaslServerBoostrap,其doBootstrap代码如下:

public RpcHandler doBootstrap(Channel channel, RpcHandler rpcHandler) {
  return new SaslRpcHandler(conf, channel, rpcHandler, secretKeyHolder);
}

根据上述代码可知,SaslServerBootstrap的doBootstrap方法实际创建了SaslRpcHandler,SaslRpcHandler负责对管道进行SASL加密。SaslRpcHandler本身也继承了RpcHandler,可看其receive方法:

@Override
public void receive(TransportClient client, ByteBuffer message, RpcResponseCallback callback) {
  if (isComplete) {
	//将消息传递给SaslRpcHandler所代理的下游Rpchandler并返回
    delegate.receive(client, message, callback);
    return;
  }
  ByteBuf nettyBuf = Unpooled.wrappedBuffer(message);
  SaslMessage saslMessage;
  try {
	//对客户端发送的消息进行SASL解密
    saslMessage = SaslMessage.decode(nettyBuf);
  } finally {
    nettyBuf.release();
  }
  if (saslServer == null) {
	//如果saslServer还未创建,则需要创建SparkSaslServer
    client.setClientId(saslMessage.appId);
    saslServer = new SparkSaslServer(saslMessage.appId, secretKeyHolder,
      conf.saslServerAlwaysEncrypt());
  }
  byte[] response;
  try {
	//使用saslServer处理已解密的消息
    response = saslServer.response(JavaUtils.bufferToArray(
      saslMessage.body().nioByteBuffer()));
  } catch (IOException ioe) {
    throw new RuntimeException(ioe);
  }
  callback.onSuccess(ByteBuffer.wrap(response));
  if (saslServer.isComplete()) {
    logger.debug("SASL authentication successful for channel {}", client);
    isComplete = true;//SASL认证交换已经完成
    if (SparkSaslServer.QOP_AUTH_CONF.equals(saslServer.getNegotiatedProperty(Sasl.QOP))) {
      logger.debug("Enabling encryption for channel {}", client);
	  //对管道进行SASL加密
      SaslEncryption.addToChannel(channel, saslServer, conf.maxSaslEncryptedBlockSize());
      saslServer = null;
    } else {
      saslServer.dispose();
      saslServer = null;
    }
  }
}

8 客户端TransportClient详解

学习完服务端RpcHandler对请求消息的处理后,接下来学习客户端发送RPC请求的原理。 TransportContext的createChannelHandler方法中调用了TransportClient的构造器,其中TranportResponseHandler的引用将赋给handler属性。

public TransportClient(Channel channel, TransportResponseHandler handler) {
  this.channel = Preconditions.checkNotNull(channel);
  this.handler = Preconditions.checkNotNull(handler);
  this.timedOut = false;
}

TranportClient一共有5个方法用于发送请求,分别如下:

  • 1)fetchChunk:从远端协商好的流中请求单个块
  • 2)stream:使用流的ID,从远端获取流数据
  • 3)sendRpc:向服务端发送RPC的请求,通过At least Once Delivery原则保证请求不会丢失
  • 4)sendRpcSync:向服务端发送异步的RPC请求,并根据指定的超时时间等待响应
  • 5)send:向服务端发送RPC的请求,但是并不期望能获取响应,因此不能保证投递的可靠性

8.1 发送RPC请求

public long sendRpc(ByteBuffer message, final RpcResponseCallback callback) {
  final long startTime = System.currentTimeMillis();
  if (logger.isTraceEnabled()) {
    logger.trace("Sending RPC to {}", getRemoteAddress(channel));
  }
  //使用UUID生成请求主键requestId
  final long requestId = Math.abs(UUID.randomUUID().getLeastSignificantBits());
  //添加requestId与RpcResponseCallback的引用之间的关系
  handler.addRpcRequest(requestId, callback);
  //发送RPC请求
  channel.writeAndFlush(new RpcRequest(requestId, new NioManagedBuffer(message))).addListener(
    new ChannelFutureListener() {
      @Override
      public void operationComplete(ChannelFuture future) throws Exception {
        if (future.isSuccess()) {
          long timeTaken = System.currentTimeMillis() - startTime;
          if (logger.isTraceEnabled()) {
            logger.trace("Sending request {} to {} took {} ms", requestId,
              getRemoteAddress(channel), timeTaken);
          }
        } else {
          String errorMsg = String.format("Failed to send RPC %s to %s: %s", requestId,
            getRemoteAddress(channel), future.cause());
          logger.error(errorMsg, future.cause());
          handler.removeRpcRequest(requestId);
          channel.close();
          try {
            callback.onFailure(new IOException(errorMsg, future.cause()));
          } catch (Exception e) {
            logger.error("Uncaught exception in RPC response callback handler!", e);
          }
        }
      }
    });
  return requestId;
}

根据上述代码得知,sendRpc方法的实现步骤如下:

  • 1)使用UUID生成请求主键requestId
  • 2)调用addRpcRequest向handler(这里的handler不是RpcHandler,而是通过TransportClient构造器传入的TransportResponseHandler)添加requestId与回调类RpcResponseCallback的引用之间的关系。TransportResponseHandler的addRpcRequest方法将更新最后一次请求的时间为当前系统时间,然后将reqeustId与RpcResponseCallback之间的映射加入到outstandingRpcs缓存中。outstandingRpcs专门用于缓存发出的RPC请求信息。
public void addRpcRequest(long requestId, RpcResponseCallback callback) {
  updateTimeOfLastRequest();
  outstandingRpcs.put(requestId, callback);
}
  • 3)调用Channel的writeAndFlush方法将RPC请求发送出去,当发送成功或者失败时会回调ChannelFutureListener的operationComplete方法。如果发送成功,就只会打印requestId、远端地址及花费时间的日志,如果发送失败,还会调用TransportResponseHandler的removeRpcRequest方法,将此次请求从outstandingRpcs缓存中移除。

请求发送成功后,客户端将等待接收服务端的响应。根据前面TranportChannelHandler的分析,返回的消息也会传递给TransportChannelHandler的channelRead方法,并由handle方法来处理。TransportResponseHandler的Handle方法分别对前文提到的6种ResponseMessage(ChunkFetchSuccess、ChunkFetchFailure、RpcResponse、RpcFailure、StreamResponse、StreamFailure)进行处理,由于服务端TransportRequestHandler使用processRpcRequest处理RpcRequest类型的消息后,返回给客户端的消息为RpcResponse或RpcFailure,查看如下代码:

} else if (message instanceof RpcResponse) {
  RpcResponse resp = (RpcResponse) message;
  RpcResponseCallback listener = outstandingRpcs.get(resp.requestId);
  if (listener == null) {
    logger.warn("Ignoring response for RPC {} from {} ({} bytes) since it is not outstanding",
      resp.requestId, getRemoteAddress(channel), resp.body().size());
  } else {
    outstandingRpcs.remove(resp.requestId);
    try {
      listener.onSuccess(resp.body().nioByteBuffer());
    } finally {
      resp.body().release();
    }
  }
} else if (message instanceof RpcFailure) {
  RpcFailure resp = (RpcFailure) message;
  RpcResponseCallback listener = outstandingRpcs.get(resp.requestId);
  if (listener == null) {
    logger.warn("Ignoring response for RPC {} from {} ({}) since it is not outstanding",
      resp.requestId, getRemoteAddress(channel), resp.errorString);
  } else {
    outstandingRpcs.remove(resp.requestId);
    listener.onFailure(new RuntimeException(resp.errorString));
  }
}

根据上述代码可知,处理RpcResponse的逻辑如下:

  • 1)使用RpcResponse对应的RpcRequest的主键requestId,从outstandingRpcs缓存中获取注册的RpcResponseCallback
  • 2)移除outstandingRpcs缓存中requestId和RpcResponseCallback的注册信息
  • 3)调用RpcResponseCallback的onSuccess方法,处理成功响应后的具体逻辑 。
  • 4)释放RpcResponse的body,回收资源

处理RpcResponse的逻辑如下:

  • 1)使用RpcFailure对应的RpcRequest的主键requestId,从outstandingRpcs缓存中获取注册的RpcResponseCallback
  • 2)移除outstangdingRpcs缓存中requestId和RpcResponseCallback的注册信息
  • 3)调用RpcResponseCallback的onFailure方法,处理失败响应后的具体逻辑。

8.2 发送获取块请求

fetchChunk的实现代码:

public void fetchChunk(
    long streamId,
    final int chunkIndex,
    final ChunkReceivedCallback callback) {
  final long startTime = System.currentTimeMillis();
  if (logger.isDebugEnabled()) {
    logger.debug("Sending fetch chunk request {} to {}", chunkIndex, getRemoteAddress(channel));
  }
  //创建StreamChunkId
  final StreamChunkId streamChunkId = new StreamChunkId(streamId, chunkIndex);
  //添加StreamChunkId与ChunkReceivedCallback之间的对应关系
  handler.addFetchRequest(streamChunkId, callback);
  //发送块请求
  channel.writeAndFlush(new ChunkFetchRequest(streamChunkId)).addListener(
    new ChannelFutureListener() {
      @Override
      public void operationComplete(ChannelFuture future) throws Exception {
        if (future.isSuccess()) {
          long timeTaken = System.currentTimeMillis() - startTime;
          if (logger.isTraceEnabled()) {
            logger.trace("Sending request {} to {} took {} ms", streamChunkId,
              getRemoteAddress(channel), timeTaken);
          }
        } else {
          String errorMsg = String.format("Failed to send request %s to %s: %s", streamChunkId,
            getRemoteAddress(channel), future.cause());
          logger.error(errorMsg, future.cause());
          handler.removeFetchRequest(streamChunkId);
          channel.close();
          try {
            callback.onFailure(chunkIndex, new IOException(errorMsg, future.cause()));
          } catch (Exception e) {
            logger.error("Uncaught exception in RPC response callback handler!", e);
          }
        }
      }
    });
}

请求发送成功后,客户端将等待接收服务端的响应。返回的消息也会传递给TransportChannelHandler的channelRead方法,根据之前的分析,消息的分析将最后交给TransportResponseHandler的handler方法来处理。服务端使用processFetchRequest方法处理ChunkFetchRequest类型的消息后返回给客户端的消息为ChunkFetchSuccess或ChunkFetchFailure,查看处理代码:

if (message instanceof ChunkFetchSuccess) {
  ChunkFetchSuccess resp = (ChunkFetchSuccess) message;
  ChunkReceivedCallback listener = outstandingFetches.get(resp.streamChunkId);
  if (listener == null) {
    logger.warn("Ignoring response for block {} from {} since it is not outstanding",
      resp.streamChunkId, getRemoteAddress(channel));
    resp.body().release();
  } else {
    outstandingFetches.remove(resp.streamChunkId);
    listener.onSuccess(resp.streamChunkId.chunkIndex, resp.body());
    resp.body().release();
  }
} else if (message instanceof ChunkFetchFailure) {
  ChunkFetchFailure resp = (ChunkFetchFailure) message;
  ChunkReceivedCallback listener = outstandingFetches.get(resp.streamChunkId);
  if (listener == null) {
    logger.warn("Ignoring response for block {} from {} ({}) since it is not outstanding",
      resp.streamChunkId, getRemoteAddress(channel), resp.errorString);
  } else {
    outstandingFetches.remove(resp.streamChunkId);
    listener.onFailure(resp.streamChunkId.chunkIndex, new ChunkFetchFailureException(
      "Failure while fetching " + resp.streamChunkId + ": " + resp.errorString));
  }
}

根据上述代码可知,处理ChunkFetchSuccess的逻辑:

  • 1)使用ChunkFetchSuccess对应的StreamChunkId,从outstandingFetches缓存中获取注册的ChunkReceivedCallback
  • 2)移除outstandingFetches缓存中StreamChunkId和ChunkReceivedCallback的注册信息
  • 3)调用ChunkReceivedCallback的onSuccess方法,处理成功响应后的具体逻辑。
  • 4)释放ChunkFetchSuccess的body,回收资源

处理ChunkFetchFailure的逻辑:

  • 1)使用ChunkFetchFailure对应的StreamChunkId,从outstandingFetches缓存中获取注册的ChunkReceivedCallback
  • 2)移除outstandingFetches缓存中StreamCHunkId和ChunkReceivedCallback的注册信息
  • 3)调用ChunkReceivedCallback的onFailure方法,处理失败响应后的具体逻辑

 

 

 

;