这张图片是作者制作的
这篇文章是在我两周以前的通讯中发布的。免费订阅 订阅这里,以更早收到我的文章。
你还记得小时候有没有那么入迷于某件事,以至于忘记了时间的流逝和身处何地吗?
小时候我踢足球,不是那种美式足球。
三个月的暑假,每周七天,每天下午四点,我会去奶奶家和其他孩子踢足球,一直踢到六点。光着脚,用一个塑料球,用四个砖头做球门——完全沉浸在踢球的乐趣中,忘记了时间和空间。
随着我渐渐长大,我很少会对任何事情投入太多。
我以为当我开始学习流计算时,那种感觉好像又回来了。我无法解释为什么会有这种相似的感觉,但可能是因为想象着将所学应用时的兴奋感,就像小时候想象自己成为球星一样兴奋。
然而,那种感觉很快就不复存在了。也许是因为我没有机会在日常工作中应用流处理,所以我的潜意识暗示我转移兴趣到其他话题。(也许这就是彼得潘永远不想长大的欲望)
今天一坐下,挠头想写些什么,突然想到一个主意:
为什么不再看看流处理呢?
当你想到实时数据处理时,你首先想到的工具是什么?
对我来说是 Apache Flink。
直接从它的首页来看:
Apache Flink 是一个用于无限和有限数据流的状态计算的分布式处理引擎和框架。
术语“无边界数据”描述的是没有界限的数据,例如,活跃电子商务应用中的用户交互事件可以不断变化,没有固定边界;数据流只有在应用停止运行时才会停止。
这张图片是作者自己创作的。
限定范围的数据可以由明确的开始和结束点界定,比如,从操作数据库中每天导出的数据。
Apache Spark也可以用于流处理,但它与Flink在这一点上有很大的不同;它将有界数据视作一等公民,并将其流数据组织成微批处理。对于Flink来说,一切都是流;批处理只是流处理的一种特殊情况。
如果你有来自Kafka、Google Cloud PubSub或Amazon Kinesis的数据流,并且想消费这些数据流,对数据进行处理,并将它们导向其他地方,Apache Flink可以提供帮助。首先定义一个Flink应用程序,包括处理逻辑,通过丰富的API,然后在YARN或Kubernetes等集群环境中部署它。
典型的 Flink 配置包括四个这样的 JVM 进程:
这是一个典型的Flink应用程序流程如下所示:
作者原创的图片。
当一个TaskManager启动时,它会将其槽注册到资源管理器(ResourceManager)。
一个应用包括逻辑数据流图(逻辑数据流图谱)和一个包含执行数据流图所需所有依赖的JAR文件。下面将更详细地介绍数据流编程。
关于任务管理器执行任务的一些注释。它可以同时处理多个任务。这些任务可以例如:
正如之前提到的,TaskManager 提供了有限数量的槽位来限制同时运行的任务数量。
这里有一个包含四个操作符的物理图的例子。每个非源或汇的操作符都有4个并行任务。还有两个TaskManager,每个TaskManager有2个插槽。
这张作者创作的图片。
为了利用Flink进行数据处理,用户必须采用数据流编程的方式。这种方式将程序表示为一个有向图,其中节点是操作符(代表计算),而边则表示数据依赖关系。
作者制作的
运算符从输入(外部来源或其他运算符)获取数据,应用相应的逻辑,并将处理后的数据输出到目的地,这些目的地可以是外部系统或之后的运算符。
一个数据流图可以在两个层次上存在,一个逻辑层面和一个物理层面:逻辑图提供了计算逻辑的高层次概览,而物理图(从逻辑方案中衍生出来)详细描述了程序的具体执行方式。
到目前为止,我们只讨论了一般的数据流编程。接下来,我们通过并行流处理的角度来进一步研究。
流处理引擎(如 Flink)通常提供一组操作来摄取、处理并输出数据流。一个操作可以是无状态的或有状态的。无状态的操作不会保存任何状态;处理事件不受历史事件的影响。无状态处理的独立性意味着并行化或重新启动处理更加简单直接。
另一种操作是带有状态的,它会保存处理过的数据的信息,比如累加计数。这种类型的操作更难并行处理或重启。
操作可以根据功能分为以下几类:摄入(输入数据)、流出(输出数据)、转换、滚动聚合或时间窗口:
作者自制的图片。
窗口操作处理与时间相关语义及状态管理紧密相连。接下来,我们将深入探讨这些概念。
在处理与时间有关的事件数据时,我们通常会遇到两个时间维度。
事件时间始终不变,但数据经过每个处理步骤时,处理时间会不断变化。这在分析事件发生的时间背景下至关重要,是一个关键点。
事件时间与处理时间之间的差异称为时间域偏差。这种偏差可能由多种原因引起,例如通信延迟等问题,还包括每个处理阶段处理所花费的时间。
这张图片是由作者制作的。
指标,如水印,是可视化偏斜的好方法。这些水印向系统传递信息,表示“比当前事件时间早的数据将不会再到来。”
在一个超级理想的世界里,偏差永远为零;我们就能立即处理所有事件,就像它们刚发生一样。
如果水印是T,系统可以假设不会有时间戳小于T的事件出现。水印提供了一个可以在准确性与延迟之间配置的权衡。及时的水印确保了低延迟,但可能提供较低的准确性;带有延迟的事件可能在水印时间之后到达。另一方面,如果水印过于宽松设置,数据可能有机会在后续到达,但由于等待时间的增加导致处理延迟。
在 Flink 中,要处理基于事件时间的数据,所有事件都必须带有时间戳,通常表示事件发生时的时间。操作符会使用该时间戳来进行基于事件时间的操作评估,例如,把事件分配给固定窗口。
除了时间戳之外,应用程序还需要提供水印事件。在Flink框架中,水印事件是具有长整数值时间戳的特殊事件;这些水印事件在流中与其他普通事件一样流动。
如果你仍然对“状态”这个词感到困惑,可以把它想象成一个与任务相关的变量,根据后续数据来维护和更新,以最终得出结果。
本文作者自制的图片。
读写状态的逻辑其实很简单,然而,高效管理状态则更具挑战。
Flink中的状态总是通过操作符注册。操作符注册的状态主要有两种类型。
Flink 提供了多种状态后端,定义了状态存储的方式和位置。Flink 允许用户为 Flink 作业选择状态后端。一些常见的选项有 Java 的堆内/外存或 StarRocks DB。
检查点机制帮助Flink实现容错,通过允许恢复状态和其对应的位置。
简单地使用检查点的方法是:
然而,这样做不太实际;它需要整个流程在保存检查点之前停止,导致更高的延迟时间。
Flink 使用 Chandy-Lamport 算法进行检查点。它不会强制应用程序暂停,并将检查点与数据处理分离。
Flink 使用一个特殊的记录,称为检查点屏障。源操作符将其注入数据流。该屏障包含一个检查点 ID,用来定义它所属的检查点,并将其流分成两部分。
我们来看看 Flink 中的检查点机制:
作者自己画的图片。
呼,总算完了,我想这个第一篇关于Flink的文章大概就是这样了。
感谢您读到这里了。
写这个引擎的过程比我想象的难多了。
实时处理与批处理从根本上说有很大的不同,因此我觉得有必要在深入研究 Flink 之前介绍一些相关概念。我们介绍了数据流编程、事件时间与处理时间的区别、水印机制以及窗口函数。然后,我们探讨了 Flink 的架构以及它如何管理状态。
我希望你学到了一些东西。如果你想读更多有关Flink或实时处理的文章,请告诉我一声。
现在轮到说再见了。
再见,下篇文章见。
_[1]文档__
[2] Fabian Hueske 和 Vasiliki Kalavri,使用 Apache Flink 进行流处理:流应用的基本原理、实现及操作 (2019)