【Spark内核源码】内置的RPC框架,Spark的通信兵(一)

目录

RPC上下文TransportContext

RPC配置TransportConf

RPC客户端工厂TransprotClientFactory

RPC服务端TransportServer


作为一个分布式计算引擎,既然是分布式,那么网络通信是肯定少不了的,在Spark中很多地方都涉及到了网络通信,各个组件之间消息传输、用户文件和资源的上传、Shuffle过程、Block的数据复制与备份等等,都少不了网络通信。

在Spark2.X之前,Spark组件通信使用Akka,用户文件和资源的上传是基于Jetty实现的HttpFileServer,Shuffle过程、Block的数据复制与备份是基于Netty实现的。Spark2.0版本之后,Spark放弃了Akka和Jetty,各个组件之间消息传输、用户文件和资源的上传、Shuffle过程、Block的数据复制与备份等等统一采用Spark的RPC框架NettyStreamManager,将通信框架统一起来了。

RPC上下文TransportContext

org.apache.spark.network.TransportContext,传输上下文,其主要作用是创建TransportServer,TransportClientFactory和使用TransportChannelHandler设置Netty Channel pipelines(Netty的通信管道)。

TransportClient 提供了两个通信协议,分别是RPC控制层和数据层。RPC的处理是在TransportContext之外的,它负责设置流,这个流以zero-copy IO的形式与数据块进行通信传输。

TransportServer和TransportClientFactory都为每一个channel创建一个TransportChannelHandler。每一个TransportChannelHandler都包含一个TransportClient,可以使得服务进程通过现有的channel发送消息给客户端。

下面是TransportContext的源码分析:

TransportContext中包含TransportConf、RpcHandler、MessageEncoder、MessageDecoder和一个创建TransportChannelHandler时使用的closeIdleConnections(布尔型)属性;两个构造方法,TransportConf、RpcHandler是必须传入TransportContext中的,closeIdleConnections选填。如下图所示:

TransportContext的成员属性和构造方法

TransportContext的主要作用之一就是创建TransprotClientFactory,创建TransportClientFactory时需要传入TransportClientBootstrap列表。

创建TransprotClientFactory

TransportContext另一个主要作用就是创建TransportServer,RPC框架的服务端,创建服务端时可以设置指定的端口,也可以指定特定的IP地址+端口,当然也可以既不指定IP地址也不指定端口(此时,端口默认为0)。创建TransportServer时还需要传入一个TransportServerBootstrap列表。

创建TransportServer

TransportContext的最后一个作用就是创建TransportChannelHandler,并使用TransportChannelHandler设置Netty Channel pipelines(Netty的通信管道)。初始化Netty通信管道,设置编码/解码,TransportChannelHandler还进行发送/接收消息处理。TransportChannelHandler包含TransportClient,在此channel上进行通信,与此channel直接关联,确保所有用户使用channel时得到的是同一个TrasportClient对象。

创建TransportChannelHandler

RPC配置TransportConf

TransportContext中包含org.apache.spark.network.util.TransportConf,TransportConf提供整个RPC框架的配置信息。TransprotConf有两个主要的成员属性,分别是配置提供者conf和模块配置名称module,还有一些关键配置信息的KEY,根据这些KEY,TransportConf为RPC框架提供相应的API可获取配置信息。

模块配置名称module与getConfKey(String suffix)方法结合,为关键配置信息的KEY赋值,赋值的格式是:

"spark." + module + "." + suffix,suffix是具体的后缀

 具体实现是:

module的使用

另一个主要的成员属性就是配置提供者ConfigProvider conf,根据配置信息的KEY从配置提供者ConfigProvider conf中得到具体的配置信息。

org.apache.spark.network.util.ConfigProvider是一个抽象类,有一个get抽象方法,其他的get、getInt、getLong、getDouble、getBoolean具体方法都是基于这个抽象方法进行的类型转换。

ConfigProvider的实现

Spark中使用org.apache.spark.network.netty.SparkTransportConf来创建TransportConf。fromSparkConf方法构建了TransportConf,该方法需要传递三个参数SparkConf、模块名module和可用内核数numUsableCores(默认为0)。如果numUsableCores小于等于0,线程数量就是系统可用处理器数量,如果大于0便和MAX_DEFAULT_NETTY_THREADS=8进行比较,线程数量取小的,因为系统不可能将全部内核数都用来网络传输,因此需要设置上限。我们可以通过在Spark的配置中手动设置serverthread和clientthread的数量来覆盖MAX_DEFAULT_NETTY_THREADS。具体代码如下:

SparkTransportConf的实现

