Spark-RPC框架解析之组件介绍及流程粗探

在 Spark 中很多地方都涉及到网络通信,比如 Spark 各个组件间的消息互通、用户文件与 Jar 包的上传、节点间的 Shuffle 过程、Block 数据的复制与备份等

因本次解析的 Spark 源码基于 2.4.8 版本,该版本使用的是基于 Spark 内置 RPC 框架的 NettyStreamManager。

  • 节点间的 Shuffle 过程和 Block 数据的复制与备份这两个部分依然沿用了 Netty
  • 通过对接口和程序进行重新设计,将各个自建间的消息互通、用户文件与 Jar 包的上传等内容统一纳入 Spark 的 RPC 框架体系中

RPC框架的基本架构

由上图可见

  • TransportContext 内部包含传输上下文的配置信息 TransportConf 和对客户端请求信息进行处理的 RpcHandler
    • RpcHandler 会在创建 TransportServer 时起作用
    • TransportConf 则是在创建 TransportClientFactory 和 TransportServer 时所必需的
  • TransportClientFactory 是 RPC 客户端的工厂类
  • TransportServer 是 RPC 服务端的实现

由图中的 1 和 2 可以观察到 TransportContext 、TransportClientFactory 、TransportServer 之间的调用关系

  1. TransportContext 和 TransportClientFactory 的关系是:通过调用 TransportContext 的 createClientFactory 方法创建传输客户端工厂 TransportClientFactory 的实例。在构造 TransportClientFactory 的实例时,还会传递客户端引导程序 TransportClientBootstrap 的列表,在此之外,TransportClientFactory 内部还存在针对每个 Socket 地址 的连接池 ClientPool,通过构建 locks 数组中的 Object 与 clients 数组中的 TransportClient 按照数组索引一一对应,相当于对每个 TransportClient 分别采用不同的锁,降低并发情况下线程间的竞争,进而减少阻塞,提高并发度,可见代码:

    public class TransportClientFactory implements Closeable {
      
      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();
          }
        }
      }
      /**......*/
      /** 并发hashmap,将Socket地址和客户端池一一对应起来*/
      private final ConcurrentHashMap<SocketAddress, ClientPool> connectionPool;
      /**......*/
    }
    
  2. 通过调用 TransportContext 的 createServer 方法创建传输服务端 TransportServer 的实例,在创建的同时还需要传递 TransportContext、host、port、RpcHandler 及服务端引导程序 TransportServerBootstrap 的列表

Spark-RPC框架包含的组件基本介绍

TransportContext : 传输上下文,包含了用于创建服务端( TransportServer ) 和传输客户端工厂 ( TransportClientFactory ) 的上下文信息,并支持使用 TransportChannelHandler 设置 Netty 提供的 SocketChannel 的 Pipeline 的实现

TransportConf : 传输上下文的配置信息

RpcHandler : 对调用传输客户端 ( TransportClient ) 的 sendRPC 方法发送的消息进行处理的程序

MessageEncoder : 对放入管道的消息内容进行编码,防止接收端读取时丢包或解析错误

MessageDecoder : 对从管道中读取的 ByteBuf 进行解析,防止丢包或解析错误

TransportFrameDecoder : 对从管道中读取的 ByteBuf 按照数据帧进行解析

RpcResponseCallback : RpcHandler 对请求的消息处理完毕后进行回调的接口

TransportClientFactory : 创建 TransportClient 的传输客户端工厂类

ClientPool : 在两个对等节点间维护的关于 TransportClient 的池。是 TransportClientFactory 的内部组件

TransportClient : RPC 框架的客户端,用于获取预先协商好的流中的连续块。TransportClient 旨在允许有效传输大量数据, 这些数据将被拆分成几百KB到几MB的块。当 TransportClient 处理从流中获取的块时,实际的设置是在传输层之外完成的。SendRPC 方法能够在客户端和服务端的同一水平线的通信进行设置

TransportClientBootstrap : 当服务器响应客户端连接时,在客户端执行一次的引导程序

TransportRequestHandler : 用于处理客户端的请求并在写完块数据后返回的处理程序

TransportResponseHandler : 用于处理服务端的响应,并对发出请求的客户端进行响应的处理i程序

TransportChannelHandler : 代理由 TransportRequestHandler 处理的请求和由 TransportResponseHandler 处理的响应,并加人传输层的处理

