分布式系统设计是一项十分复杂且具有挑战性的事情。其中,数据复制与一致性更是其中十分重要的一环。数据复制领域概念庞杂、理论性强,如果对应的算法没有理论验证大概率会出错。如果在设计过程中,不了解对应理论所解决的问题以及不同理论之间的联系,势必无法设计出一个合理的分布式系统。
本系列文章分上下两篇,以《数据密集型应用系统设计(DDIA)》(下文简称《DDIA》)为主线,文中的核心理论讲解与图片来自于此书。在此基础上,加入了日常工作中对这些概念的理解与个性化的思考,并将它们映射到Kafka中,跟大家分享一下如何将具体的理论应用于实际生产环境中。
1. 简介
1.1 简介——使用复制的目的
在分布式系统中,数据通常需要被分散在多台机器上,主要为了达到以下目的:
-
扩展性,数据量因读写负载巨大,一台机器无法承载,数据分散在多台机器上可以有效地进行负载均衡,达到灵活的横向扩展。
-
容错、高可用,在分布式系统中,单机故障是常态,在单机故障下仍然希望系统能够正常工作,这时候就需要数据在多台机器上做冗余,在遇到单机故障时其他机器就可以及时接管。
-
统一的用户体验,如果系统客户端分布在多个地域,通常考虑在多个地域部署服务,以方便用户能够就近访问到他们所需要的数据,获得统一的用户体验。
数据的多机分布的方式主要有两种,一种是将数据分片保存,每个机器保存数据的部分分片(Kafka中称为Partition,其他部分系统称为Shard),另一种则是完全的冗余,其中每一份数据叫做一个副本(Kafka中称为Replica),通过数据复制技术实现。在分布式系统中,两种方式通常会共同使用,最后的数据分布往往是下图的样子,一台机器上会保存不同数据分片的若干个副本。本系列博文主要介绍的是数据如何做复制,分区则是另一个主题,不在本文的讨论范畴。

复制的目标需要保证若干个副本上的数据是一致的,这里的“一致”是一个十分不确定的词,既可以是不同副本上的数据在任何时刻都保持完全一致,也可以是不同客户端不同时刻访问到的数据保持一致。一致性的强弱也会不同,有可能需要任何时候不同客端都能访问到相同的新的数据,也有可能是不同客户端某一时刻访问的数据不相同,但在一段时间后可以访问到相同的数据。因此,“一致性”是一个值得单独抽出来细说的词。在下一篇文章中,我们将重点介绍这个词在不同上下文之间的含义。
此时,大家可能会有疑问,直接让所有副本在任意时刻都保持一致不就行了,为啥还要有各种不同的一致性呢?我们认为有两个考量点,第一是性能,第二则是复杂性。
性能比较好理解,因为冗余的目的不完全是为了高可用,还有延迟和负载均衡这类提升性能的目的,如果只一味地为了地强调数据一致,可能得不偿失。复杂性是因为分布式系统中,有着比单机系统更加复杂的不确定性,节点之间由于采用不大可靠的网络进行传输,并且不能共享统一的一套系统时间和内存地址(后文会详细进行说明),这使得原本在一些单机系统上很简单的事情,在转到分布式系统上以后就变得异常复杂。这种复杂性和不确定性甚至会让我们怀疑,这些副本上的数据真的能达成一致吗?下一篇文章会专门详细分析如何设计算法来应对这种复杂和不确定性。
1.2 文章系列概述
本系列博文将分为上下两篇,第一篇将主要介绍几种常见的数据复制模型,然后介绍分布式系统的挑战,让大家对分布式系统一些稀奇古怪的故障有一些感性的认识。
第二篇文章将针对本篇中提到的问题,分别介绍事务、分布式共识算法和一致性,以及三者的内在联系,再分享如何在分布式系统中保证数据的一致性,进而让大家对数据复制技术有一个较为全面的认识。此外,本系列还将介绍业界验证分布式算法正确性的一些工具和框架。接下来,让我们一起开始数据复制之旅吧!
2. 数据复制模式
总体而言,最常见的复制模式有三种,分别为主从模式、多主节点模式、无主节点模式,下面分别进行介绍。
2.1 最简单的复制模式——主从模式
简介
对复制而言,最直观的方法就是将副本赋予不同的角色,其中有一个主副本,主副本将数据存储在本地后,将数据更改作为日志,或者以更改流的方式发到各个从副本(后文也会称节点)中。在这种模式下,所有写请求就全部会写入到主节点上,读请求既可以由主副本承担也可以由从副本承担,这样对于读请求而言就具备了扩展性,并进行了负载均衡。但这里面存在一个权衡点,就是客户端视角看到的一致性问题。这个权衡点存在的核心在于,数据传输是通过网络传递的,数据在网络中传输的时间是不能忽略的。

如上图所示,在这个时间窗口中,任何情况都有可能发生。在这种情况下,客户端何时算写入完成,会决定其他客户端读到数据的可能性。这里我们假设这份数据有一个主副本和一个从副本,如果主副本保存后即向客户端返回成功,这样叫做异步复制(1)。而如果等到数据传送到从副本1,并得到确认之后再返回客户端成功,称为同步复制(2)。这里我们先假设系统正常运行,在异步同步下,如果从副本承担读请求,假设reader1和reader2同时在客户端收到写入成功后发出读请求,两个reader就可能读到不一样的值。
为了避免这种情况,实际上有两种角度的做法,第一种角度是让客户端只从主副本读取数据,这样,在正常情况下,所有客户端读到的数据一定是一致的(Kafka当前的做法);另一种角度则是采用同步复制,假设使用纯的同步复制,当有多个副本时,任何一个副本所在的节点发生故障,都会使写请求阻塞,同时每次写请求都需要等待所有节点确认,如果副本过多会极大影响吞吐量。而如果仅采用异步复制并由主副本承担读请求,当主节点故障发生切换时,一样会发生数据不一致的问题。
很多系统会把这个决策权交给用户,这里我们以Kafka为例,首先提供了同步与异步复制的语义(通过客户端的acks参数确定),另外提供了ISR机制,而只需要ISR中的副本确认即可,系统可以容忍部分节点因为各种故障而脱离ISR,那样客户端将不用等待其确认,增加了系统的容错性。当前Kafka未提供让从节点承担读请求的设计,但在高版本中已经有了这个Feature。这种方式使系统有了更大的灵活性,用户可以根据场景自由权衡一致性和可用性。
主从模式下需要的一些能力
增加新的从副本(节点)
1. 在Kafka中,我们所采取的的方式是通过新建副本分配的方式,以追赶的方式从主副本中同步数据。
2. 数据库所采用的的方式是通过快照+增量的方式实现。
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集合中

本文深入探讨了分布式系统中数据复制的三大模式:主从复制、多主复制与无主复制,解析了各自的适用场景及挑战。并通过实际案例,揭示了在部分失效与不可靠时钟等分布式系统特有问题面前,如何利用共识算法确保数据的一致性。
最低0.47元/天 解锁文章
471

被折叠的 条评论
为什么被折叠?