在fromSparkConf方法中设置分别设置了服务端传输线程数(spark.$module.io.serverThreads)和客户端传输线程数(spark.$module.io.clientThreads),创建TransportConf对象时,传递的是ConfigProvider的匿名内部类,该匿名内部类实现的get方法就是调用了SparkConf的get方法。

RPC客户端工厂TransprotClientFactory

TransportContext的一个很重要的作用就是创建org.apache.spark.network.client.TransprotClientFactory。

TransprotClientFactory为其他主机维护着一个连接池,并且确保相同的远程主机返回相同的TransportClient。它还为所有的TransportCliet维护着一个共享的工作线程池。

TransportClientFactory构造方法如下:

public TransportClientFactory(
      TransportContext context,
      List<TransportClientBootstrap> clientBootstraps) {
    this.context = Preconditions.checkNotNull(context);// TransportContext
    this.conf = context.getConf();// TransportConf
    this.clientBootstraps = Lists.newArrayList(Preconditions.checkNotNull(clientBootstraps));// TransportClientBootstrap列表
    this.connectionPool = new ConcurrentHashMap<>();// 建立连接池
    this.numConnectionsPerPeer = conf.numConnectionsPerPeer();// conf中key为"spark."+module+"io.numConnectionsPerPeer"的值
    this.rand = new Random();// 在ClientPool中随机选择TransportClient

    IOMode ioMode = IOMode.valueOf(conf.ioMode());// conf中key为"spark."+module+"io.mode"的值,IO mode有两种模式:nio(默认)和epoll
    this.socketChannelClass = NettyUtils.getClientChannelClass(ioMode);// 根据ioMode匹配Channel创建模式,有两种:nio(默认)和epoll
    // TODO: Make thread pool name configurable.
    this.workerGroup = NettyUtils.createEventLoop(ioMode, conf.clientThreads(), "shuffle-client");// 创建Netty的WorkerGroup
    this.pooledAllocator = NettyUtils.createPooledByteBufAllocator(
      conf.preferDirectBufs(), false /* allowCache */, conf.clientThreads());
  }

在创建TransportClientFactory时传入了两个参数,一个是传输上下文TransportContext,还有一个是TransportClient引导程序列表List<TransportClientBootstrap>。

TransportClientBootstrap是一个接口,有两个实现类分别是:SaslClientBootstrap和EncryptionDisablerBootstrap

TransportClientBootstrap在TransportClient给用户使用之前做了一些引导工作,是对链接初始化之前做的一些准备工作,比如SASL身份验证令牌,因为建立的链接可以重复使用,因此引导程序只会执行一次。

TransportClientFactory的主要作用就是创建RPC客户端org.apache.spark.network.client.TransportClient,代码如下:

public TransportClient createClient(String remoteHost, int remotePort) throws IOException {
    // Get connection from the connection pool first.
    // If it is not found or not active, create a new one.
    // Use unresolved address here to avoid DNS resolution each time we creates a client.
    // 建立InetSocketAddress
    final InetSocketAddress unresolvedAddress =
      InetSocketAddress.createUnresolved(remoteHost, remotePort);

    // Create the ClientPool if we don't have it yet.
    // 根据InetSocketAddress,在连接池找到对应的ClientPool缓存
    // 如果找不到建立新的ClientPool缓存,缓存大小为conf中key为"spark."+module+"io.numConnectionsPerPeer"的值
    ClientPool clientPool = connectionPool.get(unresolvedAddress);
    if (clientPool == null) {
      connectionPool.putIfAbsent(unresolvedAddress, new ClientPool(numConnectionsPerPeer));
      clientPool = connectionPool.get(unresolvedAddress);
    }

    // 随机选择TransportClient缓存
    int clientIndex = rand.nextInt(numConnectionsPerPeer);
    TransportClient cachedClient = clientPool.clients[clientIndex];

    /**
     * client缓存存在并且可使用时的操作:
     * 设置TransportChannelHandler最后一次的使用时间,确保不超时;
     * 然后再检查client是否存活,存活的话创建TransportClient成功
     * */
    if (cachedClient != null && cachedClient.isActive()) {
      // Make sure that the channel will not timeout by updating the last use time of the
      // handler. Then check that the client is still alive, in case it timed out before
      // this code was able to update things.
      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;
      }
    }

    /**
     * client缓存存在或者未激活时候的操作:
     * 根据IP和端口号重新建立InetSocketAddress
     * */
    // If we reach here, we don't have an existing connection open. Let's create a new one.
    // Multiple threads might race here to create new connections. Keep only one of them active.
    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);
    }

    /**
     * 创建InetSocketAddress的过程会产生静态条件,此时标记了clientPool中的locks数组
     * 在clientPool中locks数组中的元素与TransportClient数组中的元素一对一对应关系
     * 先进入的线程会重载createClient方法,并放入到clientPool中与clientIndex对应位置上
     * 后面进入的线程就会直接得到第一个线程进来时创建好的client了
     * */
    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);
        }
      }
      // 第一个线程进来时创建client,重载了createClient方法
      clientPool.clients[clientIndex] = createClient(resolvedAddress);
      return clientPool.clients[clientIndex];
    }
  }

