Exactly Once 语义在 Flink 中的实现| 青训营笔记

115 阅读10分钟

这是我参与「第四届青训营 」笔记创作活动的的第3天。

1. 数据流和动态表

1.1数据流、动态表、连续查询

  • 数据流(stream):传统SQL和流处理对比 image.png

  •  动态表(Dynamic Table):与表示批处理数据的静态表不同,动态表是随时间变化的。可以像查询静态批处理表一样查询它们.数据库表是INSERT、UPDATE和DELETE DML语句的stream的结果,通常称为changelog stream。

  •  连续查询(Continuous Queries):对表的查询是连续不停止的,查询结果不断更新产生新的表。连续查询的结果在语义上与以批处理模式在输入表快照上执行的相同查询的结果相同。

  •  数据流、动态表、连续查询的关系:

    数据流-->动态表-->连续查询-->动态表-->数据流 image.png

1.2流到表的转换

从概念上来说,流上的每天记录都是动态表进行INSERT修改。从本质上讲,是从一个INSERT-Only(仅插入)的ChangeLog流上构建一个表。点击事件流上构建表如下图所示,且随着更多点击流记录的插入,生成的表不断增长: image.png

1.3两种连续查询对比

  •  简单的GROUP-BY Count聚合查询

    下图中,左边是输入表click,是随着时间updata增加的,右边是查询的结果表。开始clicks表中只有一条数据[Mary, ./home]时其结果表是表-1,当clicks表中新增一条数据[Bob, ./cart]时,其结果表是表-2,依次下推。每一条新数据的到来会对之前表行进update或INSERT操作,SQL语句就会根据现有数据更新的结果表。 image.png 例子1对应着Update查询,这种方式需要更新之前已经发出的结果,包括INSERT和UPDATE两种改变。改变之前已经发出的结果意味着,这种查询需要维护更多的状态(state)数据;

  •  带有窗口(window)的聚合查询

    窗口的时间间隔是1个小时,窗口-1对应的时表-1,窗口-2对应的时表-2,依次类推。和第一种查询不一样的是,每一张时表只是统计对应窗口的数据,之前窗口的数据对其没有影响,对不同窗口的查询结果是以追加的形式写入result表中的。 image.png 例子2对应着Append查询,这种方式查询的结果都是以追加的形式加入到result表中,仅包含INSERT操作。这种方式生成的表和update生成的表转换成流的方式不一样。

1.4表到流的转换

可以通过INSERT、UPDATE、DELETE像修改常规表一样去改变动态表。将动态表转换为流或将其写入外部系统时,需要对这些更改进行encode。 Flink的Table API和SQL支持三种encode改变动态表的方法:

  •  Append-only Stream(仅追加流):仅通过INSERT操作得到的动态表可以发射插入行来转换为流,这种方式转换的流中数据都是片段性的,一个片段代表一个窗口。 image.png
  •  Retract Stream(回溯流):restract stream有两种消息:添加(add)消息和回溯(retract)消息。将动态表转换为回溯(retract)流,通过将INSERT更改encode为添加消息,将DELETE更改encode为回溯消息,将UPDATE更改endcode为更新(上一个)行的回溯消息以及添加消息更新新的行 。 下图显示了动态表到回溯流的转换。 image.png 流上每条消息都有一个标识位,其中+标识INSERT操作,-标识DELETE操作。在clicks 表中第一、二行消息[Mary, ./home]和[Bob, ./cat]被转换为流中1第、2条消息,当clicks 表中第三行[Mary, ./prod?id=1]转换时,会先将已发出的第1条信息标记为DELETE告诉 下游,然后第4条消息重新插入user为Mary的消息,依次类推,这样可以保证输出结 果的正确性。

 

  •  Upsert Stream(上插流):Upsert流包括upsert消息和删除消息。 动态表要转换为upsert流需要(可能是复合的)唯一键。 通过将INSERT和UPDATE 操作encode为upsert消息,并将DELETE更改encode为删除消息,可以是具有唯一键的动态表转换为流。 流运算需要知道唯一键属性才能正确应用消息。 与回溯流的主要区别在于UPDATE使用单个消息((主键))进行编码,因此更有效。  image.png

