《Stream processing with Apache Flink》 读书笔记

《Stream processing with Apache Flink》 读书笔记

第二章 流处理基础

数据分发策略

  1. 转发 一对一转发,降低网络IO
  2. 广播 一对多广播
  3. 按key 按Key值(可能是hash)分发, 说明可以自定义实现range型分发
  4. 随机
  5. 普通Shuffle

流式处理

流数据定义

  • 无限的事件序列

延迟&吞吐

  • 无限的事件没有总处理完成时间和输入数据量大小,要看处理延迟的大小和实时吞吐量衡量程序的处理性能

延迟

  • 延迟指的是处理一个时间所需的时长 即从接收事件到成功输出处理结果的时长

吞吐

  • 系统在单位时间内可以处理的事件个数 吞吐量受限于接收速率
  • 当达系统到吞吐极限时,一味的提高吞吐量,可能导致延迟增加,接收数据缓冲区耗尽,继而数据丢失等(背压)

数据流操作

  • 无状态计算:处理事件时无需依赖已处理过的事件,不保存已处理过的事件。
  • 有状态计算:处理事件时可能需要维护之前接受的事件信息,有状态算子的状态会根据输入的事件不断更新。

数据接入和输出

  • 数据接入是指从外部数据源获取原始数据并将其转换成适合后续处理的格式的过程,实现接入操作逻辑的算子称之为数据源

转换

  • 分别处理每个事件,逐个读取事件,并对其应用某些转换操作输出一条新的输出流。

滚动聚合

  • 会根据每个到来的事件持续更新结果
  • 聚合操作都是有状态的
  • 满足结合律和交换律(即不是窗口函数)

窗口操作

  • 转换和聚合每次处理一个事件来产生输出并更新结果(可能不更新),有些操作必须收集并缓冲记录才能计算结果(如计算中位数),因为可维持的数据记录有限,因而必须对需要维持的数据量加以限制。

  • 窗口即为一个计算边界。

  • 窗口操作会持续的创建一些称为“桶”的有限事件集合,并允许基于“桶”进行计算。

  • 窗口操作决定了什么时候创建桶,事件如何分配到桶中,以及桶内的事件什么时候参与计算。

窗口定义

  1. 滚动窗口 事件分配到长度固定且互不重叠的桶中,在窗口边界通过后所有事件会发送给计算函数处理。

    • 长度:创建新桶时每个桶的大小(时长,事件个数等);
  2. 滑动窗口 将事件分配到大小固定且允许相互重叠的桶中,事件可能同时属于多个桶。

    • 间隔:每达到一定条件( 时长,事件个数等)就创建一个新的桶;

    • 长度:创建新桶时每个桶的大小(时长,事件个数等);

  3. 会话窗口 按照Session的断开划分桶。 根据Session间隔将事件划分到不同的桶中,长度不固定,划分时机也不固定。

时间语义

任何设备都可能因为某段时间的网络等问题导致数据接收时间不连续。
例如地铁中的手游,会在网络环境恢复的时候再将数据发送到服务器,这里应该使用事件时间而非接收时间进行流计算。

流计算中有两个不同的事件概念

  • 处理时间
  • 事件时间

处理时间

处理时间是当前流处理算子所在机器上的本地时钟时间,即和数据传输的速度有很大关系。

事件时间

事件时间是数据流中事件实际发生的时间,它以附加在数据流中的数据时间戳为准,在传输数据之前就已经生成,不再受到传输管道的影响。

水位线

  • 在按照事件时间进行窗口计算的时候,如何决定窗口计算的触发时机?何时才能认为已经接收了某时间点前全部的事件?

  • Flink的水位线是一个全局进度指标,它也是一个事件记录,算子收到该事件后即可以触发窗口计算或对接收数据进行排序了,这势必会造成更高的延迟,因为计算要等到接收到水位线的时候才会触发。

  • 水位线是结果的准确性与延迟之间的Trad-off,实践中系统无法获取足够多的信息来完美地确定水位线,因此它也不是一个完美的解决方案。

