Flink架构

Apache Flink® --- Stateful Computations over Data Streams

1 状态化流处理

第一章首先比较了传统数据处理架构的两个主要内容:事务型处理和分析型处理,其中事务型处理是说企业在日常运作过程中产生的各类应用的数据存储层。数据应用在每处理一条事件,都会远程访问数据库系统的事务来读取或者更新状态;对于分析型数据处理架构,进行分析就是说将事务型数据库中的数据通过ETL(提取-转换-加载)的形式拷贝到数据仓库中,从而实现对的企业的运行状况等的分析。数据仓库的分析查询一般为两类:即席查询和定期的报告查询。

在介绍完传统的数据处理架构,进入状态化流处理的基本介绍。

  • 几乎所有的数据都是以连续事件流的形式产生的,根据 官档 中对于流的介绍,流根据数据集是否为固定大小的数据集合固定区分为有界 (批处理)和无界 流;在生成流时对其进行实时处理或将流持久化到存储系统(例如文件系统或对象存储),然后再对其进行处理区分为实时记录流。
  • 状态
    • State是指流计算过程中计算节点的中间计算结果或元数据属性,如图,任何一个处理事件流的应用,如果需要跨多条记录的转换操作,都必须是有状态的,即能够存储和访问中间结果。比如 在聚合计算过程中要在state中记录中间聚合结果,比如 Apache Kafka 作为数据源时候,我们也要记录已经读取记录的offset,这些State数据在计算过程中会进行持久化(插入或更新)。所以Flink中的State就是与时间相关的,任务内部数据(计算数据和元数据属性)的快照。
    • 为什么需要状态?与批计算相比,state属于流计算特有的,因为没有failover机制,批计算要么成功,要么失败重新计算。而流计算在大多数场景下,需要增量计算,这样就需要保存之前计算的状态,用于快速回复计算。
    • Flink会将应用状态存储在本地内存或嵌入式数据库中,由于采用的是分布式架构,Flink需要对本地状态予以保护,避免因为应用或机器故障导致的数据丢失。为了实现该特性,flink会定期将状态的一致性检查点(checkpoint)写入持久化存储。
  • 有状态的流处理通常会从事件日志中读取事件记录。事件日志负责存储事件流并将其分布式化。由于事件是以追加的形式写入持久化日志中,所以其顺序后期不会再改变。写入事件日志的数据流可以多次被消费,并且由于日志的追加特性,所以无论被消费多少次都不会影响事件的顺序。将运行在Flink之上的有状态的流处理与事件日志相连能够解决很多问题:由于数据可重放,可以解决如失败恢复、bug修复、应用更新、结果修正、集群迁移和AB测试等问题。

    • 流量加工架构:用户在浏览、点击,nginx集群收集埋点数据,写入磁盘和kafka实时流(数据延迟15秒以内),生成原始日志流,日志按照上报通道拆分,转化为分通道原始日志流(kafka事件流),然后通过实时解析作业生成分通道解析后的明细实时流和分app实时流,然后进入实时L1拆分和pvmv的实时加工,最后生成pvmv的离线表。
  • 三类常见的有状态的流处理应用:

    • 事件驱动型应用:通过接收事件流触发特定业务逻辑的有状态的流式应用。如搜索推荐的实时特征计算,通常会需要在下一刷的时候能够学习到上一刷的结果。
    • 数据管道:为了满足延时性的数据需求,使用事件日志系统来延时更新数据,实现不同数据系统之间的数据同步。以一种低延迟的方式获取、转换并插入数据。
    • 流式分析:不需要考虑调度周期,不需要周期性的触发。通过持续获取事件流,以极低的延迟整合最新事件,从而不断更新结果。可以支撑仪表盘应用的数据展示。

    这三类应用在官方用例中也有介绍。

在介绍了传统数据处理架构和有状态的流处理之后,作者介绍了流处理的历史发展,流处理的历史主要是在延迟和数据准确性之间的trace,没重点关注哈。

  • Flink快览:第三代流处理引擎,精确的流处理,并且能够满足各种规模下对于高吞吐和低延迟的要求。各种优越点此处不表。

