Flink - An Overview

TL;DR 本文简要介绍了 Flink 的架构及提供的特性,梳理了流处理中几个关键概念,例如时间、窗口、流状态,并对比了流处理在一些应用场景下的优势。让读者了解,Flink 是什么,用 Flink 能做些什么。如有疏漏,欢迎指正。

Overview

Apache Flink 是一个在无界和有界数据流上进行状态计算的框架和分布式处理引擎。 Flink 已经可以在所有常见的集群环境中运行,并以 in-memory 的速度和任意的规模进行计算。

批处理针对的是有界数据流。在这种模式下,你可以选择在计算结果输出之前输入整个数据集,这也就意味着你可以对整个数据集的数据进行排序、统计或汇总计算后再输出结果。

流处理正相反,其涉及无界数据流。至少理论上来说,它的数据输入永远不会结束,因此程序必须持续不断地对到达的数据进行处理。

在 Flink 中,应用程序由用户自定义算子组成,即 streaming dataflows。这些 dataflows 形成了有向图,以一个或多个 source 开始,并以一个或多个 sink 结束。

Flink 应用程序可以消费来自消息队列或分布式日志这类流式数据源(例如 Apache Kafka 或 Kinesis)的实时数据,也可以从各种的数据源中消费有界的历史数据。同样,Flink 应用程序生成的结果流也可以 sink 到各种存储系统中。

架构

当 Flink 集群启动后,首先会启动一个 JobManger 和一个或多个的 TaskManager。由 Client 提交任务给 JobManager,JobManager 再调度任务到各个 TaskManager 去执行,然后 TaskManager 将心跳和统计信息汇报给 JobManager。TaskManager 之间以流的形式进行数据的传输。上述三者均为独立的 JVM 进程。

  • Client 为提交 Job 的客户端,可以是运行在任何机器上(与 JobManager 环境连通即可)。提交 Job 后,Client 可以结束进程(Streaming的任务),也可以不结束并等待结果返回。
  • JobManager 主要负责调度 Job 并协调 Task 做 checkpoint。从 Client 处接收到 Job 和 JAR 包等资源后,会生成优化后的执行计划,并以 Task 的单元调度到各个 TaskManager 去执行。
  • TaskManager 在启动的时候就设置好了槽位数(Slot),每个 slot 能启动一个 Task,Task 为线程。从 JobManager 处接收需要部署的 Task,部署启动后,与自己的上游建立 Netty 连接,接收数据并处理。

可以看到 Flink 的任务调度是多线程模型,并且不同 Job/Task 混合在一个 TaskManager 进程中。

JobManager

JobManager 具有许多与协调 Flink 应用程序的分布式执行有关的职责:它决定何时调度下一个 task(或一组 task)、对完成的 task 或执行失败做出反应、协调 checkpoint、并且协调从失败中恢复等等。这个进程由三个不同的组件组成:

  1. ResourceManager: ResourceManager 负责 Flink 集群中的资源提供、回收、分配 - 它管理 task slots,这是 Flink 集群中资源调度的单位。Flink 为不同的环境和资源提供者(例如 YARN、Mesos、Kubernetes 和 standalone 部署)实现了对应的 ResourceManager。在 standalone 设置中,ResourceManager 只能分配可用 TaskManager 的 slots,而不能自行启动新的 TaskManager。
  2. Dispatcher: Dispatcher 提供了一个 REST 接口,用来提交 Flink 应用程序执行,并为每个提交的作业启动一个新的 JobMaster。它还运行 Flink WebUI 用来提供作业执行信息。
  3. JobMaster: JobMaster 负责管理单个JobGraph的执行。Flink 集群中可以同时运行多个作业,每个作业都有自己的 JobMaster。

始终至少有一个 JobManager。高可用(HA)设置中可能有多个 JobManager,其中一个始终是 leader,其他的则是 standby(请参考 高可用(HA))。

TaskManagers

TaskManager(也称为 worker)执行作业流的 task,并且缓存和交换数据流。

必须始终至少有一个 TaskManager。在 TaskManager 中资源调度的最小单位是 task slot。TaskManager 中 task slot 的数量表示并发处理 task 的数量。请注意一个 task slot 中可以执行多个算子。

Task

对于分布式执行,Flink 将算子的 subtasks 链接tasks。每个 task 由一个线程执行。将算子链接成 task 是个有用的优化:它减少线程间切换、缓冲的开销,并且减少延迟的同时增加整体吞吐量。

下图中样例数据流用 5 个 subtask 执行,因此有 5 个并行线程。

Task Slots 和资源

