一、前言

  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. 前端BootstrapTable组件不同使用方法的效率各有差异

    本人需要解决的问题(#需求) 设备端批量发送数据过来,数据已按照特定字段进行排序,现在本人需要按照传过来的数据动态更新表格,表格的显示区域有限制 因为一些原因,最终确定使用 Bootstrap Tab ...

  2. 洞悉Redis技术内幕:缓存,数据结构,并发,集群与算法

    "为什么这个功能用不了?" 程序员:"清一下缓存" 上篇洞悉系列文章给大家详细介绍了MySQL的存储内幕:洞悉MySQL底层架构:游走在缓冲与磁盘之间.既然聊过 ...

  3. Docker搭建mysql:5.7版本数据库

    搭建MySQL: 1.启动测试mysql,拷贝容器内配置文件到宿主机 mkdr -P /server/docker/mysql/{data,conf} docker run -e MYSQL_ROOT ...

  4. 查看JVM中的线程名

    实例说明 在Java虚拟机中(JVM):除了用户创建的线程,还有服务于用户线程的其他线程.它们根据不同的用途被分到不同的组中进行管理.本实例将演示JVM中线程的名字及其所在组的名称. 关键技术 线程组 ...

  5. WEB与游戏开发的一些区别

    WEB与游戏开发的一些区别 前言 ​ 最近由于在准备期末考,以及准备实习.其实都没好好写过博客,但今天由于个人身边的一些事,所以对做web和做游戏开发的区别做个记录,以下都是从网上搜索到的资料文章,感 ...

  6. CentOS-Docker安装RabbitMQ(单点)

    这里注意获取镜像的时候要获取management版本的,不要获取last版本的,management版本的才带有管理界面. 获取镜像 $ docker pull rabbitmq:management ...

  7. 开始前端三大基础的js之途

                                                     初识   js                                             ...

  8. [心得体会]Spring容器的初始化

    1. Spring容器的初始化过程 public AnnotationConfigApplicationContext(Class<?>... annotatedClasses) {   ...

  9. 虚拟机centos7环境搭建,系统分区,静态IP配置

    文章目录 1.虚拟机安装centos7 2.系统分区 3.配置静态IP centos7下载地址 http://mirrors.aliyun.com/centos/7/isos/x86_64/ Cent ...

  10. ZooKeeper 分布式锁 Curator 源码 02:可重入锁重复加锁和锁释放

    ZooKeeper 分布式锁 Curator 源码 02:可重入锁重复加锁和锁释放 前言 加锁逻辑已经介绍完毕,那当一个线程重复加锁是如何处理的呢? 锁重入 在上一小节中,可以看到加锁的过程,再回头看 ...