分布式数据库的复制原理 - Quastor


如果您对后端工程感兴趣,那么设计数据密集型应用程序 (DDIA) 是必读的。数据工程世界充满了流行语和炒作,但Martin Kleppman在分解所有核心技术方面做得非常出色。
这是 DDIA 关于复制的第 5 章的摘要。
复制是将数据副本保存在多台不同机器上的地方。这些机器通过网络连接,因此您的后端服务器都可以访问它们。
您现在使用的是由多台机器组成的分布式数据库,而不是将单台机器用作数据库。
您希望在多台计算机上复制数据的原因有多种

  1. 减少延迟——印度的用户可以向位于德里的节点发送请求,而美国的用户可以向位于纽约的节点发送请求。
  2. 提高可用性- 如果其中一个节点由于某种原因出现故障,您将拥有另一个可以接管并响应数据请求的节点。
  3. 增加读取吞吐量- 多个节点能够响应读取查询,而不是仅让 1 台机器完成读取请求的所有工作。许多工作负载都是读扩展的(主要是读和一小部分写),所以提高读吞吐量是非常有帮助的。

复制的难点在于处理复制数据的更改。
当您收到修改数据库的写入请求时,如何确保所有副本都反映此写入请求?
如何阻止未更新的副本响应陈旧数据来读取请求?
有 3 种流行的策略可以将更改写入所有副本
  1. 单主复制——一个副本节点被指定为领导者。其他节点是追随者。写入请求发送到领导节点,领导节点随后会将更改传播给追随者。 这是 PostgreSQL、MongoDB、MySQL 等许多数据库使用的复制策略。
  2. 多主复制 - 这类似于单主Single Leader复制,但现在多个节点可以充当领导者并处理写入请求。多主复制通常使用外部工具实现,例如用于 MySQL 的 Tungstein Replicator、用于 PostgreSQL 的 BDR 和用于 Oracle 的 GoldenGate。
  3. 无主复制 -所有副本节点都可以接受来自客户端的写入请求,因此没有领导节点。 Riak 和 Cassandra 是使用无领导复制策略的数据库示例。亚马逊在其内部 Dynamo 系统中使用了无领导复制,因此 Riak 和 Cassandra 也被称为 Dynamo 风格。

注意 - Amazon 的 Dynamo 系统与 Amazon 的 DynamoDB 不同。DynamoDB 基于 Dynamo 的许多原则,但有不同的实现。DynamoDB 使用单主者复制。 
几乎所有分布式数据库都使用这三种方法中的一种,它们各有利弊。

然而,单主复制是分布式数据库最流行的复制策略。因此,我们将进一步深入研究单主复制。
单主复制工作原理如下

  1. 其中一个副本被设计为领导者。来自客户端的写请求将被发送给领导者,领导者会将新数据写入其本地存储。
  2. 其他副本称为追随者。每当领导者将新数据写入其本地存储时,它也会将数据更改发送给所有追随者。
  3. 每个跟随者从领导者那里获取数据更改日志,并通过应用所有新写入来更新其本地数据库副本。
  4. 当客户端想要从数据库中读取数据时,可以将读取请求查询到数据库中的任何节点——领导者或追随者。

对数据库的写入可以是异步的、同步的和半同步的。
对于异步写入,领导者将获取客户端的写入请求并更新其自己的本地存储。然后,它会响应说写入成功。响应后,领导者将向所有跟随者节点发送一条消息,其中包含来自客户端写入请求的数据更改。
通过同步写入,领导者将首先确保每个跟随者节点都已将数据更改写入其本地数据库。一旦主节点收到所有跟从节点的确认,它会回复一条写成功的消息。
对于半同步写入,主节点会等待特定数量的从节点的写入确认(这个参数可以配置),直到它响应写入成功的消息。
实际上,很少使用同步写入。使用同步写入策略,写入请求将花费极长的时间(因为您必须等待每个从节点跟随者响应)并且会经常失败(任何时候一个或多个跟随者节点没有响应)。
因此,工程师通常使用半同步策略或异步策略。
半同步和异步写入策略之间的权衡归结为您希望处理写入请求的速度(异步写入更快)以及您希望写入请求的持久性(异步写入策略在以下情况下丢失写入数据的可能性更大)领导节点在向跟随者发送写入更改之前崩溃)。
单主复制经常出现的两个问题是
  • 处理节点中断
  • 复制滞后

  