TransportServerBootstrap : 当客户端连接到服务端时在服务端执行一次的引导程序

TransportServer : RPC 框架的服务器,提供高效的、低级别的流服务

RPC 配置 TransportConf

TransportConf 拥有两个成员属性:配置提供者conf 和 配置的模块名称module

在 Spark 中,通常是使用 SparkTransportConf 创建 TransportConf,可见如下代码及说明

object SparkTransportConf {
  private val MAX_DEFAULT_NETTY_THREADS = 8
  /** 在 SparkTransportConf 中,使用 fromSparkConf 方法来构造 TransportConf */
  def fromSparkConf(_conf: SparkConf, module: String, numUsableCores: Int = 0): TransportConf = {
    /**在 SparkConf 中继承 Cloneable,从而重写的 clone方法在这里用上了*/
    val conf = _conf.clone
    
    val numThreads = defaultNumThreads(numUsableCores)
    /** 最终确定的线程数将用于设置客户端传输线程数(spark.$module.io.clientThreads属性)和服务端传输线程数(spark.$module.io.serverThreads属性 )*/
    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)
      override def get(name: String, defaultValue: String): String = conf.get(name, defaultValue)
      override def getAll(): java.lang.Iterable[java.util.Map.Entry[String, String]] = {
        conf.getAll.toMap.asJava.entrySet()
      }
    })
  }
  private def defaultNumThreads(numUsableCores: Int): Int = {
    
    /** 如果numUsableCores小于等于0,那么线程数是系统可用处理器的数量,不过系统的内核数不可能全部用于网络传输,所以这里将分配给网络传输的内核数量最多限制在MAX_DEFAULT_NETTY_THREADS = 8个 */
    val availableCores =
      if (numUsableCores > 0) numUsableCores else Runtime.getRuntime.availableProcessors()
    math.min(availableCores, MAX_DEFAULT_NETTY_THREADS)
  }
}

RPC客户端工厂 TransportClientFactory

TransportClientFactory 是用于创建 TransportClient 的工厂类,而它自身是由 TransportContext 的 createClientFactory 来进行创建的,TransportContext 中有两个重载的 createClientFactory 方法,在对 TransportClientFactory 构造的时候都会传递两个参数:TransportContext 和 TransportClientBootstrap

  /**
   * 初始化一个 `ClientFactory`,在返回新 `Client` 之前运行给定的 `TransportClientBootstraps`
   * TransportClientBootstraps 引导程序将同步执行,并且必须成功运行才能创建 `Client`。
   */
  public TransportClientFactory createClientFactory(List<TransportClientBootstrap> bootstraps) {
    return new TransportClientFactory(this, bootstraps);
  }

  public TransportClientFactory createClientFactory() {
    return createClientFactory(new ArrayList<>());
  }
TransportClientFactory 构造器的参数变量介绍
  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);
    this.workerGroup = NettyUtils.createEventLoop(
        ioMode,
        conf.clientThreads(),
        conf.getModuleName() + "-client");
    this.pooledAllocator = NettyUtils.createPooledByteBufAllocator(
      conf.preferDirectBufs(), false /* allowCache */, conf.clientThreads());
    this.metrics = new NettyMemoryMetrics(
      this.pooledAllocator, conf.getModuleName() + "-client", conf);
  }

context : 参数传递的 TransportContext 的引用

conf : TransprotConf

clientBootstraps : 参数传递的 TransportClientBootstrap 列表

connectionPool : 针对每个 Socket 地址的连接池 ClentPool 的缓存

connectionPool结构

numConnectionsPerPeer : 从 TransportConf 获取的 key 为 “spark.+模块名 + io.numConnectionsPerPeer” 的属性值。此属性值用于制定对等节点间的连接数

rand : 对 Socket地址对应的连接池 ClientPool 中缓存的 TransportClient 进行随机选择,对每个连接做负载均衡

ioMode : IO模式,即从 TransprotConf 获取 key 为 “spark.+模块名 + io.mode” 的属性值,默认为 NIO ,还支持 EPOLL

socketChannelClass : 客户端 Channel 被创建时使用的类,通过 ioMode 来匹配,默认 NioSocketChannel, Spark 还支持EpollEventLoopGroup