2 流处理基础

DataFlow 编程概述

如图,DataFlow程序可以DataFlow图(DAG)表示,由于流和转换算子组成。每个DataFlow都是以数据源Source开始数据汇Sink结束。程序中的转换和DataFlow的算子常常是一对一的关系。

  • 数据并行和任务并行
    • 数据并行:相同算子在不同的数据分区中运行
    • 任务并行:不同的算子任务并行计算
  • 数据交换策略,参考Flink数据交换策略Partitioner
    • 转发策略(forward Strategy),类似于Spark中的map
    • 广播策略(broadcast Strategy)
    • 基于键值的策略(key-based Strategy),类似于shuffle
    • 随机策略(random Strategy),负载均衡,使用场景有?

并行流处理

  • 延迟和吞吐:通过并行处理多条数据流,可以在处理更多的事件的同时降低延迟。

  • 数据流上的操作,如上图的介绍,操作与算子之间存在对应关系,通过一系列的操作实现数据流的获取、转换以及输出。

    • 区分操作是有状态还是无状态的标准是看处理事件时是否需要依赖已经处理过的事件。有状态的流处理在并行化和容错恢复上面有较高的挑战。

    • 数据接入和数据输出操作,允许流处理引擎和外部系统进行通信,数据源可以从TCP套接字、文件、Kafka主题或传感器数据接口中获得数据;数据汇是将数据以适合外部系统使用的格式输出,其写入的目标可以是文件、数据库、消息队列或监控接口。

    • 转换操作,逐个处理每个事件,应用某些转换并产生一条新的数据流。

    • 滚动操作,根据每个到来的事件持续更新结果,聚合函数必须满足可结合和可交换的条件,否则算子就需要存储整个流的历史记录。

    • 窗口操作,需要收集并缓冲记录计算结果的操作。窗口操作会持续创建一些"桶"的有限时间集合,并允许我们基于这些有限集进行计算。事件往往根据其时间或其他数据属性分配到不同的桶中,窗口的行为往往由不同的策略和语义决定。常见窗口的语义包含

      • 滚动窗口(tumbling window),长度固定、互不重叠的"桶"。长度可以是基于事件数量也可以是基于时间的
      • 滑动窗口(sliding window),指定长度+滑动间隔。
      • 会话窗口(session window),会话由发生在相邻时间内的一系列事件外加一段非活动时间组成,根据会话间隔确定不同的窗口。

      窗口操作与流处理中的时间语义和状态管理密切相关。

时间语义

流数据往往会有延迟或者以乱序到达,这个时候如何计算精准、确定的结果呢?如图,时间时间和处理时间往往会有一定的skewness。

  • 处理时间:当前流处理算子所在机器上的本地时钟时间。
  • 事件时间:数据流中事件实际发生的时间。依靠事件时间,可以保证在数据乱序的时候能够计算准确的结果,此外由于数据可重放功能的加持,时间戳允许我们对历史数据进行快进。事件时间将处理速度和结果内容解耦,但是需要克服的问题是如何解决数据延迟?怎么决定事件时间窗口的触发时机?
  • 水位线:一个全局进度指标,表示后续不再有延迟事件的到来,水位线允许我们在延迟和结果的准确性之间做出取舍。延迟事件可能会在水位线T之后到来,因此需要一些额外的代码进行处理。

状态和一致性模型

