分布式入门 | 青训营笔记

105 阅读15分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 7 天

1. 分布式理论

什么是分布式? 分布的是什么东西 ==> 分布式是计算资源的分布。

计算资源又分为 计算,存储等。每一个计算资源结点都运行一定程序。分布式使得这些程序在运行时,具有上图所说的特点。

像CAP,BASE,ACID这些理论,都是在描述分布式存储的难关

像raft,paxos都是在保证分布式存储的CA之间的平衡

因此分布式的重头戏还是在存储上,因为存储需要在C和A之间进行均衡。也就是分布式存储的管理是很复杂的。

而分布式计算,主要集中在分布式算法上,对于管理而言,反而很简单,只需要中心化的管理中心来调控计算流程即可

2. 系统模型

拜占庭问题:主要是集群节点收到攻击,被控制,被信息拦截,篡改或者伪造信息。广泛存在于一般的互联网环境。有些共识算法的前提就是集群节点不存在被攻击的情况,也就是说,不是每个共识算法都可以解决拜占庭问题。拜占庭问题导致数据不一致的原因就是安全不到位,导致结点被攻击。

Performance:通常由于网络原因,超时重发导致的在时间上的未预期 不可预测,因为我们无法量化或者控制网络状况

Omission:同样由于超时或者协议的传输不具备重发机制,或者网络干脆断掉了,导致的结点迟迟无法收到数据,同样不可预测(一次Omission是无法被预测的,因为我们无法因为一次的Omission就推测网断了)

Crash:无限的Omission,这种情况是可以预测的,因为发生这种情况,要么结点宕机了,要么网断了。但是具体的错误原因 是未知的,可以知道主机状态

Fail-Stop: 发生错误,立刻失败。不仅可以被预测,同时已知错误原因。

磁盘故障:可能导致数据不安全,这就好像磁盘成了坏蛋,会导致ADB问题。

2.1 拜占庭将军问题

握手的目的是啥?

通信双方 彼此 知道对方的意愿,双方达成一致。只有一方得知不算一致。

如果A发送给B的数据包收到了,那么B一方则处于 "待出动"状态。由于B需要让A知道B收到了A的信息,所以B要回复一个收到,那么将军A收到B的收到后,认为B已经可以出兵,只需要确认一下B的确认即可。也就是三次握手

第一次握手:B知道A要进攻

第二次握手:A知道B 知道A要进攻

第三次握手:B知道A 知道B要进攻

所以,A和B彼此知道互相都要进攻,就可以出兵了。

但是 ,由于网络问题,每一次握手都有记录失败。前两次握手失败好说,重发就行。但是第三次握手的失败,就会导致A知道了B知道A要进攻,但是B这时候犯了难? 第三次握手B没有收到A的答复,也就是无法保证第二次握手的正确性,所以如果没有第三次握手,第二次握手就是不确定的,只有同时具有第二次和第三次握手,才可以同时进攻。

那么就算第三次握手成功了,就需要第四次握手来保证第三次握手的正确性,问题的前提就是消息存在丢失性,要想保证第n-1次握手的正确性,必须引入第n次握手。

如果没有第n次握手,那么第n-1次就不能被保证,递推下去,问题就不对劲了。

纯靠网络肯定是解决不了这个问题了,那么 引入重试机制,修改消息控制字段 可以解决这个问题

那么tcp为什么三次握手?

如果两次握手,那么

第一次握手,B收到了,B处于连接建立状态

第二次握手,A收到了,A处于连接建立状态

看上去没问题。但是,如果第一次握手,A重发了k次,前k-1次都是无效的,那么由于B收到第一次握手就要建立连接,会导致k-1个连接浪费了。

如果是三次握手

第一次握手,B收到了,B处于活跃状态。

第二次握手,A收到了,A处于活跃状态

第三次握手,A不管B收不收到,A处于连接建立状态,如果B收到了,那么B就处于连接建立状态。如果B没收到,B仍处于活跃状态,此时无法通信。

tcp协议通过协议设计的本身,保证了第三次握手 最终是正确的。也就不存在用第四次握手证明第三次握手成功这样的套娃操作了。

如果第三次握手失败了,A此时建立连接,B处于活跃状态。

无非下面三个场景

  • A和B都不发数据包:B会通过keep-alive心跳包向A证明自己的存在,但是此时连接还没建立,B的包发不过去,此时不断重复第二次握手,直到第三次握手成功
  • A发送数据包:B发现一个非握手包的数据包,这下B也反应过来咋回事了,肯定是第三个握手包丢了,但是A也不管了,直接发来一个非握手包,那么B直接进入连接建立状态,并正常通信
  • B发送数据包:B发现发不过去,于是重复第二次握手,直到收到第三次握手。

