一、前言

  1. 根据维基百科的定义,两阶段提交(Two-phase Commit,简称2PC)是巨人们用来解决分布式系统架构下的所有节点在进行事务提交时保持一致性问题而设计的一种算法,也可称之为协议。
  2. 在Flink 1.4版本中,社区将两阶段提交协议中的公共逻辑进行提取和封装,发布了可供用户自定义实现特定方法来达到flink EOS特点的TwoPhaseCommitSinkFunction。本文基于Flink 1.12.4,和大家一起拜读Flink两阶段提交的源码。

二、2PC简介

1. 定义

根据维基百科的定义,两阶段提交可以归纳为一目的两角色三条件(即两个重要角色在三个可成立的条件下,实现最终的一个目的),具体如下:

一个最终目的

​ 分布式系统架构下的所有节点在进行事务提交时要保持一致性(即要么全部成功,要么全部失败)

两个重要角色

1. 协调者(Coordinator),负责统筹并下达命令工作
2. 参与者(Participants),负责认真干活并响应协调者的命令。

三个成立条件

1. 分布式系统中必须存在一个协调者节点和多个参与者节点,且所有节点之间可以相互正常通信;
2. 所有节点都采用预写日志方式,且日志可以可靠存储。
3. 所有节点不会永久性损坏,允许可恢复性的短暂损坏。

2. 原理

两阶段提交,顾名思义,即分两个阶段commit:preCommit和Commit。

以一个Coordinator和三个Participant为例,具体原理如下图:

preCommit阶段

  1. 协调者向所有参与者发起请求,询问是否可以执行提交操作,并开始等待所有参与者的响应。
  2. 所有参与者节点执行协调者询问发起为止的所有事务操作,并将undo和redo信息写入日志进行持久化。
  3. 所有参与者响应协调者发起的询问。对于每个参与者节点,如果他的事务操作执行成功,则返回“同意”消息;反之,返回“终止”消息。

commit阶段

  1. 如果协调者获取到的所有参与者节点返回的消息都为“同意”时,协调者向所有参与者节点发送“正式提交”的请求(成功情况);反之,如果任意一个参与者节点在预提交阶段返回的响应消息为“终止”,或者协调者询问阶段超时,导致没有收到所有的参与者节点的响应,那么,协调者向所有参与者节点发送“回滚提交”的请求(失败情况)。
  2. 成功情况所有参与者节点正式完成操作,并释放在整个事务期间占用的资源;反之,失败情况下,所有参与者节点利用之前持久化的预写日志进行事务回滚操作,并释放在整个事务期间占用的资源。
  3. 成功情况下,所有参与者节点向协调者节点发送“事务完成”消息;失败情况下,所有参与者节点向协调者节点发送“回滚完成”消息。
  4. 成功情况下,协调者收到所有参与者节点反馈的“事务完成”消息,完成事务;失败情况下,协调者收到所有参与者节点反馈的“回滚完成”消息,取消事务。

三、Flink 2PC源码

flink 1.12.4版本中,TwpPhaseCommintSinkFunction类中,官方提示为了实现两阶段提交协议,需要在子类中根据实际情况实现以下方法

		@Override
