深入解析Flink网络协议栈

深入解析Flink网络协议栈

原文地址:https://flink.apache.org/2019/06/05/flink-network-stack.html
05 Jun 2019 Nico Kruber

Flink的网络协议栈是Flink的核心组件之一,构成了flink-runtime模块,并且是每个Flink job运行的关键。协议栈连接了所有TaskManager的独立工作单元(subtasks)。这是用户输入的流式数据传输的载体,因此对于Flink job的吞吐量、延时的性能至关重要。TaskManager和JobManager之间的RPC采用Akka,TaskManager之间的网络协议栈采用更底层的Netty。
这篇博客文章是关于网络堆栈的系列文章中的第一篇。在接下来的篇幅中,我们将会首先概览暴露给stream operator的网络抽象,然后详细介绍物理实现和Flink做的各种优化。我们将会主要展现这些优化的效果,以及FLink在吞吐量和延时所做的权衡。本系列博客之后的文章,将会在monitoring,metrics,调优参数以及反模式等方面详细描述。

  • 逻辑视图
  • 物理传输
    • 造成反压力
  • 基于认证的流控制
    • 造成反压力
    • 我们得到了什么?问题在哪里?
  • 将记录写入网络buffer,再次读取他们
    • 将buffer信息给netty
    • buffer Builder & buffer消费者
  • 延时 vs. 吞吐量
  • 结论

逻辑视图

Flink的网络协议栈为subtask之间的通信提供了下面的逻辑视图。例如,keyBy()函数在网络shuffle时的示意图。在这里插入图片描述

上图抽象展示了下面三个概念的不同设置:

  • subtask输出类型(ResultPartitionType):
    • pipelined(bounded 或者unbounded):当有数据产生的时候,立即将数据发送给下游。数据很有可能一个接一个地以有界或无界记录流的形式发送到下游。
    • blocking:只有当整个结果都生成,才将数据发送给下游。
  • 调度模式:
    • all at once(eager):在同一时刻部署job下所有的subtask。(用于流式应用)
    • next stage on first output(lazy):当task的数据生产者产生输出的时候,才部署下游的task。
    • next stage on complete output:当task的数据生产者产生了完整的数据集合,才部署下游的task。
  • 传输
    • 高吞吐量:与一个一个发送记录相反,Flink在网络缓冲区缓冲了一批数据,并将它们一起发出。这降低了单个记录的传输消耗,提升了吞吐量。
    • 通过buffer超时的低延时:通过降低缓冲区数据超时时间,即使缓冲区没有被数据填满,从而让用户可以牺牲吞吐量来降低延时。

我们将会在第二部分描述吞吐量和低延时的优化,涉及了网络协议栈的物理层。对于这一部分,让我们详细说明一下输出和调度类型。首先,重要的是用户要知道子任务输出类型和调度类型是紧密交织在一起的,只有这两种类型的特定组合才是有效的。
pipeline的数据分区是流式风格的输出,需要目标subtask是存活的。subtask需要在结果产生之前或第一个输出之前就被调度完成。批量job生成有边界的结果分区数据,流式job产生无界的结果。
批量job可能也产生阻塞形式的结果,这依赖于operator和链接方式。在这种情况下,在接收数据的task被调度之前,完整的数据必须生成完成。这允许批量job更加高效和低利用率。
下面的表格描述了可用的组合:
在这里插入图片描述
注:N/a1目前Flink没有采用
N/a2当Batch/Streaming整合完成,这种组合才会适用流式job

另外,当subtask有多个输入,调度按两种方式开始:after all或者after any。

物理传输

为了理解物理数据通信,我们回顾一下,在Flink中不同的task可能通过slot sharing group共享同一个slot。TaskManager也可能为同一个task的subtask提供多个slot。
举例如下图。我们假设并行度为4,且两个taskmanager每人提供两个slot。TaskManager1执行A1,A2,B1,B2。TaskManager2执行A3,A4,B3,B4。在shuffle模式下,例如keyby()产生的结果,在每个TaskManager上将会有2X4个逻辑连接。这些连接可能是本地的,也可能是远程的。
在这里插入图片描述
在Flink的网络协议栈中,不同task之间的每个远程网络连接都会占用一个TCP通道。然而,如果同一个task的不同subtask被调度在同一个TaskManager中,他们连接到相同的TaskManager的链接将会被混用,共享一个TCP链接,从而降低资源占用。在我们的实例中,链接为:A.1 → B.3, A.1 → B.4,A.2 → B.3, A.2 → B.4。
在这里插入图片描述
每个子任务的结果被称为ResultPartition,每个结果被分割为单独的resultsubpartition—每个逻辑通道一个。在这种情况下的协议栈,Flink没有单独处理各个记录,而是将一组序列化的记录组装到一起发送到网络缓冲区。每个subtask自己独立使用的本地buffer池(接收和发送各一个)都被限制了大小:

