临界知识这个概念,是我上个月读《好好学习:个人知识管理精进指南》这本书学到的概念,真的有被启发到,现在觉得它对于我们深刻了解世界有着非常大的作用。
所谓临界知识,是我们经过深度思考后发现的,对于认识世界具有普遍指导意义的规律或定律,比如我们经常会看到复利模型、概率论、边际收益、二八法则这些基础概念,它们都是临界知识。通常一个临界知识,对不同的领域都具有指导意义和应用价值。
当然在编程世界中,也有很多临界知识:
那我们今天就来看看分布式系统中的一些临界知识与核心概念。
就是由一台或者多台计算机组成中心节点,数据集中存储与这个中心节点中,并且所有功能都由这个中心节点来处理。
比如很多年前的单体 Mysql、Tomcat服务。提高性能只能提升物理主机的性能,代价昂贵。
所以阿里巴巴发起了去 IOE(IBM 小型机、Oracle 数据库、EMC 高端存储)的行动。单体架构带来的企业成本越来越高,提升单机性能的性价比越来越低;稳定性和可用性很难达标。
分布式服务,就是一群独立的服务器集合共同对外提供服务。但是对于用户来说是透明的,就相当于是一台超级计算机在提供服务。
分布式系统,可以使用廉价的机器,横向扩展性能。计算机越多,CPU、内存、存储资源等也就越多,能够处理的并发访问量也越大。
分布式系统在功能上,确实要比单机系统要强大的多,但是分布式系统的设计实现和维护的难度大大提升了。下面总结了一些分布式系统常见的问题与解决方案:
任何在设计阶段考虑到的异常情况一定会在系统实际运行中发生,但在系统实际运行遇到的异常却很有可能在设计阶段未考虑到,所以,除非需求指标允许,在系统设计阶段不能放过任何异常情况。
通常,一个设计精良的分布式系统,都会对异常做充分的处理,比如 Kafka,Spark 等知名框架。
中心化的设计理念比较简单,分布式集群中的节点按照角色分工,大体上分为两种角色:
在 Hadoop 1.x 的设计中,JobTracker 和 TaskTracker 便是一种中心化的设计,它具有单点故障的问题。
中心化思想设计存在的问题:
在去中心化设计里,通常没有 Master/Slave 的概念,所有的角色都是一样的,地位是平等的。
全球互联网就是一个典型的去中心化的分布式系统,联网的任意节点设备 down 机,都只会影响很小范围的功能。
去中心化设计的核心设计在于整个分布式系统中不存在一个区别于其他节点的”管理者”,因此不存在单点故障问题。
但由于不存在” 管理者”节点所以每个节点都需要跟其他节点通信才得到必须要的机器信息,而分布式系统通信的不可靠性,则大大增加了上述功能的实现难度。
实际上,真正去中心化的分布式系统并不多见。
反而动态中心化分布式系统正在不断涌出。在这种架构下,集群中的管理者是被动态选择出来的,而不是预置的,并且集群在发生故障的时候,集群的节点会自发的举行"会议"来选举新的"管理者"去主持工作。最典型的案例就是ZooKeeper及Go语言实现的Etcd。
当一个计算集群中有多台服务器一起工作时,各台服务器状态由于网络、硬件等原因,会产生数据不一致的情况,这是分布式系统最常遇到的问题。
分布式一致性的级别有很多种,不同的分布式系统的实现,可以采取不同的一致性级别。
通常,在 OLTP 系统中,大多数都是强一致性;而在大数据的系统,比如 HDFS ,则是最终一致性。
2PC(two-phase commit protocol,两阶段提交协议),2PC 是一个非常经典的强一致、中心化的原子提交协议。
这里所说的中心化是指协议中有两类节点:一个是中心化协调者节点(coordinator)和 N 个参与者节点(participant)。
1、在分布式事务发起者向分布式事务协调者发送请求的时候,事务协调者向所有参与者发送事务预处理请求(vote request)。
2、这个时候参与者会开启本地事务并开始执行本地事务,执行完后不会 commit,而是向事务协调者报告是否可以处理本事务。
分布式事务协调者收到所有参与者反馈后,所有参数者节点均响应可以提交,则通知参与者和发起者执行 commit,否则 rollback。
2PC 原理简单,实现方便,很多分布式技术的分布式事务方案,都是基于 2PC 进行了改进,或者提供了补偿方案来设计。
从流程上面可以看出,最大的缺点就是在执行过程中节点都处于阻塞状态。
各个操作数据库的节点都占用着资源,只有当所有节点准备完毕,事务协调者才会通知进行全局 commit/rollback,参与者进行本地事务 commit/rollback 之后才会释放资源,对性能影响比较大。
协调者是整个 2PC 的核心,一旦事务协调者出现故障,会导致参与者收不到 commit/rollback 的通知,从而导致参与者节点一直处于事务无法完成的中间状态。
在第二阶段的时候,如果发生局部网络问题或者局部参与者机器故障等问题,一部分参与者执行了 Commit ,而发生故障的参与者收不到 commit/rollback 消息,那么就会导致节点间数据不一致。
必须收到所有参与者的正反馈才提交事务:如果有任意一个事务参与者的响应没有收到,则整个事务回滚失败。
总体来说,3 PC 相较于 2PC 来说,多了第一个询问阶段,即询问所有节点是否做好准备提交事务,避免某节点宕机,导致所有节点都占用资源的问题。
通过第一阶段的确认,第二阶段所有事务参与者执行事务成功的概率大大增加。
分布式事务协调者询问所有参与者是否可以进行事务操作,参与者根据自身健康情况,是否可以执行事务操作响应(y/n)。
同 2PC 的第一阶段,只是加入了超时的概念。如果协调者收到的预提交响应为拒绝或者超时,则执行中断事务操作,通知各参与者中断事务
同 2PC 的第二阶段,同样加入了超时的概念。如果协调者收到执行者反馈超时,则发送中断指令。
并且参与者在一定时间内,未收到协调者的指令,则会自动提交本地事务。
好,到目前为止,我们已经掌握了如何保证分布式一致性的 2 种方法(2PC 和 3PC),这就是临界知识。但是它毕竟是一个理论,如何把它运用到实践中?我们可以参考著名的分布式计算框架 Flink,它是如何利用 2PC 来保证数据一致性的。
首先什么是 Exactly Once,也就是数据恰好被计算一次,不多计算也不少计算。
那么,什么情况下,数据会被多计算或者少计算?答案是一些 Operator 算子挂掉了。它消费了 Kafka 数据,但是在计算过程中发生了问题,计算结果没有被及时保存下来,这就造成了少计算的问题。
所以当应用程序故障时,为了保证数据消费 Exactly Once, Flink 是通过周期性进行 Checkpoint 机制来解决这个问题。每次做 Checkpoint 的时候,会把当前消费 Kafka 的 offset,计算结果等写入到状态后端中。任务挂了的时候,只需要从最近的一次成功的Checkpoint 中,拿到 offset 和 计算结果,从这个地方接着开始消费和计算就行了。
举个例子:假设我们设置 1 分钟一次 Checkpoint,第 10 次 Checkpoint的时候,partition0 offset 消费到了 50000,PV 统计结果为(app1,30000),(app2,35000)。
又接着消费了 10s,offset 已经消费到了 50100,PV 结果为(app1,30010)(app2,35010),任务挂了,该怎么办?
很简单,只需要从最近的一次 Checkpoint 的 offset 50000 处接着消费,PV 值也要从(app1,30000),(app2,35000)开始算即可。
当然是不能从 offset = 50100,PV 结果为(app1,30010)(app2,35010)开始算的,因为此时还没有进行 Checkpoint,这个状态根本没有保存下来。
上面只是 Flink 内部使用 Checkpoint 来保证数据一致性,那么 Flink 的结果最终是要写入到 Sink 端的存储中的。但是 Flink 的 Sink 算子可能是多个,同时往外部存储里面写,该如何保证 Flink 和 外部存储之间的 Exactly Once?
这就要使用上面我们说的二阶段提交了,Flink 将二阶段提交的逻辑放在 Checkpoint 的过程之中。
实现类为:TwoPhaseCommitSinkFunction
Flink 的 JobManager 对应到 2PC 的协调者,Operator 实例对应到 2PC 的参与者。
TwoPhaseCommitSinkFunction定义了如下 5 个抽象方法:
// 处理每一条数据
protected abstract void invoke(TXN transaction, IN value, Context context) throws Exception;
// 开始一个事务,返回事务信息的句柄
protected abstract TXN beginTransaction() throws Exception;
// 预提交(即提交请求)阶段的逻辑
protected abstract void preCommit(TXN transaction) throws Exception;
// 正式提交阶段的逻辑
protected abstract void commit(TXN transaction);
// 取消事务,Rollback 相关的逻辑
protected abstract void abort(TXN transaction);
这五个方法在什么时候会被执行呢?
(1)任务执行的第一个阶段
TwoPhaseCommitSinkFunction 对应的所有并行度在本次事务中 invoke 全部成功(往 Kafka 发送数据);等到 Flink 开始 Checkpoint 时,会执行 snapshotState() 方法,这个方法会对本次事务进行预提交 preCommit() 。如果 invoke() 和 preCommit() 全部成功了,才表示第一个阶段成功了。
如果在第一个阶段中,有机器故障或者 invoke() 失败或者 preCommit() 失败,都可以理解为 2PC 的第一个阶段返回了 No,即投票失败,会执行 2PC 第二阶段的 rollback,对应到 TwoPhaseCommitSinkFunction 中,就是 abort 方法。
在第一个阶段结束时,数据会被写入到外部存储。如果外部存储的事务隔离级别为读已提交时(Read Committed),并不能读取到我们的写入的数据,因为没有执行 commit 操作。
(2)任务执行的第二个阶段
当所有的实例做快照完成,并且都执行完 preCommit 时,会把快照完成的消息发送给 JobManager,JobManager 收到后会认为本次 Checkpoint 完成了,会向所有的实例发送 Checkpoint 完成的通知(Notify Checkpoint Completed),当 Sink 算子收到这个通知之后,就会执行 commit 方法正式提交。
此时外部存储就可以读取到我们提交的数据了。
至此,我们已经分享完了分布式一致性 2 个重要的理论,2PC 和 3PC,并且粗略的剖析了 Flink 如何使用 2PC 来保证端到端一致性的。
但是这还远远不够,因为还有一些分布式一致的算法,Paxos 算法、Raft 算法、ZAB 协议、抽屉(鸽巢)原理、Quorum NWR 等等需要去了解,道阻且长,后续还会继续分享。