每个 worker(TaskManager)都是一个 JVM 进程,可以在单独的线程中执行一个或多个 subtask。为了控制一个 TaskManager 中接受多少个 task,就有了所谓的 task slots(至少一个)。

每个 task slot 代表 TaskManager 中资源的固定子集。例如,具有 3 个 slot 的 TaskManager,会将其托管内存 1/3 用于每个 slot。分配资源意味着 subtask 不会与其他作业的 subtask 竞争托管内存,而是具有一定数量的保留托管内存。注意此处没有 CPU 隔离;当前 slot 仅分离 task 的托管内存。

通过调整 task slot 的数量,用户可以定义 subtask 如何互相隔离。每个 TaskManager 有一个 slot,这意味着每个 task 组都在单独的 JVM 中运行(例如,可以在单独的容器中启动)。具有多个 slot 意味着更多 subtask 共享同一 JVM。同一 JVM 中的 task 共享 TCP 连接(通过多路复用)和心跳信息。它们还可以共享数据集和数据结构,从而减少了每个 task 的开销。

默认情况下,Flink 允许 subtask 共享 slot,即便它们是不同的 task 的 subtask,只要是来自于同一作业即可。结果就是一个 slot 可以持有整个作业管道。允许 slot 共享有两个主要优点:

  • Flink 集群所需的 task slot 和作业中使用的最大并行度恰好一样。无需计算程序总共包含多少个 task(具有不同并行度)。
  • 容易获得更好的资源利用。如果没有 slot 共享,非密集 subtask(source/map() )将阻塞和密集型 subtask(window) 一样多的资源。通过 slot 共享,我们示例中的基本并行度从 2 增加到 6,可以充分利用分配的资源,同时确保繁重的 subtask 在 TaskManager 之间公平分配。

概念

Dataflows

Flink 程序本质上是分布式并行程序。在程序执行期间,一个流有一个或多个流分区(Stream Partition),每个算子有一个或多个子任务(Operator Subtask)。每个子任务彼此独立,并在不同的线程中运行,或在不同的计算机或容器中运行。

Flink 算子之间可以通过一对一(one-to-one)模式或重分配(redistributing) 模式传输数据:

  • 一对一模式(例如上图中的 Sourcemap() 算子之间)可以保留元素的分区和顺序信息。这意味着 map() 算子的 subtask [1] 输入的数据以及其顺序与 Source 算子的 subtask [1] 输出的数据和顺序完全相同,即同一分区的数据只会进入到下游算子的同一分区。
  • 重分配模式(例如上图中的 map()keyBy/window 之间,以及 keyBy/windowSink 之间)则会更改数据所在的流分区。该模式下,每个算子会将数据发送到多个目标子任务中,例如 keyBy() (通过散列键重新分区)、broadcast() (广播)或 rebalance() (随机重新分发)。在重分配数据的过程中,元素只有在每对输出和输入子任务之间才能保留其之间的顺序信息(例如, keyBy/window 的 subtask [2] 接收到的 map() 的 subtask [1] 中的元素都是有序的)。因此,上图所示的 keyBy/windowSink 算子之间数据的重新分发时,不同键(key)的聚合结果到达 Sink 的顺序是不确定的。

时间

详情可见👉 官方文档

对于大多数流应用而言,能够使用同一份代码处理实时数据及重新处理历史数据,产生确定并且一致的结果非常有价值。

在处理流式数据时,我们通常更需要关注事件本身发生的顺序而不是事件被传输以及处理的顺序,因为这能够帮助我们推理出一组事件(事件集合)是何时发生以及结束的。例如电子商务交易或金融交易中涉及到的事件集合。

为了满足上述这类的实时流处理场景,我们通常会使用记录在数据流中的事件时间的时间戳,而不是处理数据的机器时钟的时间戳。

  • 事件时间(Event-Time):设备时钟,记录事件发生的时间
  • 摄入时间(Ingestion-Time):设备时钟,记录事件发送到服务器的时间
  • 处理时间(Processing-Time):服务器时钟,记录服务器处理事件时的时间

使用事件时间

事件时间的强大之处在于,无论是在处理实时的数据还是重新处理历史的数据,基于事件时间创建的流计算应用都能保证结果是一样的,即幂等性。

一个使用处理时间引发的问题: 如果根据处理时间来衡量请求频率,看起来重启后出现了请求高峰,但是实际上请求频率是稳定的。

流状态

详情可见👉 官方文档

Flink 中的算子可以是有状态的。这意味着如何处理一个事件可能取决于该事件之前所有事件数据的累积结果。Flink 中的状态不仅可以用于简单的场景(例如统计仪表板上每分钟显示的数据),也可以用于复杂的场景(例如训练作弊检测模型)。

