12 | Quorum NWR算法:想要灵活地自定义一致性,没问题!

你好,我是韩健。

不知道你在工作中有没有遇到这样的事儿:你开发实现了一套AP型的分布式系统(我在04讲提到了AP型系统的特点,你可以回顾一下),实现了最终一致性。业务也接入了,运行正常,一起看起来都那么美好。

可是,突然有同事说,我们要拉这几个业务的数据做实时分析,希望数据写入成功后,就能立即读取到新数据,也就是要实现强一致性(Werner Vogels提出的客户端侧一致性模型,不是指线性一致性),数据更改后,要保证用户能立即查询到。这时你该怎么办呢?首先你要明确最终一致性和强一致性有什么区别。

  • 强一致性能保证写操作完成后,任何后续访问都能读到更新后的值;
  • 最终一致性只能保证如果对某个对象没有新的写操作了,最终所有后续访问都能读到相同的最近更新的值。也就是说,写操作完成后,后续访问可能会读到旧数据。

其实,在我看来,为了一个临时的需求,我们重新开发一套系统,或者迁移数据到新系统,肯定是不合适的。因为工作量比较大,而且耗时也长,而我建议你通过Quorum NWR解决这个问题。

也就是说,在原有系统上开发实现一个新功能,就可以满足业务同学的需求了。因为通过Quorum NWR,你可以自定义一致性级别,通过临时调整写入或者查询的方式,当W + R > N时,就可以实现强一致性了。

其实,在AP型分布式系统中(比如Dynamo、Cassandra、InfluxDB企业版的DATA节点集群),Quorum NWR是通常都会实现的一个功能,很常用。对你来说,掌握Quorum NWR,不仅是掌握一种常用的实现一致性的方法,更重要的是,后续用户可以根据业务的特点,灵活地指定一致性级别。

为了帮你掌握Quorum NWR,除了带你了解它的原理外,我还会以InfluxDB企业版的实现为例,带你看一下它在实际场景中的实现,这样你可以在理解原理的基础上,掌握Quorum NWR的实战技巧。

首先,你需要了解Quorum NWR中的三个要素,N、W、R。因为它们是Quorum NWR的核心内容,我们就是通过组合这三个要素,实现自定义一致性级别的。

Quorum NWR的三要素

N表示副本数,又叫做复制因子(Replication Factor)。也就是说,N表示集群中同一份数据有多少个副本,就像下图的样子:

从图中你可以看到,在这个三节点的集群中,DATA-1有2个副本,DATA-2有3个副本,DATA-3有1个副本。也就是说,副本数可以不等于节点数,不同的数据可以有不同的副本数。

需要你注意的是,在实现Quorum NWR的时候,你需要实现自定义副本的功能。也就是说,用户可以自定义指定数据的副本数,比如,用户可以指定DATA-1具有2个副本,DATA-2具有3个副本,就像图中的样子。

当我们指定了副本后,就可以对副本数据进行读写操作了。那么这么多副本,你要如何执行读写操作呢?先来看一看写操作,也就是W。

W,又称写一致性级别(Write Consistency Level),表示成功完成W个副本更新,才完成写操作:

从图中你可以看到,DATA-2的写副本数为2,也就说,对DATA-2执行写操作时,完成了2个副本的更新(比如节点A、C),才完成写操作。

那么有的同学会问了,DATA-2有3个数据副本,完成了2副本的更新,就完成了写操作,那么如何实现强一致性呢?如果读到了第三个数据副本(比如节点B),不就可能无法读到更新后的值了吗?别急,我讲完如何执行读操作后,你就明白了。

R,又称读一致性级别(Read Consistency Level),表示读取一个数据对象时需要读R个副本。你可以这么理解,读取指定数据时,要读R副本,然后返回R个副本中最新的那份数据:

从图中你可以看到,DATA-2的读副本数为2。也就是说,客户端读取DATA-2的数据时,需要读取2个副本中的数据,然后返回最新的那份数据。

这里需要你注意的是,无论客户端如何执行读操作,哪怕它访问的是写操作未强制更新副本数据的节点(比如节点B),但因为W(2) + R(2) > N(3),也就是说,访问节点B,执行读操作时,因为要读2份数据副本,所以除了节点B上的DATA-2,还会读取节点A或节点C上的DATA-2,就像上图的样子(比如节点C上的DATA-2),而节点A和节点C的DATA-2数据副本是强制更新成功的。这个时候,返回给客户端肯定是最新的那份数据。

