本文通过阅读 Spark 网络传输相关的代码,位置在包 org.apache.spark.network 中,来了解 Spark 对网络传输的实现。
Spark 底层的网络传输当前通过 Netty 实现。
TransportContext 是传输服务的上下文,他可以创建传输服务端 TransportServer 与客户端工厂 TransportClientFactory,并且使用 TransportChannelHandler 设置 Netty 服务端与客户端的 pipelines。
网络传输的客户端 TransportClient 提供了两种通信方向:控制相关的 RPC 与 数据相关的"块获取(chunk fetching)"。其中对 RPC 的处理是在 TransportContext 之外执行(比如在用户提供的处理器中);另外他负责将流中的无组织数据通过零拷贝的方式设置进一个一个块组成的流中。
TransportServer 和 TransportClientFactory 都需要为每个 channel 创建一个 TransportChannelHandler, 每个TransportChannelHandler 都包含一个 TransportClient ,这可以让服务端使用现存的 channel 响应消息。
pipeline 设置
设置 Netty pipelines 的代码如下:
code 1
public TransportChannelHandler initializePipeline(
SocketChannel channel,
RpcHandler channelRpcHandler) {
try {
TransportChannelHandler channelHandler = createChannelHandler(channel, channelRpcHandler);
ChunkFetchRequestHandler chunkFetchHandler =
createChunkFetchHandler(channelHandler, channelRpcHandler);
ChannelPipeline pipeline = channel.pipeline()
.addLast("encoder", ENCODER)
.addLast(TransportFrameDecoder.HANDLER_NAME, NettyUtils.createFrameDecoder())
.addLast("decoder", DECODER)
.addLast("idleStateHandler",
new IdleStateHandler(0, 0, conf.connectionTimeoutMs() / 1000))
// NOTE: Chunks are currently guaranteed to be returned in the order of request, but this
// would require more logic to guarantee if this were not part of the same event loop.
.addLast("handler", channelHandler);
// Use a separate EventLoopGroup to handle ChunkFetchRequest messages for shuffle rpcs.
if (conf.getModuleName() != null &&
conf.getModuleName().equalsIgnoreCase("shuffle")
&& !isClientOnly) {
pipeline.addLast(chunkFetchWorkers, "chunkFetchHandler", chunkFetchHandler);
}
return channelHandler;
} catch (RuntimeException e) {
logger.error("Error while initializing Netty pipeline", e);
throw e;
}
}
由代码可知,网络传输使用一个 pipeline 来解决客户端与服务端的消息处理,这里分别描述消息的发送与消息的接收。
消息发送相关 Handler
消息发送的 Handler 即拥有出站功能的 Handler ,按照消息处理顺序如下 :
- encoder
- idleStateHandler
encoder
encoder 即 编码器,出站 Handler,他负责将发送方发送的Message实例编码为 Spark 自定义的消息段格式,以让接收方可以方便进行消息处理。每个消息段的格式如下表所示:
Message 接口代表Spark的消息,有许多的子类代表不同的消息类型。
| Message total Length | Message Type | Body(Option) |
|---|---|---|
| 8 bytes | 1 bytes |
一共有三部分:
- Frame total Length:该消息段的总长度。一共占 8 个字节,存储一个 64 位 long 类型整型。
- Message Type: 消息类型(
Message.type()),以区分消息以使用对应处理方式。占一个字节,存储一个 8 位整型,即最多可以表示 128 种类型。 - Body: 最后就为数据部分,当然一个消息可以不需要数据部分。
idleStateHandler 貌似在消息发送时没有作用,在下一节介绍。
消息接受相关 Handler
消息接受相关的 handler 即具有入站功能的 Handler,按照消息处理顺序如下:
- frameDecoder
- decoder
- idleStateHandler
- transportChannelHandler(
TransportChannelHandler的实例) - chunkFetchHandler
frameDecoder
首先是 frameDecoder,他负责将接收到的 ByteBuf 按照消息格式解码为一个个消息段,使一个 ByteBuf 对应一个完整的消息,如此后面 handler 可以直接放心的使用协议规则去处理每一个传来的ByteBuf.
此外,frameDecoder 可以配置拦截器,拦截器会拦截发送到 frameDecoder 的数据,依次将每个 ByteBuf 灌入到拦截器中做一些其他的操作,且被拦截器拦截的ByteBuf 不会出现在之后的 handler 中,这个过程直到拦截器不再接受数据。Spark 使用拦截器完成数据上传的处理。
decoder
即消息解码器,他负责将 ByteBuf 解码为 Message实例。一个入站 Handler, 与 编码器相对应,他会根据消息段中的 Message Type 来把消息转换为对应类型消息对象(Message)。
idleStateHandler
idleStateHandler 顾名思义是一个空闲状态处理器,他是一个双向处理器,即入站出站处理器。他负责检查 channel 的活跃状况——在一定时间内(默认 120s)是否有读写操作,如果持续没有任何操作,就向其后的 handler 发送一个 IdleStateEvent,触发其后的 handler 的 userEventTriggered 方法来进行一些处理。
transportChannelHandler
transportChannelHandler 是需要模块用户参与定义的 处理器,该处理器同时可以定义服务端对请求消息的处理与客户端对响应消息的处理。用户使用 org.apache.spark.network.server.RpcHandler 参与消息处理的定义。
每个TransportChannelHandler 都包含一个 TransportClient ,这可以让服务端使用现存的 channel 响应消息。所以他提供了双向传输的功能,服务端可以作为客户端向另一方发送请求,虽然带入了一定的复杂性,但避免了 channel 冗余。
transportChannelHandler 还负责处理超时(空闲)连接,transportChannelHandler 在 idleStateHandler 之后,他实现的 userEventTriggered 方法,idleStateHandler 发送超时事件过来的时候,如果发现在配置的超时时间内没有进行任何写入或读取操作,就将持有的 TransportClient 设为超时并关闭 Netty 的 ChannelHandlerContext,避免资源的无效占用。另外还会检查是否还存在没有被响应的请求,有表示是非正常超时,连接可能已经死亡,会打出 error 级别的日志给与提醒。
chunkFetchHandler
chunkFetchHandler 只有在使用网络传输的模块为 shuffle 时才会被设置到 pipeline 上。这个入站 Handler 是为了处理 ChunkFetchRequest 块获取请求而专门准备的。为了避免同时几百个块请求作用在同一个 server 上时,因为磁盘访问的阻塞导致整个server被阻塞而不能接受其他的请求。
总的来说,利于 netty, 该 pipeline 定义了 Spark 消息与数据传输的基本过程。主要包括按照消息格式解编码数据、处理超时、以及需要外部自定义的消息处理过程。简化了传输的实现。
传输处理器 TransportChannelHandler
TransportChannelHandler 是处理网络消息的主要抽象,他的构建过程如下:
code 2
public class TransportContext {
...
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());
return new TransportChannelHandler(client, responseHandler, requestHandler,
conf.connectionTimeoutMs(), closeIdleConnections, this);
}
...
}
他需要一个 TransportClient、响应与请求处理器、TransportContext 以及超时与是否处理空闲连接的配置。而 请求处理器TransportRequestHandler需要一个RpcHandler 来参与定义请求的处理。而对于 TransportResponseHandler 他只根据响应的类型与id执行每个请求预定义的回调函数完成响应的处理。
在 TransportChannelHandler 读取到消息后,会根据消息的类型,调用请求处理器或响应处理器的处理方法,特别的,他不会处理ChunkFetchRequest 消息,如 code3 所示:
code 3
public class TransportChannelHandler extends SimpleChannelInboundHandler<Message>
@Override
public void channelRead0(ChannelHandlerContext ctx, Message request) throws Exception {
// 调用具体的处理器处理
if (request instanceof RequestMessage) {
requestHandler.handle((RequestMessage) request);
} else if (request instanceof ResponseMessage) {
responseHandler.handle((ResponseMessage) request);
} else {
ctx.fireChannelRead(request);
}
}
@Override
public boolean acceptInboundMessage(Object msg) throws Exception {
// 是块获取请求,则不理睬。
if (msg instanceof ChunkFetchRequest) {
return false;
} else {
return super.acceptInboundMessage(msg);
}
}
}
所以 TransportChannelHandler更像是一个代理处理器,只有他显露给 Netty pipeline,而具体的处理过程是由具体的两个处理器兄弟完成的。
另外上面提到的在 TransportChannelHandler 中的超时处理逻辑如下 code 4,可以发现传送给他的 client,只是为了设置超时。而这里的超时的判断标准是 channel 是否长时间没有再有读写请求,而不会顾忌还有没有未完成的请求。:
code 4
public class TransportChannelHandler extends SimpleChannelInboundHandler<Message>
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
IdleStateEvent e = (IdleStateEvent) evt;
synchronized (this) {
// 检查是否超时
boolean isActuallyOverdue =
System.nanoTime() - responseHandler.getTimeOfLastRequestNs()

本文详细解读Spark源码中网络传输的实现,基于Netty框架,涵盖pipeline设置、消息处理、客户端与服务端的角色,以及TransportChannelHandler、TransportClient和TransportServer的工作原理。讲解了消息发送的encoder、接收的frameDecoder和IdleStateHandler等组件,以及RPC请求处理和流数据传输的流程。
最低0.47元/天 解锁文章
2100

被折叠的 条评论
为什么被折叠?