public void snapshotState(FunctionSnapshotContext context) throws Exception {
// this is like the pre-commit of a 2-phase-commit transaction
// we are ready to commit and remember the transaction checkState(
currentTransactionHolder != null,
"bug: no transaction object when performing state snapshot"); long checkpointId = context.getCheckpointId();
LOG.debug(
"{} - checkpoint {} triggered, flushing transaction '{}'",
name(),
context.getCheckpointId(),
currentTransactionHolder); preCommit(currentTransactionHolder.handle);
pendingCommitTransactions.put(checkpointId, currentTransactionHolder);
LOG.debug("{} - stored pending transactions {}", name(), pendingCommitTransactions); currentTransactionHolder = beginTransactionInternal();
LOG.debug("{} - started new transaction '{}'", name(), currentTransactionHolder); state.clear();
state.add(
new State<>(
this.currentTransactionHolder,
new ArrayList<>(pendingCommitTransactions.values()),
userContext));
} /** 调用该方法可在事务内进行写值操作。 */
protected abstract void invoke(TXN transaction, IN value, Context context) throws Exception; /** 调用该方法可以开启一个新事务。 */
protected abstract TXN beginTransaction() throws Exception; /**
* 调用该方法可预提交上一步创建的事务。
* 注:预提交必须执行所有必要的步骤来为将来可能发生的提交准备事务。此后事务可能仍会中止,但底层实现必须确保对已预提交事务的提交调用始终成功
* <p>Usually implementation involves flushing the data.
*/
protected abstract void preCommit(TXN transaction) throws Exception; /**
* 调用该方法正式提交预提交的事务。
* 注:如果方法执行失败(即提交事务失败),Flink应用会重启,然后调用recoverAndCommit方法,重新提交该事务
*/
protected abstract void commit(TXN transaction); /**
* 执行失败后,调用该方法用来恢复事务提交操作。
* 注:用户自定义实现必须确保该方法的调用最终会被执行成功。如果调用仍然失败,flink应用会被重启并且再次执行调用;反复执行,如果最终失败(用户重启策略配置的为一定的次数的重启),则会导致数据的丢失。
* 另外,事务执行的顺序和他们创建时的顺序保持一致。
*/
protected void recoverAndCommit(TXN transaction) {
commit(transaction);
} /** 调用该方法取消事务 */
protected abstract void abort(TXN transaction); /** 执行失败后,取消被协调者拒绝的事务 */
protected void recoverAndAbort(TXN transaction) {
abort(transaction);
} /**
* 恢复用户上下文后的子类调用的回调函数,用来处理已经提交或者取消并且不会再处理的事务操作。
*/
protected void finishRecoveringContext(Collection<TXN> handledTransactions) {}

TwpPhaseCommintSinkFunction是一个抽象类实现了CheckpointedFunction接口和CheckpointListener接口。

实现CheckpointedFunction接口方法如下:

		@Override
/**
* 父类中该方法的定义为当需要请求检查点的快照时可调用此方法。
* 子类中的方法对检查点的操作进行了事务相关的耦合:
* 1. 校验事务状态,调用该方法时事务不能为null。
* 2. 预提交当前的事务并将该事务加入待提交事务列表,为后续正式提交做准备。
* 3. 开启新的事务。
* 4. 清空存储事务状态的列表,然后记录当前待提交的事务。
*/
public void snapshotState(FunctionSnapshotContext context) throws Exception {
// this is like the pre-commit of a 2-phase-commit transaction
// we are ready to commit and remember the transaction /** 1. 检查事务状态,如果事务对象为空,则抛异常 */
checkState(
currentTransactionHolder != null,
"bug: no transaction object when performing state snapshot"); long checkpointId = context.getCheckpointId();
LOG.debug(
"{} - checkpoint {} triggered, flushing transaction '{}'",
name(),
context.getCheckpointId(),
currentTransactionHolder);
/** 2.1 预提交当前的事务 --TODO 最终会调用实现类中preCommit的逻辑*/
preCommit(currentTransactionHolder.handle);
/** 2.2 记录当前预提交事务到待提交事务列表中 */
pendingCommitTransactions.put(checkpointId, currentTransactionHolder);
LOG.debug("{} - stored pending transactions {}", name(), pendingCommitTransactions);
/** 3. 开启新的事务 */
currentTransactionHolder = beginTransactionInternal();
LOG.debug("{} - started new transaction '{}'", name(), currentTransactionHolder);
/** 4. 清空记录事务状态的列表,并记录当前预提交的事务 */
state.clear();
state.add(
new State<>(
this.currentTransactionHolder,
new ArrayList<>(pendingCommitTransactions.values()),
userContext));
} /**
* 调用该方法初始化检查点状态。
* 子类中的初始化方法对事务进行了相关的耦合操作:
* 1. 获取检查点的状态列表
* 2. 循环状态列表,获取提交待提交的事务,并记录待提交的事务。
* 3. 终止未提交的事务,并记录未提交的事务。
* 4. 开启新的事务
*/
@Override
public void initializeState(FunctionInitializationContext context) throws Exception {
/** 1. 获取检查点的状态列表 */
state = context.getOperatorStateStore().getListState(stateDescriptor); boolean recoveredUserContext = false;
if (context.isRestored()) {
LOG.info("{} - restoring state", name());
/** 2. 循环状态列表,获取提交待提交的事务,并记录待提交的事务 */
for (State<TXN, CONTEXT> operatorState : state.get()) {
// 获取用户上下文
userContext = operatorState.getContext();
// 获取待提交的事务
List<TransactionHolder<TXN>> recoveredTransactions =
operatorState.getPendingCommitTransactions();
List<TXN> handledTransactions = new ArrayList<>(recoveredTransactions.size() + 1);
for (TransactionHolder<TXN> recoveredTransaction : recoveredTransactions) {
// If this fails to succeed eventually, there is actually data loss
// 恢复并提交待提交的事务
recoverAndCommitInternal(recoveredTransaction);
// 记录待提交的事务
handledTransactions.add(recoveredTransaction.handle);
LOG.info("{} committed recovered transaction {}", name(), recoveredTransaction);
}
/** 3. 终止未提交的事务,并记录未提交的事务 */
{
TXN transaction = operatorState.getPendingTransaction().handle;
recoverAndAbort(transaction);
handledTransactions.add(transaction);
LOG.info(
"{} aborted recovered transaction {}",
name(),
operatorState.getPendingTransaction());
} /** 回收用户上下文配置 */
if (userContext.isPresent()) {
finishRecoveringContext(handledTransactions);
recoveredUserContext = true;
}
}
} // if in restore we didn't get any userContext or we are initializing from scratch
// 如果在恢复中没有获取到用户上下文,则进行上下文初始化
if (!recoveredUserContext) {
LOG.info("{} - no state to restore", name()); userContext = initializeUserContext();
}
// 情况待提交事务列表
this.pendingCommitTransactions.clear(); /** 4. 开启新事务 */
currentTransactionHolder = beginTransactionInternal();
LOG.debug("{} - started new transaction '{}'", name(), currentTransactionHolder);
}

