这是我参与「第四届青训营」笔记创作活动的第2天
数据流和动态表
| 特性 | SQL | 流处理 |
|---|---|---|
| 处理数据的有界性 | 数据的表是有界的 | 流是一个无限元组序列 |
| 处理数据的完整性 | 执行查询可以访问完整的数据 | 执行查询无法访问所有的数据 |
| 执行时间 | 批处理查询产生固定大小结果后终止 | 查询不断更新结果,永不停止 |
在流上定义表
动态表:与表示批处理数据的静态表不同,动态表是随时间变化的。可以像查询静态批处理表一样查询它们。
数据表和流是可以互相转换的
首先数据流经过处理转换为动态表(Dynamic Table),经过连续查询(Continuous Query)会转换为另一个动态表(Dynamic Table)再转换为数据流i(Stream)
上面显示了单击事件流(左侧)转换为表(右侧)。
当插入更多的点击流记录时,结果表将不断增长。
连续查询
将数据流转换为动态表之后,就可以进行连续查询了
连续查询:
- 查询永永不终止
- 查询结果会不断更新,产生一个新的动态表
在任何时候,连续查询的结果在语义上与以批处理模式在输入表快照。上执行的相同查询的结果相同。
在本图中,当第三行新增加一个Mary流的点击事件记录时候,由于查询是不间断的,查询结果会同步更新到动态表的cnt列中。
当左面进来一条Liz的时候,右面会插入一条新的记录
查询产生仅追加数据的动态表
图中查询的是一个Window时间(本案例为一小时)内,某窗口用户的点击次数
每个Window的数据是互不影响的,新Window的统计数据不会对过去一个小时的数据产生影响(不会更新以前的记录)
两个连续查询的对比
虽然这两个示例查询看起来非常相似(都计算分组计数聚合),但它们在-一个重要方面不同:
- 第一个查询更新先前输出的结果,即定义结果表的
changelog流包含INSERT和UPDATE操作; - 第二个查询只附加到结果表,即结果表的
changelog流只包含INSERT操作
第一个查询可能会更新历史结果,而第二个查询因为窗口是不重叠的,所以不会更新历史数据
Retract消息的产生
在流式数据的场景中会有一种比较特殊的概念——React消息
如图中所示,Mary首先触发了一次点击事件,先往下游发一个+1的消息,随后Bob点击了cart页面,也像下游发送+1的消息,随后Mary又点击了prod的页面,按道理应该像下游发送一条+1的消息,之前Mary已经下放了一次+1的消息,这个时候需要告诉下游,数据需要更新(先发一条回溯消息/React消息,把之前Mary+1的消息进行回溯撤回,于是图中有了-1的消息,之后再向下游发更新后的消息Marry,2,以此来告诉下游对之前的数据进行了更新 )
状态
回到查询方式1,我们需要统计一个用户对网站的所有点击次数,这个时候,我们的查询是需要记录某用户之前点击的次数的,当用户新点击一个页面,需要对之前的数据进行累加求和以输出正确的结果,因此,该查询是需要统计每个用户当前点击网页的所有次数,并把结果存储起来,当后续用户又有新的点击事件进来,我们才能正确输出对应的结果,这个时候就需要用到状态。
需要存储每个用户的URL计数,以便能够增加该计数并在输入表接收新行时发送新结果。
总结
回到这张图,详细总结一下整个过程
为了在数据流中实现SQL语义,需要定义数据表(Dynamic Table)
当触发一些请求或事件后,源源不断传入的数据流会转换为一张大的动态表,在这张表上经过连续不断的查询(连续查询),查询的表可能会对查询的数据进行Update(新增、React消息),因此查询的结果也是动态表,对于查询结果的动态表,也可以转化为输出流。
为了保证后续的流输入是正确的,查询往往是有状态的。
- 数据流和动态表之间的转换;
- 在数据流的查询不会终止;
- 查询可能会有状态,用来不断更新查询的结果。
不同数据处理保证的语义
- At-most-once: 出现故障的时候,啥也不做。数据处理不保证任何语义,处理时延低;
这种情况,可以降低系统开销,将资源全部用来处理数据流,保证了数据处理的高效。
- At-least-once: 保证每条数据均至少被处理一次,一条数据可能存在重复消费。
是上一条语义的增强
- Exactly-once: 最严格的处理语义,从输出结果来看,每条数据均被消费且仅消费一-次,仿佛故障从 未发生。
Exactly-Once和Checkpoint
状态快照与恢复
快照(还原点)就是将历史数据保存下来,故障发生的时候,可以回到原来的状态,保证数据的处理逻辑对下游来说不丢不重。
在上图中,共有三个算子(source、sum_even和sum_odd)分别用来读取数据、奇数累加器和偶数累加器
如果想对某一时刻的状态进行备份,对于Source来说,需要记录数据消费到哪个位点了,而对于奇数、偶数累加和算子而言,要记录当前的累加和,当有新的数据进来,再进行累加,所以可以看出,三个算子都是有状态的。将数据保存到远端(认为永不丢失)
制作快照的时间点
制作快照必然不能是随便一个时间点,可能会造成数据丢失
在本案例中,假设Source读入5,奇数累加器为4(1+3),偶数累加器为6(2+4)
此时还没有将5传给下游累加。
这个时候制作快照,会把当前算子的状态保存下来(Source,5)(sum_even,6)(sum_odd,4)
当故障发生的时候,Source被还原为5,由于Source认为已经读到了5已经读到了,已经下发给下游过了,就会继续读入6,而5就不会被发送给下游处理,相当于把5这个数据丢失了。
状态恢复的时间点:需要等待所有处理逻辑消费完成
一个简单的快照制作算法:
- 暂停处理输入的数据;
- 等待后续所有处理算子消费当前已经输入的数据;
- 待2处理完后,作业所有算子复制自己的状态并保存到远端可靠存储;
- 恢复对输入数据的处理
Chandy-Lamport算法
快照制作的开始
JM会对每一个source算子都接收到JM发送的Checkpoint Barrier标识状态快照制作的开始
Source算子的处理
当Source收到Checkpoint Barrier的时候,会短暂停止自己的处理逻辑,同时
各个source保存自己状态后,向所有连接的下游继续发送Checkpoint Barrier,同时告知JM自己状态已经制作完成。随后,向n个下游算子发送分别发送一个Checkpoint Barrier
Checkpoint Alignment
算子会等待所有上游的barrier到达后才开始快照的制作; 已经制作完成的,上游算子会继续处理数据,并不会被下游算子制作快照的过程阻塞。
快照制作和处理数据的解耦
Checkpoint的结束
所有算子都告知JM状态制作完成后,整个Checkpoint就结束了。
Checkpoint对作业性能的影响
- 解耦了快照制作和数据处理过程,各个算子制作完成状态快照后就可以正常处理数据,不用等下游算子 制作制作完成快照;
- 在快照制作和Barrier Alignment过程中需要暂停处理数据,仍然会增加数据处理延迟;
- 快照保存到远端也有可能极为耗时。
Flink端到端Exactly-Once实现
端到端Exactly-Once语义
- Checkpoint能保证每条数据都对各个有状态的算子更新一次,sink 输出算子仍然可能下发重复的数据;
- 严格意义的端到端的Exactly-once语义需要特殊的sink算子实现。
两阶段提交协议
在多个节点参与执行的分布式系统中,为了协调每个节点都能同时执行或者回滚某个事务性(一系列行为,要么都执行,要么都不执行)的操作,引入了一个中心节点来统一处理所有节点的执行逻辑,这个中心节点叫做协作者( coordinator) ,被中心节点调度的其他业务节点叫做参与者( participant) 。
预提交阶段
1.协作者向所有参与者发送一一个commit消息; 2.每个参与的协作者收到消息后,执行事务,但是不真正提交; 3.若事务成功执行完成,发送一个成功的消息(vote yes) ;执行失败,则发送一个失败的消息(vote no)
提交阶段
若协作者成功接收到所有的参与者vote yes的消息:
- 协作者向所有参与者发送一个 1commit消息;
- 每个收到 commit消息的参与者释放执行事务所需的资源,并结束这次事务的执行;
- 完成步骤2后,参与者发送一-个ack消息给协作者;
- 协作者收到所有参与者的ack消息后,标识该事务执行完成。
Flink中的2PC Sink
状态制作开始,JM向算子发送Checkpoint Barrier
执行成功Vote Yes,执行失败Vote no,第一阶段提交结束
反馈状态制作是否完成
总结
- 事务开启:在sink task向下游写数据之前,均会开启一个事务,后续所有写数据的操作均在这个事务中执行,事务未提交前,事务写入的数据下游不可读;
- 预提交阶段: JobManager 开始下发Checkpoint Barrier,当各个处理逻辑接收到barrier后停止处理后续据,对当前状态制作快照,此时sink也不在当前事务下继续处理数据( 处理后续的数据需要新打开下一个事务)。状态制作成功则向JM成功的消息,失败则发送失败的消息;
- 提交阶段: 若JM收到所有预提交成功的消息,则向所有处理逻辑( 包括sink )发送可以提交此次事务的消息,sink 接收到此消息后,则完成此次事务的提交,此时下游可以读到这次事务写入的数据;若JM有收到预提交失败的消息,则通知所有处理逻辑回滚这次事务的操作,此时sink则丢弃这次事务提交的数据下。
\