workerGroup :根据 Netty 的规范,客户端只有 worker 组,所以此处创建workerGroup。workerGroup 的实际类型是NioEventLoopGroup

pooledAllocator : 汇集 ByteBuf 但对本地线程缓存禁用的分配器

TransportClientFactory内部实现逻辑
一、 客户端引导程序 TransportClientBootstrap

TransportClientFactory 中的 clientBootstraps 属性是TransportClientBootstrap 的列表。TransportClientBootstrap 是在 TransportClient 上执行的客户端引导程序,主要对连接建立时进行一些初始化的准备,验证,加密,并且该连接是可以进行重用的

TransportClientBootstrap 有三个实现类:AuthClientBootstrap 、EncryptionDisablerBootstrap 以及 SaslClientBootstrap,其作用大致都是调用了各自的 doBootstrap 方法进行一些加解密动作

二、 创建 RPC 客户端 TransportClient

Spark 的各个模块都可以使用 TransportClientFactory 创建 RPC 客户端 TransportClient,每个 TransportClient 实例只能和一个远程的 RPC 服务端通信,所以 Spark 的组件如果需要和多个 RPC 服务端通信,就需要持有多个 TransportClient 实例(实际从缓存中获取 TransportClient),可见如下代码

 public TransportClient createClient(String remoteHost, int remotePort)
      throws IOException, InterruptedException {
    /**
    第一步:调用 InetSocketAddress 的静态方法 createUnresolved 构建 InetSocketAddress,这种构建方式可以在缓存中已经有 TransportClient 时避免不必要的域名解析
    */
    final InetSocketAddress unresolvedAddress =
      InetSocketAddress.createUnresolved(remoteHost, remotePort);

   /**
   第二步:从 connectionPool 中获取与此地址对应的 ClientPool,如果没有,则需要新建 ClientPool,并放入缓存 connectionPool中
   */
    ClientPool clientPool = connectionPool.get(unresolvedAddress);
    if (clientPool == null) {
      /**
      如果是新建的 ClientPool,那么需要根据 numConnectionsPerPeer 的大小(使用 "spark.${模块名}.io.numConnectionsPerPeer"属性配置),从 ClientPool 中随机选择一个 TransportClient
      */
      connectionPool.putIfAbsent(unresolvedAddress, new ClientPool(numConnectionsPerPeer));
      clientPool = connectionPool.get(unresolvedAddress);
    }
		
    // 第三步: 随机从 clientPool 中拿取 TransportClient
    int clientIndex = rand.nextInt(numConnectionsPerPeer);
    TransportClient cachedClient = clientPool.clients[clientIndex];

    /**
    如果随机拿出来的 TransportClient 是有用的,那么更新 TransportClient 的 channel 中配置的 TransportChannelHandler 的最后一次使用时间,确保 channel 没有超时
    */
    if (cachedClient != null && cachedClient.isActive()) {
      TransportChannelHandler handler = cachedClient.getChannel().pipeline()
        .get(TransportChannelHandler.class);
      synchronized (handler) {
        handler.getResponseHandler().updateTimeOfLastRequest();
      }

      // 检查 TransportClient 是否是激活状态,最后返回此 TransportClient 给调用房
      if (cachedClient.isActive()) {
        logger.trace("Returning cached connection to {}: {}",
          cachedClient.getSocketAddress(), cachedClient);
        return cachedClient;
      }
    }

   /**
   由于缓存中没有 TransportClient 可用,于是调用 InetSocketAddress 的构造器创建 InetSocketAddress 对象(直接使用InetSocketAddress 的构造器创建 InetSocketAddress 会进行域名解析),在这一步骤多个线程可能会产生竞态条件(由于没有同步处理,所以多个线程极有可能同时执行到此处,都发现缓存中没有TransportClient 可用,于是都使用的 InetSocketAddress 构造器创建 InetSocketAddress )
   */
    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 的过程中产生的竞态条件如果不妥善处理,会产生线程安全问题,按照随机产生的数组索引,locks 数组中的锁对象可以对 clients 数组中的 TransportClient 一对一进行同步。即便之前产生了竞态条件,但是在这一步只能有一个线程进入临界区。在临界区内,先进人的线程调用重载的 createClient 方法创建 TransportClient 对象并放入 ClientPool 的 clients 数组中。当率先进入临界区的线程退出临界区后,其他线程才能进入,此时发现 ClientPool 的 clients 数组中已经存在了 TransportClient 对象,那么将不再创建 TransportClient,而是直接使用它
    简而言之 在这里加了一把锁,让先进入的线程通过 createClient 方法拿到 client,从而让后续进来的线程直接使用当前这个 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);
        }
      }
      // 这里重载的 createClient 方法才是真正创建 TransportClient 的方法
      clientPool.clients[clientIndex] = createClient(resolvedAddress);
      return clientPool.clients[clientIndex];
    }
  }
