flink的分界线对齐和水印对齐

【背景】

flink的分界线对齐和水印对齐是两个不同的概念,需要加以区分。

另外,还会介绍下flink 1.14提出的缓冲区消胀功能

分界线对齐barrier alignment

在讲分界线对齐之前,得先讲一下flink的checkpoint流程。

flink的checkpoint流程

Flink采用轻量级分布式快照实现应用容错

分布式快照最关键的是能够将数据流切分,Flink中使用Barrier(屏障)来切分数据流。Barrier会周期性地注入数据流中,作为数据流的一部分,从上游到下游被算子处理。Barriers会严格保证顺序,不会超过其前边的数据。Barrier将记录分割成记录集,两个Barrier之间的数据流中的数据隶属于同一个检查点。每一个Barrier都携带一个其所属快照的ID编号。Barrier随着数据向下流动,不会打断数据流,因此非常轻量。在一个数据流中,可能会存在多个隶属于不同快照的Barrier,并发异步地执行分布式快照,如图所示。

具体流程如下:

2.启动checkpoint

在 JobManager 中有一个“检查点协调器”(checkpoint coordinator),专门用来协调处理检查点的相关工作。检查点协调器会定期向 TaskManager 发出指令,要求保存检查点(带着检查点 ID);TaskManager 会向每个数据源(source)任务发送一条带有新检查点ID的消息,通过这种方式来启动检查点。这里可以看出,checkpoint是由jobmanager启动的。

2.source任务的“暂停+发送+恢复”

当source任务收到消息时,它会暂停发出新的数据,在状态后端触发本地状态的检查点保存(保存的内容包括偏移量等算子状态)。状态后端在状态检查点完成保存后会通知source任务,而source任务会向jobmanager确认检查点完成。然后source任务向所有传出的流分区广播带着检查点ID的分界线(barriers),其实就是将带有检查点 ID 的分界线(barrier)插入到当前的数据流中,然后像正常的数据一样像下游传递。通过将分界线注入到输出流中,源函数(source function)定义了检查点在流中所处的位置。

在发出所有分界线后,source任务就可以继续常规操作,发出新的数据了不需要所有checkpoint完成

3.barrier alignment分界线对齐

barrier会被广播到所有连接的并行任务,以确保每个任务从它的每个输入流中都能接收到

这些barriers被注入数据流并与记录一起作为数据流的一部分向下流动。 barriers永远不会超过记录,数据流严格有序。 barriers将数据流中的记录分为进入当前快照的记录和进入下一个快照的记录。每个barriers都带有快照的ID,并且barriers之前的记录都进入了该快照,而barriers之后的数据会包含在之后的检查点中。 barriers不会中断流的流动,非常轻量级。 来自不同快照的多个barriers可以同时在流中出现,这意味着可以同时发生各种快照。

如上图,barriers在数据流源处被注入并行数据流中。快照n的barriers被插入的位置(我们称之为Sn)是快照所包含的数据在数据源中最大位置。例如,在Apache Kafka中,此位置将是分区中最后一条记录的偏移量。 将该位置Sn报告给checkpoint协调器(Flink的JobManager)。

当任务收到一个新检查点的barrier时,它会等待这个检查点的所有输入分区的barrier到达(比如说他有三个输入流,需要从这三个输入流中都获取到barriers n才可以)。在等待的过程中,任务并不会闲着,而是会继续处理尚未提供barrier的流分区中的数据。对于那些barrier已经到达的分区,如果继续有新的数据到达,会被缓存起来(但不会被处理),否则,它会搞混属于快照n的记录和属于快照n + 1的记录(可以这样理解:他有三个输入流a,b,c,首先从a中接收到了barriers n,但是b/c的barriers n还没到,如果他继续处理a的后面的数据,就会导致在处理b/c的barriers n的数据的同时,还会处理了a的barriers n+1的数据,计算状态会混在一起,这个checkpoint也就没法做了)。这个等待所有分界线到达的过程,称为“分界线对齐”(barrier alignment)

