Exactly-Once 语义在Flink中的实现 | 青训营笔记
这是我参与「第四届青训营 」笔记创作活动的的第2天。
一. 本堂课重点内容
- 数据流和动态表:数据流和动态表可以互相转换
- 处理无限数据流的算子可以是有状态的
- Exactly-Once + Checkpoint用于实现故障前后的状态快照制作和恢复
- Exactly-Once的实现:支持两阶段提交协议的下游存储可以结合Flink checkpoint机制实现严格意义上端到端的Exactly-Once语义实现
二. 详细知识点介绍
1. 数据流和动态表 (Stream & Dynamic Table)
如何在数据流上执行SQL语句 + 状态
1.0 课前基础
- 连续查询 Continuous Queries:
- Append-only Stream:只有INSERT消息
- Retract Stream:同时包含INSERT和DELETE消息
- Upsert Stream:同时包含UPSERT消息和DELETE消息
- Change-Log:包含INSERT/UPDATE/DELETE等的数据流
1.1 传统SQL与流处理
| 特征 | SQL | 流处理 |
|---|---|---|
| 处理数据有界性 | 处理表有界 | 流是一个无限元组序列 |
| 处理数据的完整性 | 执行查询可以访问完整数据 | 查询无法访问所有的数据 |
| 执行时间 | 批处理查询产生固定大小结果后终止 | 查询不断更新结果,永不终止 |
1.2 数据流和动态表转换
graph LR
1(stream) --> 2[dynamic table] --> 3[continuous query + STATE] -->4[dynamic table]-->5(stream)
1.3 数据流生成动态表
- 数据流有新数据时,生成新的快照表
- 动态表:与表示批处理的静态表不同,动态表随时间变化。(在任意时刻,可以视为静态表,因此)可以像查询静态批处理表一样查询他们。
1.4 连续查询
SELECT user,COUNT(url)as cnt
FROM clicks
GROUP BY user
- 动态表-SQL->(对应当前快照的)动态表
- 流式查询,查询从不终止
- 查询结果会不断更新,产生一个新的动态表
- 在任何时候,连续查询的结果在语义上与以批处理模式在输入表快照上执行的相同查询的结果相同。
1.5 查询产生仅追加数据的动态表
SELECT user,
TUMBLE_END(cTime, INTERVAL '1' HOURS) AS endT, count(url) AS cnt
FROM clicks
GROUP BY
user, TUMBLE(cTime, INTERVAL '1'HOURS)
- 在TUMBLE WINDOW中统计一段时间内更新的内容
- 因为窗口不重叠所以不会更新之前的表
1.6 连续查询和查询仅追加的对比
- 第一个查询更新之前输出的结果,定义结果表的changelog流包含INSERT和UPDATE
- 第二个查询只附加结果表,即结果表的changelog只包含INSERT
1.7 Retract消息的产生
- 通知下游需要先回撤之前已经发送的结果,然后发送新结果。
- 流式场景的特殊情况 需要回撤已经发送的数据流
1.8 状态
- 需要存储每个用户的URL计数以便增加该计数并在输入表接受新行时发送新结果
- 为了保证后续正确,查询需要有状态,用来不断更新查询的结果
2. Exactly-Once和Checkpoint
当故障发生时FLink如何从故障中恢复,保证对数据消费的不重不丢
2.0 课前基础
- 作业一致性保证
- at-most-once:每条数据最多一次
- 出现故障的时候do nothing
- 数据处理不保证任何语义,处理时延低
- 准确性要求不高、但数据量大时适用
- at-least-once:每条数据至少一次
- 但可能重复消费
- exactly-once:每条数据都被消费且只消费一次
- 最严格的处理语义
- 仿佛没有发生故障
- at-most-once:每条数据最多一次
- Checkpoint:Flink实现各个计算逻辑状态的快照算法,也指一次状态快照
- Checkpoint barrier:用于标示状态快照的制作,也将数据划分成不同消费区间
- Checkpoint Alignment:等待多个上游Checkpoint barrier到达的现象
- JobManager:负责协调和管理Checkpoint
2.1 状态快照及恢复
- 设置一个故障恢复的时间点——状态快照,一旦发生故障,只需要恢复到对应的时间点
- 记录当前数据流计算到的位点(source发送到的位置)
- 记录所有下游算子当前状态
- 保存到可靠的远端存储
2.2 制作快照的时间点
- 状态恢复的时间点:需要等待所有处理逻辑消费完成source保留状态及之前的数据
- 一个简单的快照制作算法
- 暂停处理输入的数据
- 等待后续所有处理算子消费当前已经输入的数据
- 待处理完后,作业所有算子复制自己的状态并保存到远端可靠存储
- 恢复对输入数据的处理
- 需要停止作业处理逻辑,需要等待可能很漫长的时间,不方便实现
2.3 Chandy-Lamport算法
2.3.1 快照制作的开始
每一个source算子都接收到JobManager发送的一个Checkpoint Barrier标示状态快照制作的开始
2.3.2 Source算子的处理
各source保存自己的状态后,向所有连接的下游继续发送Checkpoint barrier,同时告知JobManager自己的状态快照已经制作完成。
- 完成自己的状态快照后source恢复处理后续数据,无需等待下游
2.3.3 Barrier Alignment
- 各个上游barrier通知会有先后
- 算子会等到所有上游的barrier到达后才开始快照的制作
- 已经制作完成的上游算子会继续处理数据,并不会被下游算子制作快照的过程阻塞
- 已经接收到barrier的上游如果继续给下游发送数据,下游会缓存起来而不会进行执行
- 类似过程也会发生到Sink
2.3.4 快照制作和处理数据的解耦
- 快照制作不需要所有部分停止处理,而每个算子只需要完成自己的快照并且传递barrier后即可恢复处理数据
2.3.5 checkpoint结束
- 所有下游算子全部通知JobManager快照制作完成,checkpoint结束
2.4 checkpoint对作业性能的影响
- 解耦了快照制作和数据处理过程,各算子制作完成状态快照后就可以正常处理数据,不用等待下游算子
- 在快照制作和barrier alignment过程中需要暂停处理数据,仍会增加数据处理延迟
- 快照保存到远端也可能极为耗时
3. Flink端到端Exactly-Once的实现
Flink本身的Checkpoint机制如何与外部存储结合实现端到端的不丢不重语义
3.0 课前基础
- 两阶段提交协议 Two-phase commit protocol
- 一系列保证原子性操作的集合、即操作同时执行或都不执行 Transaction
- 消息中间件 Kafka (无限消息队列)
- State Backend: 用于管理和保存状态到远端的可靠存储
3.1 端到端语义
3.2 两阶段提交协议 two-phase commit
在多个节点参与执行的分布式系统中,为了协调每个节点都能同时执行或者回滚某个事物性(保证原子性)操作,引入了一个中心节点来统一处理所有节点的执行逻辑,这个中心节点叫做协作者coordinator,被中心节点调度的其他业务节点叫做参与者participant。
- coordinator向participants发送query to commit
- participant prepare/abort 投票yes or no
- coordinator commit/abort 发送commit/rollback
- participant acknowledgement
3.2.1 预提交阶段
- 协作者向所有参与者发送一个commit信息 prepare
- 每个参与者收到消息后执行事务但不真正提交
- 若事物成功执行完成,发送成功的消息;若失败则发送一个失败的消息 (认同/不允许)
3.2.2 提交阶段
- 若协作者成功接收到所有参与者vote yes的消息
- 协作者向每个参与者发送commit消息
- 每个收到commit消息的参与者释放执行事务所需的资源,并结束这次事物的执行;
- 完成后参与者发送一个ack给协作者
- 协作者收到所有参与者ack后,标示该事物成功执行
- 若协作者收到vote no或超时
- 协作者向每个参与者发送rollback消息
- 每个收到rollback消息的参与者回滚事务的执行操作,并且释放执行事务所需的资源;
- 完成后参与者发送一个ack给协作者
- 协作者收到所有参与者ack后,标示该事物成功回滚
3.3 Flink中的2PC Sink
-
协作者:JobManager
-
参与者:Source,Window,DataSink
-
第一阶段:JobManager给Source发送一个checkpoint barrier;Source生成状态快照存储到远端,向JM发送状态(vote yes/no),并向下游发送checkpoint barrier;下游所有算子也做类似步骤;
-
第二阶段:JM收到所有下游算子的状态,通知所有下游算子checkpoint completed;Sink此时才会向下游Kafka提交数据;
3.4 总结
- 事务开启:在sink task向下游写数据之前,均会开启一个事务,后续所有写数据的操作均在这个事务中执行,事务未提交前写入的数据下游不可读;
- 预提交阶段:JM开始下发barrier,当各个处理逻辑接收到barrier后停止处理后续数据,对当前状态制作快照,此时sink也不再当前事务下继续处理数据(处理后续需要打开下一个事务)。状态制作成功则向JM发送成功消息,失败则发送失败的消息。
- 提交阶段:若JM收到所有预提交成功的消息,则向所有处理逻辑(包括Sink)发送可以提交此次事务的消息,Sink接收到此消息后则完成此次事务的提交,此时下游可以读到这次事务写入的数据;若JM有收到预提交失败的消息,则通知所有处理逻辑回滚这次事务的操作,此时Sink则丢弃这次事务提交的数据。
三.实践练习例 - 账单计算服务
从Kafka中读取账单信息,进行去重和聚合处理后写入MySQL中。
- 执行步骤
- 在上次记录的位点后从Kafka中读取固定大小的数据
- 对该批数据进行去重和聚合计算
- 处理完成后写入MySQL中,若全部写入成功则记录下当前读取到的消息的终止位置;若处理或写入失败则不记录位点;
- 存在的问题
- 非严格意义上的端到端Exactly-Once语义:若该批数据处理完成后在写入MySQL中发生异常,则存在部分数据写入的情况,下次作业启动后这部分数据仍然会重复写入
- 去重能力有限:只能在当前处理的一批数据内进行去重,无法在批与批之间进行去重
- Flink解决方案优势
- 严格意义上端到端的Exactly-once语义:下游读到的数据不丢不重
- 增强的去重能力:可以在更长的时间维度对数据进行去重 (但不保证唯一性,去重算子保留的状态太多,查找消耗太大)
四.课后个人总结
- 复杂知识点
- SQL利用数据流转换为动态表进行动态Query
- Checkpoint机制实现状态快照制作恢复
- 两阶段提交协议保证Exactly-Once
- 本人总结 本节课主要学习了Flink如何实现故障恢复中Exactly-Once的处理,以及两阶段提交协议。虽然课程在概念上介绍了这些内容,但实际的代码实现并不简单。