private TransportClient createClient(InetSocketAddress address)
      throws IOException, InterruptedException {
    logger.debug("Creating new connection to {}", address);
		
    // 构建根引导程序 Bootstrap 并对其进行配置,下面可以看到 option 都是进行配置的代码
    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);

    if (conf.receiveBuf() > 0) {
      bootstrap.option(ChannelOption.SO_RCVBUF, conf.receiveBuf());
    }

    if (conf.sendBuf() > 0) {
      bootstrap.option(ChannelOption.SO_SNDBUF, conf.sendBuf());
    }

   // 创建 AtomicReference 的 TransportClient 和 Channel 对象,用于在多线程程序中操作这两者的引用
    final AtomicReference<TransportClient> clientRef = new AtomicReference<>();
    final AtomicReference<Channel> channelRef = new AtomicReference<>();

  // 为根引导程序设置管道初始化回调函数,此回调函数将调用 context(TransportContext) 的 initializePipeline 方法初始化Channel 的 pipeline
    bootstrap.handler(new ChannelInitializer<SocketChannel>() {
      @Override
      public void initChannel(SocketChannel ch) {
        TransportChannelHandler clientHandler = context.initializePipeline(ch);
        // TransportClient 和 Channel 对象设置原子引用 clientRef 和 channelRef 中
        clientRef.set(clientHandler.getClient());
        channelRef.set(ch);
      }
    });

    // 在这里,使用根引导程序连接远程服务器,当连接成功对管道初始化时回调初始化回调函数
    long preConnect = System.nanoTime();
    ChannelFuture cf = bootstrap.connect(address);
    if (!cf.await(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 设置客户端引导程序,即设置 TransportClientFactory 中的 TransportClientBootstrap 列表
      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;
  }
三、 创建 RPC 服务端 TransportServer

TransportServer 是 RPC 框架的服务端,可提供高效、低级别的流服务

TransportContext 的 createServer 方法用于 创建 TransportServer,可见代码:

  // 这四个 createServer 重载方法,最终调用了 TransportServer 的构造器来创建 TransportServer 实例
 
  /** Create a server which will attempt to bind to a specific port. */
  public TransportServer createServer(int port, List<TransportServerBootstrap> bootstraps) {
    return new TransportServer(this, null, port, rpcHandler, bootstraps);
  }

  /** 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);
  }

  /** Creates a new server, binding to any available ephemeral port. */
  public TransportServer createServer(List<TransportServerBootstrap> bootstraps) {
    return createServer(0, bootstraps);
  }

  public TransportServer createServer() {
    return createServer(0, new ArrayList<>());
  }

TransportServer 的构造器中的各个变量

  • context : 参数传递的 TransportContext 的引用
  • conf : 指 TransportConf,这里通过调用 TransportContext 的 getConf 获取
  • appRpcHandler : RPC 请求处理器 RpcHandler
  • Bootstraps : 参数传递的 TransportServerBootstrap 列表
  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));

    boolean shouldClose = true;
    try {
      // 在这里调用了 init 方法对于 TransportServer 进行初始化
      init(hostToBind, portToBind);
      shouldClose = false;
    } finally {
      if (shouldClose) {
        JavaUtils.closeQuietly(this);
      }
    }
  }

以下代码中可以看到对于 TransportServer 初始化的具体步骤与实现

private void init(String hostToBind, int portToBind) {
		// 第一步:根据 Netty 的API文档,Netty 服务器需同时创建 bossGroup 和 workerGroup
    IOMode ioMode = IOMode.valueOf(conf.ioMode());
    EventLoopGroup bossGroup = NettyUtils.createEventLoop(ioMode, 1,
      conf.getModuleName() + "-boss");
    EventLoopGroup workerGroup =  NettyUtils.createEventLoop(ioMode, conf.serverThreads(),
      conf.getModuleName() + "-server");
	  // 第二步:创建一个汇集 byteBuff 但对本地线程缓存禁用的分配器
    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)
      .option(ChannelOption.SO_REUSEADDR, !SystemUtils.IS_OS_WINDOWS)
      .childOption(ChannelOption.ALLOCATOR, allocator);

    this.metrics = new NettyMemoryMetrics(
      allocator, conf.getModuleName() + "-server", conf);

    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) {
        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);
  }