你看,通过设置R为2,即使读到前面问题中的第三份副本数据(比如节点B),也能返回更新后的那份数据,实现强一致性了。

除此之外,关于NWR需要你注意的是,N、W、R值的不同组合,会产生不同的一致性效果,具体来说,有这么两种效果:

  • 当W + R > N的时候,对于客户端来讲,整个系统能保证强一致性,一定能返回更新后的那份数据。
  • 当W + R <= N的时候,对于客户端来讲,整个系统只能保证最终一致性,可能会返回旧数据。

你可以看到,Quorum NWR的原理并不复杂,也相对比较容易理解,但在这里,我想强调一下,掌握它的关键在于如何根据不同的场景特点灵活地实现Quorum NWR,所以接下来,我带你具体问题具体分析,以InfluxDB企业版为例讲解一下。

如何实现Quorum NWR?

在InfluxDB企业版中,可以在创建保留策略时,设置指定数据库(Database)对应的副本数,具体的命令,就像下面的样子:

  • create retention policy “rp_one_day” on “telegraf” duration 1d replication 3

通过replication参数,指定了数据库telegraf对应的副本数为3。

需要你注意的,在InfluxDB企业版中,副本数不能超过节点数据。你可以这么理解,多副本的意义在于冗余备份,如果副本数超过节点数,就意味着在一个节点上会存在多个副本,那么这时冗余备份的意义就不大了。比如机器故障时,节点上的多个副本是同时被影响的。

InfluxDB企业版,支持“any、one、quorum、all”4种写一致性级别,具体的含义是这样的。

  • any:任何一个节点写入成功后,或者接收节点已将数据写入Hinted-handoff缓存(也就是写其他节点失败后,本地节点上缓存写失败数据的队列)后,就会返回成功给客户端。
  • one:任何一个节点写入成功后,立即返回成功给客户端,不包括成功写入到Hinted-handoff缓存。
  • quorum:当大多数节点写入成功后,就会返回成功给客户端。此选项仅在副本数大于2时才有意义,否则等效于all。
  • all:仅在所有节点都写入成功后,返回成功。

我想强调一下,对时序数据库而言,读操作常会拉取大量数据,查询性能是挑战,是必须要考虑优化的,因此,在InfluxDB企业版中,不支持读一致性级别,只支持写一致性级别。另外,我们可以通过设置写一致性级别为all,来实现强一致性。

你看,如果我们像InfluxDB企业版这样,实现了Quorum NWR,那么在业务临时需要实现强一致性时,就可以通过设置写一致性级别为all,来实现了。

内容小结

以上就是本节课的全部内容了,本节课我主要带你了解了Quorum NWR的原理、InfluxDB企业版的Quorum NWR实现。我希望你明确这样几个重点。

  1. 一般而言,不推荐副本数超过当前的节点数,因为当副本数据超过节点数时,就会出现同一个节点存在多个副本的情况。当这个节点故障时,上面的多个副本就都受到影响了。

  2. 当W + R > N时,可以实现强一致性。另外,如何设置N、W、R值,取决于我们想优化哪方面的性能。比如,N决定了副本的冗余备份能力;如果设置W = N,读性能比较好;如果设置R = N,写性能比较好;如果设置W = (N + 1) / 2、R = (N + 1) / 2,容错能力比较好,能容忍少数节点(也就是(N - 1) / 2)的故障。

最后,我想说的是,Quorum NWR是非常实用的一个算法,能有效弥补AP型系统缺乏强一致性的痛点,给业务提供了按需选择一致性级别的灵活度,建议你的开发实现AP型系统时,也实现Quorum NWR。

课堂思考

我提到实现Quorum NWR时,需要实现自定义副本的能力,那么,一般设置几个副本就可以了,为什么呢?欢迎在留言区分享你的看法,与我一同讨论。

最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