实现CheckpointListener接口方法如下:

		/**
* checkPoint完成之后会调用该方法,主要负责对预提交事务的正式提交。
*/
@Override
public final void notifyCheckpointComplete(long checkpointId) throws Exception { /** 1. 获取所有待提交的事务列表 */
Iterator<Map.Entry<Long, TransactionHolder<TXN>>> pendingTransactionIterator =
pendingCommitTransactions.entrySet().iterator();
Throwable firstError = null;
/** 2. 循环提交待提交事务列表中的事务 */
while (pendingTransactionIterator.hasNext()) {
Map.Entry<Long, TransactionHolder<TXN>> entry = pendingTransactionIterator.next();
Long pendingTransactionCheckpointId = entry.getKey();
TransactionHolder<TXN> pendingTransaction = entry.getValue();
// 只提交早于checkpointId的事务
if (pendingTransactionCheckpointId > checkpointId) {
continue;
} LOG.info(
"{} - checkpoint {} complete, committing transaction {} from checkpoint {}",
name(),
checkpointId,
pendingTransaction,
pendingTransactionCheckpointId);
// 超时警告
logWarningIfTimeoutAlmostReached(pendingTransaction);
try {
// 第二阶段提交事务
commit(pendingTransaction.handle);
} catch (Throwable t) {
if (firstError == null) {
firstError = t;
}
} LOG.debug("{} - committed checkpoint transaction {}", name(), pendingTransaction);
// 从待提交事务列表中移除已经提交过的事务
pendingTransactionIterator.remove();
} if (firstError != null) {
throw new FlinkRuntimeException(
"Committing one of transactions failed, logging first encountered failure",
firstError);
}
} @Override
public void notifyCheckpointAborted(long checkpointId) {}

四、总结

借助Flink的CheckPoint机制和2PC协议,对于Sink端,用户只要自定义实现TwoPhaseCommitSinkFunction就可以避免外部系统打乱Flink现存的EOS生态。