Flink 应用程序可以在分布式群集上并行运行,其中每个算子的各个并行实例会在单独的线程中独立运行,并且通常情况下是会在不同的机器上运行。

状态算子的并行实例组在存储其对应状态时通常是按照键(key)进行分片存储的。每个并行实例算子负责处理一组特定键的事件数据,并且这组键对应的状态会保存在本地。

如下图的 Flink 作业,其前三个算子的并行度为 2,最后一个 sink 算子的并行度为 1,其中第三个算子是有状态的,并且你可以看到第二个算子和第三个算子之间是全互联的(fully-connected),它们之间通过网络进行数据分发。通常情况下,实现这种类型的 Flink 程序是为了通过某些键对数据流进行分区,以便将需要一起处理的事件进行汇合,然后做统一计算处理。

Flink 应用程序的状态访问都在本地进行,因为这有助于其提高吞吐量和降低延迟。通常情况下 Flink 应用程序都是将状态存储在 JVM 堆上,但如果状态太大,我们也可以选择将其以结构化数据格式存储在高速磁盘中。

窗口

详情可见👉 官方文档

窗口是处理无限流的核心,因为窗口在无限流上定义了一个有限的元素集合,在这些有限集上执行运算。下面简单介绍 Flink 中涉及的窗口类型及其特性。

Tumbling Windows

滚动窗口将每个元素分配到一个指定大小的窗口中。通常滚动窗口有一个固定的大小,并且不会出现重叠。例如:如果指定了一个5分钟大小的滚动窗口,无限流的数据会根据时间划分成[0:00 - 0:05)、[0:05, 0:10)、[0:10, 0:15)等窗口,如下图所示。

默认的话窗口会根据时间对齐,即如果是一小时的滚动窗口,则划分后的窗口为 1:00:00.000 - 1:59:59.999, 2:00:00.000 - 2:59:59.999 等等。Flink 提供了 offset 参数,如果指定了 offset,例如 15min,则将得到1:15:00.000 - 2:14:59.999, 2:15:00.000 - 3:14:59.999 的窗口集合。

Sliding Windows

滑动窗口不同于滚动窗口,滑动窗口的窗口可以重叠。

滑动窗口有两个参数:slide 和 size。slide 为每次滑动的步长,size 为窗口的大小。

  1. slide < size,则窗口会重叠,每个元素会被分配到多个窗口。
  2. slide = size,则等同于滚动窗口。
  3. slide > size,则为跳跃窗口,窗口之间不重叠且有间隙。

通常情况下大部分元素符合第一种情形,窗口是重叠的。因此,滑动窗口在计算移动平均数(moving averages)时很实用。例如,计算过去 5 分钟数据的平均值,每 10 秒钟更新一次,可以设置 slide 为 10秒,size 为 5 分钟。

Session Windows

会话窗口根据 session 来对元素进行分组。会话窗口与滚动窗口和滑动窗口相比,没有窗口重叠,没有固定窗口大小。相反,当它在一个固定的时间周期内不再收到元素,即 session 断开时,这个窗口就会关闭。例如,对于用户的鼠标点击流,我们可以根据用户进行区分(group by user_id),分析每个用户每天高频使用鼠标的时间段。

Global Windows

全局窗口将所有元素汇集到一个集合。这种窗口通常只与自定义 trigger 配合使用。否则,由于窗口永远不会结束,因此不会触发任何窗口计算。

窗口计算的一些注意点

窗口可以被指定为一个非常长的时间区间,例如天、周、月。不过这意味着维护大量的流状态,通常来说有以下原则来帮助评估其占用的存储空间:

  1. Flink 会对每个窗口的每个元素创建一个副本。对于滚动窗口,每个元素只会唯一创建一个副本(因为每个元素唯一属于一个窗口)。然而,对于滑动窗口来说,每个元素会窗口多个副本。因此,一个步长秒级的天级滑动窗口不是一个好主意。
  2. ReduceFunction、AggregateFunction 都能够大大减少存储,因为每个窗口只会存储一个计算后的值,而非每个元素一个值。
  3. 使用 Evictor 对聚合计算进行预处理,淘汰不必要的元素。

应用

Apache Flink 功能强大,支持开发和运行多种不同种类的应用程序。它的主要特性包括:批流一体化、精密的状态管理、事件时间支持以及精确一次的状态一致性保障等。Flink 不仅可以运行在包括 YARN、 Mesos、Kubernetes 在内的多种资源管理框架上,还支持在裸机集群上独立部署。在启用高可用选项的情况下,它不存在单点失效问题。事实证明,Flink 已经可以扩展到数千核心,其状态可以达到 TB 级别,且仍能保持高吞吐、低延迟的特性。世界各地有很多要求严苛的流处理应用都运行在 Flink 之上。

