数据系统解决方案 (数据传输延迟的原因)

能够容忍节点的failure只是想要拷贝的理由之一,其他的理由还包括扩展性(比单个机器处理更过的请求)以及延迟(从地理上,把复制品放在离用户更近的地方)。

Leader-based复制品要求所有的写请求都经由某单一节点,但读请求可以查询任意复制品。这对于负载是大量读请求而写请求只占很小百分比的情况(网页的一般情形),是很有吸引力的:创建很多的follower,并且把读请求分散在这些follower间。这可以去除leader的负载,并且允许用就近的复制品为读请求服务。

在这种读请求扩展read-scaling的架构下,你可以简单的通过增加更多的follower来提升只读请求的能力。然而,这种方法只对于异步的复制品是实际可行的,如果你想对follower采用同步机制的话,一个节点的failure或者是网络瘫痪就会使得整个系统不能再写数据。并且,你有的节点越多,那么单个节点的宕机可能性就越大,因此,一个完全同步的配置是不现实的。

不幸的是,如果一个应用的读来自一个已不的follower,如果这个follower的更新有所落后,那么应用看到的将是过时的信息。这会导致明显的数据库的不一致:

如果你在leader和follower那同时运行同样的查询,你会得到不一样的结果,因为并不是所有的写请求都已经反应到follower那。

这种不一致性就是一个临时的状态 - 如果你停下对数据的写请求并且等一会,最终eventually follower会赶上并且和leader保持一致的。因为这个原因,这一影响被称为evetual consistency

术语“eventually最终”有一点故意的模糊性质:一般来说,对于复制品落后多少更新是没有限制的。在正常操作下,一个发生在leader和反应到follower之间的写请求的延迟,也叫拷贝延迟replication lag,可能不到一秒,在实际中不会被注意到。然而,如果系统的运行接近系统的最大容量或者网络上有问题,那么这个lag就会很容易增加到几秒或是几分钟。

当lag很大时,那么刚才被提及的不一致性就不是理论上的问题了,对于应用程序来说也是一个实际的问题。下面我们将讨论,拷贝延迟发生时的3个问题以及解决它们的方法。

读自己的写请求

许多应用会让用户提交一些数据然后查看他们提交的内容。这可能是一个消费者数据库中的一条记录,或是对一条对讨论主题的评论,或是其他的等等。当新数据被提交时,它必定会发送给leader,但是当用户查看数据时,他可能会从follower那读取。这对于数据频繁被查看但偶尔被写的用例特别合适。

使用异步拷贝时,会有如下图展示的问题:如果用户在写数据后很快的查看数据,那么新的数据会还没有到达复制品。对于用户来说,看上就好像虽然数据提交了,但丢失了,所以他们是有理由不高兴的。

数据系统-拷贝延迟问题

在这样的情况下,我们需要read-after-write consistency,也被称为read-your-writes consitency。这是对用户重载页面后,他们总是能看到他们自己提交的数据一种保证。但对于其他用户却不能保证什么:其他用户的更新也许是不可见的,直到一段时间之后。然而,它确保了用户自己输入已经被正确的存储了。

那么我们应该怎么在leader-based的复制品中实现这种read-after-write consistency呢?有许多不同的技术可以实现。下面我们罗列一下:

  • 当读取一些用户已经修改的东西时,读请求的响应是来自leader的;否则读请求的响应来自follower。这要求我们需要一些方式在没有实际查询的情况下,知道是不是有些东西被修改了。比方说,社交网络上的用户简介一般只可以被简介的拥有者修改,而不是其他人。因此一个简单的规则就是:总是从leader那读取用户拥有的简介,而其他用户从follower那获取用户简介。
  • 如果应用中的大部分东西是可能被用户编辑的,那么上述这个方法就是不可行的,因为大部分的东西都会从leader那读取,这对于读请求的扩展性是不利的。在这样的情况下,需要其他的标准来决定是否从leader那读取。比方说,你可以追踪最近一次更新的时间,规定最近一次的更新后的一分钟内的读请求,都是由leader响应的。你也可以监测follower的拷贝延迟,并且防止对落后leader超过一分钟更新的followers的请求。
  • 客户端可以记下最近一次更新的时间戳,然后系统就可以确保为这个用户读请求服务的复制品的更新至少是要跟这个时间戳是一致的。如果复制品不满足这个日期,那么读请求要么由其他的复制品处理,要么这个查询一直等待知道这个复制品追上这个日期的更新。这个时间戳可以是一个logical timestamp(标明写的顺序的什么,诸如写的序列号)或是真实的系统时钟(这会使时钟同步变得很关键)
  • 如果你的复制品是分散在好几个数据中心(为了在地理上靠近用户或高可用性的目的),那么就会带来额外的复杂性。leader需要处理的任何请求都必须路由到包含leader的数据中心。

当同一用户从多个设备(例如台式机Web浏览器和移动应用程序)访问您的服务时,就会出现另一种复杂情况。在这种情况下,您可能希望提供跨设备cross-device的read-after-write consistency:如果用户在一个设备上输入了一些信息,然后在另一台设备上查看了该信息,则他们应该看到刚刚输入的信息。

