目录
RPC客户端工厂TransprotClientFactory
作为一个分布式计算引擎,既然是分布式,那么网络通信是肯定少不了的,在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的主要作用之一就是创建TransprotClientFactory,创建TransportClientFactory时需要传入TransportClientBootstrap列表。
TransportContext另一个主要作用就是创建TransportServer,RPC框架的服务端,创建服务端时可以设置指定的端口,也可以指定特定的IP地址+端口,当然也可以既不指定IP地址也不指定端口(此时,端口默认为0)。创建TransportServer时还需要传入一个TransportServerBootstrap列表。
TransportContext的最后一个作用就是创建TransportChannelHandler,并使用TransportChannelHandler设置Netty Channel pipelines(Netty的通信管道)。初始化Netty通信管道,设置编码/解码,TransportChannelHandler还进行发送/接收消息处理。TransportChannelHandler包含TransportClient,在此channel上进行通信,与此channel直接关联,确保所有用户使用channel时得到的是同一个TrasportClient对象。
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是具体的后缀
具体实现是:
另一个主要的成员属性就是配置提供者ConfigProvider conf,根据配置信息的KEY从配置提供者ConfigProvider conf中得到具体的配置信息。
org.apache.spark.network.util.ConfigProvider是一个抽象类,有一个get抽象方法,其他的get、getInt、getLong、getDouble、getBoolean具体方法都是基于这个抽象方法进行的类型转换。
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。具体代码如下:
在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方法主要做了以下几件事:
- 初始化根引导程序并进行配置
- 根引导程序设置初始化管道
- 使用根引导程序连接远程服务器
- 执行TransportClient引导程序列表
- 返回TransportClient对象
TransportClinetFactory中连接池的结构如下
多个链接对应对个客户端缓存,缓存中每个客户端缓存对应一个锁,用于避免竞争,并采用随机负载均衡的方式从缓存中获取客户端。
RPC服务端TransportServer
TransportContext另一个很重要的作用就是创建org.apache.spark.network.server.TransportServer。
TransportContext有4个createServer重载方法(介绍TransportContext已经提到过了),但其实都是在用这个构造器创建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件事:
- 创建boosGroup和workerGroup(Netty服务端需要同时创建)。ioMode的建立与TransportClientFactory中创建TransportClient的方式是一样的。
- 创建ByteBuf分配器,对本地线程缓存禁用。ByteBuf由事件循环线程分配,执行线程释放,本地缓存会延迟回收,加大开销,所有禁用。
- 创建根引导程序并配置。
- 初始化管道,执行TransportServer引导程序列表。
- 绑定IP 端口号。
未完待续~~~