网站制作定制18,建设微网站的特色,wordpress tree,wordpress 动漫主题1.简介
1.1简介——使用复制的目的
在分布式系统中#xff0c;数据通常需要被分散在多台机器上#xff0c;主要为了达到以下目的#xff1a; 扩展性#xff0c;数据量因读写负载巨大#xff0c;一台机器无法承载#xff0c;数据分散在多台机器 上可以有效地进行负载均衡…1.简介
1.1简介——使用复制的目的
在分布式系统中数据通常需要被分散在多台机器上主要为了达到以下目的 扩展性数据量因读写负载巨大一台机器无法承载数据分散在多台机器 上可以有效地进行负载均衡达到灵活的横向扩展。容错、高可用在分布式系统中单机故障是常态在单机故障下仍然希望 系统能够正常工作这时候就需要数据在多台机器上做冗余在遇到单机故 障时其他机器就可以及时接管。统一的用户体验如果系统客户端分布在多个地域通常考虑在多个地域部署 服务以方便用户能够就近访问到他们所需要的数据获得统一的用户体验。 数据的多机分布的方式主要有两种一种是将数据分片保存每个机器保存数据的部分分片Kafka 中称为 Partition其他部分系统称为 Shard另一种则是完全的冗余其中每一份数据叫做一个副本Kafka 中称为 Replica通过数据复制技术实现。在分布式系统中两种方式通常会共同使用最后的数据分布往往是下图的样子一台机器上会保存不同数据分片的若干个副本。本系列博文主要介绍的是数据如何做复制分区则是另一个主题不在本文的讨论范畴 复制的目标需要保证若干个副本上的数据是一致的这里的“一致”是一个十分不确定的词既可以是不同副本上的数据在任何时刻都保持完全一致也可以是不同客户端不同时刻访问到的数据保持一致。一致性的强弱也会不同有可能需要任何时候不同客端都能访问到相同的新的数据也有可能是不同客户端某一时刻访问的数据不相 同但在一段时间后可以访问到相同的数据。因此“一致性”是一个值得单独抽出来细说的词。在下一篇文章中我们将重点介绍这个词在不同上下文之间的含义。此时大家可能会有疑问直接让所有副本在任意时刻都保持一致不就行了为啥还要有各种不同的一致性呢我们认为有两个考量点第一是性能第二则是复杂性。 性能比较好理解因为冗余的目的不完全是为了高可用还有延迟和负载均衡这类提升性能的目的如果只一味地为了地强调数据一致可能得不偿失。复杂性是因为分布式系统中有着比单机系统更加复杂的不确定性节点之间由于采用不大可靠的网络进行传输并且不能共享统一的一套系统时间和内存地址后文会详细进行说明这使得原本在一些单机系统上很简单的事情在转到分布式系统上以后就变得异常复杂。这种复杂性和不确定性甚至会让我们怀疑这些副本上的数据真的能达成一致吗下一篇文章会专门详细分析如何设计算法来应对这种复杂和不确定性。 2.数据复制模式
总体而言最常见的复制模式有三种分别为主从模式、多主节点模式、无主节点模式下面分别进行介绍。
2.1 最简单的复制模式——主从模式
简介 对复制而言最直观的方法就是将副本赋予不同的角色其中有一个主副本主副本将数据存储在本地后将数据更改作为日志或者以更改流的方式发到各个从副本后文也会称节点中。在这种模式下所有写请求就全部会写入到主节点上读请求既可以由主副本承担也可以由从副本承担这样对于读请求而言就具备了扩展性并进行了负载均衡。但这里面存在一个权衡点就是客户端视角看到的一致性问题。这个权衡点存在的核心在于数据传输是通过网络传递的数据在网络中传输的时间是不能忽略的。 如上图所示在这个时间窗口中任何情况都有可能发生。在这种情况下客户端何时算写入完成会决定其他客户端读到数据的可能性。这里我们假设这份数据有一个主副本和一个从副本如果主副本保存后即向客户端返回成功这样叫做异步复制1。而如果等到数据传送到从副本 1并得到确认之后再返回客户端成功称为同步复制2。这里我们先假设系统正常运行在异步同步下如果从副本承担读请求假设 reader1 和 reader2 同时在客户端收到写入成功后发出读请求两个 reader 就可能读到不一样的值。
为了避免这种情况实际上有两种角度的做法第一种角度是让客户端只从主副本读取数据这样在正常情况下所有客户端读到的数据一定是一致的Kafka 当前的做法另一种角度则是采用同步复制假设使用纯的同步复制当有多个副本时任何一个副本所在的节点发生故障都会使写请求阻塞同时每次写请求都需要等待所有节点确认如果副本过多会极大影响吞吐量。而如果仅采用异步复制并由主副本承担读请求当主节点故障发生切换时一样会发生数据不一致的问题
很多系统会把这个决策权交给用户这里我们以 Kafka 为例首先提供了同步与异步复制的语义通过客户端的 acks 参数确定另外提供了 ISR 机制而只需要 ISR中的副本确认即可系统可以容忍部分节点因为各种故障而脱离 ISR那样客户端将不用等待其确认增加了系统的容错性。当前 Kafka 未提供让从节点承担读请求的设计但在高版本中已经有了这个 Feature。这种方式使系统有了更大的灵活性用户可以根据场景自由权衡一致性和可用性
2.1.1主从模式下需要的一些能力
增加新的从副本节点
在 Kafka 中我们所采取的的方式是通过新建副本分配的方式以追赶的方式从主副本中同步数据。数据库所采用的的方式是通过快照 增量的方式实现。 a. 在某一个时间点产生一个一致性的快照。 b. 将快照拷贝到从节点。 c. 从节点连接 到主节点请求所有快照点后发生的改变日志。 d. 获取到日志后应用日志到自己的 副本中称之为追赶。 e. 可能重复多轮 a-d。
处理节点失效
从节点失效——追赶式恢复 针对从节点失效恢复手段较为简单一般采用追赶式恢复。而对于数据库而言从节点可以知道在崩溃前所执行的最后一个事务然后连接主节点从该节点将拉取所有的事件变更将这些变更应用到本地记录即可完成追赶。 对于 Kafka 而言恢复也是类似的Kafka 在运行过程中会定期项磁盘文件中写入 checkpoint共包含两个文件一个是 recovery-point-offset-checkpoint记录已经写到磁盘的 offset另一个则是 replication-offset-checkpoint用来记录高水位下文简称 HW由 ReplicaManager 写入下一次恢复时Broker 将读取两个文件的内容可能有些被记录到本地磁盘上的日志没有提交这时就会先截断Truncate到 HW 对应的 offset 上然后从这个 offset 开始从 Leader 副本拉取数据直到认追上 Leader被加入到 ISR 集合中
主节点失效—节点切换
主节点失效则会稍稍复杂一些需要经历三个步骤来完成节点的切换。
确认主节点失效由于失效的原因有多种多样大多数系统会采用超时来判定节点失效。一般都是采用节点间互发心跳的方式如果发现某个节点在较长时间内无响应则会认定为节点失效。具体到 Kafka 中它是通过和Zookeeper下文简称 ZK间的会话来保持心跳的在启动时 Kafka 会在ZK 上注册临时节点此后会和 ZK 间维持会话假设 Kafka 节点出现故障这里指被动的掉线不包含主动执行停服的操作当会话心跳超时时ZK上的临时节点会掉线这时会有专门的组件Controller监听到这一信息并认定节点失效。选举新的主节点。这里可以通过通过选举的方式民主协商投票通常使用共识算法或由某个特定的组件指定某个节点作为新的节点Kafka 的Controller。在选举或指定时需要尽可能地让新主与原主的差距最小这样会最小化数据丢失的风险让所有节点都认可新的主节点是典型的共识问题——这里所谓共识就是让一个小组的节点就某一个议题达成一致下一篇文章会重点进行介绍。重新配置系统是新的主节点生效这一阶段基本可以理解为对集群的元数据进行修改让所有外界知道新主节点的存在Kafka 中 Controller 通过元数据广播实现后续及时旧的节点启动也需要确保它不能再认为自己是主节点从而承担写请求。
问题
虽然上述三个步骤较为清晰但在实际发生时还会存在一些问题 假设采用异步复制在失效前新的主节点与原主节点的数据存在 Gap选举完成后原主节点很快重新上线加入到集群这时新的主节点可能会收到冲突的写请求此时还未完全执行上述步骤的第三步也就是原主节点没有意识到自己的角色发生变化还会尝试向新主节点同步数据。这时一般的做法是将原主节点上未完成复制的写请求丢掉但这又可能会发生数据丢失或不一致假设我们每条数据采用 MySQL 的自增 ID 作为主键并且使用Redis 作为缓存假设发生了 MySQL 的主从切换从节点的计数器落后于主节点那样可能出现应用获取到旧的自增 ID这样就会与 Redis 上对应 ID取到的数据不一致出现数据泄露或丢失。 假设上面的问题原主节点因为一些故障永远不知道自己角色已经变更则可能发生“脑裂”两个节点同时操作数据又没有相应解决冲突没有设计这一模块就有可能对数据造成破坏。 此外对于超时时间的设定也是个十分复杂的问题过长会导致服务不可用设置过短则会导致节点频繁切换假设本身系统处于高负载状态频繁角色切换会让负载进一步加重团队内部对 Kafka 僵尸节点的处理逻辑。
异步复制面临的主要问题—复制滞后 如前文所述如果我们使用纯的同步复制任何一台机器发生故障都会导致服务不可写入并且在数较多的情况下吞吐和可用性都会受到比较大的影响。很多系统都会采用半步复制或异步复制来在可用性和一致性之间做权衡
2.2 多主节点复制
前文介绍的主从复制模型中存在一个比较严重的弊端就是所有写请求都需要经过主节点因为只存在一个主节点就很容易出现性能问题。虽然有从节点作为冗余应对容错但对于写入请求实际上这种复制方式是不具备扩展性的。此外如果客户端来源于多个地域不同客户端所感知到的服务相应时间差距会非常大。因此有些系统顺着传统主从复制进行延伸采用多个主节点同时承担写请求主节点接到写入请求之后将数据同步到从节点不同的是这个主节点可能还是其他节点的从节点。复制模式如下图所示可以看到两个主节点在接到写请求后将数据同步到同一个数据中心的从节点。此外该主节点还将不断同步在另一数据中心节点的数据由于每个主节点同时处理其他主节点的数据和客户端写入的数据因此需要模型中增加一个冲突处理模块最后写到主节点的数据需要解决冲突。 使用场景
a. 多数据中心部署 一般采用多主节点复制都是为了做多数据中心容灾或让客户端就近访问用一个高大上的名词叫做异地多活在同一个地域使用多主节点意义不大在多个地域或者数据中心部署相比主从复制模型有如下的优势 1.性能提升性能提升主要表现在两个核心指标上首先从吞吐方面传统的主从模型所有写请求都会经过主节点主节点如果无法采用数据分区的方式进行负载均衡可能存在性能瓶颈采用多主节点复制模式下同一份数据就可以进行负载均衡可以有效地提升吞吐。另外由于多个主节点分布在多个地域处于不同地域的客户端可以就近将请求发送到对应数据中心的主节点可以最大程度地保证不同地域的客户端能够以相似的延迟读写数据提升用户的使用体验。 2.容忍数据中心失效对于主从模式假设主节点所在的数据中心发生网络故障需要发生一次节点切换才可将流量全部切换到另一个数据中心而采用多主节点模式则可无缝切换到新的数据中心提升整体服务的可用性。 b. 离线客户端操作 除了解决多个地域容错和就近访问的问题还有一些有趣的场景其中一个场景则是在网络离线的情况下还能继续工作例如我们笔记本电脑上的笔记或备忘录我们不能因为网络离线就禁止使用该程序我们依然可以在本地愉快的编辑内容图中标记为 Offline 状态当我们连上网之后这些内容又会同步到远程的节点上这里面我们把本地的 App 也当做其中的一个副本那么就可以承担用户在本地的变更请求。联网之后再同步到远程的主节点上。
c. 协同编辑 这里我们对离线客户端操作进行扩展假设我们所有人同时编辑一个文档每个人通过 Web 客户端编辑的文档都可以看做一个主节点。这里我们拿美团内部的学城内部的 Wiki 系统举例当我们正在编辑一份文档的时候基本上都会发现右上角会出现“xxx 也在协同编辑文档”的字样当我们保存的时候系统就会自动将数据保存 到本地并复制到其他主节点上各自处理各自端上的冲突。另外当文档出现了更新时学城会通知我们有更新需要我们手动点击更新来更新我们本地主节点的数据。书中说明虽然不能将协同编辑完全等同于数据库复制 但却是有很多相似之处也需要处理冲突问题
冲突解决
通过上面的分析我们了解到多主复制模型最大挑战就是解决冲突下面我们简单看下《DDIA》中给出的通用解法在介绍之前我们先来看一个典型的冲突。
a. 冲突实例 在图中由于多主节点采用异步复制用户将数据写入到自己的网页就返回成功了但当尝试把数据复制到另一个主节点时就会出问题这里我们如果假设主节点更新时采用类似 CAS 的更新方式时更新时都会由于预期值不符合从而拒绝更新。针对这样的冲突书中给出了几种常见的解决思路。
b. 解决思路
避免冲突 所谓解决问题最根本的方式则是尽可能不让它发生如果能够在应用层保证对特定数据的请求只发生在一个节点上这样就没有所谓的“写冲突”了。继续拿上面的协同编辑文档举例如果我们把每个人的都在填有自己姓名表格的一行里面进行编辑这样就可以最大程度地保证每个人的修改范围不会有重叠冲突也就迎刃而解了。收敛于一致状态 然而对更新标题这种情况而言冲突是没法避免的但还是需要有方法解决。对于单主节点模式而言如果同一个字段有多次写入那么最后写入的一定是最新的。ZK、KafkaController、KafkaReplica 都有类似 Epoch 的方式去屏蔽过期的写操作由于所有的写请求都经过同一个节点顺序是绝对的但对于多主节点而言由于没有绝对顺序的保证就只能试图用一些方式来决策相对顺序使冲突最终收敛 这里提到了几种方法 给每个写请求分配 Uniq-ID例如一个时间戳一个随机数一个 UUID 或 Hash值最终取最高的 ID 作为最新的写入。如果基于时间戳则称作最后写入者获胜LWW这种方式看上去非常直接且简单并且非常流行。但很遗憾文章一开始也提到了分布式系统没有办法在机器间共享一套统一的系统时间所以这个方案很有可能因为这个问题导致数据丢失时钟漂移。 每个副本分配一个唯一的 IDID 高的更新优先级高于地域低的这显然也会丢失数据。用户自行处理 其实把这个操作直接交给用户让用户自己在读取或写入前进行冲突解决这种例子也是屡见不鲜Github 采用就是这种方式。 这里只是简单举了一些冲突的例子其实冲突的定义是一个很微妙的概念。《DDIA》第七章介绍了更多关于冲突的概念感兴趣同学可以先自行阅读在下一篇文章中也会提到这个问题
c. 处理细节介绍 此外在书中将要结束《复制》这一章时也详细介绍了如何进行冲突的处理这里也简单进行介绍。 这里我们可以思考一个问题为什么会发生冲突通过阅读具体的处理手段后我们可以尝试这样理解正是因为我们对事件发生的先后顺序不确定但这些事件的处理主体都有重叠比如都有设置某个数据的值。通过我们对冲突的理解加上我们的常识推测会有这样几种方式可以帮我们来判断事件的先后顺序。
直接指定事件顺序 对于事件发生的先后顺序我们一个最直观的想法就是两个请求谁新要谁的那这里定义“最新”是个问题一个很简单的方式是使用时间戳这种算法叫做最后写入者获胜 LWW。但分布式系统中没有统一的系统时钟不同机器上的时间戳无法保证精确同步那就可能存在数据丢失的风险并且由于数据是覆盖写可能不会保留中间值那么最终可能也不是一致的状态或出现数据丢失。如果是一些缓存系统覆盖写看上去也是 可以的这种简单粗暴的算法是非常好的收敛冲突的方式但如果我们对数据一致性要求较高则这种方式就会引入风险除非数据写入一次后就不会发生改变。从事件本身推断因果关系和并发 上面直接简单粗暴的制定很明显过于武断那么有没有可能时间里面就存在一些因果关系呢如果有我们很显然可以通过因果关系知道到底需要怎样的顺序如果不行再通过指定的方式呢 例如
这里是书中一个多主节点复制的例子这里 ClientA 首先向 Leader1 增加一条数据x1然 Leader1 采用异步复制的方式将变更日志发送到其他的 Leader 上。在复制过程中ClientB 向 Leader3 发送了更新请求内容则是更新 Key 为 x 的 Value使 ValueValue1。 原图中想表达的是update 的日志发送到 Leader2 的时间早于 insert 日志发送到Leader2 的时间会导致更新的 Key 不存在。但是这种所谓的事件关系本身就不是完全不相干的书中称这种关系为依赖或者 Happens-before。
我们可能在 JVM 的内存模型JMM中听到过这个词在 JMM 中表达的也是多个线程操作的先后顺序关系。这里如果我们把线程或者请求理解为对数据的操作区别在于一个是对本地内存数据另一个是对远程的某处内存进行修改线程或客户端都是一种执行者区别在于是否需要使用网络那这两种 Happens-before 也 就可以在本质上进行统一了都是为了描述事件的先后顺序而生。书中给出了检测这类事件的一种算法并举了一个购物车的例子如图所示以餐厅扫码点餐的场景为例 图中两个客户端同时向购物车里放东西事例中的数据库假设只有一个副本。
首先 Client1 向购物车中添加牛奶此时购物车为空返回版本 1Value 为[ 牛奶 ]。此时 Client2 向其中添加鸡蛋其并不知道 Client1 添加了牛奶但服务器可以知道因此分配版本号为 2并且将鸡蛋和牛奶存成两个单独的值最后将两个值和版本号 2 返回给客户端。此时服务端存储了 [ 鸡蛋 ] 2 [ 牛奶 ]1。同理Client1 添加面粉这时候 Client1 只认为添加了 [ 牛奶 ]因此将面粉与牛奶合并发送给服务端 [ 牛奶面粉 ]同时还附带了之前收到的版本号 1此时服务端知道新值 [ 牛奶面粉 ] 可以替换同一个版本号中的旧值[ 牛奶 ]但 [ 鸡蛋 ] 是并发事件分配版本号 3返回值 [ 牛奶面粉 ] 3 [ 鸡蛋 ]2。同理Client2 向购物车添加 [ 火腿 ]但在之前的请求中返回了 [ 鸡蛋 牛 奶 ]因此和火腿合并发送给服务端 [ 鸡蛋牛奶火腿 ]同时附带了版本号2服务端直接将新值覆盖之前版本 2 的值 [ 鸡蛋 ]但 [ 牛奶面粉 ] 是并发事件因此存储值为 [ 牛奶面粉 ] 3 [ 鸡蛋牛奶火腿 ] 4 并分配版本号 4。最后一次 Client 添加培根通过之前返回的值里知道有 [ 牛奶面粉鸡蛋 ]Client 将值合并 [ 牛奶面粉鸡蛋培根 ] 联通之前的版本号一起发送给服务端服务端判断 [ 牛奶面粉鸡蛋培根 ] 可以覆盖之前的 [ 牛奶面粉 ] 但 [ 鸡蛋牛奶火腿 ] 是并发值加以保留。
通过上面的例子我们看到了一个根据事件本身进行因果关系的确定。书中给出了进 一步的抽象流程 1.服务端为每个主键维护一个版本号每当主键新值写入时递增版本号并将新版本号和写入值一起保存。 2.客户端写主键写请求比包含之前读到的版本号发送的值为之前请求读到的值和新值的组合写请求的相应也会返回对当前所有的值这样就可以一步步进行拼接。 3.当服务器收到有特定版本号的写入时覆盖该版本号或更低版本号的所有值保留高于请求中版本号的新值与当前写操作属于并发。有了这套算法我们就可以检测出事件中有因果关系的事件与并发的事件而对于并发的事件仍然像上文提到的那样需要依据一定的原则进行合并如果使用LWW依然可能存在数据丢失的情况。因此需要在服务端程序的合并逻辑中需要在购物车这个例子中比较合理的是合并新值和旧值即最后的值是 [ 牛奶鸡蛋面粉火腿培根 ]但这样也会导致一个问题假设其中的一个用户删除了一项商品但是 union 完还是会出现在最终的结果中这显然不符合预期。因此可以用一个类似的标记位标记记录的删除这样在合并时可以将这个商品踢出这个标记在书中被称为墓碑Tombstone。 2.3 无主节点复制
之前介绍的复制模式都是存在明确的主节点从节点的角色划分的主节点需要将数据复制到从节点所有写入的顺序由主节点控制。但有些系统干脆放弃了这个思路去掉了主节点任何副本都能直接接受来自客户端的写请求或者再有一些系统中会给到一个协调者代表客户端进行写入以 Group Commit 为例由一个线程积攒所 有客户端的请求统一发送与多主模式不同协调者不负责控制写入顺序这个限制的不同会直接影响系统的使用方式。
处理节点失效
假设一个数据系统拥有三个副本当其中一个副本不可用时在主从模式中如果恰好是主节点则需要进行节点切换才能继续对外提供服务但在无主模式下并不存在这一步骤如下图所示 这里的 Replica3 在某一时刻无法提供服务此时用户可以收到两个 Replica 的写入成功的确认即可认为写入成功而完全可以忽略那个无法提供服务的副本。当失效的节点恢复时会重新提供读写服务此时如果客户端向这个副本读取数据就会请求到过期值
为了解决这个问题这里客户端就不是简单向一个节点请求数据了而是向所有三个副本请求这时可能会收到不同的响应这时可以通过类似版本号来区分数据的新旧类似上文中并发写入的检测方式。这里可能有一个问题副本恢复之后难道就一直让自己落后于其他副本吗这肯定不行这会打破一致性的语义因此需要一个机 制。有两种思路
客户端读取时对副本做修复如果客户端通过并行读取多个副本时读到了过期的数据可以将数据写入到旧副本中以便追赶上新副本。反熵查询一些系统在副本启动后后台会不断查找副本之间的数据 diff将diff 写到自己的副本中与主从复制模式不同的是此过程不保证写入的顺序并可能引发明显的复制滞后。
上文中的实例我们可以看出这种复制模式下要想保证读到的是写入的新值每次只从一个副本读取显然是有问题的那么需要每次写几个副本呢又需要读取几个副本呢这里的一个核心点就是让写入的副本和读取的副本有交集那么我们就能够保证读到新值了。 直接上公式:wrN。其中 N 为副本的数量w 为每次并行写入的节点数r为每次同时读取的节点数这个公式非常容易理解就不做过多赘述。不过这里的公式虽然看着比较直白也简单里面却蕴含了一些系统设计思考 Quorum 一致性的局限性
看上去这个简单的公式就可以实现很强大的功能但这里有一些问题值得注意 1.首先Quorum 并不是一定要求多数重要的是读取的副本和写入副本有重合即可可以按照读写的可用性要求酌情考虑配置。 2.另外对于一些没有很强一致性要求的系统可以配置 wr N这样可以等待更少的节点即可返回这样虽然有可能读取到一个旧值但这种配置可以很大提升系统的可用性当网络大规模故障时更有概率让系统继续运行而不是由于没有达到 Quorum 限制而返回错误。 3.假设在 wrN 的情况下实际上也存在边界问题导致一些一致性问题 a. 首先假设是 Sloppy Quorum一个更为宽松的 Quorum 算法写入的 w和读取的 r 可能完全不相交因此不能保证数据一定是新的。 b.如果两个写操作同时发生那么还是存在冲突在合并时如果基于 LWW仍然可能导致数据丢失。 c. 如果写读同时发生也不能保证读请求一定就能取到新值因为复制具有滞后性上文的复制窗口。 d.如果某些副本写入成功其他副本写入失败磁盘空间满且总的成功数少于 w那些成功的副本数据并不会回滚这意味着及时写入失败后续还是可能读到新值。虽然看上去 Quorum 复制模式可以保证获取到新值但实际情况并不是我们想象的样子这个协议到最后可能也只能达到一个最终的一致性并且依然需要共识算法的加持 3.分布式系统的挑战
这部分存在的意义主要想让大家理解为什么一些看似简单的问题到了分布式系统中就会变得异常复杂。顺便说一声这一章都是一些“奇葩”现象并没有过于复杂的推理和证明希望大家能够较为轻松愉悦地看完这些内容。
3.1 部分失效 这是分布式系统中特有的一个名词这里先看一个现实当中的例子。假设老板想要处理一批文件如果让一个人做需要十天。但老板觉得有点慢于是他灵机一动想到可以找十个人来搞定这件事然后自己把工作安排好认为这十个人一天正好干完于是向他的上级信誓旦旦地承诺一天搞定这件事。他把这十个人叫过来把任务分配给了他们他们彼此建了个微信群约定每个小时在群里汇报自己手上的工作进度并强调在晚上 5 点前需要通过邮件提交最后的结果。于是老板就去愉快的喝茶去了但是现实却让他大跌眼镜。首先有个同学家里信号特别差报告进度的时候只成功报告了 3 个小时的然后老板在微信里问也收不到任何回复最后结果也没法提交。另一个同学家的表由于长期没换电池停在了下午四点结果那人看了两次表都是四点所以一点都没着急 中间还看了个电影慢慢悠悠做完交上去了他还以为老板会表扬他提前了一小时交结果实际上已经是晚上八点了。还有一个同学因为前一天没睡好效率极低而且也没办法再去高强度的工作了。结果到了晚上 5 点只有 7 个人完成了自己手头上的工作 这个例子可能看起来并不是非常恰当但基本可以描述分布式系统特有的问题了。在分布式的系统中我们会遇到各种“稀奇古怪”的故障例如家里没信号网络故障 )不管怎么叫都不理你或者断断续续的理你。另外因为每个人都是通过自己家的表看时间的所谓的 5 点需要提交结果在一定程度上旧失去了参考的绝对价值。因此作为上面例子中的“老板”不能那么自信的认为一个人干工作需要 10天就可以放心交给 10 个人让他们一天搞定。我们需要有各种措施来应对分派任务带来的不确定性回到分布式系统中部分失效是分布式系统一定会出现的情况。作为系统本身的设计人员我们所设计的系统需要能够容忍这种问题相对单机系统来说这就带来了特有的复杂性。
3.2 分布式系统特有的故障
不可靠的网络
对于一个纯的分布式系统而言它的架构大多为 Share Nothing 架构即使是存算分离这种看似的 Share Storage它的底层存储一样是需要解决 Share Nothing 的。所谓 Nothing这里更倾向于叫 Nothing but Network网络是不同节点间共享信息的唯一途径数据的传输主要通过以太网进行传输这是一种异步网络也就是网络本身并不保证发出去的数据包一定能被接到或是何时被收到。这里可能发生各种错误如下图所示 请求丢失 请求正在某个队列中等待 远程节点已经失效 远程节点无法响应 远程节点已经处理完请求但在 ack 的时候丢包 远程接收节点已经处理完请求但回复处理很慢 本文认为造成网络不可靠的原因不光是以太网和 IP 包本身其实应用本身有时候异常也是造成网络不可靠的一个诱因。因为我们所采用的节点间传输协议大多是TCPTCP 是个端到端的协议是需要发送端和接收端两端内核中明确维护数据结构来维持连接的如果应用层发生了下面的问题那么网络包就会在内核的 Socket Buffer 中排队得不到处理或响应得不到处理。
应用程序 GC。处理节点在进行重的磁盘 I/O导致 CPU 无法从中断中恢复从而无法处理网络请求。由于内存换页导致的颠簸。这些问题和网络本身的不稳定性相叠加使得外界认为的网络不靠谱的程度更加严 重。因此这些不靠谱会极大地加重上一章中的 复制滞后性进而带来各种各样的一致性问题
应对之道
网络异常相比其他单机上的错误而言可能多了一种不确定的返回状态即延迟而且延迟的时间完全无法预估。这会让我们写起程序来异常头疼对于上一章中的问题我们可能无从知晓节点是否失效因为你发的请求压根可能不会有人响应你。因此我们需要把上面的“不确定”变成一种确定的形式那就是利用“超时”机制。 这里引申出两个问题 假设能够检测出失效我们应该如何应对 a. 需要避免往失效的节点上发数据服务发现模块中的健康检查功能。 b. 如果在主从复制中如果主节点失效需要出发选举机制Kafka 中的临时节点掉线Controller 监听到变更触发新的选举Controller 本身的选举机制。 c. 如果服务进程崩溃但操作系统运行正常可以通过脚本通知其他节点以便新的节点来接替Kafka 的僵尸节点检测会触发强制的临时节点掉线。 d. 如果路由器已经确认目标节点不可访问则会返回 ICMP 不可达ping 不通走下线。 如何设置超时时间是合理的 很遗憾地告诉大家这里面实际上是个权衡的问题短的超时时间会更快地发现故 障但同时增加了误判的风险。这里假设网络正常那么如果端到端的 ping 时间为 d处理时间为 r那么基本上请求会在 2dr 的时间完成。但在现实中我们无法假 设异步网络的具体延迟实际情况可能会更复杂。因此这是一个十分靠经验的工作。
不可靠的时钟
说完了“信号”的问题下面就要说说每家的“钟表”——时钟了它主要用来做两件事
描述当前的绝对时间描述某件事情的持续时间
在 DDIA 中对于这两类用途给出了两种时间一类成为墙上时钟它们会返回当前的日期和时间例如 clock_gettime(CLOCK_REALTIME) 或者 System.currentTimeMills但这类反应精确时间的 API由于时钟同步的问题可能会出现回拨的情况。因此作为持续时间的测量通常采用单调时钟例如 clock_gettime(CLOCK_MONOTONIC) 或者 System.nanoTime。高版本的 Kafka 中把请求的相应延迟计 算全部换成了这个 API 实现应该也是这个原因。这里时钟同步的具体原理以及如何会出现不准确的问题这里就不再详细介绍了感兴趣的同学可以自行阅读书籍。下面将介绍一下如何使用时间戳来描述事件顺序的 案例并展示如何因时钟问题导致事件顺序判断异常的 这里我们发现Node1 的时钟比 Node3 快当两个节点在处理完本地请求准备写Node2 时发生了问题原本 ClientB 的写入明显晚于 ClientA 的写入但最终的结果却由于 Node1 的时间戳更大而丢弃了本该保留的 x1这样如果我们使用LWW一定会出现数据不符合预期的问题。 由于时钟不准确这里就引入了统计学中的置信区间的概念也就是这个时间到底在一个什么样的范围里一般的 API 是无法返回类似这样的信息的。不过Google的 TrueTime API 则恰恰能够返回这种信息其调用结果是一个区间有了这样的API确实就可以用来做一些对其有依赖的事情了例如 Google 自家的 Spanner就是使用 TrueTime 实现快照隔离。
如何在这艰难的环境中设计系统
上面介绍的问题是不是挺“令人绝望”的你可能发现现在时间可能是错的测量可能是不准的你的请求可能得不到任何响应你可能不知道它是不是还活着……这种环境真的让设计分布式系统变得异常艰难就像是你在 100 个人组成的大部门里面协调一些工作一样工作量异常的巨大且复杂。
但好在我们并不是什么都做不了以协调这件事为例我们肯定不是武断地听取一个人的意见让我们回到学生时代。我们需要评选一位班长肯定我们都经历过投票、唱票的环节最终得票最多的那个人当选有时可能还需要设置一个前提需要得票超过半数。映射到分布式系统中也是如此我们不能轻易地相信任何一台节点的信息因为它有太多的不确定因此更多的情况下在分布式系统中如果我们需要就某个事情达成一致也可以采取像竞选或议会一样大家协商、投票、仲裁决定一项提议达成一致真相由多数人商议决定从而达到大家的一致和统一这也就是后面要介绍的分布式共识协议。这个协议能够容忍一些节点的部分失效或者莫名其妙的故障带来的问题让系统能够正常地运行下去确保请求到的数据是可信的。下面给出一些实际分布式算法的理论模型根据对于延迟的假设不同这里介绍三种系统模型
同步模型 该模型主要假设网络延迟是有界的我们可以清楚地知道这个延迟的上下界不管出现任何情况它都不会超出这个界限。半同步模型大部分模型都是基于这个假设 半同步模型认为大部分情况下网络和延迟都是正常的如果出现违背的情况偏差可能会非常大。异步模型 对延迟不作任何假设没有任何超时机制。而对于节点失效的处理也存在三种模型这里我们忽略恶意谎言的拜占庭模型就剩下两种。崩溃 - 终止模型Crash-Stop该模型中假设一个节点只能以一种方式发生故障即崩溃可能它会在任意时刻停止响应然后永远无法恢复。崩溃 - 恢复模型节点可能在任何时刻发生崩溃可能会在一段时间后恢复并再次响应在该模型中假设在持久化存储中的数据将得以保存而内存中的数据会丢失。而多数的算法都是基于半同步模型 崩溃 - 恢复模型来进行设计的