在无限数据集的处理过程中,状态尤为重要,那么有状态处理算子面临很多技术挑战,如 状态管理:系统需要高效地管理状态并保证他们不受状态更新的影响。状态划分:状态的并行化如何处理,大多数情况下可以使用分区器对各个部分的状态进行独立管理。状态恢复:保证状态可恢复并且结果正确。

  • 任务故障
    • 对比Spark引擎,流处理会在程序实际执行之前翻译成物理dataflow图(物理执行图),其中会包含很多相连的并行任务。
    • 对于输入流的每个事件,任务都需要经历:1.接收事件并存在本地缓存区 2. 选择性地更新内部状态 3. 产生输出记录。每个步骤都可能会发生故障。
    • task的故障,分布式的重试
  • 结果保障,结果的一致性
    • 所谓的结果保障,是指流处理引擎内部状态的一致性,也就是我们关注故障恢复之后应用代码能够看到的状态值。保障状态的一致性和结果的一致性是不同的。
    • 不同类型的结果保障
      • 至多一次,事件可以被随意丢弃,既不恢复丢失的状态,也不重放丢失的事件。
      • 至少一次,不丢事件,但是可能事件重放会被计算多次。
      • 精确一次,不丢事件、并且每个事件对于内部状态的更新都只有一次。引擎需要确保内部状态的一致性,即在故障恢复之后,引擎需要知道某个事件对应的更新是否已经反应到状态中。
      • 端到端的精确一次,在整个数据处理管道上的结果都是正确的,从数据来源组件到数据终点组件。

第三章讲述Flink的分布式架构、关于时间语义和状态中的处理以及容错机制。除了需要理解其内部原理还需要考虑到流式应用的性能及行为。

系统架构

参考官方文档,Flink具有比较好的可扩展性,专注于分布式流处理,而分布式系统的其他挑战诸如分配和管理集群的计算资源、进程协调,持久且高可用的数据存储及故障恢复都依赖已有的集群基础设施和服务实现。

  • 组件

    如图,两类进程:JobManager和TaskManager,看上去和YARN好像的说。客户端不是进程,负责生成和发送DataFlow给JobManager。

    四个组件:JobManager、ResourceManager、TaskManager和Dispatcher

    • JobManager:控制单个应用程序执行的主进程。JobGragh、ExecutionGragh、
    • ResourceManager:负责管理Flink的处理资源单元------TaskManager处理槽。
    • TaskManager:Flink的工作进程,每个TaskManger拥有一定数量的处理槽。
      • TaskManager 在启动后,会向RM注册处理槽
      • 接收到RM的指示后会向JobManager提供处理槽,之后JobManager向处理槽中分配任务来执行。
      • 执行期间,运行同一应用不同任务的TaskManager之间会产生数据交换。
    • Dispatcher:会跨多个作业执行,提供了一个接口来让我们提交需要执行作业的应用。
      • 它的优越性是???直接走客户端不行么?真是脱裤子放屁
  • 应用部署:两种部署方式,框架方式和库方式。

  • 任务执行

    • 每个worker(TaskManager)都是一个JVM进程,可以在不同的线程中执行一个或多个子任务。为了控制 TaskManager 接受多少任务,它有所谓的任务槽(至少一个)。
    • 每个任务槽代表 TaskManager 的固定资源子集。分配资源意味着子任务不会与其他作业的子任务竞争托管内存,而是保留一定数量的托管内存。
    • TaskManager可以同时执行多个任务,这些任务可以属于同一个算子(数据并行)、也可以是不同算子(任务并行),甚至可以是不同的应用。
    • 每个处理槽执行算子的一个并行任务。TaskManager内部多线程的执行任务。
  • 高可用设置

    需要考虑如何重启故障进程、并且恢复应用状态

    • TaskManager:需要足够数据的处理槽,实现任务的并行计算。
    • JobManager:高可用是必要的,因为JobManager用于控制流式应用程序执行和保存计算过程中的元数据。这会产生单点故障 (SPOF):如果 JobManager 崩溃,则无法提交新程序并且正在运行的程序会失败。参考官网文档
      • Zookeeper,JobGraph、Jar文件以及最新检查点在远程存储的状态句柄。

4 Flink中的数据传输

在任务的运行过程中设计数据的交换,taskmanager在将数据从发送端传送到接收端并不是逐个发送记录的,Flink 的网络模块会在每个记录传输之前将他们搜集到数据缓冲区。

网络缓冲区是子任务之间进行数据交换的最小单元。每个子任务的结果为ResultPartitions,每个结果都会拆分为单独的ResultSubPartitions.