处理节点中断
节点中断是不可避免的,尤其是当您使用具有许多跟随节点的大型分布式数据库时。
有两种类型的节点中断:追随从节点中断和主节点领导者中断。
  • 追随者从节点失败:Catch-up recovery

如果跟随节点发生故障,那么它可以很容易地恢复。追随者在本地非易失性存储中记录从领导者接收到的所有数据更改。因此,追随者知道它处理的最后一笔交易。
追随者将向领导者查询自上次事务以来发生的所有更改,然后更新其本地状态以匹配当前状态。
  • 主节点领导者失败:故障转移

处理领导者的失败更加棘手。需要将追随者节点之一提升为新的领导者,并且必须重新配置客户端以将其写入发送给新的领导者。其他追随者也必须开始使用来自新领导者的数据更改。
此过程称为故障转移。
故障转移过程有很多可能出错的地方
  • 如果使用异步复制,那么新的领导者在失败之前可能还没有收到旧领导者的所有写入。这意味着较弱的耐用性保证。
  • 当原来的领导者重新上线时,他们可能被错误地配置为认为他们仍然是领导者。这是一种常见的故障,通常被称为裂脑。
  • 由于您的数据库在故障转移过程发生时无法接受新的写入,因此可能会出现负载问题。如果领导节点经常失败,那么这可能会阻塞数据库。

 
复制滞后
当您使用具有半同步或异步写入的单领导主节点复制策略时,您会经常遇到一致性问题,即客户端将从尚未完全更新的跟随从节点读取陈旧数据。
这种不一致是一种暂时的状态,如果你稍等片刻,那么所有的追随者最终都会赶上来。因此,这种效应称为最终一致性。
但是,最终一致性是一个模糊的术语,并没有指定复制滞后多长时间。可能是几秒钟甚至几分钟。
因此,即使具有“最终一致性”,复制滞后对您的用户来说也是一个大问题。
为了缓解这些问题,您可以使用多种方法来减少用户面临的一些常见问题。
我们将介绍其中一些方法以及它们解决的问题。
  • 读你自己的写入Read Your Own Writes

假设您正在构建一个 Twitter :用户可以从他们的计算机上发布一条推文,这将向您的分布式数据库发送一个写请求。
这个写请求是异步复制的,所以主节点在改变本地状态后会响应写成功。然后,它将更改发送到所有跟随节点。
如果用户在发布推文后立即刷新页面并尝试重新加载,则对用户先前推文的新读取请求可能会转到尚未通知新推文的关注者节点。因此,用户的推特资料在他刷新后不会显示他的新推文。显然,这可能会让用户非常沮丧。
读你自己的写入一致性是一种解决方案,它保证如果用户重新加载页面,他们将始终看到他们自己提交的任何更新。它不对其他用户做出任何承诺。
可以通过多种方式实现。一种可能的方法是跟踪用户上次提交更新的时间。如果他们在过去一分钟内提交了更新,那么他们的读取请求应该由领导主节点处理。
  • 单调读取

异步复制会导致一些追随者节点在更新方面落后于其他追随者节点。因此,用户可能会访问该网站并从最新的关注者节点获取推文。之后,他可能会重新加载他的页面,然后从落后的追随者节点获取推文。这将导致他的 twitter 提要“回到过去”,因为他获得的数据是陈旧的。这显然意味着糟糕的用户体验。
单调读取是这种异常不会发生的保证:实现此保证的一种方法是确保每个用户始终从同一个跟随者节点读取(不同的用户可以从不同的副本读取)。可以根据用户 ID 的哈希值而不是随机地选择跟随从节点。
  •  一致性前缀读取

假设您的 twitter 应用上有用户 A 和用户 B。用户 A 在推特上发布了一张他的狗的照片。用户 B 在推特上回复了那张照片,并称赞了这只狗。
两条推文之间存在因果关系,如果您看不到用户 A 的推文,用户 B 的回复推文就没有任何意义。
现在用户 C 关注用户 A 和用户 B。如果用户 A 的推文比用户 B 的推文经历了更多的复制滞后,那么用户 C 可能会看到用户 B 的回复推文而没有收到用户 A 的推文。
一致性前缀读取是解决此异常的保证:该保证表明,如果一系列写入以特定顺序发生,那么任何阅读这些写入的人都会看到它们以相同的顺序出现。
如果数据库始终以相同的顺序应用写入,则可以解决此问题,但是当您的数据库被分片时会出现复杂情况。查看 DDIA 了解更多详情。