Flink EOS如何防止外部系统乱入--两阶段提交源码的更多相关文章

  1. 字节跳动流式数据集成基于Flink Checkpoint两阶段提交的实践和优化

    背景 字节跳动开发套件数据集成团队(DTS ,Data Transmission Service)在字节跳动内基于 Flink 实现了流批一体的数据集成服务.其中一个典型场景是 Kafka/ByteM ...

  2. Ext.NET 4.1 系统框架的搭建(后台) 附源码

    Ext.NET 4.1 系统框架的搭建(后台) 附源码 代码运行环境:.net 4.5  VS2013 (代码可直接编译运行) 预览图: 分析图: 上面系统的构建包括三块区域:North.West和C ...

  3. Java生鲜电商平台-秒杀系统微服务架构设计与源码解析实战

    Java生鲜电商平台-秒杀系统微服务架构设计与源码解析实战 Java生鲜电商平台-  什么是秒杀 通俗一点讲就是网络商家为促销等目的组织的网上限时抢购活动 比如说京东秒杀,就是一种定时定量秒杀,在规定 ...

  4. flink-----实时项目---day07-----1.Flink的checkpoint原理分析 2. 自定义两阶段提交sink(MySQL) 3 将数据写入Hbase(使用幂等性结合at least Once实现精确一次性语义) 4 ProtoBuf

    1.Flink中exactly once实现原理分析 生产者从kafka拉取数据以及消费者往kafka写数据都需要保证exactly once.目前flink中支持exactly once的sourc ...

  5. 【Android 系统开发】CyanogenMod 13.0 源码下载 编译 ROM 制作 ( 手机平台 : 小米4 | 编译平台 : Ubuntu 14.04 LTS 虚拟机)

                 分类: Android 系统开发(5)                                              作者同类文章X 版权声明:本文为博主原创文章 ...

  6. Flink sql 之AsyncIO与LookupJoin的几个疑问 (源码分析)

    本文源码基于flink 1.14 被同事问到几个关于AsyncIO和lookUp维表的问题所以翻了下源码,从源码的角度解惑这几个问题 对于AsyncIO不了解的可以看看之前写的这篇  <Flin ...

  7. 【Android 系统开发】Android框架 与 源码结构

    一. Android 框架 Android框架层级 : Android 自下 而 上 分为 4层; -- Linux内核层; -- 各种库 和 Android运行环境层; -- 应用框架层; -- 应 ...

  8. bootstrap_栅格系统_响应式工具_源码分析

    -----------------------------------------------------------------------------margin 为负 ​使盒子重叠 ​等高 等高 ...

  9. Unity2018.3全新Prefab预制件系统深入介绍视频教程+PPT+Demo源码

    Unity在2018.3推出了重新设计的Prefab预制件系统.这一全新的设计终于为一个长达十年的问题画上了完美的句号, 这个问题就是:“什么时候Unity可以提供嵌套式的预制件系统,俗称Nested ...

随机推荐

  1. 12、创建mysql用户及赋予用户权限

    1.通过help命令查看grant的用法: CREATE USER 'jeffrey'@'localhost' IDENTIFIED BY 'password'; GRANT ALL ON db1.* ...

  2. 利用ONT测序检测真核生物全基因组甲基化状态

    摘要 甲基化在真核生物基因组序列中广泛存在,其中5mC最为普遍,在真核生物基因组中也有发现6mA.捕获基因组中的甲基化状态的常用技术是全基因组甲基化测序(WGBS)和简化甲基化测序(RRBS),而随着 ...

  3. Docker:PostgreSQL-11配置数据持久化

    卷的原理图: 主机中的本地目录作为Docker容器内的持久存储卷装载,以便在主机和Docker容器之间共享数据.如果主机希望访问或定期备份在Docker容器内运行的DB服务器写入文件夹的数据或数据库, ...

  4. 关于Feign的Fallback处理

    Feign的不恰当的fallback Feign的坑不少,特别与Hystrix集成之后. 在微服务引入Feign后,上线不久后便发现,对于一个简单的查询类调用,在下游返回正常的"404-资源 ...

  5. hdu 1145(Sticks) DFS剪枝

    Sticks Problem Description George took sticks of the same length and cut them randomly until all par ...

  6. 0shell变量

    1.定义变量 2.使用变量 3.修改变量的值 4.将命令的结果赋值给变量 5.只读变量 6.删除变量 一.变量 1.定义变量 在 Bash shell 中,每一个变量的值都是字符串,无论你给变量赋值时 ...

  7. Linux | 搜索命令

    grep grep 命令用于在文本中执行关键词搜索,并显示匹配的结果,格式:grep[选项][文本] grep命令的参数及其作用 参数 作用 -b 将可执行文件当作文本文件对待 -c 公显示找到的行数 ...

  8. DWA局部路径规划算法论文阅读:The Dynamic Window Approach to Collision Avoidance。

    DWA(动态窗口)算法是用于局部路径规划的算法,已经在ROS中实现,在move_base堆栈中:http://wiki.ros.org/dwa_local_planner DWA算法第一次提出应该是1 ...

  9. Pytest单元测试框架之FixTure内置临时文件tmpdir操作

    1.前言:某些接口需要引用上个接口返回的值,作为下个接口的入参,但笔者又不想在本地维护及创建此文件,此时引出fixture内置函数中的临时文件存储tmpdir 2.首先下面的源码是使用flask框架写 ...

  10. Jupyter notebook总是卡在int[*]怎么解决?

    Jupyter notebook总是卡在int[*]怎么解决? 先看看后台的日志是怎么回事 运行Jupyter notebook会有一个命令行在运行,可以看看出现在error附近的的句子的意思再具体搜 ...