一、前言

  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. Prometheus(一):Web服务环境监控

    写在前面 现每个后端的同学的日常都在跟服务(接口)打交道,维护老的比较大单体应用.按业务拆得相对比较细的新服务.无论企业内部用的,面向用户的前端的服务.流量大的有流量小的,有重要的有不那么重要的. 但 ...

  2. redis广播/订阅模式演示

    参考博客 http://www.pianshen.com/article/7183315879/ 1.首先在本地启动redis服务 2.启动4个客户端 redis-cli 3.将其中三个客户端设置监听 ...

  3. sqlite用法总结

    p.p1 { margin: 0; font: 16px "Helvetica Neue"; color: rgba(0, 0, 255, 1) } p.p2 { margin: ...

  4. 分库分表框架ShardingSphere入门学习1

    背景 传统的将数据集中存储至单一数据节点的解决方案,在性能.可用性和运维成本这三方面已经难于满足互联网的海量数据场景. 从性能方面来说,由于关系型数据库大多采用 B+ 树类型的索引,在数据量超过阈值的 ...

  5. python log装饰器

    def log(func): #将原函数对象的指定属性复制给包装函数对象, 默认有 module.name.doc,或者通过参数选择 @functools.wraps(func) def wrappe ...

  6. c语言:随机函数应用

    #include <stdio.h> #include <time.h>//声明time 时间不可逆转一直在变 #include <math.h> #include ...

  7. 认识vue-cli脚手架

    ps:脚手架系列主要记录我自己(一名前端小白)对脚手架学习的一个过程,如有不对请帮忙指点一二! [抱拳] 作为一名前端开发工程师,平时开发项目大多都离不开一个重要的工具,那就是脚手架.下面让我们来了解 ...

  8. Python图表库Matplotlib 组成部分介绍

    图表有很多个组成部分,例如标题.x/y轴名称.大刻度小刻度.线条.数据点.注释说明等等. 我们来看官方给的图,图中标出了各个部分的英文名称 Matplotlib提供了很多api,开发者可根据需求定制图 ...

  9. K8s基本概念资料

    https://www.cnblogs.com/menkeyi/p/7134460.html

  10. python 实现自动部署测试环境

    预设条件 产品运行在Linux CentOS6 X64上 python3,Djanggo,Cherrypy安装好手动安装过程 登录服务器 检查是否有以前的版本的产品在运行,有,停掉 如果有原来的代码包 ...