在处理时间和事件时间的选择上,处理时间拥有更低的延迟,事件时间拥有更高的准确性,也是一个需要权衡的点。

状态和一致性模型

状态

  • 在任何一个稍微复杂的数据处理中都需要状态,为了生成结果,函数会在一段时间或基于一定个数的事件来累积状态,有状态算子同时使用传入的事件和内部状态来计算输出。

  • 由于流式算子处理的是无穷尽的数据,所以必须避免内部状态无限增长,为了限制状态大小,算子通常都会只保留到目前为止所见事件的摘要或概览,这可能是一个数值,累计值,一个对迄今为止全部事件的抽样一个缓冲等数据结构。

  1. 状态管理 系统需要高效地管理状态并保证它们不受并发更新的影响
  2. 状态划分 将状态按照Key划分,并独立管理每一部分即可做到状态并行化(分区划分)。
  3. 状态恢复 即使出现故障也要确保结果正确,如何从状态中恢复算子。

任务故障

流计算中通过重新处理所有输入来重建故障期间丢失的算子状态在时间、计算资源上代价都比较高。

任何任务都需要执行以下步骤:

  1. 接收事件,存在本地缓冲区
  2. 选择性的更新状态
  3. 产生输出记录

这三步都可能发生故障。

结果保障

分为两种保障,状态结果一致性保障和输出的一致性保障,这里的保障是指输出的一致性。

  1. 至多一次(At most once)
    最简单的保障即为不做任何事情,既不重放,也不恢复丢失的状态。
  2. 至少一次(At latest once)
    数据的计算可能会重复,但绝对不会丢失,即不管丢失的状态,直接重放数据。
  3. 精确一次(Exactly once)
    最严格的一致性保障,每个事件对于内部状态的更新都仅有一次,恢复每个算子的状态,不需要重复消费数据。
  4. 端到端精确一次(End to end exactly once)
    Source-Execute-Sink,整个数据处理管道上结果都是正确的,通过幂等输出可实现精确一次的语义。

第三章 Apache Flink 架构

  1. JM进程(JobManager),每个应用都由一个不同的JM(StandAlone则是线程,Yarn则是进程)掌控,其会包含一个JobGraph即逻辑DataFlow图,以及一个包含了全部资源文件的Jar。JM将JobGraph转化成ExecutionGraph的物理DataFlow图,该图包含可以并行执行的任务。JM会从ResourceManager申请执行任务必须的TaskSlot,成功后将 ExecutionGraph中的任务分发给 TaskManager执行。执行过程中JobManager还会负责从中协调,创建Checkpoint、SavePoint和状态恢复等。 (个人认为StandAlone下的JM类似于Spark Master,Yarn模式下则类似于Spark Driver,起到资源申请、回收、协调,任务生成、分配、监控的作用;那么任务的协调者在哪,应该是JM的一个线程)。
  2. 对于不同的资源环境(Yarn、Standalone、Kubernetes等),Flink有不同的ResourceManager来适配,RS负责申请、创建、回收Flink的处理资源(TaskSlot),当JM申请TaskSlot时,RM会指定一个空闲的TaskManager将其TaskSlot提供给该JM,除了StandAlone模式外TMSlot不足时则会向resource provider申请Container启动新的TM。
  3. TM(TaskManager),Flink的苦工进程,每个TM提供一定数量的线程Slot,StandAlone模式启动时会创建多个TM,每个TM有一定数量的线程Slot,启动后向RM注册Slot。
  4. Dispatcher会跨Job运行,提供REST接口用于提交Flink应用,一旦提交一个应用,Dispathcer便会启动一个JM并将应用转交给它。

应用部署

框架模式