(数据流与动态表资料:t.zoukankan.com/love-yh-p-1…

2. Exactly-Once 和 Checkpoint

2.1 Exactly-Once

查询运行中出现故障的三种处理方式:

1.At-most-once(每条数据消费至多一次):出现故障的时候,啥也不做。数据处理不保证任何语义,处理时延低;

2.At-least-once(每条数据消费至少一次):保证每条数据均至少被处理一次,一条数据可能存在重复消费。

3.Exactly-once(每条数据都被消费且仅被消费一次):最严格的处理语义,从输出结果来看,每条数据均被消费且仅消费一次,仿佛故障从未发生。

2.2checkpoint

Flink中以Checkpoint(相当于快照)方式做到查询的Exactly-once.

JobManager负责协助管理完成整个快照

状态恢复的时间点:需要等待所有处理逻辑消费完成source保留状态及之前的数据。

  •  并行度为1的checkpoint image.png 一个简单的快照制作算法:

      1.暂停source处理输入的数据;
    
      2.等待后续所有处理算子消费当前已经输入的数据;
    
      3.待2处理完后,作业所有算子复制自己的状态并保存到远端可靠存储;
    
      4.恢复对source输入数据的处理
    
  •  并行度为2的checkpoint image.png

    快照制作:

      1. Checkpoint开始:每一个source算子都接收到JM发送的 Checkpoint Barrier标识状态快照制作的开始
    
      2. Source算子状态处理:各个source保存自己状态后,向所有连接的下游继续发送Checkpoint Barrier,同时告知JM自己状态已经制作完成。
    
      3. Barrier对齐:算子会等待所有上游的barrier到达后才开始快照的制作;已经制作完成的上游算子会继续处理数据,并不会被下游算子制作快照的过程阻塞.
    
      4. Checkpoint结束:所有算子都告知JM状态制作完成后,整个Checkpoint就结束了。
    
  •  Checkpoint对作业性能的影响

    1.解耦了快照制作和数据处理过程,各个算子制作完成状态快照后就可以正常处理数据,不用等下游算子制作制作完成快照;

    2.在快照制作和Barrier Alianment过程中要暂停处理数据,仍然会增加数据处理延时

    3.快照保存到远端也有可能极为耗时。

(Checkpoint资料:baijiahao.baidu.com/sid=1694435…

3. 端到端 Exactly-Once 实现

flink内部通过CheckPoint barrier (分割线) 实现的检查点算法保证了exactly-once语义,但这只是flink内部的一致性,如果要实现端到端的exactly-once,需要整个流程中的组件全部实现,包括了数据源和持久化存储。而整个流处理的过程中,一致性的级别取决于所有组件中一致性最弱的组件。

具体可分为以下几点:

1)flink内部---依赖CheckPoint完成

2)source端---使用可以记录数据位置并重设读取位置的组件(如kafka)

3)sink端---保证发生错误时不会重复发送

这里主要讲sink端,用两阶段提交协议保证事务的原子性。

3.1两阶段段提交协议(2pc)

在多个节点参与执行的分布式系统中,为了协调每个节点都能同时执行或者回滚某个事务性的操作引入了一个中心节点来统一处理所有节点的执行逻辑,这个中心节点叫做协作者(coordinator).被中心节点调度的其他业务节点叫做参与者(participant)

  •  预提交阶段

    1.  协作者向所有参与者发送一个commit消息;

    2.  每个参与的协作者收到消息后,执行事务,但是不真正提交;

    3. 若事务成功执行完成,发送一个成功的消息(vote yes);执行失败,则发送一个失败的消息(vote no)

  •  提交阶段

    若协作者成功接收到所有的参与者vote yes的消息:

    1. 协作者向所有参与者发送一个commit消息;

    2. 每个收到commit消息的参与者释放执行事务所需的资源,并结束这次事务的执行;

    3. 完成步骤2后,参与者发送一个ack消息给协作者;

    4. 协作者收到所有参与者的ack消息后,标识该事务执行完成。

    若协作者有收到参与者vote no的消息(或者发生等待超时):

    1. 协作者向所有参与者发送一个rollback消息;

    2. 每个收到rollback消息的参与者回滚事务的执行操作,并释放事务所占资源;

    3. 完成步骤2后,参与者发送一个ack消息给协作者;

    4. 作者收到所有参与者的ack消息后,标识该事务成功完成回滚。

3.2 Flink中的2pc(two-phase commit)

image.png image.png image.png

  1. 事务开启:在sink task向下游写数据之前,均会开启一个事务,后续所有写数据的操作均在这个事务中执行,事务未提交前,事务写入的数据下游不可读;

  2. 预提交阶段: JobManager 开始下发Checkpoint Barrier,当各个处理逻辑接收到barrier后停止处理后续数据,对当前状态制作快照,此时 sink也不在当前事务下继续处理数据(处理后续的数据需要新打开下一个事务)。状态制作成功则向JM成功的消息,失败则发送失败的消息;

  3. 提交阶段:若JM收到所有预提交成功的消息,则向所有处理逻辑(包括sink)发送可以提交此次事务的消息,sink接收到此消息后,则完成此次事务的提交,此时下游可以读到这次事务写入的数据;若JM有收到预提交失败的消息,则通知所有处理逻辑回滚这次事务的操作,此时 sink则丢弃这次事务提交的数据下。

(2pc资料:blog.csdn.net/weixin_4205…

4.Flink案例

从Kafka中读取账单消息,然后写入到MySQL中 image.png

  •  执行行步骤:
  1. 在上次记录的位点之后,从 Kafka中读取固定大小的数据;

  2. 对该批数据进行去重和聚合计算;

  3. 处理完成后写入MySQL中,若全部写入成功,则记录下当前读取到的消息的终止位置;若处理或者写入失败,则不记录位点;

  4. 跳回步骤1

  •  存在的问题:
  1. 非严格意义上的瑞到端的Exactly-Once语义:若该批数据处理完成后,在写入MySQL中发生异常,则存在部分数据写入的情信况,下次作业启动后,这部分数据仍然会重复写入;

  2. 去重能力有限:只能在当前处理的一批数据内进行去重,无法在批与批之间进行去重;

  • 优势:
  1.  严格意义上的端到端的Exactly-Once 语义:下游读到的数据是不丢不重的;

  2. 增强的去重能力:可以在更长的时间维度对数据进行去重。