Flink源码解析(十九)——Flink Task Netty数据通信过程解析
在前两篇随笔中解析了Flink Task数据读写过程,其中Task写数据逻辑最终将数据Buffer存放到ResultSubpartition的buffers队列中,依据上下游Task节点分布关系,Task读数据逻辑分别从LocalInputChannel或RemoteInputChannel读取上游Task发送的数据。本篇随笔继续解析上下游Task数据交互过程,即Flink利用Netty通信机制将ResultSubPartition的buffers队列数据发送到下游Task RemoteInputChannel中的过程。
Netty通信框架实现过程主要包含客户端和服务端两种,通常情况下Flink TaskManager即是客户端又是服务端,客户端用于请求读取上游Task数据,服务端用于响应发送数据到下游Task中。而Netty请求响应数据逻辑主要在ChannelHandler组件中实现。下面分别解析客户端ChannelHandler、服务端ChannelHandler实现过程及数据交互过程。
一、Netty Client&Server初始化过程
1、NettyShuffleEnvironment实例的创建:
在随笔十四第二节中解析了TaskManager启动过程,其中涉及到ShuffleEnvironment组件的生成,在该组件中就涉及到Netty客户端、服务端创建过程。TaskManagerServices.fromConfiguration(...)方法是创建NettyShuffleEnvironment组件的入口,可知在NettyShuffleEnvironment组件创建完成后马上调用start()方法开始Netty客户端、服务端初始化过程。下面先解析NettyShuffleEnvironment实例的创建。
获取ShuffleEnvironment上下文信息,生成NettyShuffleServiceFactory实例对象,通过该工厂类生成NettyShuffleEnvironment类型的ShuffleEnvironment实例。
在创建NettyShuffleEnvironment实例时会先生成Netty类型的连接管理器ConnectionManager,即NettyConnectionManager类型。
在NettyConnectionManager构造函数中会构建TaskManager的Netty服务端、Netty客户端、PartitionRequestClient实例和Netty客户端服务端有关的ChannelHandler信息。
如刚开始所述,NettyShuffleEnvironment实例创建完成后会调用NettyShuffleEnvironment实例的start()方法,继而进入到NettyConnectionManager.start()方法中。
2、Netty客户端、Netty服务端初始化过程:
(1)、先来简述下Netty Bootstrap启动过程涉及到的8个基本步骤。
1)、设置EventLoopGroup线程组。
2)、设置Channel通道类型。
3)、设置监听端口。
4)、设置通道参数。
5)、装配Handler流水线。
6)、启动、端口绑定
7)、等待通道关闭。
8)、关闭EventLoopGroup线程组。
(2)、客户端初始化,客户端初始化阶段比较简单,只是新建一个bootstrap引导实例并设置一些连接参数,在当前阶段Task并未启动也不知道该连哪个节点,也未装配Netty重要的Handler流水线信息。
创建客户端bootstrap引导类实例。
设置bootstrap引导类实例的EventLoopGroup线程组信息,设置通道类型。
设设置通道参数。
以上即为NettyClient初始化过程,等Task启动开始后会继续设置Netty剩下的重要工作。
(3)、服务端初始化,在TaskManager启动过程中,NettyServer初始化过程比较完整。创建服务端bootstrap引导类实例,添加ChannelHandlers流水线信息,最后启动了server服务。
首先准备好ChannelHandler流水线装配信息,开始NettyServer初始化工作。
ChannelHandler流水线装配信息如下,包含SSLHandler、消息编码器、解码器、PartitionRequestServerHandler分区请求服务端处理器、PartitionRequestQueue分区请求队列Handler等。
NettyServer初始化开始后新建Bootstrap引导类实例,并设置EventLoopGroup线程组信息,设置通道类型等。
启动,绑定TaskManager节点端口,设置连接参数。装配ChannelHandler流水线信息等。
后面会详细解析PartitionRequestServerHandler分区请求服务端处理器、PartitionRequestQueue分区请求队列Handler功能。
以上即为NettyClient、NettyServer初始化过程。
二、客户端请求数据
在随笔十六中介绍StreamTask初始化过程时提到StreamTask.restoreGates()方法,方法中有2个比较重要的步骤。其中initializeStateAndOpenOperators()负责初始化算子链中各个算子及状态。方法末尾有第二步重要操作,NettyClient请求数据操作。即遍历InputGate实例,依次调用inputGate.requestPartitions()方法,各自请求数据操作。
(1)、NettyPartitionRequestClient实例创建。
如下图转入internalRequestPartitions()方法中,遍历InputGate的InputChannel集合,请求对应的子分区数据。
在Netty通信中,着重分析RemoteInputChannel请求数据过程。在前面提到TaskManager启动阶段NettyClient初始化操作并不完整,没有设置Handler流水线等重要信息,而在StreamTask初始化阶段Task启动信息已比较完整,可以开始NettyClient的Handler流水线设置操作,该操作包含在PartitionRequestClient实例创建过程中。PartitionRequestClient实例创建完成后开始请求分区数据,后面详述请求分区数据行为。由此可知每个RemoteInputChannel都包含一个partitionRequestClient,每个RemoteInputChannel对应上游一个Task实例,这些上游Task实例通常分布在不同的节点。
NettyPartitionRequestClient实例创建过程如下。
在connect(...)方法中,NettyClient.connect(...)负责完成客户端Handler流水线的设置,设置的Handler包含消息编码器、解码器及CreditBasedPartitionRequestClientHandler基于信用值的分区请求客户端处理器。客户端Handler流水线设置完成后,生成NettyPartitionRequestClient实例,由此实例开始请求分区数据。
(2)、发起子分区数据请求。
NettyPartitionRequestClient实例开始请求上游子分区数据,requestSubpartition(...)方法第二个参数代表上游Task的结果子分区index,可知同一个InputChannel请求上游同一个算子不同Task实例的相同index的结果子分区。Flink数据读写过程基于信用值实现,因此客户端在开始请求数据时会主动发送自己的信用值,即inputChannel.getInitialCredit(),信用值即为客户端用于接收数据的空闲Buffer数。PartitionRequest实例创建完成后,客户端通过Channel.writeAndFlush(...)方法将消息实例发送到服务端。
至此,客户端信用值发送及子分区数据请求操作完成,下面继续解析服务端相应数据操作。
三、服务端响应数据
以Netty通信框架实现的Flink数据交互过程,在服务端响应数据时通常是通过调用ChannelHander.channelRead()方法实现。在上面解析NettyServer初始化时,Handler流水线里提到PartitionRequestServerHandler处理器。Flink就是通过PartitionRequestServerHandler.channelRead()调用channelRead0()方法实现数据响应操作。
如下图,在PartitionRequestServerHandler.channelRead0()方法实现中会为每一个PartitionRequest创建一个CreditBasedSequenceNumberingViewReader类型的reader,每个reader都有一个初始信用值凭证,初始值大小等于RemoteInputChannel独占Buffer数。
reader创建后接着会创建一个PipelinedSubpartitionView类型的ResultSubpartitionView实例,reader通过ResultSubpartitionView实例来从对应的ResultSubpartition读取数据。
梳理下ResultPartition、ResultSubPartition、InputChannel的关系。在上下游Task数据交互中,一个上游Task实例对应一个ResultPartition,每个ResultPartition通常有多个ResultSubPartition,每个ResultSubPartition对应一个下游Task实例。每个下游Task实例会请求ResultPartition里的一个ResultSubPartition。如果上游一个Task实例有4个ResultSubPartition,下游就有4个Task实例来消费,每个Task实例都会发起一个PartitionRequest请求,上游Task实例就会创建4个reader,一个reader对应读取一个ResultSubPartition。
ResultSubpartitionView实例创建完成后会开始数据的发送操作。
在ChannelHandler内部实现中fireUserEventTriggered(...)方法会调用userEventTriggered(...)方法,如下图,PartitionRequestQueue会将reader放到可用reader队列里,然后触发数据的读取操作。
可知reader在读取数据Buffer后会通过channel.writeAndFlush(...)方法将数据发送给下游NettyClient端。
在reader读取数据过程中可知会判断buffer类型是否是数据Buffer并且服务端信用值会减去1判断是否大于0,标识NettyClient端是否有可用Buffer来接收服务端发送的数据。
ResultSubpartitionView实例通过PipelinedSubpartition实例获取数据。PipelinedSubpartition实例从自己的buffers队列里获取已有的数据。而buffers队列数据的生成可参考随笔十七中Task写数据过程。
PipelinedSubpartition实例从自己的buffers队列里获取数据后会将自己的buffersInBacklog变量减1,代表结果子分区数据积压数减1。
以上是上下游Task实例在开始阶段下游客户端Task实例向上游服务端Task实例主动请求数据的过程。如果Flink应用运行过程中暂时没有数据产生后又恢复生产数据,此时客户端不知道上游服务端数据生成情况,不能及时请求数据。这种情况下数据一般由服务端主动发送数据。服务端主动发送数据情况分为两种,一是ResultSubpartition每写满一个buffer数据后会主动尝试发送数据,二是在定时调用flush()方法时主动发送数据。两种情况对应的实现如下:
以上为上游Task实例以服务端身份发送buffer数据过程,接下来解析下游Task客户端接收数据过程。
四、客户端接收数据
下游Task客户端接收数据的入口是CreditBasedPartitionRequestClientHandler.channelRead(...),Handler接收到消息数据后会先进行解码操作,然后获取对应的RemoteInputChannel并判断处于可用状态,最后将接收到的Buffer数据存放在RemoteInputChannel.receivedBuffers队列中。
将接收到的Buffer数据存放在RemoteInputChannel.receivedBuffers队列中。
如果receivedBuffers队列之前处于空闲状态,则存放数据Buffer之后RemoteInputChannel会将自己重新入队到InputGate.inputChannelsWithData队列中,让InputGate可以消费RemoteInputChannel中接收的数据。
RemoteInputChannel会将自己重新入队到InputGate.inputChannelsWithData队列中。
唤醒inputChannelsWithData对象上阻塞的线程,让InputGate可以消费RemoteInputChannel中接收的数据。
五、客户端Credit信用值反馈
RemoteInputChannel将自己重新加入到InputGate.inputChannelsWithData队列后紧接着会反馈当前Task客户端最新的信用值,期望上游Task发送多少数据Buffer。反馈入口为RemoteInputChannel.onSenderBacklog(...)
RemoteInputChannel需要的空闲Buffer数=上游积压的Buffer数据量+初始信用值,可知客户端需要的Buffer数略大于上游Task生产者当前产生的Buffer数。
如果RemoteInputChannel没有足够的空闲Buffer数则会向LocalBufferPool申请浮动的Buffer。申请到多少浮动Buffer就增加多少信用值,如果申请不到Buffer就将当前BufferManager加入到LocalBufferPool监听队列中,等LocalBufferPool有可用的Buffer时触发BufferManager的Buffer申请操作。
将上一步申请到的浮动Buffer数作为信用值发送给上游Task服务端,让服务端判断还能发送多少Buffer数据。
以上是RemoteInputChannel.onSenderBacklog()方法申请到的Buffer数作为信用值反馈给上游Task服务端。在Flink系统中还有两个地方会触发信用值的反馈,一是LocalBufferPool回收空闲Buffer时会将Buffer分配给等待队列中的申请者并触发信用值反馈。二是RemoteInputChannel独占Buffer队列中的Buffer元素被释放时会增加信用值并反馈给上游Task服务端。
下图是第一种情况,LocalBufferPool回收空闲Buffer时会将Buffer分配给等待队列中的申请者并触发信用值反馈。
下图是第二种情况,RemoteInputChannel独占Buffer队列中的Buffer元素被释放时会增加信用值并反馈给上游Task服务端。
以上是客户端反馈信用值的过程,下面继续解析服务端接收信用值的处理过程。
六、服务端接收并解析Credit信用值消息
上游Task服务端调用PartitionRequestServerHandler.channelRead0(...)方法接收credit信用值,增加相应的reader信用值并将该reader添加到可用reader队列中使这个reader可以继续轮询读取ResultSubPartition的数据。如下图所展示其过程。
上述是服务端接收信用值反馈消息的处理过程。
总结下,下游Task客户端主动发送初始信用值后,上下游Task之间将循环进行这个数据交互过程,上游Task服务端接收信用值并发送数据到下游Task客户端,下游Task客户端反馈新的信用值credit给上游Task,使数据进行持续的产生和消费。