#channels * buffers-per-channel + floating-buffers-per-gate

通常情况下每个TaskManager不需要配置buffer的全部数量。Configuring the Network buffers章节详细描述了怎样设置。

反压(1)

只要有一个subtask缓冲池耗尽,那么所有的数据传输都会被阻塞。如下图:
在这里插入图片描述
B4subtask的缓冲池资源耗尽,则整个公共池资源阻塞,导致整体不可用。
为了防止这种情况,Flink1.5提供了自己的流控制机制。

基于认证的流控制

基于认证的流量控制确保“网络上”的任何东西在接收端都有处理能力。这依赖于使用网络缓存作为之前的Flink机制的一个扩展。不仅仅共享一个本地缓冲池,每个远程输入channel拥有自己的buffer集合,且这些集合的使用是排他的。由于本地缓冲池中的buffer对每个通道都是可用的,相当于在各个通道之间流转使用,所以被称为浮动缓存。
接受者将根据发送端的认证,声明哪些buffer是自己可用的(1个buffer = 1个认证)。每个结果子分区将会保存他的通道认证轨迹。在认证可用的情况下,buffer仅仅面向低层次网络协议栈,每个发送buffer顺次减少认证分。除了buffer,我们也发送当前backlog的大小信息,这些信息描述了子分区队列中有多少个buffer在等待。接受者将会使用这些信息,按照其中的backlog大小申请浮动buffer。但是真实大小是不定的,可能分配一些,也可能根本不分配buffer。接受者将会使用接收到的buffer,并且监听接下来的可用buffer。
在这里插入图片描述
认证流控制使用buffers-per-channel来定义多少buffer是排他的,通过floating-buffers-per-gate定义多少缓冲池作为本地浮动缓冲池。

反压

当接受者不能运行,他可用的信用分将会达到0,接受者将会停止发送数据。只有这个逻辑通道发生反压,混用的TCP通道并没有被阻塞。其他的接受者并不会被影响。

我们得到了什么?问题在哪里?

由于在流控制的情况下,复用通道中的一个逻辑通道,不能阻塞其他的逻辑通道,总体的资源利用率应该增加。另外,通过完全控制有多少数据在“网络中”,我们也能够提升checkpoint对齐:没有流控制,通道将会一定延迟后占满网络缓冲区,然后才能判断出接收方停止读取数据。在这个期间,大量的缓冲数据将被滞留。任何一个checkpoint屏障必须在buffre中排队,等待,直到所有问题被处理。
然而,接收端额外的声明信息将会导致额外的消耗,尤其是使用SSL加密的通道的时候。单个输入通道不能使用缓冲池中所有的缓冲区,因为排他缓冲区不能共享。如果生产数据的速度快于返回认证的速度,将会消耗更多的时间发送数据。虽然可能影响job的性能,但是通常情况下使用流量控制有更高的性能。可以通过增加buffers-per-channel来增加排他缓冲区的大小,代价是占用更多的内存。
另外一个值得注意的是,当使用认证流控制时:由于我们在发送者和接受者之间缓存更少的数据,用户会更早的发现反压。如果想要保留流控制的同时缓冲更多的数据,可以考虑通过floating-buffers-per-gate增加缓存。

在这里插入图片描述
在这里插入图片描述
可以通过设置taskmanager.network.credit-model: false关闭流控制。

将记录写入网络buffer,再次读取他们

