网络监控工作中最重要的环节可能就是监控背压了,所谓背压是指系统接收数据的速率高于其处理速度 [1]。这种现象将给发送者带来压力,而导致它的原因可能有两种情况:
[1] 如果你不熟悉背压,不了解它与 Flink 的交互方式,建议阅读我们在 2015 年发表的关于背压的文章(https://www.ververica.com/blog/how-flink-handles-backpressure)。
当背压出现时,它将一路向上游传导并最终到达你的源,还会减慢它们的速度。这本身并不是一件坏事,只是表明你缺乏足够的资源处理当前的负载。但你可能想要做一些改进,在不动用更多资源的前提下处理更高的负载。为此你需要找到(1)瓶颈在哪里(位于哪个任务 / 操作符)和(2)产生瓶颈的原因。Flink 提供了两种识别瓶颈的机制:
Flink 的 Web UI 大概是快速排除故障时的首选,但它存在一些缺点,我们将在下面解释。另一方面,Flink 的网络指标更适合持续监控和推断是哪些瓶颈导致了背压,并分析这些瓶颈的本质属性。我们将在下文中具体介绍这两个部分。在这两种情况下,你都需要从所有的源和汇中找出背压的根源。调查工作的起点一般来说是最后一个承受背压的操作符;而且最后这个操作符很可能就是背压产生的源头。
背压监视器只暴露在 Flink 的 WebUI[2] 中。由于它是仅在请求时才会触发的活动组件,因此目前无法通过监控指标来提供给用户。背压监视器通过 Thread.getStackTrace() 对 TaskManager 上运行的所有任务线程采样,并计算缓存请求中阻塞任务的样本数。这些任务之所以会阻塞,要么是因为它们无法按照网络缓冲区生成的速率发送这些缓存,要么就是下游任务处理它们的速度很慢,无法保证发送的速率。背压监视器将显示阻塞请求与总请求的比率。由于某些背压被认为是正常 / 临时的,所以监视器将显示以下状态:
虽说你也可以调整刷新间隔、样本数或样本之间的延迟等参数,但通常情况下这些参数用不着你来调整,因为默认值提供的结果已经够好了。
[2] 你还可以通过 REST API 访问背压监视器:/jobs/:jobid/vertices/:vertexid/backpressure
背压监视器可以帮助你找到背压源自何处(位于哪个任务 / 操作符)。但你没法用它进一步推断背压产生的原因。此外,对于较大的作业或较高的并行度来说,背压监视器显示的信息就太乱了,很难分析,还可能要花些时间才能完整收集来自 TaskManager 的数据。另请注意,采样工作可能还会影响你当前作业的性能。
网络指标和任务 I/O 指标比背压监视器更轻量一些,而且会针对当前运行的每个作业不断更新。我们可以利用这些指标获得更多信息,收集到的信息除了用来监测背压外还有其他用途。和用户关系最大的指标有:
numRecordsOut、numRecordsIn
警告:为了完整起见,我们将简要介绍 outputQueueLength 和 inputQueueLength 这两个指标。它们有点像 [out、in] PoolUsage 指标,但这两个指标分别显示的是发送方子任务的输出队列和接收方子任务的输入队列中的缓存数量。但想要推断缓存的准确数量是很难的,而且本地通道也有一个很微妙的特殊问题:由于本地输入通道没有自己的队列(它直接使用输出队列),因此通道的这个值始终为 0(参见 FLINK-12576,https://issues.apache.org/jira/browse/FLINK-12576);在只有本地输入通道的情况下 inputQueueLength = 0。
总的来说,我们不鼓励使用outputQueueLength和inputQueueLength,因为它们的解析很大程度上取决于运算符当前的并行度以及独占缓存和浮动缓存的配置数量。相比之下,我们建议使用各种 *PoolUsage 指标,它们会为用户提供更详尽的信息。
注意:如果你要推断缓存的使用率,请记住以下几点:
任何至少使用过一次的传出通道总是占用一个缓存(Flink 1.5 及更高版本)。
Flink 1.8 及较早版本:这个缓存(即使是空的!)总是在 backlog 中计 1,因此接收器试图为它保留一个浮动缓存区。
Flink 1.9 及以上版本:只有当一个缓存已准备好消费时才在 backlog 中计数,比如说它已满或已刷新时(请参阅 FLINK-11082)
接收器仅在反序列化其中的最后一条记录后才释放接收的缓存。
后文会综合运用这些指标,以了解背压和资源的使用率 / 效率与吞吐量的关系。后面还会有一个独立的部分具体介绍与延迟相关的指标。
有两组指标可以用来监测背压:它们分别是(本地)缓冲池使用率和输入 / 输出队列长度。这两种指标的粒度粗细各异,可惜都不够全面,怎样解读这些指标也有很多说法。由于队列长度指标解读起来有一些先天困难,我们将重点关注输入和输出池的使用率指标,该指标也提供了更多细节信息。
outPoolUsage low | outPoolUsage high | |
---|---|---|
inPoolUsage low | 正常 | 注意(产生背压,当前状态:上游暂未出现背压或已经解除背压) |
inPoolUsage high (Flink 1.9+) | 如果所有上游任务的 outPoolUsage 都很低,则只需要注意(可能最终会产生背压); 如果任何上游任务的 outPoolUsage 变高,则问题(可能在上游导致背压,还可能是背压的源头) | 问题(下游任务或网络出现背压,可能会向上游传递) |
我们甚至可以通过查看两个连续任务的子任务的网络指标来深入了解背压产生的原因:
第一种情况通常是因为任务正在执行一些应用到所有输入分区的耗时操作;后者通常是某种偏差的结果,可能是数据偏斜或资源可用性 / 分配偏差。后文的“如何处理背压”一节中会介绍这两种情况下的应对措施。
如果 floatingBuffersUsage 没到 100%,那么就不太可能存在背压。如果它达到了 100%且所有上游任务都在承受背压,说明这个输入正在单个、部分或全部输入通道上承受背压。你可以使用 exclusiveBuffersUsage 来区分这三种情况:
exclusiveBuffersUsage low | exclusiveBuffersUsage high | |
---|---|---|
floatingBuffersUsage low + 所有上游 outPoolUsage low | 正常 | [3] |
floatingBuffersUsage low + 任一上游 outPoolUsage high | 问题(可能是网络瓶颈) | [3] |
floatingBuffersUsage high + 所有上游 outPoolUsage low | 注意(最终只有一些输入通道出现背压) | 注意(最终多数或全部输入通道出现背压) |
floatingBuffersUsage high + 任一上游 outPoolUsage high | 问题(只有一些输入通道在承受背压) | 问题(多数或全部输入通道都在承受背压) |
[3] 不应该出现这种情况
除了上面提到的各个指标的单独用法外,还有一些组合用法可以用来探究网络栈的深层状况:
假设你确定了背压的来源,也就是瓶颈所在,下一步就是分析为什么会发生这种情况。下面我们按照从基本到复杂的顺序列出了导致背压的一些潜在成因。我们建议首先检查基本成因,然后再深入研究更复杂的成因,否则就可能得出一些错误的结论。
另外回想一下,背压可能是暂时的,可能是由于负载高峰、检查点或作业重启时数据 backlog 待处理导致的结果。如果背压是暂时的,那么忽略它就行了。此外还要记住,分析和解决问题的过程可能会受到瓶颈本身的影响。话虽如此,这里还是有几件事需要检查一下。
首先,你应该检查受控机器的基本资源使用情况,如 CPU、网络或磁盘 I/O 等指标。如果某些资源在被全部或大量占用,你可以执行以下操作:
一般来说,长时间的垃圾回收工作会引发性能问题。你可以打印 GC 调试日志(通过 -XX: +PrintGCDetails)或使用某些内存 /GC 分析器来验证你是否处于这种状况下。由于 GC 问题的处理与应用程序高度相关,并且独立于 Flink,因此我们不会在此详细介绍(可参考 Oracle 的垃圾收集调整指南,https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/index.html 或 Plumbr 的 Java 垃圾回收手册,https://plumbr.io/java-garbage-collection-handbook)。
如果 CPU 瓶颈来自于一个或几个线程,而整台机器的 CPU 使用率仍然相对较低,则 CPU 瓶颈可能就很难被发现了。例如,48 核计算机上的单个 CPU 线程瓶颈只会带来 2%的 CPU 使用率。可以考虑使用代码分析器,因为它们可以显示每个线程的 CPU 使用情况,这样就能识别出热线程。
与上面的 CPU/ 线程瓶颈问题类似,共享资源上较高的线程争用率可能会导致子任务瓶颈。还是要请出 CPU 分析器,考虑查找用户代码中的同步开销 / 锁争用——虽然我们应该避免在用户代码中添加同步性,这可能很危险!还可以考虑调查共享系统资源。例如,默认 JVM 的 SSL 实现可以从共享的 /dev/urandom 资源周围获取数据。
如果你的瓶颈是由数据偏差引起的,可以尝试将数据分区更改为几个独立的重键,或实现本地 / 预聚合来清除偏差或减轻其影响。
除此之外还有很多情况。一般来说,为了削弱瓶颈从而减少背压,首先要分析它发生的位置,然后找出原因。最好从检查哪些资源处于充分利用状态开始入手。
追踪各个可能环节出现的延迟是一个独立的话题。在本节中,我们将重点关注 Flink 网络栈中的记录的等待时间——包括系统网络连接的情况。在吞吐量较低时,这些延迟会直接受输出刷新器的缓存超时参数的影响,或间接受任何应用程序代码延迟的影响。处理记录的时间比预期的要长或者(多个)计时器同时触发——并阻止接收器处理传入的记录——时,网络栈内后续记录的等待时间会大大延长。我们强烈建议你将自己的指标添加到 Flink 作业中,以便更好地跟踪作业组件中的延迟,并更全面地了解延迟产生的原因。
Flink 为追踪通过系统(用户代码之外)的记录延迟提供了一些支持。但默认情况下此功能被禁用(原因参见下文!),必须用 metrics.latency.interval 或 ExecutionConfig #setLatencyTrackingInterval() 在 Flink 的配置中设置延迟追踪间隔才能启用此功能。启用后,Flink 将根据 metrics.latency.granularity 定义的粒度生成延迟直方图:
这些指标通过特殊的“延迟标记”收集:每个源子任务将定期发出包含其创建时间戳的特殊记录。然后,延迟标记与正常记录一起流动,不会在线路上或缓存队列中超过正常记录。但是,延迟标记不会进入应用程序逻辑,并会在那里超过正常记录。因此,延迟标记仅测量用户代码之间的等待时间,而不是完整的“端到端”延迟。但用户代码会间接影响这些等待时间!
由于 LatencyMarker 就像普通记录一样位于网络缓冲区中,它们也会因缓存已满而等待,或因缓存超时而刷新。当信道处于高负载时,网络缓冲区数据不会增加延迟。但是只要一个信道处于低负载状态,记录和延迟标记就会承受最多 buffer_timeout/2 的平均延迟。这个延迟会加到每个连接子任务的网络连接上,在分析子任务的延迟指标时应该考虑这一点。
只要查看每个子任务暴露的延迟追踪指标,例如在第 95 百分位,你就应该能识别出是哪些子任务在显著影响源到汇延迟,然后对其做针对性优化。
注意:Flink 的延迟标记假设集群中所有计算机上的时钟都是同步的。我们建议设置自动时钟同步服务(如 NTP)以避免延迟结果出错。
警告:启用延迟指标会显著影响集群的性能(设置为 subtask 粒度时尤其明显),因为多出来了大量的指标以及使用维护成本非常高的直方图。强烈建议仅将它们用于调试目的。
本文讨论了如何监控 Flink 的网络栈,主要涉及识别背压:发生的位置,源头位置,以及(可能)发生的原因。这可以通过两种方式执行:使用背压监视器处理简单状况并调试会话;使用 Flink 的任务和网络栈指标实现持续监控、更深入的分析和更低的运行时开销。背压可以由网络层本身引起,但在大多数情况下是由高负载下的某些子任务引起的。通过对这些指标的分析研究可以区分这两种场景。我们还提供了一些监控资源使用情况和追踪可能来自源到汇的网络延迟的手段。
本系列的下一篇文章将重点关注调优技巧和应该避免的反模式。
原文链接:
https://flink.apache.org/2019/07/23/flink-network-stack-2.html
你也「在看」吗?👇
朋友会在“发现-看一看”看到你“在看”的内容