在该模式下,Flink应用需要打包成一个Jar文件,通过客户端提交到Dispatcher,JobManager或是Yarn-ResourceManager。

  • JM:立即执行;
  • DisPatcher、Yarn-ResourceManager:启动一个JM,将应用转交给它执行。

库模式

Flink应用会绑定到一个特定应用的容器镜像(如Docker镜像),镜像中包含着JM、Flink-RM代码,镜像启动后自动加载JM&RM,并将绑定的Job提交运行,另一个和作业无关的镜像负责部署并运行TM。

任务执行

TaskManager运行多个任务|算子同时执行,TM通过提供固定的Slot控制并行任务数,每个TMSlot执行任务的一部分(算子的一个并行任务)。

TaskManager会在同一个JVM中以线程的形式执行任务。

故障恢复

TaskManager故障

TaskManager挂了导致Slot数量不足,JM会向RM申请Slot,如果没有足够的Slot则会等待。

JobManager故障

JobManager挂掉将会导致流处理无法继续运行(这在StandAlone中会导致Flink的单点故障,之后提供了HA机制)。

HA-JM会将JAR包写出到远程存储中,并通过ZK记录目录。

故障时JM下的所有任务都会被取消,新JM会:

  1. 向ZK请求目录,获取JobGraph、Jar、已经应用最新的检查点在远程存储的状态句柄;
  2. 重新向RM申请Slot;
  3. 重启应用,并使用最近一次检查点重置任务状态。

WaterMark

WM在Flink是一种特殊的record,它会被operator tasks接收和释放。

WaterMark transfer

image-20210913110337014

当收到WM大于所有分区目前拥有的WM,就会把event-time clock更新为所有WM中最小的那个,并广播这个最小的WM。即便是多个streams输入,机制也一样,只是增加Paritition WM数量。这种机制要求获得的WM必须是累加的,而且task必须有新的WM接收,否则clock就不会更新,task的timers就不会被触发。另外,当多个streams输入时,timers会被WM比较离散的stream主导,从而使更密集的stream的state不断积累。

WaterMark init

  1. SourceFunction,实现数据源函数,生成WM
  2. periodic assigner,自定义周期分配函数,从每行record中解析时间,周期性响应获取当前水位线的查询请求
  3. punctuated assigner,自定义定点分配函数,从某一行record中解析时间,用于根据特殊记录生成水位的情况。

WM的生成要紧靠数据源算子,因为经过其他算子处理后原有的顺序可能乱序且无法分辨事件时间戳。

State

  • 算子状态 operator state
  • 键值状态 keyed state

Operator State

算子task对于自身的状态,不能访问其他任务的state

  • 列表状态 list state
  • 联合列表 union list state
  • 广播状态 broadcast state
    保证算子每个task状态都相同

Keyed Sate

相同key的record共享一个state,不能访问其他key的state

  • value state
    每个key一个值,这个值可以是复杂的数据结构
  • list state
    每个key一个list
  • map state
    每个key一个map

State Backends

为保证快速访问状态,每个并行任务都会把状态维护在本地,状态的管理则由state backend负责,它主要负责本地状态管理和将状态以checkpoint的形式写出到remote storage。

state backend决定了state如何被存储、访问和维持。它的主要职责是本地state管理和checkpoint state到远程。在管理方面,可选择将state存储到内存还是磁盘。

Scaling Stateful Operators

Flink会根据input rate调整并发度。对于stateful operators有以下4种扩缩容方式:

  • keyed state:根据key group来调整,即分为同一组的key-value会被分到相同的task
  • list state:所有list entries会被收集并重新均匀分布,当增加并发度时,可能某些task没有state,要新建list
  • union list state:增加并发时,广播所有list entries,rescaling后,task自行决定保留哪些entries
  • broadcast state:广播状态到新任务上,直接停掉多出的任务。

Checkpoints ,State Recovery and Savepoints

通过将算子状态重置到没计算那些数据的时刻实现精确一次的状态一致性;