如果发送端和接收端的任务运行在不同的TaskManager进程中,他们就会用到操作系统的网络栈进行通信;如果发送端和接收端的任务运行在同一个TaskManager进程中,这个时候就不会设计师网络通信。

  • 基于信用值的流量控制

    更多细节可以参考Flink的网络堆栈

  • 任务链接

    类似于Spark stage中的内存计算,将多个算子的函数被"融合"到一个任务中,在同一个线程中执行。

    • 优点是能够有效降低任务之间的通信开销
    • 有时候需要将长任务进行切分,或者将两个计算量大的任务分配到不同的处理槽中。

事件时间处理

  • 时间戳:将记录和特定时间点进行关联,这些时间点通常是记录所对应事件的发生时间。

  • 水位线:在事件时间应用中推断每个任务当前的事件时间,基于时间的算子会使用这个时间来触发计算并推动进度前进。

    • 数据准确性和延迟之间的trade
  • 水位线传播和事件时间

    Flink内部将水位线实现为特殊的记录,他们可以通过算子任务进行接收和发送。任务内部的时间服务有维护计时器,他们依靠水位线来激活。当任务接收到一个水位线会进行三个操作:

    • 基于水位线记录的时间戳更新内部事件时间时钟。
    • 任务的时间服务会找出所有触发时间小于更新后事件时间的计时器。对于每个到期的计时器,调用回调函数,利用它来执行计算或发出记录。
    • 任务根据更新后的事件时间将水位线发出。

    partition watermark,任务会给每个分区都维护一个分区水位线,分区传入最新水位线记录时,任务会更新分区水位线,同时任务的事件时钟更新为 min(所有分区水位线)。事件时间向前推动会优先处理因此触发的所有计时器,然后将事件时间向所有下游分区广播。

  • 时间戳分配和水位线生成

    时间戳和水位线从哪来的问题。

    • SourceFunction在数据源完成,水位线作为一种特殊的记录发出
    • AssignedWithPeriodicWatermarks周期分配器,从每条记录中读取时间戳,并周期性地响应获取当前水位线的查询请求
    • AssignedWithPunctuatedWatermarks定点分配器,

5 状态管理

状态后端对状态进行存储和维护,有状态的应用如何通过状态再分配实现扩缩容。

  • 算子状态:作用范围是某个算子任务。

    列表状态、联合列表状态和广播状态

  • 键值分区状态:Flink为每个键值都维护了一个状态实例,该实例总是位于那个处理对应键值记录的算子任务上。进行分区的键值映射状态。

  • 状态后端:

    • 管理本地状态
    • 将状态以检查点的形式写入远程存储
  • 有状态算子的扩缩容

    • 对于有状态算子,如何修改并行度是个复杂的问题。对于上述所说的列表状态、联合列表状态和键值分区状态的扩缩容方式都是有些差别的。
    • 带有键值分区状态的算子在扩缩容的时候会根据新的任务数量对键值重新分区。但是键值分区状态在扩缩容的时候不是单个键值的重分区,而是已知会把键值状态分组,以键值组为最小单元进行扩缩容。
    • 列表状态的算子在扩缩容的时候会根据状态的个数进行重新分区。
    • 联合列表状态的方式是进行广播,根据下游任务自行确认哪些保留哪些删除。

6 检查点、保存点以及状态恢复

Flink通过检查点以及故障恢复机制实现 精确一次的状态保证机制。

  • 一致性的检查点:有状态的流式应用的一致性检查点是在所有任务处理完等量的原始输入后对全部任务状态进行一个拷贝。

  • 从一致性检查点中恢复:一旦应用发生故障,会从某个一致性的点进行任务的恢复,并重启处理进程。

  • Flink 检查点算法:为了标识每个检查点编号,这样就把一条数据流从逻辑上分成了两个部分。JobManager会向每个数据源任务发送一个新的检查点编号,检查点从数据源走到数据汇。检查点编号从上游广播到下游,下游任务会等待所有上游分区发送检查点编号,若编号为发送则继续处理该分区的数据,若上游分区已经发送过来检查点编号,则之后的记录则需要被缓冲起来,直至收到全部上游的检查点编号,将状态存储检查点并向下游广播分隔符。检查点分隔符走到数据汇之后,数据汇会依次执行分隔符对齐、将自身状态写入检查点并且向JobManager确认已收到分隔符等一系列动作。JM在接收到所有检查点并且确认消息后,会将此次检查点标志为完成。