下图是上面描述的高层概览图。
在这里插入图片描述
当创建一个对象并传递它,例如通过Collector.collect(),Flink将对象传递给RecordWriter。RecordWriter将对象序列化成二进制,最终将二进制放入网络缓冲。
RecordWriter 首先使用SpanningRecordSerializer将对象序列化成灵活的堆字节数组。然后试着将这些字节写入对应的网络通道buffer。
在接受者端,底层网络协议栈(netty)将接收到的数据buffer写入对应的输入buffer。task的线程将会最终从这些buffer中读取数据。task将数据交给RecordReader反序列化, 具体的操作使用SpillingAdaptiveSpanningRecordDeserializer实现。
和序列化一端类似,反序列化必须处理各种特殊情况,例如对象被切分到多个网络缓冲区中。这可能是因为数据对象过大超过buffer大小(
32KiB by default, set via taskmanager.memory.segment-size),或者因为buffer剩余的大小不够用。

将buffer信息给netty

在上图,基于认证的流控制算法实际上在Netty Server/Client组件中实现,RecordWriter写入的缓冲区总是以空状态添加到结果子分区中,然后逐渐填充(序列化的)记录。但是Netty实际上何时获取buffer?显然系统不可能无论何时数据可用都立即获取他们。因为这样会增加线程间通信和同步的消耗,以及使整个buffer变得过时。
在Flink中,Netty server在三种情况下消费buffer:

  • 写入一个数据后,buffer被填满
  • buffer到了超时时间
  • 接到一个特殊的消息,例如checkpoint屏障。

Flush after Buffer Full

RecordWriter使用当前记录的本地序列化缓冲区,并将这些字节逐渐写入位于相应结果子分区队列的一个或多个网络缓冲区。虽然一个RecordWriter可以在多个子分区上工作,但是每个子分区只能分配给一个RecordWriter。另一边,Netty server从多个子分区读取数据,并混合写入一个通道中。这是使用网络缓冲作为中间件实现生产者-消费者模式的经典场景。当完成1)序列化,2)将数据写入buffer,RecordWriter更新相应的缓冲写index。当buffer被写满,RecordWriter从本地缓冲池申请一个新的缓冲,并将当前记录的剩余字节,或者下一个记录的信息写入新的缓冲。这时如果Netty server还未知晓,将会4)通知Netty server数据可用。无论何时Netty能够处理此通知,Netty server将会获取此buffer,通过tcp通道发送这些数据。
在这里插入图片描述

Flush after Buffer Timeout

为了支持低延迟的场景,我们不能仅仅依赖于buffer写满时触发向下游传输数据。可能存在这样的场景,通信通道并没有太多的数据传输,不需要为了几条数据增加了延时。因此不管数据是否准备好,一个定期的操作会发送数据:输出净化器。可以通过StreamExecutionEnvironment#setBufferTimeout设置周期长度,可以看成延迟的上限。下图展示了flusher怎样同其他组件协作:RecordWriter负责序列化和写入网络缓存,输出flusher负责在超时的情况下通知Netty server。当Netty处理这些通知,他将从缓存消费掉可用数据信息,并更新缓存读索引。Netty server下次会继续从读索引的位置读取数据。
在这里插入图片描述

注意:严格意义上来讲,Flusher并不能保证什么。他只是负责向Netty发送通知。这意味着输出Flusher对于通道是否发生反压是没有任何影响的。

Flush after special event

RecordWriter发出的一些特殊事件也会触发立即清空缓存。最重要的事件就是checkpoint屏障或者end-of-partition事件。

Further remarks

与低于1.5版本的Flink相比,我们直接将网络缓冲放在了子分区队列中,而且在每个flush操作的时候并没有关闭这些buffer。这给我们带来下面几个好处:

  • 更少的同步开销(输出flusher和RecordWriter都是独立的)
  • 在高负载的场景下,Netty达到了瓶颈,我们可以依旧在部分缓冲区中处理数据。
  • Netty通知显著减少

然而,可能会在低负载的情况下发现CPU利用率和TCP发包率有所增加。这是因为当有数据发生,Flink使用任何可用的CPU指令周期来降低延时。一旦负载增加,buffer填满数据的情况增加,Flink会自我调整。高负载场景不会被影响,甚至是因为减少了同步消耗,有更好吞吐率。

Buffer Builder & Buffer Consumer

如果想更深入的了解生产者消费者的原理,可以阅读BufferBuilder和BufferConsumer两个类。

延时 vs. 吞吐量

下图是在延时0-100ms-10000ms情况下,Flink两个版本的吞吐量比较。Flink1.5在100ms的情况下,能够达到最高吞吐量的75%。
在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章