Flink系列知识讲解之:深入了解 Flink 的网络协议栈

Flink 的网络协议栈是组成 flink-runtime 模块的核心组件之一,也是每个 Flink 任务的核心。它连接着来自所有任务管理器的各个工作单元(子任务)。这是流数据流过的地方,因此对 Flink 作业的吞吐量和延迟性能都至关重要。与通过 Akka 使用 RPC 的任务管理器和工作管理器之间的协调通道不同,任务管理器之间的网络堆栈依赖于使用 Netty 的低级应用程序接口。

本文初步介绍Flink的网络协议栈实现原理和各种优化策略,以及Flink在吞吐量和延迟之间的权衡。

网络协议栈-逻辑视角

Flink 的网络协议栈在子任务之间进行通信时,例如在 keyBy() 表示的网络shuffle过程中,为子任务提供了以下逻辑视图。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Flink会将整个数据流DAG切分成多个subtask子任务的拓扑结构来表示。上图显示了上下游子任务间的网络shuffle逻辑图,它对以下三个概念的不同设置进行了抽象:

  • 子任务输出类型(ResultPartitionType)
    • 流水线(有界或无界):数据产生后立即向下游发送,可能是逐一发送,可以是有界记录流,也可以是无界记录流。
    • 阻塞:只在产生完整结果时才向下游发送数据。
  • 子任务调度类型:
    • 同时部署(急迫): 同时部署作业的所有子任务(用于流式应用)。
    • 在首次输出的下一阶段部署(懒惰): 一旦下游任务中的任何一个生产者产生了输出,就立即部署下游任务。
    • 完整输出后的下一阶段部署: 当任何或所有下游任务的生产者都生成了完整的输出数据集时,再部署下游任务。
  • 吞吐:
    • 高吞吐量: Flink 不会逐一发送每条记录,而是将大量记录缓冲到网络缓冲区,然后一起发送。这降低了每条记录的成本,提高了吞吐量。
    • 通过缓冲区超时降低延迟: 通过缩短发送未完全填满缓冲区的超时时间,可以牺牲吞吐量来换取低延迟。

我们将在下面介绍网络协议栈物理层的章节中了解吞吐量和低延迟优化。在这一部分,让我们再详细介绍一下输出和调度类型。首先,我们必须知道,子任务输出类型和调度类型密切相关,只有两者的特定组合才有效。

流水线式结果分区(result partitions)是一种流式输出,需要一个实时的下游目标子任务来发送数据。该下游子任务可以在上游子任务产生结果之前或首次输出时被调度启动。批处理任务会产生有界的结果分区,而流水作业则会产生无界的结果。

批处理任务也可能以阻塞方式生成结果,这取决于所使用的操作符和连接模式。在这种情况下,必须先生成完整结果,然后才能调度起下游的接收任务。这样,批处理任务就能以更低的资源使用率更高效地工作。

下表总结了有效的组合:

Output Type Scheduling Type Applies to...
pipelined, unbounded all at once next stage on first output Streaming jobs n/a¹
pipelined, bounded all at once next stage on first output n/a² Batch jobs
blocking next stage on complete output Batch jobs

¹ 目前Flink还未使用。

² 批处理/流处理统一完成后,这可能会适用于流作业。

网路协议栈-物理传输

为了理解数据的物理传输,请记住,在 Flink 中,不同的子任务可以通过插槽共享组(slot sharing groups)共享同一个插槽。TaskManager也可以提供多个插槽,以便将同一任务的多个子任务调度到同一个TaskManager上。

在下图示例中,我们假设并行度为 4,部署两个TaskManager,每个TaskManager提供 2 个插槽(slot)。TaskManager 1 执行子任务 A.1、A.2、B.1 和 B.2,TaskManager 2 执行子任务 A.3、A.4、B.3 和 B.4。在任务 A 和任务 B 之间的网络shuffle连接中,例如利用 keyBy() 进行的连接,每个TaskManager需要处理 2x4 个逻辑连接,其中有些是本地连接,有些是远程连接:

在 Flink 的网络协议栈中,不同任务之间的每个(远程)网络连接都有自己的 TCP 通道。但是,如果同一任务的不同子任务被调度到同一个TaskManager上,它们与下游的网络连接将被多路复用,共享一个 TCP 通道,以减少资源使用。因此,需要注意在Flink中,网络数据交换是发生在TaskManager之间的,而不是task之间,在同一个TaskManager中的不同task会复用同一个TCP网络连接。

在我们的示例中,这将适用于 A.1 → B.3、A.1 → B.4,以及 A.2 → B.3 和 A.2 → B.4,如下图所示:

每个子任务的输出结果被称为结果分区(ResultPartition),每个分区又被分成不同的结果子分区(ResultSubpartition)--- 每个逻辑通道一个。此时,Flink 不再处理单个记录,而是将一组序列化记录放入到网络缓冲区中。 每个子任务在其本地缓冲池(发送端和接收端各一个)中可使用的缓冲区数量最多限制为:

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

单个 TaskManager 上的缓冲区总数通常不需要配置。如有需要,请参阅配置网络缓冲区文档,了解如何进行配置。

反压(一)

每当子任务的发送缓冲区池用完时(缓冲区位于结果子分区的缓冲区队列或下层 Netty 支持的网络堆栈内),生产者就会受阻,无法继续工作,并承受反向压力(Backpressure)。接收器的工作方式与此类似:下层网络堆栈中任何传入的 Netty 缓冲区都需要通过网络缓冲区提供给 Flink。如果相应子任务的缓冲池中没有可用的网络缓冲区,Flink 将停止从该通道读取数据,直到有可用的缓冲区为止。 这将对该子任务上的所有上游发送子任务造成有效的反向压力,因此也会扼杀其他接收子任务。下图说明了子任务 B.4 的反压情况,它将对上游任务造成反向压力,并阻止子任务 B.3 接收和处理更多缓冲区,即使它仍有容量。

为了防止这种情况发生,Flink 1.5 引入了自己的流量控制机制。

基于Credit-based的流控

基于Credit-based的流量控制可确保接收方有能力处理接收到的任何内容。它基于网络缓冲区的可用性。在这种流控机制下,每个远程输入通道都有自己的一组专属缓冲区(exclusive buffers),而不是只有一个共享的本地缓冲池。相反,本地缓冲池中的缓冲区被称为浮动缓冲区(floating buffers),因为它们会四处浮动,每个输入通道都可以使用。

接收方将向发送方发送基于credit的缓冲区可用性(1 buffer = 1 credit)。每个结果子分区(ResultSubPartition)都将跟踪其信道credit (channel credits)。当接收方将自己的可用credit值发给上游的输出方时,输出方会判断该credit值,只有在credit可用的情况下(credit > 0)才会被转发到下层网络协议栈尝试往下游发送。同时,每发送一个缓冲区的数据,credit值就会减少一个。在发送端,除了发送缓冲区的数据外,还会发送有关当前积压大小的信息,其中z说明上游有多少缓冲区正在该子分区的队列中等待被消费。接收方根据该积压信息会去请求适当数量的浮动缓冲区(Floating Buffers),以加快积压处理速度。它将尝试获取与积压大小一样多的浮动缓冲区,但这并不总是可能的,我们可能会只得到一些缓冲区或根本没有缓冲区。接收器将使用已获取的缓冲区,并等待有更多缓冲区可用时再继续申请使用。

比如,上图显示了credit-based的流控机制流程:

  • TaskManager 2的子任务B.4的独占缓冲区(exclustion buffers)只包含了两个buffer缓冲池,因此会通知给上游TaskManager 1的子任务A.2,告知其channel credit = 2。
  • Subtask A.2发送端根据下游接收端发送的credit值,选择发送2个buffer缓冲区的数据到下游,同时还会发送当前输出队列中积压的大小信息,比如这里的Backlog size = 5。
  • Subtask B.4接收端的独占缓冲区接收Subtask A.2发送的2个buffer缓冲区的数据,此时独占缓冲区中没有额外的可用缓冲区可以使用,因此更新其channel credit = 0。
  • Subtask B.4接收端接收到Backlog size的积压大小后,向共享的浮动缓冲区申请可用的buffer缓冲池,以此来加快处理上游的积压数据。
  • 如果能申请到额外的可用缓冲池,则可以继续处理上游的积压数据。否则,会将新的channel credit=0发送给Subtask A.2,从而使用Subtask A.2无法继续发送上游数据,从而产生反压。