从上面的创建过程可以知道TransportClinetFactory采用随机负载均衡的方式,从clientPool(也就是缓存)中获取client。

第一个线程进行创建client操作时,重载了私有的createClient方法,这个方法才是真正创建TransportClient方法,代码如下:

private TransportClient createClient(InetSocketAddress address) throws IOException {
    logger.debug("Creating new connection to {}", address);

    /** 构建初始引导程序并进行配置 */
    Bootstrap bootstrap = new Bootstrap();
    bootstrap.group(workerGroup)
      .channel(socketChannelClass)
      // Disable Nagle's Algorithm since we don't want packets to wait
      .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<>();

    /** 初始引导设置初始化channel */
    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";

    // Execute any client bootstraps synchronously before marking the Client as successful.
    long preBootstrap = System.nanoTime();
    logger.debug("Connection to {} successful, running bootstraps...", address);
    try {
      /** 执行TransportClient引导程序列表 */
      for (TransportClientBootstrap clientBootstrap : clientBootstraps) {
        clientBootstrap.doBootstrap(client, channel);
      }
    } 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;
  }

createClient方法主要做了以下几件事:

  1. 初始化根引导程序并进行配置
  2. 根引导程序设置初始化管道
  3. 使用根引导程序连接远程服务器
  4. 执行TransportClient引导程序列表
  5. 返回TransportClient对象

TransportClinetFactory中连接池的结构如下

链接池结构

多个链接对应对个客户端缓存,缓存中每个客户端缓存对应一个锁,用于避免竞争,并采用随机负载均衡的方式从缓存中获取客户端。

RPC服务端TransportServer

TransportContext另一个很重要的作用就是创建org.apache.spark.network.server.TransportServer。

TransportContext有4个createServer重载方法(介绍TransportContext已经提到过了),但其实都是在用这个构造器创建TransportServer的。

TransportServer构造器

对一些成员变量context、conf、appRpcHandler、bootstraps进行赋值过后,开始执行init(String hostToBind, int portToBind)对TransportServer初始化。

init(String hostToBind, int portToBind)的代码如下:

private void init(String hostToBind, int portToBind) {
    /**
     * ioMode同TransportClientFactory初始化时是一样的
     * conf中key为"spark."+module+"io.mode"的值,IO mode有两种模式:nio(默认)和epoll
     * */
    IOMode ioMode = IOMode.valueOf(conf.ioMode());
    /**
     * 1、创建Netty服务端需要同时创建bossGroup和workerGroup
     * */
    EventLoopGroup bossGroup =
      NettyUtils.createEventLoop(ioMode, conf.serverThreads(), "shuffle-server");
    EventLoopGroup workerGroup = bossGroup;

    /**
     * 2、创建ByteBuf分配器,对本地线程缓存禁用
     * (ByteBuf由事件循环线程分配,执行线程释放,本地缓存会延迟回收,加大开销,所有禁用)
     * */
    PooledByteBufAllocator allocator = NettyUtils.createPooledByteBufAllocator(
      conf.preferDirectBufs(), true /* allowCache */, conf.serverThreads());

    /**
     * 3、创建跟引导程序并配置
     * */
    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());
    }

    /**
     * 4、初始化管道,执行TransportServer引导程序列表
     * */
    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);
      }
    });

    /**
     * 5、绑定IP 端口号
     * */
    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一共做了5件事:

  1. 创建boosGroup和workerGroup(Netty服务端需要同时创建)。ioMode的建立与TransportClientFactory中创建TransportClient的方式是一样的。
  2. 创建ByteBuf分配器,对本地线程缓存禁用。ByteBuf由事件循环线程分配,执行线程释放,本地缓存会延迟回收,加大开销,所有禁用。
  3. 创建根引导程序并配置。
  4. 初始化管道,执行TransportServer引导程序列表。
  5. 绑定IP 端口号。

未完待续~~~

猜你喜欢

转载自blog.csdn.net/lazy_moon/article/details/83116767