重启策略

  • 检查点对性能的影响

    Flink的检查点算法可以在不停止整个应用程序的情况下,生成一致的分布式检查点。但是,它可能会增加应用程序的处理延迟。Flink对此有一些调整措施,可以在某些场景下显得对性能的影响没那么大。

    当任务将其状态保存到检查点时,它其实处于一个阻塞状态,而此时新的输入会被缓存起来。由于状态可能变得非常大,而且检查点需要通过网络将数据写入远程存储系统,检查点的写入很容易就会花费几秒到几分钟的时间------这对于要求低延迟的应用程序而言,显然是不可接受的。在Flink的设计中,真正负责执行检查点写入的,其实是状态后端。具体怎样复制任务的状态,取决于状态后端的实现方式。例如,文件系统(FileSystem)状态后端和RocksDB状态后端都支持了异步(asynchronous)检查点。触发检查点操作时,状态后端会先创建状态的本地副本。本地拷贝完成后,任务就将继续常规的数据处理,这往往并不会花费太多时间。一个后台线程会将本地快照异步复制到远程存储,并在完成检查点后再回来通知任务。异步检查点的机制,显著减少了任务继续处理数据之前的等待时间。此外,RocksDB状态后端还实现了增量的检查点,这样可以大大减少要传输的数据量。

    为了减少检查点算法对处理延迟的影响,另一种技术是调整分界线对齐的步骤。对于需要非常低的延迟、并且可以容忍"至少一次"(at-least-once)状态保证的应用程序,Flink可以将检查点算法配置为,在等待barrier对齐期间处理所有到达的数据,而不是把barrier已经到达的那些分区的数据缓存起来。当检查点的所有barrier到达,算子任务就会将状态写入检查点------当然,现在的状态中,就可能包括了一些"提前"的更改,这些更改由本该属于下一个检查点的数据到来时触发。如果发生故障,从检查点恢复时,就将再次处理这些数据:这意味着检查点现在提供的是"至少一次"(at-least-once)而不是"精确一次"(exactly-once)的一致性保证。

  • 保存点
    Flink的恢复算法是基于状态检查点的 。Flink根据可配置的策略,定期保存并自动丢弃检查点。检查点的目的是确保在发生故障时可以重新启动应用程序,所以当应用程序被显式地撤销(cancel)时,检查点会被删除掉。除此之外,应用程序状态的一致性快照还可用于除故障恢复之外的更多功能。

    Flink中一个最有价值,也是最独特的功能是保存点(savepoints)。原则上,创建保存点使用的算法与检查点完全相同,因此保存点可以认为就是具有一些额外元数据的检查点。 Flink不会自动创建保存点,因此用户(或者外部调度程序)必须明确地触发创建操作。同样,Flink也不会自动清理保存点。第10章将会具体介绍如何触发和处理保存点。
    使用保存点

    有了应用程序和与之兼容的保存点,我们就可以从保存点启动应用程序了。这会将应用程序的状态初始化为保存点的状态,并从保存点创建时的状态开始运行应用程序。虽然看起来这种行为似乎与用检查点从故障中恢复应用程序完全相同,但实际上故障恢复只是一种特殊情况,它只是在相同的集群上以相同的配置启动相同的应用程序。而从保存点启动应用程序会更加灵活,这就可以让我们做更多事情了。

    可以从保存点启动不同但兼容的应用程序。这样一来,我们就可以及时修复应用程序中的逻辑bug,并让流式应用的源尽可能多地提供之前发生的事件,然后重新处理,以便修复之前的计算结果。修改后的应用程序还可用于运行A / B测试,或者具有不同业务逻辑的假设场景。这里要注意,应用程序和保存点必须兼容才可以这么做------也就是说,应用程序必须能够加载保存点的状态。

    可以使用不同的并行度来启动相同的应用程序,可以将应用程序的并行度增大或减小。

    可以在不同的集群上启动同样的应用程序。这非常有意义,意味着我们可以将应用程序迁移到较新的Flink版本或不同的集群上去。

    可以使用保存点暂停应用程序,稍后再恢复。这样做的意义在于,可以为更高优先级的应用程序释放集群资源,或者在输入数据不连续生成时释放集群资源。

    还可以将保存点设置为某一版本,并归档(archive)存储应用程序的状态。

    保存点是非常强大的功能,所以许多用户会定期创建保存点以便能够及时退回之前的状态。我们见到的各种场景中,保存点一个最有趣的应用是不断将流应用程序迁移到更便宜的数据中心上去。
    从保存点启动应用程序

    前面提到的保存点的所有用例,都遵循相同的模式。那就是首先创建正在运行的应用程序的保存点,然后在一个新启动的应用程序中用它来恢复状态。之前我们已经知道,保存点的创建和检查点非常相似,而接下来我们就将介绍对于一个从保存点启动的应用程序,Flink如何初始化其状态。

    应用程序由多个算子组成。每个算子可以定义一个或多个键控状态和算子状态。算子由一个或多个算子任务并行执行。因此,一个典型的应用程序会包含多个状态,这些状态分布在多个算子任务中,这些任务可以运行在不同的TaskManager进程上。

    保存点中的状态拷贝会以算子标识符(operator ID)和状态名称(state name)组织起来。算子ID和状态名称必须能够将保存点的状态数据,映射到一个正在启动的应用程序的算子状态。从保存点启动应用程序时,Flink会将保存点的数据重新分配给相应的算子任务。

    请注意,保存点不包含有关算子任务的信息。这是因为当应用程序以不同的并行度启动时,任务数量可能会更改。

    如果我们要从保存点启动一个修改过的应用程序,那么保存点中的状态只能映射到符合标准的应用程序------它里面的算子必须具有相应的ID和状态名称。默认情况下,Flink会自动分配唯一的算子ID。然而,一个算子的ID,是基于它之前算子的ID确定性地生成的。因此,算子的ID会在其前序算子改变时改变,比如,当我们添加了新的或移除掉一个算子时,前序算子ID改变,当前算子ID就会变化。所以对于具有默认算子ID的应用程序而言,如果想在不丢失状态的前提下升级,就会受到极大的限制。因此,我们强烈建议在程序中为算子手动分配唯一ID,而不是依靠Flink的默认分配。