credit-based的流量控制将使用`buffers-per-channel来指定独占缓冲区的数量(必选),并使用 floating-buffers-per-gate来指定本地缓冲区池的数量(可选),从而达到与无流量控制时相同的缓冲区限制。选择这两个参数的默认值,是为了在网络健康、延迟正常的情况下,使用流量控制时的最大(理论)吞吐量至少与不使用流量控制时相同。您可能需要根据实际的往返时间和带宽来调整这些参数。

对于本地缓冲池的数量设置来说,如果没有足够的可用缓冲区,每个缓冲池将获得相同份额的全局可用缓冲区(± 1)。

反压(二)

与没有流量控制的接收方反压机制相比,credit值提供了更直接的控制:如果接收方的消费速度无法跟上上游的生成速度,其可用credit值最终将为 0,从而阻止发送方将缓冲区转发到下层网络协议栈。同时,只有这个逻辑通道存在向上的反压,不需要阻止其他接收端从复用的 TCP 通道读取数据。因此,其他接收方在处理可用缓冲区时不会受到影响。

流控机制的作用

有了流量控制,相比与之前的没有流量机制方案,TaskManager间的TCP连接通道不会被过多的数据量所阻塞,从而能够实现多个子任务在复用同一个TCP连接时,一个逻辑通道不会阻塞另一个逻辑通道的处理,以此实现整体资源利用率的提高。

此外,通过对网络中传输数据量的完全控制,我们还能改进检查点对齐(checkpoint alignments):如果没有流量控制,发送端是无法感知到下游接收端的处理能力的,只会源源不断的将数据转发到下层网络协议栈然后发往下游。当一段时间后,接收端的处理速度跟不上数据生产速度,接收端缓冲区已满,源源不断的数据被堆积在网络连接通道中,这种情况下,任何的checkpoint barrier都需要在发送端的缓冲区后面排队,必须等待前面的所有缓冲区都处理完毕后才能开始("障碍永远不会超过记录!")。

不过,接收方需要发送的额外的credit信息可能会带来一些额外成本,尤其是在使用 SSL 加密通道的设置中。此外,单个输入通道无法使用缓冲池中的所有缓冲区,因为独占缓冲区不是共享的。同时,由于存在流控机制,发送端无法立即开始发送尽可能多的可用数据,因此如果上游任务产生数据的速度快于接收到的credit值代表的下游可用缓冲区大小,可能需要更长的时间来发送数据。

虽然这可能会影响任务的性能,但由于流量控制的所有优点,它通常是更好的选择。可能会希望通过增加每个输入通道的独占缓冲区的数量,但代价是使用更多内存。不过,与之前的实现相比,总体内存使用量可能仍然较低,因为较低的网络协议栈不再需要缓冲大量数据,因为总是可以立即将数据传输到 Flink。

在使用credit-based的流量控制时,可能还会注意到一件事:当在发送方和接收方之间的缓冲池设置的较小时,可能会更早地遇到反压。不过,这也是人们所希望的,毕竟缓冲更多数据并不会带来任何好处。如果想缓冲更多数据,但又想保持流量控制,可以考虑通过每门浮动缓冲区(floating-buffers-per-gate)来增加浮动缓冲区的数量。

Advantages Disadvantages
• 多个子任务在复用同一个TCP连接时,一个逻辑通道不会阻塞另一个逻辑通道的处理,提高资源利用率 • 改进了检查点对齐 • 减少内存使用量(减少低层网络中的数据量) • 额外的credit信息 • 额外的积压信息(与缓冲信息捎带,几乎没有开销) • 潜在的往返延迟 • 反压可能会出现较早

注意 如果需要关闭基于credit-based的流量控制,可将此添加到 flink-conf.yaml 中:

taskmanager.network.credit-model: false

不过,该参数已被弃用,最终将与非credit-based流量控制代码一起被删除。

网络协议栈内部原理详解

下图进一步详细介绍了从发送操作符算子收集记录到接收操作符算子获取记录的网络堆栈及其周边组件:

发送端算子创建记录并将其传递(例如通过 Collector#collect())后,记录将被交给 RecordWriter类,RecordWriter 会将记录从 Java 对象序列化为字节序列,最终进入网络缓冲区 ,如上所述进行传递。RecordWriter 首先使用 SpanningRecordSerializer 将记录序列化为灵活的堆上字节数组。然后,它会尝试将这些字节写入目标网络通道的相关网络缓冲区。我们将在下面的章节中讨论最后一部分。

在接收端,下层网络堆栈(netty)会将接收到的缓冲区记录写入相应的输入通道。接收端任务的线程最终会从这些队列中读取数据,并尝试在RecordReader的帮助下,通过 SpillingAdaptiveSpanningRecordDeserializer 将累积的字节反序列化为 Java 对象。与序列化器类似,该反序列化器也必须处理跨越多个网络缓冲区的记录等特殊情况,这可能是因为记录大于网络缓冲区(默认为 32KiB,通过 taskmanager.memory.segment-size 设置),也可能是因为序列化的记录被添加到了网络缓冲区,而该缓冲区没有足够的剩余字节。不过,Flink 会使用这些字节,并继续将其余字节写入新的网络缓冲区。

将缓冲区刷新到 Netty

在上图中,基于credit-based的流量控制机制实际上位于 "Netty 服务器"(和 "Netty 客户端")组件中,RecordWriter 正在写入的缓冲区总是以空的状态(empty state)添加到结果子分区中,然后逐渐填入(序列化的)记录。但 Netty 到底什么时候才能获得缓冲区呢?显然,它不能在字节可用时就获取它们,因为这不仅会因跨线程通信和同步而增加大量成本(需要线程通信频繁地从缓冲区获取字节数据),还会使整个缓冲区过时。

在Flink中,有三种情况可以使缓冲区可供Netty服务器使用:

  • 向缓冲区写入记录时,缓冲区已满
  • 缓冲区超时
  • 发送checkpoint barrier之类的特殊事件。
缓冲区满后刷新

RecordWriter 使用本地序列化缓冲区处理当前记录,并逐步将这些字节写入相应结果子分区队列中的一个或多个网络缓冲区。尽管一个 RecordWriter 可以在多个子分区上工作,但每个子分区只有一个 RecordWriter 在向其写入数据 。另一方面,Netty 服务器会从多个结果子分区读取数据,并如上所述将相应的结果子分区复用到一个通道中。这是一种经典的生产者-消费者模式 ,网络缓冲区位于中间,如下图所示。

  • 在(1)将数据序列化和(2)将数据写入缓冲区后,RecordWriter 会相应地更新缓冲区的writer index。
  • 一旦缓冲区完全填满,RecordWriter将 (3) 从本地缓冲区中获取一个新缓冲区,用于当前记录的剩余字节或下一条记录,并将新缓冲区添加到子分区队列中。
  • 这将 (4) 通知 Netty 服务器数据可用。只要 Netty 有能力处理该通知,它就会 (5) 提取缓冲区并通过适当的 TCP 通道发送。
缓冲区超时后刷新

为了支持低延迟使用场景,我们不能仅仅依靠缓冲区满来向下游发送数据。在某些情况下,某个通信通道可能没有太多记录流过,从而不必要地增加了实际拥有的少量记录的延迟。因此,存在一个周期性的进程(the output flusher),它会周期性地尝试将缓冲区中等待的可用数据发往下游。周期性间隔可通过 StreamExecutionEnvironment#setBufferTimeout 进行配置,并作为延迟的上限(适用于低吞吐量通道)。但是严格来说,output fluster不提供任何保证--它只向 Netty 发送通知,而 Netty 可以随意/按容量接收。这也意味着,如果通道被反压,即使满足了超时条件,也无法将缓冲区数据flush到下游。

下图显示了它与其他组件的交互方式:

  • RecordWriter 会像之前一样序列化并写入网络缓冲区。
  • 与此同时,如果 Netty 尚未意识到数据可用,Output Flusher可能会(3,4)通知 Netty 服务器数据可用(类似于上述 "缓冲区已满 "的情况)。
  • Netty 处理该通知 (5) 时,会从缓冲区中读取可用数据,并更新缓冲区的writer index。缓冲区将保留在队列中--Netty 服务器端对该缓冲区的任何进一步操作都将在下一次继续从reader index中读取数据。
特殊事件后刷新

如果通过 RecordWriter 发送了一些特殊事件后也会立即触发缓冲区刷新。最重要的事件是checkpoint barrier或分区结束事件(end-of-partition events),这些事件显然应该快速处理,而不是等待Output Flusher启动。

字节缓冲区在两个Task之间的传输

上面这张图展示了一个细节更加丰富的流程,描述了一条数据记录从生产者传输到消费者的完整生命周期如下:

  1. 最初,MapDriver生成数据记录(通过Collector收集)并传递给RecordWriter对象。RecordWriter包含一组序列化器,每个消费数据的Task分别对应一个。ChannelSelector会选择一个或多个序列化器处理记录。例如,如果记录需要被广播,那么就会被交给每一个序列化器进行处理;如果记录是按照hash进行分区的,ChannelSelector会计算记录的哈希值,然后选择对应的序列化器。
  2. 序列化器会将记录序列化为二进制数据,并将其存放在固定大小的buffer中(一条记录可能需要跨越多个buffer)。这些buffer被交给BufferWriter处理,写入到ResulePartition(RP)中。RP有多个子分区(ResultSubpartitions-RSs)构成,每一个子分区都只收集特定消费者需要的数据。在上图中,需要被第二个reducer(在TaskManager2中)消费的记录被放在RS2中。由于第一个Buffer已经生成,RS2就变成可被消费的状态了(注意,这个行为实现了一个streaming shuffle),接着它通知JobManager。
  3. JobManager查找RS2的消费者,然后通知TaskManager2一个数据块已经可以访问了。通知TM2的消息会被发送到InputChannel,该inputchannel被认为是接收这个buffer的,接着通知RS2可以初始化一个网络传输了。然后,RS2通过TM1的网络栈请求该buffer,然后双方基于Netty准备进行数据传输。网络连接是在TaskManager(而非特定的task)之间长时间存在的。
  4. 一旦Buffer被TM2接收,它同样会经过一个类似的结构,起始于InputChannel,进入InputGate(它包含多个IC),最终进入一个反序列化器(RecordDeserializer),它会从buffer中将记录还原成指定类型的对象,然后将其传递给接收数据的Task。

数据交换机制的具体实现

数据交换从本质上来说就是一个典型的生产者-消费者模型,上游算子生产数据到ResultPartition中,下游算子通过InputGate消费数据。由于不同的Task可能在同一个TaskManager中运行,也可能在不同的TaskManager中运行:对于前者,不同的Task其实就是同一个TaskManager进程中的不同的线程,它们的数据交换就是在本地不同线程间进行的;对于后者,必须要通过网络进行通信。下图所示分别为数据在一个taskmanager内的流转、以及在不同的taskmanager之间的流转:

缓冲区生成器和缓冲区消费者

如果您想深入了解 Flink 是如何实现生产者-消费者机制的,请仔细研究一下 Flink 1.5 中引入的 BufferBuilderBufferConsumer类。虽然读取可能只针对每个缓冲区,但写入则是针对每条记录,因此在 Flink 中,所有网络通信都是在热路径(hot path)上进行的。因此,我们很清楚,我们需要在任务线程和 Netty 线程之间建立一个轻量级连接,这样就不会产生太多同步开销。如需了解更多详情,建议查看源代码

延迟与吞吐量

引入网络缓冲区是为了提高资源利用率和吞吐量,但代价是某些记录在缓冲区等待的时间会更长一些。虽然可以通过缓冲区超时给出等待时间的上限,但您可能想知道更多关于延迟和吞吐量这两个维度之间的权衡,因为很明显,两者是不可兼得的。

下图显示了缓冲区超时的各种值,从 0(每条记录刷新一次)到 100ms(默认值)不等,并显示了在一个有 100 个节点、每个节点有 8 个插槽(slot)的集群上运行一项没有业务逻辑、因此只测试网络协议栈的作业时产生的吞吐率。为便于比较,我们还绘制了 Flink 1.4 在添加低延迟改进(如上所述)之前的情况。

正如上图所示,在 Flink 1.5 以上版本中,即使缓冲超时时间设置的很低,如 1 毫秒(用于低延迟场景),也能提供高达默认超时 75% 的最大吞吐量。

总结

现在我们已经了解了结果分区(ResultPartition)、不同的网络连接以及批处理和流式处理的调度类型。还了解了基于credit-based的流量控制和网络协议栈的内部工作原理,从而可以推理出与网络相关的调优参数和某些作业行为。本系列未来的博文将以这些知识为基础,深入探讨更多操作细节,包括需要关注的相关指标、进一步的网络协议栈调整以及需要避免的常见反模式。

相关推荐
司晓杰6 小时前
Flink 实时数据处理中的问题与解决方案
大数据·flink
lisacumt6 小时前
【Flink CDC】Flink CDC的Schema Evolution表结构演变的源码分析和流程图
大数据·flink·流程图
严文文-Chris8 小时前
【一个HTTP请求和一个HTTP会话的区别】
网络·网络协议·http
Elastic 中国社区官方博客8 小时前
在不到 5 分钟的时间内将威胁情报 PDF 添加为 AI 助手的自定义知识
大数据·人工智能·安全·elasticsearch·搜索引擎·pdf·全文检索
玉成2269 小时前
Elasticsearch:索引mapping
大数据·elasticsearch·搜索引擎
运维&陈同学9 小时前
【Logstash01】企业级日志分析系统ELK之Logstash 安装与介绍
大数据·linux·elk·elasticsearch·云原生·自动化·logstash
xing.yu.CTF11 小时前
网络协议安全
网络·网络协议·安全
菠萝派爱跨境12 小时前
利用轮换IP的强大功能
大数据·服务器·网络·网络协议·tcp/ip·ip
司晓杰12 小时前
使用 Flink CDC 构建 Streaming ETL
大数据·数据仓库·flink·etl
申尧强12 小时前
flink异步流(async stream)解析
大数据·flink