四、管道初始化

在创建 TransportClient 的代码中和 创建 TransportServer 的代码中,都有在管道初始化回调函数中调用了上下文对象的方法:TransportContext.initializePipeline,initializePipeline方法将调用 Netty 的API对管道进行初始化,在该方法中一开始就调用了创建 TransportChannelHandler 的方法:

  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, conf.maxChunksBeingTransferred());
    // 这里新建 TransportChannelHandler 的时候基本是将具体处理的handler装载进去,所以可以得知具体处理消息的其实是TransportChannelHandler
    return new TransportChannelHandler(client, responseHandler, requestHandler,
      conf.connectionTimeoutMs(), closeIdleConnections);
  }

从上面的实现可以看到,真正对 TransportClient 的创建是在这里进行的,TransportClient 用于处理请求响应的是 TransportResponseHandler,而并不是具体的 RpcHandler。TransportChannelHandler 实际在服务端将代理 TransportRequestHadnler 对请求消息进行处理,并在客户端代理 TransportResponseHandler 对响应进行处理

接下来会对管道进行设置

  public TransportChannelHandler initializePipeline(
      SocketChannel channel,
      RpcHandler channelRpcHandler) {
    try {
      TransportChannelHandler channelHandler = createChannelHandler(channel, channelRpcHandler);
      channel.pipeline()
        // 这里的 ENCODER(即MessageEncoder)派生自 Netty 的 ChannelOutBoundHandler 接口
        .addLast("encoder", ENCODER)
        // DECODER,channelHandler,TransportFrameDecoder 派生自 ChannelInBoundHandler 接口
        .addLast(TransportFrameDecoder.HANDLER_NAME, NettyUtils.createFrameDecoder())
        .addLast("decoder", DECODER)
        // IdleStateHandler 同时实现了 ChannelOutBoundHandler 和 ChannelInBoundHandler 接口
        .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;
    }
  }

根据 Netty 的API行为,通过 addLast 方法注册多个handler的时候,ChannelInBoundHandler 按照先后顺序执行,ChannelOutBoundHandler 按照先后逆序执行,因此在(服务端/客户端)管道的两端处理请求和响应的流程可以如图:

管道处理请求和响应

未完待续

  • 25
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spark-submit是Spark自带的提交脚本,用于将Spark应用程序提交到集群中运行。Spark-submit可以将应用程序打包成一个JAR包并提交到集群中运行,也可以直接提交一个Python文件或者一个Scala文件。 使用Spark-submit提交应用程序时,需要指定以下参数: 1. --class:指定主类名,如果是Java应用程序,需要指定该参数;如果是Scala应用程序,可以省略该参数,Spark-submit会自动查找Scala文件中的main函数。 2. --master:指定运行模式,可以是local、yarn、mesos等。 3. --deploy-mode:指定部署模式,可以是client或者cluster,如果是client模式,则Driver运行在提交任务的机器上;如果是cluster模式,则Driver运行在集群中的某个节点上。 4. --executor-memory:指定Executor的内存大小。 5. --total-executor-cores:指定Executor的总核数。 6. --num-executors:指定Executor的个数。 7. 应用程序的JAR包路径或者Python/Scala文件路径。 例如,使用Spark-submit提交一个Java应用程序,命令如下: ``` ./bin/spark-submit --class com.spark.example.WordCount --master yarn --deploy-mode client --executor-memory 2g --total-executor-cores 4 --num-executors 2 /path/to/WordCount.jar /path/to/input /path/to/output ``` 其中,--class指定了Java应用程序的主类名为com.spark.example.WordCount,--master指定了运行模式为yarn,--deploy-mode指定了部署模式为client,--executor-memory指定了每个Executor的内存大小为2g,--total-executor-cores指定了Executor总核数为4,--num-executors指定了Executor的个数为2,最后两个参数为输入和输出路径。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值