这里的发不过去是协议栈层面的,并非网卡层面。

A这一方的tcp协议栈 处于连接建立状态,可以通过网卡把数据发给B,B收到的tcp数据包是三次握手之后的包

但是B是实打实的发不出去,因为此时的socket处于活跃状态,并非已建立状态,这时候协议栈会排错,重发第二次握手包

四次挥手,和三次握手一摸一样,多了一次是因为中断连接前可能还有数据要处理,这样第二次握手实际上分为

  1. B回复收到
  2. B发送自己的FIN包

中间可能有数据传输。

tcp通过连接状态这样的控制信息,使得tcp连接建立和断开具有自适应的容错机制,避免了套娃握手

上面的拜占庭问题指的是数据丢失。

还有一种问题就是数据被篡改。

解决数据篡改的问题,引入了一种分阶段协商的思想。

即行动纲领均由一人发出,其他三个人商议这个纲领对不对。这里要注意,问题的关键不在于进攻真确还是撤退正确,不管谁正确,我们的系统只要保证所有人的行动一致即可。不能说一个进攻两个撤退。或者两个进攻一个撤退。

如果头子叛变,那么正常的三个人的下一次协商,总能决策出一个一致性的。

而如果followerA叛变,followerB和followerC仍然占据大多数人。

2.2 共识性和一致性

最终一致性:适用于一致性要求不是那么大的情况。

比如订票软件,我们看到的价位是648,但是结账的时候系统提示:由于价位变动,变成了688,这就是不一致的点,总之在可以接受的范围内。

2.3 时间和时间顺序

逻辑时钟的思想在MVCC的undo log,以及raft的commit机制中都有用到。

为什么不采取更精准的物理时间呢?逻辑时钟实际上就是换了一个参考系,描述时间都是A相对B的时间,这个时间是不连续的。 如果事件A和事件P不存在任何的关联,那么事件A所在的时间点和事件P没有可比性。可以认为事件A和事件P处于不同事务。

采取物理时间,会导致很大误差。不论采取中心化的时间服务器,还是去中心化的realtime api这一类,都有很大误差。而使用逻辑时间,不仅减去了不同事务之间时间的比较,还减少了同步时间的开销

有了逻辑时间的思想,可以设计很多分布式一致性算法,逻辑时钟解决了很多网络故障问题,使得 超时的写不会被commit,每次读到的数据都是比较新的,不会突然读到旧数据

3.理论基础

3.1 CAP理论

实际的共时性算法,并不是二极管,要么放弃C,要么放弃A,而是兼顾二者。允许一定程度的不可用以及不一致,尽量达到一个二者的平衡点。

故障转移:

  • 主从结点之间同步数据,如果主节点挂了,切换到备用节点(切换过程中,可能导致一定的不可用)。
  • 如果主节点的同步数据也挂了,那么务必会导致一定的不一致。

这就是在C和A之间的均衡。反之,如果只要C不要A,那么在结点P1宕机时,P2不对外提供服务,如果只要A不要C,就算P1宕机了,P2正常提供服务,不过提供的是旧数据。

很多分布式算法都是基于故障转移的。

3.2 ACID理论

这里的C,和分布式系统的C不一样。

数据库ACID里的C,指的是状态的一致。比如元素集合S,执行事务前状态为S1,执行事务后,状态还是S1,这个状态可以是集合元素的特征构成,比如100个人转账,我们就定义状态为100个人的总钱数。

隔离性涉及到锁操作,较高级别的隔离性必然会导致性能开销。不过随着技术的进步,也在从悲观锁,到乐观锁,再到MVCC,尽量的避免上锁。

3.3 BASE理论

ACID实际上是大单体保证A和C的一个理论。

BASE通过牺牲一定的一致性,保证了高可用性。BASE允许系统存在中间状态(比如之前提到过的,正在写入的状态,或者正在同步的状态)。处于这个状态的结果就是:状态中的读可能存在不一致。但是只要过了这个状态,最终是一致的,要么状态执行失败,rollback,那么读到的全是旧的,要么成功commit,读到的全是新的。

4. 分布式事务

4.1 两阶段提交

这是我们之前描述的 错误转移 思想。

为了保证可用性,就需要进行主从备份,而主从备份需要数据同步,数据同步失败就会牺牲一致性。这是没办法的。

我们只能说尽量设计出一些方法,让数据同步更精准,少损失点一致性。

paxos算法就是基于lomport逻辑时钟 和 两阶段提交实现的。当然还引入了不同角色的集群节点。

可靠的存储设备:

  • 用保证度高的物理存储设备实现?
  • 采取 分布式存储? ==> 主流:把存储也做成分布式的,保证可靠性。即有多个存储节点,彼此之间也用可靠的分布式算法

