有了Flink 数据传输(1)的基础,接下来看看数据在一个job中的传输细节
先通过一个job了解一下数据传输的流程,再拓展到跨TaskManager,之后讨论taskmanager之间使用netty进行数据传输的相关细节
文章目录
一、数据传输
1. 本地的数据传输
下图展示了一个job的StreamGraph,逻辑比较简单,将数据拉平,然后sum聚合后输出到flink的控制台,其中sum和sink print组成了一个算子链,这减少了他们之间数据通讯的成本,提高了传输效率。
接着我们以FlatMap所在线程与下游 sum() 所在线程的通信为例,讨论下数据是如何在上下游算子之间传输的。
这里提到的线程其实就是slot
值得关注的是这两个线程共享同一块内存,数据交互通过wait()/notifyAll来同步。具体过程如下:
当FlatMap所在线程写入结果到ResultSubPartition并flush结果到Buffers之后,RS将会唤醒(inputChannelWithData.notifyAll())sum所在线程。唤醒后,从Buffers中获取数据,经过反序列化传递给用户代码处理。
当没有buffer可以消费时,sum所在线程将会堵塞(inputChannelWithData.wait())
当有buffer且唤醒了sum所在线程,但是sum没有能力消费时,这时因为Buffers没有被及时释放,所以FlatMap就不能继续flush数据,这就形成了反压,接着反压现象会传递到source。
2. 远程的数据传输
远程线程的算子间的数据传递和本地类似,不同在于通讯和Buffer的处理:
当下游算子没有Buffer可以消费时,会通过PartitionRequestClient向FlatMap所在进程发起RPC请求,
远程的PartitionRequestServerHandler接到请求之后,读取ResultPartitionManager的Buffer,并返回给下游算子对应的client。
3. 同一线程的数据传输
如上图的算子链:sum->Print to std,两个算子是在同一个线程中(同一个slot中)运行,数据不需要序列化和反序列化,也不需要共享的Buffers。当sum处理完数据后会调用Collector发送数据,sink这边调用processElement方法接收并处理数据。
二、Buffer的读写模型
1. MemorySegment
通过上面的分析知道,跨线程的数据传递都需要经历数据的序列化、flush Buffer、read Buffer、数据反序列化的过程。
MemorySegment抽象了Flink内存管理,代表了Flink管理的一块内存,其中有两个主要的实现类:HeapMemorySegment代表堆内内存的读写,HybridMemorySegment代表了堆外内存的读写。
1.1. MemorySegment角度下的数据处理
观察下数据在MemorySegment的处理过程
当Task1有Buffer空间时,“A”被Task1处理序列化到LocalBufferPool(缓冲池1)中,接着被发送到Task2的LocalBufferPool(缓冲池2)中,Task2读取“A”然后再反序列化,交由程序处理。
1.2. 本地传输
当task1,2运行在一个TaskManager节点时,buffer可以直接交给下一个task。当Task2消费了该Buffer,buffer就会被缓冲池1回收。如果这时Task2处理速度比Task1慢,那Buffer的回收速度就赶不上Task1取用Buffer的速度,导致缓冲池1无可用的Buffer,Task1就堵塞等待,这就形成了反压,也就是Task1的降速。反压会逆向传递,直到source算子。
1.3 远程传输
当Task1,2不在一个TaskManager运行时,Buffer会被发送到网络(TCP Channel)后,等接收端消费完Buffer后再回收。
传输过程
Task1通过Netty的水位值机制保证不往网络中写入太多数据。如果Netty输出的缓冲字节数导致网络中数据超过了高水位值,那Netty会等到其降低(接收端消费了数据)到低水位值才继续写入数据。这保证了网络中不能有太多数据。
Task2会从LocalBufferPool中申请Buffer,然后拷贝网络中的数据到buffer中。如果池中没有可用的Buffer,则会停止从TCP连接中读取数据。
反压
此时如果Task2停止消费网络中的数据,网络中的缓冲数据就会堆积,当到达Netty水位时,Task1也会停止发送缓存。
同时因为Buffer中的数据没有被消费,Task1的Buffer就不能回收到缓冲池,之后到达Task1的数据因为申请不到Buffer而堵塞了往ResultSubPartition中写数据。
2. 了解Netty水位
2.1 数据通过netty进行的网络传输
当使用Netty作为网络通信框架时,业务数据传输之前,先将数据发送到Netty的缓冲区中,然后再发送到TCP的缓冲区,最后发送到网路中。
2.2 水位
Netty通过高低水位控制向Netty缓冲区写入数据的多少。
向Netty缓冲区写入数据时,会判断写入的数据总量是否超过了设置的高水位值,如果超过了就设置通道(Channel)不可写状态。当Netty缓冲区中的数据写入到TCP缓冲区之后,Netty缓冲区的数据量变少,当低于低水位值的时候,就设置通过(Channel)可写状态。
// 代码位置: io.netty.channel.DefaultChannelPipeline.HeadContext#write
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
unsafe.write(msg, promise);
}
// 代码位置: io.netty.channel.AbstractChannel.AbstractUnsafe#write
@Override
public final void write(Object msg, ChannelPromise promise) {
ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
// 数据写入到Netty缓冲区
outboundBuffer.addMessage(msg, size, promise);
}
// 代码位置: io.netty.channel.ChannelOutboundBuffer#addMessage
public void addMessage(Object msg, int size, ChannelPromise promise) {
Entry entry = Entry.newInstance(msg, size, total(msg), promise);
if (tailEntry == null) {
flushedEntry = null;
tailEntry = entry;
} else {
Entry tail = tailEntry;
tail.next = entry;
tailEntry = entry;
}
if (unflushedEntry == null) {
unflushedEntry = entry;
}
// 高水位判断
incrementPendingOutboundBytes(entry.pendingSize, false);
}
// 代码位置: io.netty.channel.ChannelOutboundBuffer#incrementPendingOutboundBytes(long, boolean)
private void incrementPendingOutboundBytes(long size, boolean invokeLater) {
if (size == 0) {
return;
}
long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, size);
// 如果Netty缓冲区的数据总量已经超过高水位,则设置不可写状态
if (newWriteBufferSize > channel.config().getWriteBufferHighWaterMark()) {
setUnwritable(invokeLater);
}
}
// 默认的高低水位值
private static final int DEFAULT_LOW_WATER_MARK = 32 * 1024;
private static final int DEFAULT_HIGH_WATER_MARK = 64 * 1024;
参考
https://www.jianshu.com/p/5748df8428f9