只有输入流是可重放的应用才能支持精确一次的状态一致性。

检查点&恢复机制仅能重置流应用内部的状态,根据采用的sink,在恢复过程中可能会有结果记录重复发送多次到下游系统。

Checkpoint

每个operator有自己的checkpoint

  • 9:1+3+5
  • 6:2+4
image-20210912231441807

State Recovery

重放数据并计算

  • 12:2+4+6
  • 9:1+3+5
image-20210912231632661

Checkpointing Algorithm

Flink checkpointing在分布式开照算法Chandy-Lamport的基础上实现。有一种特殊的record叫checkpoint barrier(由JM产生),它带有checkpoint ID来把流进行划分。在CB前面的records会被包含到checkpoint,之后的会被包含在之后的checkpoint。

Streaming Example

image-20210912233534228
  • Sink1:2= 0+2
  • Sink2:5= 1+(1+3)

JM init checkpoint

image-20210912233947377

checkpoint由JobManager通过向每个数据源任务发送带有新checkpoint ID的消息来启动。

Source Checkpoint

image-20210913111853842
  1. source收到JM新checkpoint ID消息,停止发送recordes
  2. 触发state backend对本地state的checkpoint,广播带checkpoint ID的CB到下游task
  3. 本地state的checkpoint完成后,state backend唤醒source
  4. source向JM确定相应的checkpoint 已经完成,之后source继续发送records

source通过将CB注入其输出,确定了checkpoint的位置。

Execution Checkpoint

CB对齐

image-20210913115108075

CB对齐后state checkpoint

image-20210913120848438

恢复处理数据

image-20210913121337483

当下游获得一个带有新checkpoint ID的CB时:

  1. 暂停处理并缓存这个CB对应的source的数据,其他没有发送CB的source的数据会继续处理。
  2. 等待老ID的CB都到齐(老checkpoint ID对应数据都收集全了)
  3. 对state对老checkpoint ID对应结果进行checkpoint
  4. 广播带老checkpoint ID的CB到下游
  5. 直到所有老CB被广播到下游,才开始处理排队在缓冲区的数据
  6. 开始处理缓存中这个CB对应的source的数据,处理完缓存后开始处理流数据

Sink Checkpoint

image-20210913123957555

最后,当所有sink收齐全部CB会向JM发送CB,确定checkpoint已完成,JM会将此checkpoint标记完成。

Performace Implications of Checkpointing

task state存入checkpoint时会阻塞,当state很大时,复制整个state并发送到远程storage会很费时。

Flink 设计中state backend负责checkpoint,因此如何实现具体的copy完全取决于state backend实现,比如File state backend和RocksDB state backend支持异步checkpoint,触发checkpoint时,backend会快照所有本地state的修改(直至上一次checkpoint),然后马上让task继续执行,后台线程异步发送快照到远程storage,完成后就会通知task。

  • RocksDB state backend支持asynchronous and incremental的checkpoints。增量checkpoint可以有效降低数据量

  • 在等待其余CB时,已经完成checkpoint的source数据需要排队。但如果不使用exactly-once使用at-least-once则不需要等待。但当所有老CB到齐,进行checkpoint时,state中会包含新checkpoint的数据。

SavePoint

checkpoints加上一些额外的元数据,功能也是在checkpoint的基础上丰富。不同于checkpoints,savepoint不会被Flink自动创建(由用户或者外部scheduler触发创造)和销毁。savepoint可以重启不同但兼容的作业,从而:

  • 修复bugs进而修复错误的结果,也可用于A/B test或者what-if场景。
  • 调整并发度
  • 迁移作业到其他集群、新版Flink

也可以用于暂停作业,通过savepoint查看作业情况,甚至有用户通过该功能不断将应用实例迁移到成本最低的数据中心。

posted @ 2021-09-13 13:28  一根咸鱼干  阅读(75)  评论(0编辑  收藏  举报