相关推荐
Nu11PointerException13 分钟前
JAVA笔记 | ResponseBodyEmitter等异步流式接口快速学习
笔记·学习
亦枫Leonlew2 小时前
三维测量与建模笔记 - 3.3 张正友标定法
笔记·相机标定·三维重建·张正友标定法
考试宝2 小时前
国家宠物美容师职业技能等级评价(高级)理论考试题
经验分享·笔记·职场和发展·学习方法·业界资讯·宠物
黑叶白树4 小时前
简单的签到程序 python笔记
笔记·python
@小博的博客4 小时前
C++初阶学习第十弹——深入讲解vector的迭代器失效
数据结构·c++·学习
幸运超级加倍~4 小时前
软件设计师-上午题-15 计算机网络(5分)
笔记·计算机网络
南宫生4 小时前
贪心算法习题其四【力扣】【算法学习day.21】
学习·算法·leetcode·链表·贪心算法
懒惰才能让科技进步5 小时前
从零学习大模型(十二)-----基于梯度的重要性剪枝(Gradient-based Pruning)
人工智能·深度学习·学习·算法·chatgpt·transformer·剪枝
love_and_hope5 小时前
Pytorch学习--神经网络--搭建小实战(手撕CIFAR 10 model structure)和 Sequential 的使用
人工智能·pytorch·python·深度学习·学习
Chef_Chen5 小时前
从0开始学习机器学习--Day14--如何优化神经网络的代价函数
神经网络·学习·机器学习