精选留言

  • 沈伟敏

    2020-04-03 08:54:54

    AP系统之上通过Quorum NWR实现了强一致性,这个是不是违背了“CAP不可能三角”。这个要怎么理解呢?
    作者回复

    加一颗星:),不违背,这么理解,尽管通过N、W、R组合,可以实现不同的一致性级别,但对于同一份数据而言,CP和AP是不可兼得。

    2020-04-15 01:33:04

  • 姜川

    2020-03-26 09:10:49

    如果我们用raft协议的最终一致性来做整个系统的核心协议,这时出现了要强一致的需求,我们就可以不改变raft协议的前提下,引入NWR,raft协议已经保证了W是超过半数以上的,那我们就让R也超过半数以上,就能保证基于raft协议还能做出强一致性的需求了,或者改动raft协议,让其可以拥有配置写ALL的能力也可以
    作者回复

    加一颗星:),理论上是可以的,但在实际中,没必要,因为我们在领导者节点上执行读操作,就能实现强一致性了。

    2020-04-07 02:02:54

  • 竹马彦四郎的好朋友影法師

    2020-05-05 20:07:22

    "当 W + R > N 的时候,对于客户端来讲,整个系统能保证强一致性,一定能返回更新后的那份数据。"
    顿时明白了韩老师在本文开头说的 "Werner Vogels 提出的客户端侧一致性模型,不是指线性一致性"
    确实,图1中的AP系统其实并没有实现线性一致性,但是该AP系统实现了客户端侧一致性模型。
    作者回复

    加一颗星:),最最大部分场景,都不需要“eactly once”的线性一致性,只要能保证写操作完成后,就能一直和持续读取到新数据,就可以了。

    2020-05-07 00:48:08

  • Michael Tesla

    2020-05-31 16:10:59

    老师,写入 W 个节点失败,有可能有脏数据残留,是不是需要使用分布式事务呢?
    作者回复

    加一颗星:),不需要的,实现操作的冥等性和状态机,就可以了,比如,“SET X = 1”,因为重试,最终提交了2次,那么,经过状态机的运算后,最终的X值还是1。

    2020-07-21 23:06:36

  • kylexy_0817

    2020-04-26 22:23:50

    联想起另一门视频课程,MongoDB,好像也是采取Quorum NWR算法实现数据强一致性^_^
    作者回复

    加一颗星:),writeConcern/readConcern原理与这个类似。

    2020-05-07 00:08:58

  • 右耳听海

    2020-03-14 23:26:21

    老师能讲下为什么w+r>n时是强一致性的吗
    作者回复

    本质上是因为这是不管怎么读,都能读取到已更新的那个副本,比如你这么想,三节点集群(n为3),写一致性级别(w)为3,读一致性级别(r)为1,也就是说,只要写成功了,所有的副本都是成功更新过的,这时不管读取哪个节点,都能读取到最新的数据。

    2020-03-15 09:32:29

  • 每天晒白牙

    2020-03-09 07:35:27

    关于思考题,老师在文中提到的资料有一些参考点:
    在需要提供高性能和高可用性的分布式存储系统中,副本的数量即 n 通常超过 2 个。
    只关注容错的系统通常使用 n=3 (W=2和R=2配置)。
    需要提供非常高读取负载的系统通常会复制超出容错要求的数据
    作者回复

    加一颗星:)

    2020-04-08 02:32:08

  • fy

    2020-09-03 17:00:46

    问个问题,假设322配置,第一次写入只成功了一个节点,返回客户端错误,这时候有个读取操作,刚好获取到了上一次写入成功的那一个副本,然后返回了这份数据,那不是和之前的操作结果矛盾了么
    作者回复

    加一颗星:),是存在这个现象,此时能保证“写成功后,一定能读取到更新后的值”,但不能保证“写失败,只能读取到之前的值”,如果需要实现这一点,咱们需要实现事务,也就是说,当咱们在实现系统时,需要根据场景特点选择适合的技术。

    2020-10-13 00:49:56

  • 小晏子

    2020-03-09 10:02:46

    我理解课后思考的问题是”为什么只需要备份数据到部分节点,不需要备份到所有节点“,因为是AP系统,所以为了提升效率,备份数据到N个副本就可以认为是数据写入成功了,整个系统因为是最终一致性,系统内部会异步同步节点之间的数据,所以最终所有节点上的数据肯定会一致的,另外工程实践里N>=3, 是因为冗余数据是保证可靠性的手段,如果N=2,那么损失一个节点就退化为单节点了。
    作者回复

    加一颗星:),一般2副本或3副本,就可以了。

    2020-04-08 02:25:17

  • Joshua

    2020-03-09 09:21:59

    每次写入数据都要产生对应版本号是吧
    作者回复

    加一颗星:),取决于场景,比如,KV数据就需要,而时序数据不需要,因为一条时序数据记录是没有“新旧”之说的。

    2020-04-08 02:30:56

  • longyi

    2020-03-09 22:08:34

    老师,你在文中提到”读取指定数据时,要读 R 副本,然后返回 R 个副本中最新的那份数据”
    ,问题是我们怎么去判断那个副本是最新的呢?
    作者回复

    加一颗星:),比如版本号、时间戳等。

    2020-04-08 01:52:37

  • 侧耳倾听

    2020-04-15 11:29:50

    明白老师的意思了,系统已经在运行,副本数和写入数都是已定的,理论上不能动态调整的,所以我们只能调整R来实现强一致性,实时读取最新数据,所以我们只需要在新的功能里计算好R的数目,然后接入存储系统读取数据,然后在本地通过版本号或者日期取最新数据即可。
    作者回复

    加一颗星:),赞。我再补充下,W也是可以动态调整的,为什么呢?因为W表示的是,完成一个写操作,需要同时成功更新多少个副本,比如N为3,W为1,3副本,肯定是需要更新3个副本的,但W为1,也就是说,只要我们完成了一个副本的更新,就可以返回写成功给客户端。另外,如果此时R为1,则实现的是最终一致性,会读到旧数据,但最终会读到新数据。

    2020-04-15 16:14:28

  • 艾瑞克小霸王

    2020-03-09 00:54:14

    这里的一个节点的意思是不是一个raft集群? 采用多个raft集群做数据分片和多副本?
    作者回复

    加一颗星:),节点指的就是服务器节点,这是个新算法,和Raft没有关系呢。

    2020-04-08 02:33:31

  • Bryant.C

    2020-09-04 16:12:38

    我理解Kafka是不是也不支持读一致性级别,通过ack为-1设置强一致
    作者回复

    加一颗星:),这个机制类似写一致性级别。

    2020-12-01 22:45:12

  • 月迷津渡

    2020-03-23 16:23:55

    这篇核心写的很清晰,我有个小问题,关于W和R的平衡当中提到要写优化就要R=N,W=1 这样就满足W+R>N,对于这个W=1是会变吗就是不同数据可以写到不同节点 但是还是满足W=1,因为我在考虑当这个持有数据的唯一节点挂了,那节点所持有数据就不可修复了吧。所以后面提到读大多数和写大多数节点是一种满足容错的配置。
    另外我想到如果集群中某个节点挂了的话N会动态变化吗?因为节点挂了就意味着数据不可用在机器网络或硬件坏掉的情况下数据也无法恢复到其他新启动节点?我记得mongo里就是配置readConcern和WriteConcern就是用的majortiy 这种感觉就是应对节点变化的一种策略(动态大多数而并非指定指定一个固定的值)吧。
    作者回复

    加一颗星:),一般而言,N不需要动态的,故障只是偶尔发生的,而且能很快修复的,我们需要考虑的是,如果在故障发生时,保证系统的稳定运行,“读大多数、写大多数”是能实现这点的。

    2020-04-07 03:09:28

  • 吴小智

    2020-03-19 22:42:09

    按需求配置,再好不过了
    作者回复

    加一颗星:)

    2020-04-05 01:05:10

  • 2020-03-18 10:03:20

    进行R操作时,R>2,读出的数据怎么知道哪个数据是最新的呢?时序数据库里面本来带了时间戳,可以靠这个判断,但还是需要保障整个系统时序一致,其他非时序系统怎么办?
    作者回复

    加一颗星:),加个时间戳。

    2020-04-07 04:07:47

  • 2020-03-15 15:09:11

    influxdb 不是国产的开源时序数据库吗? 为什么没有找中文文档呢?
    作者回复

    美国的,可考虑科学上网,培养阅读英文文档的习惯:),坚持下来,会收获很大。

    2020-03-16 01:37:29

  • iron_man

    2020-03-10 11:44:55

    any:任何一个节点写入成功后,或者接收节点已将数据写入 Hinted-handoff 缓存(也就是写其他节点失败后,本地节点上缓存写失败数据的队列)后,就会返回成功给客户端。
    这里“本地节点上缓存写失败数据的队列“是什么意思,不太明白,是指缓存备份吗?本地缓存写失败了再写到这个缓存里面?
    作者回复

    加一颗星:),是的,用于失败重传的缓存备份,也就是,如果写远程节点失败了,将数据缓存下来,然后再重传。

    2020-04-08 01:34:41

  • qinsi

    2020-03-09 16:36:36

    W+R=N时应该也是最终一致吧
    作者回复

    是的

    2020-03-14 20:22:38