在这种情况下,有一些额外的问题需要考虑:

  • 要求记录用户最近一次更新的时间戳的方法会变得更困难,因为运行在一台机器上的代码不知道在另一台机器上更新了什么。元数据需要集中化。
  • 如果你的复制品是分散在不同数据中心的,那么就不能呢个确保不同设备的链接是路由到同一个数据中心的。例如,如果用户的台式计算机使用家庭宽带连接,而其移动设备使用蜂窝数据网络,则设备的网络路由可能会完全不同。如果你的方法是从leader那读取数据,你也许需要首先把来自用户设备的所有请求都路由到同一个数据中心。

单调读Monotonic Reads

异常的第二种情况可能是当动异步的follower那读取数据时,用户有可能看到一些时光倒流moveing backward in time的情形。

这可能发生在当用户从几个不同的复制品那读取好几次时。比方说,下图展示了用户2345执行了两次一样的查询,第一次是和一个几乎没有lag的复制品,然后是和一个有很大lag的复制品(这种情形是很有可能的,当用户刷新网页时,每个请求都会路由到随机的服务器)。第一次查询会返回一个用户1234添加的最新评论,但第二次查询并没有返回任何东西,因此滞后的follower还没有看到那个写的数据。也就是说,第二次查询看到的系统,在时间上早于第一次看到的。如果第一次没有返回任何东西,那其实并不糟糕,因为用户2345本来就不会知道用户1234最见增加了一条评论。然而,如果用户2345在第一次看到了用户1234的评论,但在之后又消失了,这会让用户很困惑。

数据系统-拷贝延迟问题

Monotonic reads单调读,可以保证这样的异常不会发生。它比强一致性要弱一些,但比最终一致性要强。读取数据时,您可能会看到一个旧值;单调读取仅表示如果一个用户依次进行几次读取,他们将看不到时间*退倒**-即,他们在先前读取较新的数据之后将不会读取到较旧的数据。

实现单调读取的一种方法是确保每个用户始终从同一副本进行读取(不同的用户可以从不同副本进行读取)。例如,可以基于用户ID的哈希而不是随机地选择副本。但是,如果该副本失败,则用户的查询将需要重新路由到另一个副本。

前缀一致性读Consistent Prefix Reads

拷贝延迟异常的第三个问题是关于对因果关系的违反。让我们想象如下Mr.Poon和Mrs.Cake的对话:

Mr.Poons: Mrs.Cake,你可以看见多远的未来?

Mrs.Cake:通常10秒左右,Mr.Poons。

这两个句子之间存在因果关系:Cake夫人听到了Poons先生的问题并回答了。

现在想象第三个人通过follower监听这个对话。也就是说,Cake夫人说的话机会没有滞后的经过follower,而Poons先生的说的则会有一个更大的拷贝延迟。如下图,所以对于观察者来说,她听到的对话是这样的:

Mrs.Cake:通常10秒左右,Mr.Poons。

Mr.Poons: Mrs.Cake,你可以看见多远的未来?

对于观察者来说,Cake夫人在Poons先生问问题之前就回答了这个问题。这种精神力量令人印象深刻,但却令人困惑。

数据系统-拷贝延迟问题

防止这种异常需要另一种保证:一致的前缀阅读consistent prefix reads。这种保证是说,如果一系列写入是以一定顺序发生,那么阅读这些写入的任何人都会看到它们以相同的顺序出现。

这是分区(分片)数据库中的一个特殊问题。如果数据库始终以相同顺序应用写入,则读取总是看到一致的前缀,因此不会发生此异常。但是,在许多分布式数据库中,不同的分区是独立运行的,因此没有全局的写入顺序:当用户从数据库中读取数据时,他们可能会看到数据库的某些部分处于较旧的状态,而某些部分处于较新的状态。

一种解决方案是确保将因果相关的所有写入均写入同一分区,但是在某些应用程序中无法有效完成。还有一些算法可以明确跟踪因果关系。

拷贝延迟的解决方案

当在一个最终会一致的系统上工作时,考虑当拷贝延迟增加到几分钟或者几小时,应用程序应该如何应为是很有价值的。如果答案是“没问题”,那就太棒了。然而,如果结果导致了糟糕的用户体验,那么设计一个诸如read-after-write这样更强的保证,是很重要的。拷贝实际上是异步的,但假装是同步的,这是解决问题的根源。

就像之前讨论的,应用程序总有提供一个比底层数据库更强的保证,比如,通过在leader上执行某些读操作。然而,在应用程序中处理这些问题是很复杂的并且很容易出错。

最好是应用开发人员不用担心这些微妙的拷贝问题,并且相信数据库会做正确的事情。这也就是为什么transactions事务存在的原因:它们是让数据库提供更强保证的方式以使应用更简单。

单节点的事务已经存在了很久。然而,随着分布式数据库(拷贝或分区)的前行,许多系统都已经放弃了它们,声称对于性能和可用性来说开销太大,并且断言在可扩展系统中最终一致性是不可避免的。这些陈述中有一些是对的,但太简单化。