在MongoDB中实现集群范围内的逻辑时钟和因果一致性(四)

388 阅读8分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第25天,点击查看活动详情

[论文阅读]在MongoDB中实现集群范围内的逻辑时钟和因果一致性(四)

下图是MongoDB Client中使用因果一致性的例子,这符合我的之前的猜测,使用时,你必须传递cluster_time,以期达到因果一致性读的能力。

image.png

下文是一个流程图:为了实现因果一致性(图中为read your writes),在secondary replication读时等待T1的apply(或者,我们可以forward到leader replication..)

preview 本文章节部分内容选自: 论文地址:dl.acm.org/doi/10.1145…

实现集群范围内的逻辑时钟

术语 "ClusterTime "是指一个节点的逻辑时钟的时间值。正如在设计选择部分所讨论的,我们选择了混合逻辑时钟HLC的设计。尽管逻辑时钟的数据结构相似,但逻辑时钟的分配算法有几个不同之处,使我们能够减轻旨在破坏数据库的恶意攻击或错误编程的客户端。MongoDB只有在状态改变的 "事件 "发生时才会增加时间。发送和接收消息不是事件,因为它们不改变节点的状态,而所有针对MongoDB的写入都是时间递增的事件。

事件和非事件之间的区别使我们能够将分布和递增ClusterTime的概念分开。每个分片的主节点的进程总是负责执行MongoDB的状态变化。客户端只参与非事件(消息的发送和接收),使得客户端没有必要增加ClusterTime。也就是说,客户端保持着对他们发送或接收的每个消息分配最大的ClusterTime的责任。

MongoDB的ClusterTime遵守以下规则。

ClusterTime Increment规则。

只有当主节点的复制操作日志(OPLOG)被写入时,ClusterTime才会被递增("ticks")。

ClusterTime分配规则。

集群节点(mongod、mongos、配置服务器、客户端)在发送消息时总是跟踪并包括最大的已知ClusterTime。

A1.2 ClusterTime增量

ClusterTime由

为了在事件发生时将ClusterTime与OPLOG条目联系起来,MongoDB首先计算出节点上的下一个ClusterTime值,然后用这个值来创建一个OpTime(带选举项)。这个OpTime就是写到OPLOG中的内容。这次更新不需要改变OpTime的格式,仍然与物理时间联系在一起。所有使用OPLOG的现有工具,如备份和恢复仍然向前兼容。

image-20211014180601255

A1.3 ClusterTime分布

每个节点都会跟踪它所见过的ClusterTime的最大值[36]。每个节点都把这个值加到它发送的每个消息中。

让我们来说明ticking和gossip是如何在一个多分片系统上一起工作的。"ticking"可能只在能够写入的数据节点上进行(每个分片的主节点)。推进可能发生在所有节点上,包括客户节点。其中推进是简单地更新到该节点所知道的ClusterTime的一个新的最大值。MongoDB进程可以签署新的值。签名不能由客户端生成。

image-20211014181057471图6.ClusterTime的增量和分布。

  1. 客户端向主站发送一个写命令,该消息包括其当前的ClusterTime值:T1。
  2. 主节点收到消息后,如果T1大于主节点的当前ClusterTime值,则将其ClusterTime推进到T1。
  3. 主节点在为写入准备OpTime的过程中,将集群时间 "滴答 "到T2。这是唯一一次产生集群时间的新值。
  4. 主节点写到OPLOG。
  5. 结果返回给客户端,它包括新的ClusterTime T2。
  6. 客户端将其ClusterTime推进到T2。

A1.4 防范恶意攻击

如前所示,节点将其逻辑时钟提前到它们在客户端消息中收到的最大群集时间。下一个oplog条目将在OpTime的时间戳部分使用这个值。但是一个恶意的客户端可以修改他们在消息中发送的最大ClusterTime。例如,它可以发送<最大的集群时间-1> 。这个值一旦写入复制集节点的Oplogs,将不可递增,节点将无法接受任何变化(针对数据库的写入)。从这种情况下恢复的唯一方法是卸下数据,清理它,然后用正确的OpTime重新加载回来。这种恶意攻击会使受影响的分片脱机,影响整个系统的可用性。为了减轻这种风险,MongoDB增加了一个HMAC-SHA1签名,用来验证服务器上的ClusterTime值。ClusterTime值可以被任何节点读取,但只有MongoDB进程可以签署新的值。签名不能由客户端生成。下面是一个分发ClusterTime的文件的例子:

image-20211014181350597

keyId用于查找生成哈希值的密钥。密钥只在MongoDB进程中存储和生成。这封住了ClusterTime值,因为时间只能在能够访问签名密钥的服务器上递增。每次mongodmongos(MongoDB服务器数据节点或查询路由器)收到包括ClusterTime大于其逻辑时钟值的消息时,它们将通过使用密钥与消息中的keyId生成签名来验证它。如果签名不匹配,该消息将被拒绝。

A1.5 处理运维的错误

恶意客户端影响ClusterTime的风险通过签名得到了缓解,但仍有可能通过改变时钟将ClusterTime提前到 "end of the time"。这可能是由于运维的错误操作而发生的。一旦含有 ‘end of time "时间戳的OpTime的数据被提交给大多数节点,它就不能被改变。为了缓解这种情况,我们对改变的速度进行了限制。一个节点上的ClusterTime不能被提前超过由 所定义的秒数。maxAcceptableLogicalClockDriftSecs参数定义的秒数(默认值是一年)。

A2 因果一致性的实现

A2.1 从所有操作中返回OperationTime

当一个写事件从客户端发出时,该客户端不知道与该写相关的时间,因为该时间是在消息发出后分配的。但处理该写的节点知道,因为它增加了它的ClusterTime,并将该写应用到了OPLOG中。为了让客户端知道写入的ClusterTime,它将会包括在响应的operationTime字段中。为了确保客户端知道所有事件的时间,每个响应(包括错误)都将包括operatingTime字段,代表稳定的集群时间--即执行命令时添加到OPLOG中的最新项目的集群时间。现在,为了使后续的读取因果关系一致,客户端将在请求的afterClusterTime字段中传递它需要读取的数据的确切时间收到的operationTime。数据节点需要返回相关ClusterTime大于或等于请求的afterClusterTime值的数据。

下面是一个关于 "read you own write "在复制集中如何工作的说明

image-20211014182230264

图7.返回一个operation time

  1. 客户端向主服务器发送一个写操作。w:1 "意味着主站将不等待数据被复制到辅助站,并将在其提交到OPLOG后立即返回。
  2. 主程序计算出ClusterTime,并按照前几节所述的方式进行标记。
  3. 主程序返回结果,并将存储在OpTime中的ClusterTime值作为响应的操作时间域。
  4. 客户端有条件地用返回的操作时间值更新其本地的lastOperationTime值。
  5. 客户端向二级节点发送了一个读。为了确保它能 "读你自己的写",它在请求中包括afterClusterTime字段,并传递它从写中得到的operationTime值。
  6. 二级系统检查具有请求的操作时间的数据是否在其OPLOG中。如果没有,它就等待,直到它被复制出来。
  7. 结果将返回给客户。

A2.2 在没有状态变化的情况下处理时间

前面的例子中的情景没有描述当客户写到一个分片,然后执行一个读,读到一个分片,这个分片的逻辑时钟落后于请求的时间。如果没有新的写入,读取就会停滞。在这种情况下,mongod会强制进行写入,以确保客户端不会永远停滞。

image-20211014182449602

图8.集群中的逻辑时钟同步

  1. 客户端向Shard A发送了一个写操作。
  2. Shard A上的主处理器计算ClusterTime,并按照前几节所述的方式进行跟踪。
  3. Shard A返回的结果是写在OPLOG上的operationTime。
  4. 客户端有条件地用返回的operatingTime值更新其本地lastOperationTime值。
  5. 客户端向Shard B发送了一个读,为了确保它能 "Read your own write",它在请求中包含了afterClusterTime字段,并传递了它从写中得到的operatingTime值。
  6. Shard B检查具有请求的OpTime的数据是否在其opog中。如果没有,它就执行一个noop写入。
  7. 结果被返回给客户端(在这个例子中,它是空的,因为它搜索的数据不在Shard B上)。