没收到参与者的commit?

  • 具体要看一致性算法咋实现的,对于raft而言,只要commit了,就会导致任期号(逻辑时间)变化,因此主结点除了收到ACK,还可以查看任期号判断参与者是否commit了。

网络分区

  • 一部分commit了,一部分没commit。也是要看一致性算法咋实现的,一致性算法会尽量不让commit程度低的结点当leader,这样可以慢慢的让那些commit程度低的结点慢慢commit到较为完备的状态
4.2 三阶段提交

拆分为了 canCommit,这个多出来的提交阶段,可以直接对数据进行预读,如果发现数据是不对头的,可以直接退出。

4.3 MVCC

MVCC就是逻辑时钟的一个运用,在innodb中,加入了一些版本控制的字段,对ACID中的隔离级别进行控制。使用MVCC,可以尽量少加锁的情况下,进行隔离。

5. 共识协议

5.1 Quorum NWR

证明

反证:如果W + R <= N

那么 R <= N - W

N - W :还有多少主机处于旧的状态(W成功之后,主机才是正确状态)。

R : 发生了多少次成功读取(读到正确数据)。

为了保证强一致性,这里的所有的R都需要是正确的, 但是如果反证法之后,如果R全都是正确的,并且R <= N - w。这意味着读新数据的次数 小于等于 旧数据的总量。也就是说,R次读,可能读到的都是旧数据(也可能读到一个新的,返回新的)。反正下一轮w总是会同步的,也就是达到最终一致

反之,R次读取,总有一次读到的是新的,根据逻辑时钟,可以判断出新的数据是哪一次读到的,从而返回新的数据

因此,要保证强一致性,还是最终一致性,完全取决于用户行为。

但是,强一致性 ==> W + R > N,但是 W + R > N 无法推出强一致性

原因:数据存在覆盖。如图,在第二次写时,R = 2, W = 2 > N = 3,但是,居然读出了不一样的值。这是由于数据是以覆盖形式更新的,而不是追加形式(非覆盖)。如果是追加形式,那么第二次写的表现形式为 记录v3=3,v2=2,而覆盖写则会读到第一次写的旧版本。如果是追加写,可以保证每次读到的都是任期内的新副本

5.2 raft协议
  • 如何保证日志可靠:每个结点都有日志副本(分布式存储)
  • 三个角色:leader,follower(典型的主从容错),candidate(中间状态)

数据采取只追加写,避免数据覆盖(append only)

任期号:逻辑时钟的标准

选举过程

  • 发起RequestVote,如果网络无误,并且其他节点收到,其他节点会根据逻辑时钟,判断请求发起者是否具备了足够的任期内日志,如果可以,则返回成功响应,表示投票,否则返回失败,表示不投给你
  • 一次选举可能谁也没选举出来
  • 选举成功后,leader会发送心跳包。这个心跳包不仅可以让其他candidate乖乖当follower,还可以干一些其他的事(比如强制commit等)

日志同步过程

  • leader先写入自己本地log
  • 发起日志追加条目
  • follower也追加,并返回leader
  • leader收到半数以上的回复,则进行提交。(只有新log被半数以上结点存储了,才认为可提交,否则认为新log不可提交)
  • follower在leader提交后的下一轮心跳包,才可以通过leader是否提交,判断自己该不该提交

总结:leader发送日志追加请求,如果半数以上follower都成功追加, 则提交,提交后下一轮,要求所有follower也提交。leader和follower的提交不在同一轮

避免集群脑裂,也是raft的主要考虑点

旧leader上线后,如果此时仍有新leader,由于旧leader的任期号太旧,可以意识到自己的逻辑时钟过于落后,可能已经不是leader身份了,此时也会降级

5.3 和paxos算法区别

详见paxos算法,这个算法也是三个角色,两阶段提交,逻辑时钟时间戳。有效的保证了网络延时,重发,分区等的一致性

区别就是,paxos是去中心化的,效率能高一些,相对的同步和数据恢复就复杂了点。

raft是中心化的,中心化的leader对外提供服务,效率低了些,同步和数据恢复就简单了些。

6. 分布式实战

6.1 分布式计算

分布式的计算,比如spark,hadoop,基于map reduce 的大数据计算。

同样需要管理计算节点。可以采取中心化调度, 中心化就是方便。

分布式计算过程无非就是一个链路调用,哪里错了点哪里即可。不用像分布式存储那么复杂

6.2 分布式存储

分布式存储和分布式计算不同的是,就是那个CAP的规则了。

如果是分布式计算,那么管理起来还是比较方便的,谁挂了重来就行。反正数据是分布式存储提供的。

那么分布式存储的正确性,直接影响了分布式计算的输入,这时候,用我们的raft算法来保证一致性,再好不过了。

而且还可以有自己的优化,架构等。