事件驱动型应用

事件驱动型应用是一类具有状态的应用,它从一个或多个事件流提取数据,并根据到来的事件触发计算、状态更新或其他外部动作。

相反,事件驱动型应用是基于状态化流处理来完成。在该设计中,数据和计算不会分离(传统架构中,需要实时请求数据库获取上下文数据),应用只需访问本地(内存或磁盘)即可获取数据。系统容错性的实现依赖于定期向远程持久化存储写入 checkpoint。下图描述了传统应用和事件驱动型应用架构的区别。

例如:对于用户发文流,应用需要检查某篇文章是否涉嫌抄袭,前面的 pipeline 已经通过 NLP 提取相应的 embedding 向量。那么本应用只需要去查询当前文章的 embedding 是否与 Flink 本地维护的其他元素的 embedding 状态值相近即可。可以理解为,Flink 用内存+磁盘换取数据库调用,并且其状态的维护是精确且可靠的。

数据分析应用

数据分析任务需要从原始数据中提取有价值的信息和指标。传统的分析方式通常是利用批查询,或将事件记录下来并基于此有限数据集构建应用来完成。为了得到最新数据的分析结果,必须先将它们加入分析数据集并重新执行查询或运行应用程序,随后将结果写入存储系统或生成报告。

借助一些先进的流处理引擎,还可以实时地进行数据分析。和传统模式下读取有限数据集不同,流式查询或应用会接入实时事件流(例如 Kafka),并随着事件消费持续产生和更新结果。这些结果数据可能会写入外部数据库系统或以内部状态的形式维护。指标展示看板可以从外部数据库读取数据或直接查询应用的内部状态。

如下图所示,Apache Flink 同时支持流式及批量分析应用。

通常来说,流式分析相比于批式分析有几个优势:

  1. 流式分析省掉了周期性的数据导入和查询过程,因此从事件中获取指标的延迟更低
  2. 批式查询必须处理那些由定期导入和获取数据导致的边界问题,而流式查询则无须考虑该问题。例如前面提到的滚动窗口统计,批式分析需要精确地周期性调度,而调度本身引入了调度时间以及应用冷启动时间,会有一定误差。
  3. 而容错性方面,Flink 提供了故障恢复机制,而批式计算通常需要由多个独立部件组成,需要周期性地调度提取数据和执行查询、分析。一旦某个组件出错,则会影响后续步骤。

数据管道应用

提取-转换-加载(ETL)是一种在存储系统之间进行数据转换和迁移的常用方法。ETL 作业通常会周期性地触发,将数据从事务型数据库拷贝到分析型数据库或数据仓库。

数据管道和 ETL 作业的用途相似,都可以转换、丰富数据,并将其从某个存储系统移动到另一个。但数据管道是以持续流模式运行,而非周期性触发。因此它支持从一个不断生成数据的源头读取记录,并将它们以低延迟移动到终点。例如:数据管道可以用来监控文件系统目录中的新文件,并将其数据写入事件日志;另一个应用可能会将事件流物化到数据库或增量构建和优化查询索引。

下图描述了周期性 ETL 作业和持续数据管道的差异。

Flink 为多种数据存储系统(如:Kafka、Kinesis、Elasticsearch、JDBC数据库系统等)内置了连接器。和周期性 ETL 作业相比,数据管道可以明显降低将端到端数据传输的延迟。此外,由于它能够持续消费和发送数据,因此用途更广,支持用例更多。

例如,对于用户发文流,经过一系列前置 pipeline 处理,提取了关键词等信息,Flink 作业将数据转化为所需格式 sink 到数据库,并 sink 到另一个事件日志流进行一系列后处理,如送审核、写索引,等等。

容错机制

后续将深入解析 Flink 容错机制的实现,也可以参考👉 官方文档

通过状态快照和流重放两种方式的组合,Flink 能够提供可容错的,精确一次计算的语义。这些状态快照在执行时会获取并存储分布式 pipeline 中整体的状态,它会将数据源中消费数据的偏移量记录下来,并将整个 job graph 中算子获取到该数据(记录的偏移量对应的数据)时的状态记录并存储下来。当发生故障时,Flink 作业会恢复上次存储的状态,重置数据源从状态中记录的上次消费的偏移量开始重新进行消费处理。而且状态快照在执行时会异步获取状态并存储,并不会阻塞正在进行的数据处理逻辑。

Reference

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!