当任务从所有输入分区都收到barrier时,它就会在状态后端启动一个检查点的保存(因为这里采用了分界线对齐,因此检查点保存指的是保存状态数据到存储上,而不是保留流数据;如果采用分界线不对齐,则检查点保存指的是保存状态数据+中间的流数据到存储上并继续向所有下游连接的任务广播检查点分界线。所有的检查点barrier都发出后,该任务就开始处理之前缓冲的数据。在处理并发出所有缓冲数据之后,任务就可以继续正常处理输入流了

最终,检查点分界线会到达输出(sink)任务(流式DAG的末端)。当sink任务接收到barrier时,它也会先执行“分界线对齐”,然后将自己的状态保存到检查点,并向job manager确认已接收到barrier。一旦从应用程序的所有任务收到一个检查点的确认信息,job manager就会将这个检查点记录为已完成.

注意:在这个过程中,只要有一个算子A的checkpoint失败,所有算子都需要回滚到上一个checkpoint,而无法单独重试这个失败的算子的checkpoint,因为如果单独重试这个算子A,而不进行回滚,就会发现哪怕让source重新发出之前的barrier,流中的数据也是最新的,而不是前面做checkpoint时的数据,毕竟你没有回滚。估计各种状态也是有所错乱,情况太复杂了,还不如回滚后重新做checkpoint. 

如果分界线不对齐会怎么样

分界线对齐的缺点:

Checkpoint Barrier对齐时,必须等待所有上游通道都处理完,在这个对齐过程中,算子只会继续处理的来自未出现 Barrier Channel 的数据,而其余 Channel 的数据会被缓存写入输入队列而不会被处理,直至在队列满后被阻塞。因此,分布式快照的结束依赖于 barrier 的流动,而反压则会限制 barrier 的流动,屏障到达下游算子的延迟就会变大,导致快照的完成时间变长甚至超时(经常遇到的问题)。无论是哪种情况,都会导致 Checkpoint 的时间点落后于实际数据流较多。这时作业的计算进度是没有被持久化的,处于一个比较脆弱的状态,如果作业出于异常被动重启或者被用户主动重启,作业会回滚丢失一定的进度。如果反压长久不能得到解决,Checkpoint 连续超时且没有很好的监控,快照数据与实际数据之间的差距就越来越明显,一旦作业failover,回滚丢失的进度可能高达一天以上,对于实时业务这通常是不可接受的。更糟糕的是,这种情况下进行回滚,作业恢复后需要重新处理的数据是天量的,又会导致积压,通常带来更大的反压,形成一个恶性循环。这种情况下,一般需要配置kafka的消费lag监控,提前介入

解决思路:不进行分界线对齐

Flink 1.11版本中通过FLIP-76引入了非对齐检查点(unaligned checkpoint)的feature

unaligned checkpoint允许一个算子子任务不需要等待所有上游通道的Checkpoint Barrier,而是当算子的所有输入流中的第一个屏障到达算子的输入缓冲区时(当屏障和数据A一起到达输入缓冲区时,哪怕屏障位于数据A后面,也能越过数据A来提前进行checkpoint),立即进行checkpoint并将这个屏障发往下游(输出缓冲区)。由于第一个屏障没有被阻塞,它的步调会比较快,超过一部分缓冲区中的数据。算子会标记两部分数据:一是屏障首先到达的那条流中被超过的数据,二是其他流中位于当前检查点屏障之前的所有数据(当然也包括进入了输入缓冲区的数据),如下图中标黄的部分所示,可以理解为:第一个到达 Barrier 会在算子的缓存数据队列(包括输入 Channel 和输出 Channel)中往前跳跃一段距离,而被”插队”的数据和其他输入 Channel 在其 Barrier 之前的数据会被写入快照中。

缓冲区消胀(Buffer Debloating)

1.14版本推出的新功能 

背景

Flink 中每条消息都会被放到网络缓冲(network buffer) 中,并以此为最小单位发送到下一个 subtask,以便有效利用快速网络的高带宽。 为了维持连续的高吞吐,Flink 在传输过程的输入端和输出端使用了网络缓冲队列而且会使用部分(或全部)网络缓冲内存

每个 subtask 都有一个输入队列来接收数据和一个输出队列来发送数据到下一个 subtask。 在 pipeline 场景,拥有更多的中间缓存数据可以使 Flink 提供更高、更富有弹性的吞吐量,但是也会增加快照时间。

只有所有的 subtask 都收到了全部注入的 checkpoint barrier 才能完成快照。 

对齐的 checkpoints 中,checkpoint barrier 会跟着网络缓冲数据在 job graph 中流动。 对齐的 Checkpoint 随着数据在毫秒级的时间内流过网络缓冲区缓冲数据越多,checkpoint barrier 流动的时间就越长。

非对齐的 checkpoints 中,缓冲数据越多,checkpoint 就会越大,因为这些数据都会被持久化到 checkpoint 中。

当 Flink 应用出现(暂时的)反压时(例如外部系统反压或遇到数据倾斜),往往会导致网络缓冲区中存放了相对应用当前吞吐(因反压而降低)所需的带宽过多的数据。更加不利的是,缓冲的数据越多意味着 Checkpoint 机制需要做越多的工作。对齐的 Checkpoint 需要等待更多的数据得到处理,非对齐的 Checkpoint 则需要持久化更多排队中的数据。

就轮到缓冲区去膨胀登场了之前,配置缓冲数据量的唯一方法是指定缓冲区的数量和大小。然而,因为每次部署的不同很难配置一组完美的参数。 Flink 1.14 新引入的缓冲消胀机制尝试通过自动调整缓冲数据量到一个合理值来解决这个问题。

它将网络栈从持有最多 X 字节的数据改为持有需要接收端 X 毫秒计算时间处理的数据。默认值是 1000 毫秒,意味着网络栈会缓冲下游任务 1000 毫秒所能处理的数据量。通过持续的测量和调整,系统能够在不断变化的情况下保持这一特性。因此,Flinkk对齐式 Checkpoint 具备了稳定的、可预测的对齐时间,反压时存放在非对齐式 Checkpoint中的数据量也极大程度减少了。(自己的理解:下游缓存数据量少了,这里少缓存的数据应该不是丢了而是让下游因为没有网络缓存而发送credit为0,通知上游不要再发数据了

总结来说,缓冲消胀功能计算 subtask 可能达到的最大吞吐(始终保持繁忙状态时)并且通过自动调整缓冲数据量(网络内存的用量)来使得数据的消费时间达到配置值,在确保高吞吐的同时最小化缓冲区中的数据量,最终可以最小化 Checkpoint 的延迟和开销。

缓冲区去膨胀可以作为非对齐式 Checkpoint 的补充,甚至是替代选择

配置

可以通过设置 taskmanager.network.memory.buffer-debloat.enabled true 来开启缓冲消胀机制。 通过设置 taskmanager.network.memory.buffer-debloat.target duration 类型的值来指定消费缓冲数据的目标时间。 默认值应该能满足大多数场景。

这个功能使用过去的吞吐数据来预测消费剩余缓冲数据的时间。如果预测不准,缓冲消胀机制会导致以下问题:

  • 没有足够的缓存数据来提供全量吞吐。
  • 有太多缓冲数据对 checkpoint barrier 推进或者非对齐的 checkpoint 的大小造成不良影响。

如果您的作业负载经常变化(即,突如其来的数据尖峰,定期的窗口聚合触发或者 join ),您可能需要调整以下设置:

  • taskmanager.network.memory.buffer-debloat.period:这是缓冲区大小重算的最小时间周期。周期越小,缓冲消胀机制的反应时间就越快,但是必要的计算会消耗更多的CPU。
  • taskmanager.network.memory.buffer-debloat.samples:调整用于计算平均吞吐量的采样数。采集样本的频率可以通过 taskmanager.network.memory.buffer-debloat.period 来设置。样本数越少,缓冲消胀机制的反应时间就越快,但是当吞吐量突然飙升或者下降时,缓冲消胀机制计算的最佳缓冲数据量会更容易出错。
  • taskmanager.network.memory.buffer-debloat.threshold-percentages防止缓冲区大小频繁改变的优化(比如,新的大小跟旧的大小相差不大)。

更多详细和额外的参数配置,请参考配置参数

您可以使用以下指标来监控当前的缓冲区大小:

  • estimatedTimeToConsumeBuffersMs:消费所有输入通道(input channel)中数据的总时间。
  • debloatedBufferSize:当前的缓冲区大小。

限制 

当前,有一些场景还没有自动地被缓冲消胀机制处理。

多个输入和合并 

当前,吞吐计算和缓冲消胀发生在 subtask 层面。

如果您的 subtask 有很多不同的输入或者有一个合并的输入,缓冲消胀可能会导致低吞吐输入却要有太多缓冲数据,而高吞吐的输入却缓冲了太少的数据。当不同的输入吞吐差别比较大时,这种现象会更加的明显。我们推荐您在测试这个功能时重点关注这种 subtask。

缓冲区的尺寸和个数

当前,缓冲消胀仅在使用的缓冲区大小上设置上限。实际的缓冲区大小和个数保持不变。这意味着缓冲消胀机制不会减少作业的内存使用。您应该手动减少缓冲区的大小或者个数。

此外,如果您想减少缓冲数据量使其低于缓冲消胀当前允许的量,您可能需要手动的设置缓冲区的个数。

并行度问题

目前,使用默认配置时,缓冲消胀机制可能无法在高并行度(高于 ~200;出现问题的并行度的实际值因作业而异,但通常应该超过几百)下正确执行。如果您发现吞吐量降低或检查点时间高于预期,我们建议将 floating buffers   (taskmanager.network.memory.floating-buffers-per-gate) 的数量从默认值增加到至少等于并行度的数量。

水印对齐 watermark alignment

水印是什么

一个Watermark就是一个标识, 一个时间戳为t的Watermark表示Event Time小于或等于t的事件都已经到达. 有了这个前提, 基于Event Time的窗口计算才能产生准确的结果, 例如, 如果一个时间窗口的结束时间为t0, 当前已经产生的最大Watermark为t1, 并且t1>t0, 那么现在触发该窗口的计算可以得到准确的结果, 因为属于该窗口的数据都已经到达. 

为什么要水印对齐

在并行流下多Partition数据源中可能产生的另一个问题是Event Time倾斜. Event Time倾斜即各个Partition中数据的Event Time推进不一致, 部分Partition中的Event Time与其他Partition中的Event Time存在较大差距. 在这种情况下, 由于Watermark对齐机制, 就导致了下游Low Watermark不能推进, 而Event Time推进较快的Partition的数据又被不断读入, 对于需要Watermark触发的window算子就会缓存大量数据窗口触发计算watermark达到某个时间才会整体watermark最低的partition watermark决定的,可以看这个图:.

举例来说, 如果我们从某个Kafka Topic的开头读取历史数据, 各个分区的Event Time很可能并不同步, 如果一个Partition的Event Time明显比其他Partition慢, 那么由于Watermark对齐, window算子的Low Watermark会被拖慢, 而其他分区的数据又在不断读入, 这就造成了大量的数据缓存.

从表象上来看, 空闲Source和Event Time倾斜都会造成大量的数据缓存, 不过这两个问题是存在本质区别的:

  • 空闲Source是某一Partition在一段时间内没有数据写入, 经过一段时间后又有数据写入, 在这个过程中数据的Event Time存在跳跃式推进, 也就是说这段时间内确实没有数据, 而不是数据迟到. 在这种情况下我们可以将这个Partition标识为空闲从而直接忽略.
  • 然而在Event Time倾斜问题中, 各个Partition中并不存在Event Time的跳跃式推进, 也就是说并不存在某个Partition在某段时间内没有数据, 而是各个Partition的Event Time推进不一致. 这也就无法通过将某个Partition标识为空闲解决.

怎么进行水印对齐

在 Flink 1.17 中, FLIP-217通过对 Source 算子内部的 split 进行数据对齐发射,完善了 watermark 对齐功能。这一改进使得 Source 中 watermark 进度更加协调,从而减轻了下游算子的缓存过多数据,增强了流作业执行的整体效率。

具体来说

Flink源码 - 从Kafka Connector看Source接口重构 - Liebing's Blog 这里的说法,随着Flink的快速发展, 旧的Source接口(SourceFunction)出现了诸多问题其中之一就是分区(Partition/Shard/Split)并没有抽象的接口: 这一问题使得难以实现一些数据源无关的通用特性, 例如Event Time对齐, Watermark对齐等.

新的Source实现是在JobMaster中, 引入了一个新的组件SourceCoordinator, 其包含的SourceCoordinatorContext 负责通过RPC与Task中的SourceOperator通信. SplitEnumerator是一个抽象接口, 用户可针对不同的数据源做具体的实现, 主要用于发现Split并分配给指定的Reader. SplitEnumeratorSourceCoordinator共享一个SourceCoordinatorContext, 并通过它进行交互.从宏观角度来看, FLIP-27最重要的变动就是将数据源的发现和数据的读取拆分开来, 作为两个独立的组件分别运行在JobMaster和Task中, 彼此之间通过RPC进行通信. 在这个整体框架下, 很多细节问题就变得很好解决了, 比如Watermark的对齐可以通过全局的SourceCoordinator方便地实现.

在FLIP-27中, 最核心的两个接口是SplitEnumeratorSourceReader. 其中:

  • SplitEnumerator用于发现Splits并将其分配给SourceReader.
  • SourceReader用于从给定的Splits中读取数据.

如果各个Split之间的Watermark差距过大, SourceReader会调用SplitReaderpauseOrResumeSplits()pause()来暂停当前任务, 之后runOnce()将进入步骤4.1. 后续会调用pauseOrResumeSplits()resume()来继续任务.

  • 20
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值