设计数据密集型应用——复制(5)

在这里插入图片描述

1. 写在最前面

最近在看《Designing Data-Intensive Application》这本书,讲的是在设计一个数据密集型的应用的时候有哪些难点。为了防止自己又一次草草的读完,不能将书本上的知识内化为自己的知识,所以还是老规矩写一份读书笔记,不仅加深理解还能方便后续复习,一举多得。

作为一个不走寻常路的人,看书也都是看心情的。所以「出人意料」的第一篇文章的总结是书中的第二部分——分布式数据中的第五章——复制。

2. 复制

2.1 复制的目的

在开始了解复制之前,思考一个问题——数据复制的目的是什么?

  • 提高可用性(即使系统的一部分出现故障,系统也能继续工作
  • 减少延迟(使数据与用户在地理上更接近
  • 提供读取吞吐量(伸缩可以接受读请求的机器数量

2.2 复制的方式

如果复制的数据不会随时间而改变,那复制就很简单,将数据复制到每个节点一次即可。复制的困难之处在于处理复制数据的变更(change)。以下涵盖了几乎所有分布式数据库中的复制算法:

  • 单领导者(single leader)
  • 多领导者(multi leader)
  • 无领导者(leaderless)

3 单领导者

3.1 架构图

在这里插入图片描述

3.2 待解决的问题

  • 选择同步复制还是异步复制?

    做选择最好的方式就是列出优缺点,然后一条条对比,嗯,真香。

    优点缺点
    同步复制保证从库与主库有一致的最新副本数据从库无响应时,主库无法继续写入
    异步复制即使从库落后时,主库仍可继续写入主库失效且不可恢复,未复制给从库的写入将丢失
    半同步复制兼顾了同步与异步二者的优点复制成本增加

    注:半同步的复制架构,是为主库设置一个同步的从库和一个异步的从库

  • 如何确保新增的从库拥有主库数据的精确副本?

    • 在不锁定主库的情况下,获取某个时刻主库的快照(ps MySQL 中可以使用 innobackupex
  • 将快照复制到新的从库节点

    • 从库连接主库,并拉取快照之后发生的所有数据变更。这要求快照与主库复制日志中的位置精确关联。
    • 当从库处理完快照之后积压的数据变更,我们就说它赶上了主库。即可以继续处理主库上产生的数据变化。
  • 如何处理节点宕机?

    • 从库失效——追赶恢复

    • 主库失效——故障切换

      注1:故障切换(failover)—— 将一个从库提升为新的主库,重新配置客户端,以将它们的写操作发送给新的主库,其他从库拉取来自新主库的变更,着整个过程我们称之为故障切换。

      注 2:主库的故障切换在细节处理上会有一系列问题,比如提升异步复制的从库会丢失掉部分数据、两个节点都认为自己是主库(脑裂)、标记主库的不可用的超时时间如何设置……等。

3.3 复制的实现

主库与从库之间的数据复制如何实现?——「魔鬼藏在细节里」

  • 基于语句的复制——主库记录下它执行的每个写入请求并将该语句日志发送给从库。eg. 对于关系数据库来说,意味着每个 insert、update 或 delete 语句都被转发给每个从库。

    注:这种方式有坑,以下三种情况均有。

    • 调用非确定性函数的函数,可能会在每个副本上生成不同的值。
    • 使用自增列,或者依赖于数据库中的现有数据,则必须在每个副本上按照完全相同的顺序执行它们。当有多个并发执行的事务时,这可能成为一个限制。
    • 有副作用的语句(例如,触发器、存储过程、用户定义的函数)可能会在每个副本上产生不同的副作用。
  • 传输预写日志(WAL)

    WAL 包含哪些磁盘块中的哪些字节发生更改。这使复制与存储引擎紧密耦合。

  • 逻辑日志复制(基于行)

    复制和存储引擎不同的日志格式,这样可以使日志从存储引擎内部分离出来。比如 MySQL 的二进制日志

    注:这种方式的优点是,使领导者和追随者能够运行不同版本的数据库软件甚至不同的存储引擎。

  • 基于触发器的复制

    触发器允许在数据库系统中发生数据更改(写入事务)时自动执行自定义应用程序代码。

3.4 解决复制延迟遇到的问题

在异步复制的场景下,数据保证的是最终一致性,即如果停止写入数据库一段时间,从库最终会赶上并与主库保持一致。但「最终」一词含糊不清,宏观来看就是副本落后的程度没有限制,即复制延迟的问题,复制延迟导致以下常见问题。

  • 用户写入后从旧副本读取数据的问题,导致用户以为写入失败?
    • 解决方案:使用写后读(read-after-write) 的一致性来防止这种异常。写后读的定义可以是直接从主库读、加入逻辑时钟来判断是否当前读取副本是否有最新数据等。
  • 用户先从较新的副本读取数据,刷新后又从落后的副本读取数据,导致前后两次看到的数据不一致问题?
    • 解决方案:确保每个用户总是同一个副本来进行读取。
  • 丢失掉用户事件写入的先后顺序问题?比如 A、B 两个操作,原本 A 在 B 之前执行,但是 B 的从库延迟较低,而 A 的从库延迟较高,则原本 A、B 的顺序,在从从库读取的时候极有可能变成 B、A。
    • 解决方案:如果一系列写入按照某个顺序发生,那么任何读取这些写入时,也会看到它们以同样的顺序出现,比如讲任何因果相关的写入都写入到相同的分区。

4. 多领导者

在单个数据中心内部使用多个主库很少有意义,因为好处很少超过复杂性的代价。

4.1 架构图

在这里插入图片描述

多领导者配置中可以在每个数据中心都有主库,上图展示了这个架构。在每个数据中心内使用常规的主从复制;在数据中心之间,每个数据中心的主库都会将其更改复制到其他数据中心的主库中。

注:单领导者的多数据中心的情况为,主库必须在其中一个数据中心,副本分散在其他不同的数据中心,所有写入都必须经过主库所在的数据中心。

4.2 多主复制的应用场景

运维多个数据中心

在运维多个数据中心的情况,对比单领导者和多领导者优劣势。

单领导者多领导者
性能写入必须穿过互联网,进入主库所在的数据中心,会增加延迟。写入均在本地数据中心进行,并与其他数据中心异步赋值,延迟较低。
容忍数据中心停机主库所在数据中心发生故障是,需要进行故障切换到其他数据中心的从库。每个数据中心可以独立于其他的数据中心工作,当发生故障的数据中心归队时,复制会自动赶上。
容忍网络问题数据中心之间的通信要穿过公共互联网数据中心内的本地网络更可靠

注:有些数据库默认情况下支持多主复制,但是使用外部工具也很常见,例如 MySQL 的 Tungsten Replicator、用于 PostgreSQL 的 BDR 以及用于 Oracle 的 GoldenGate。

​ 多主复制往往被认为是危险的领域,应该尽量避免。

思考🤔:多领导者模式下,一个数据中心出现问题,单数据中心提供服务的模式下,会不会导致插入的数据 ID 不在连续,出现空洞?

需要离线操作的客户端

多主复制的另一种适用场景是:应用程序在断网之后仍需要继续工作。例如,考虑手机、笔记本电脑和其他设备上的日历应用。无论设备目前是否有联网,你需要能够随时查看你的会议,输入新的会议。如果离线状态下进行任何更改,则设备下次上线时,需要与服务器和其他设备同步。

协同编辑

实时协同编辑应用程序允许多个人同时编辑文档,比如 Google Docs 允许多人同时编辑文本文档或电子表格。当一个用户编辑文档时,所做的更改将立即应用到本地副本,并异步复制到服务器和编辑同一文档的任何其他用户。

4.3 处理写入冲突

冲突的定义:当两个写操作并发地修改同一条记录中的同一个字段,并将其设置为两个不同的值。

同步与异步冲突检测:

  • 单领导者数据库中,第二个写入将被阻塞,并等待第一个写入完成,或者中止第二个写入事务,强制用户重试。
  • 多领导者数据库中,两个写入都是成功的,并且在稍后的时间点仅仅异步地检测到冲突。不过此时要求用户解决冲突就来不及了。

避免冲突:处理冲突最简单的策略就是避免它们,如果应用程序可以确保特定记录的所有写入都通过同一个领导者,那么冲突就不会发生。「由于多领导者复制的场景下,避免冲突是经常推荐的方法」。比如,在用户可以编辑的数据应用程序中,确保来自特定用户的请求始终到同一数据中心,并使用该数据中心的领导者进行读写。

收敛至一致性的状态

  • 单领导者数据库按顺序进行写操作,如果一个字段有多个更新,最后一个写操作将决定该字段的最终值。
  • 多领导者的模式下,没有明确的写入顺序,所以可以设计一种复制方案,确保数据在所有副本中最终都是一致的。以下是常见的确保收敛到一致性的方案:
    • 给每一个写入一个唯一的 ID ,挑选最高 ID 的写入作为胜利者,即最后写入胜利(LWW, last write wins)
    • 为每个副本分配一个唯一的 ID,ID 编号更高的写入具有更高的优先级。(ps 好像木懂啊
    • 以某种方式将冲突值合并在一起
    • 用一种可保留所有信息的显示数据结构来记录冲突,并编写解决冲突的应用程序代码(ps 好像 git merge 解决冲突

自定义冲突解决逻辑

作为解决冲突最合适的方式可能取决于应用程序,大多数多主复制工具允许使用应用程序代码编写解决冲突逻辑。

  • 写时执行:数据库系统检测到复制更改日志中存在冲突,就会调用冲突处理程序。
  • 读时执行:当检测到冲突时,所有冲突写入被存储。下一次读取数据时,会将这些多个版本的数据返回给应用程序。应用程序提示用户或自动解决冲突,并将结果写回到数据库。

4.4 复制拓扑

复制拓扑描述写入从一个节点传播到另一个节点的通信路径。三领导者复制拓扑图如下:

在这里插入图片描述

最普遍的拓扑是 All-to-all topology,其中每个领导者将写入同步到其他领导者,但是这一会受限,比如,默认情况下 MySQL 仅支持 Circular topology。

注:Circular topology 和 Star topology 的问题是,如果一个节点发生故障,则可能会切断其他节点之间的复制消息流,导致它们无法通信,直到节点被修复。

5. 无领导者

5.1 架构图

在这里插入图片描述

一些存储系统采用不同的方法,放弃主库的概念,并允许任何副本直接接受来自客户端的写入。在无领导者实现中,大致分为两类:

  • 客户端直接将写入发送到几个副本。
  • 协调者(coordinator)节点代表客户端进行写入,但是与主库数据库不同的时候,协调者不执行特定的写入顺序。

注:亚马逊的 Dynamo 以及受 Dynamo 启发的 Riak、Cassandra 和 Voldemort 都是无领导者复制模型的开源数据存储。

5.2 当节点故障时写入数据库

思考:在带有三个副本的数据库中,其中一个副本目前不可用的情况下,单领导者和无领导者的处置策略是什么?

  • 单领导者:需要执行故障切换,加入新的副本节点
  • 无领导者:无需进行故障切换,确保其中两个副本成功写入视为写入成功,即可容忍一个副本不可用

深入思考:如果用户从落后的副本节点读取数据,会有什么问题?

  • 读取落后的副本数据会导致数据不一致,落后的副本节点丢失了数据

继续深入思考:如何修复副本数据落后的问题?

  • 读修复:客户端并行的读取多个节点,检查副本落后,并将新值写回到该副本
  • 反熵过程:数据存储具有后台进程,不断查找副本之间的数据差异,并将落后的数据从一个副本复制到另一个副本

再次继续深入思考:在无领导者复制策略下,读写的法定人数是否有规律可循?

  • 如果有 n 个副本,每个写入必须由 w 节点确认才能被认为是成功的,并且必须至少为每个读取查询 r 个节点,其中 w + r > n,遵循这些 r、w 值的读写成为法定人数,可以认为 r、w 是有效读写所需要的最低票数。

    注:遵循 w + r > n 这种策略,就一定不会返回陈旧值了嘛?不一定哦,事物千变万化,错综复杂,你以为你以为的就是你以为的嘛……

5.3 检测并发写入

允许多个客户端同时写入,意味着会发生冲突。这与多领导复制的写入冲突类似。在 4.3 「处理写冲突」中已经简要介绍了一些解决冲突的方法,在多领导一节中更深入的探讨下这个问题。

最后写入胜利(LWW)

每个副本只需存储「最近」的值,并允许「更旧」的值被覆盖和抛弃。此处确定一种方法来确定哪个写是「最近的」,然后每个写入最终都被复制到每个副本,那么复制最终会收敛到相同的值。

注 1:写入没有自然顺序,但是可以强制任意排序,比如,为每个写入附加一个时间戳。

注 2:LWW 会丢失并发写入,如果丢失数据不可接受的情况,则 LWW 是解决冲突的一个很烂选择。

「此前发生」的关系和并发

思考:判断两个操作发生关系并决定处置策略?

答案:以 A、B 两个操作为例,将有以下三种关系。

  • A 操作在 B 操作之前

  • B 操作在 A 操作之前

  • A、B 操作同时发生

如果一个操作发生在另一个操作之前,则后面的操作应该覆盖比较早的操作。如果这些操作是并发的,则存在需要解决的冲突。比如 LWW、合并写入等。

捕获「此前发生」关系

下图显示了 1、2 两个用户同时向同一个购物车添加商品。箭头表示哪个操作发生在其他操作之前,意味着后面的操作知道或依赖于较早的操作。在这个例子中,客户端永远无法完全掌握服务器上的数据,因为另个一个客户端也在同时进行操作,但是,旧版本的值最终会被覆盖,不会丢失任何写入。

在这里插入图片描述

服务器可以通过查看版本号来确定两个操作是否是并发的,该算法的工作原理如下:

  • 服务器为每个键保留一个版本号,每次写入键时都增加版本号,并将新版本号与写入值一起存储
  • 当客户端读取键时,服务器将返回所有未覆盖的值以及最新的版本号。客户端写入前必须读取。
  • 客户端写入键时,必须包含之前读取的版本号,并且必须将之前读取的所有值合并在一起。
  • 当服务器接收到具有特定版本号写入时,它可以覆盖该版本号或更低版本的所有值,但是它必须保持所有值更高的版本号。

合并同时写入的值

这种算法可以确保没有数据被丢弃,但是不幸的是,客户端需要做一些额外的工作: 如果多个操作并发发生,则必须通过合并并发写入值来解决问题。

版本向量

在「捕获此前发生关系」的小结中,使用了版本向量的方式,来捕获操作之间的依赖关系。但是改该方式限制的是,仅在使用一个副本的情况下。

思考:在多副本并发写入的情况下,如果捕获操作之间的依赖关系?

答案: 除了对每个键使用版本号之外,还需要在每个副本中使用版本号(所有副本的版本号集合成为版本向量)。每个副本在处理写入时增加自己的版本号,并且跟踪其他副本中看到的版本号。这个信息指出了要覆盖哪些值,要保留哪些值。

6. 碎碎念

本来以为写的会很快,但是在仔细阅读的时候发现这章概括的很多知识点在实际的业务中均有体现,所有就把大部分知识点都摘抄了过来,不管怎么说,写完了还是要照例完结撒花的。

哦,这个清明假期看了《铁娘子》,看完之后觉得很震撼,人与人之间的不同是一个又一个细小的选择所造成的,但是绝大多数人不信。摘抄两句觉得很意义的话放在最后,以警示自己吧。

  • It used to be about trying to do something.

    過去人們總是努力要做成某件事,

    Now its about trying to be someone.

    現在是要努力成為某個人。

  • Watch your thoughts , for they become words.
    注意你的思想,它們會變為言語。

    Watch your words , for they become actions.
    注意你的言語,它們會變為行動。

    Watch your actions , for they become habits.
    注意你的行動,它們會變為習慣。

    Watch your habits , for they become character.
    注意你的習慣,它們會變為性格。

    Watch your character , for it becomes your destiny.
    注意你的性格,它會變為你的命運。

7. 参考资料

  • 《Designing Data-Intensive Application》—— 第五章
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
设计数据密集型应用 (DDIA) 是一本由Martin Kleppmann撰写的专业著作,它深入探讨了如何构建高效可扩展的数据系统和应用程序。这本书通过详细解析不同领域的实际案例,帮助读者了解数据密集型应用的核心原则和最佳实践。 在DDIA中,作者首先介绍了数据系统的核心概念,例如数据模型、一致性、可靠性和可扩展性。然后,他详细阐述了各种数据存储和处理技术,包括关系型数据库、NoSQL数据库、消息队列、流处理等等。通过这些技术的比较和分析,读者可以了解它们的优缺点,并为自己的应用选择最合适的工具。 在第二部分,作者讨论了如何设计数据密集型应用的不同组件,包括数据复制和容错、数据分区和分片、数据一致性和并发控制。他提供了一些建议和模式,以帮助读者解决应用中的常见问题,例如数据冲突、性能瓶颈和容量规划。 最后,作者探讨了如何针对不同的应用场景选择合适的数据系统架构,包括关系型数据库、键值存储、文档数据库和图形数据库。他还介绍了流处理和批处理的概念,并介绍了一些实现这些架构的工具和技术。 通过阅读DDIA,读者可以获得设计和构建数据密集型应用所需的全面知识。这本书不仅适合软件工程师和系统架构师,还适用于对数据系统和应用感兴趣的任何技术人员。无论是构建社交网络、大规模数据分析系统还是电子商务网站,DDIA都是一本不可或缺的参考书。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值