1.主从分片
我们上面讨论的主从复制模型,是基于每一个副本都有全量的数据集的,如果我们将这个主从复制的粒度变小一点,比如可以指定每一个副本最大为 128 M,对于全量数据集按 128 M 拆分成多个副本,在每一个主从复制副本集内部做同步复制,这其实就是水平分片和主从复制的组合方式,也是当前分布式存储系统中非常流行的数据复制方案。比如,我们有 4 台存储机器,每台机器可以存储 3 * 128 M 数据,当前我们的数据集合总量为 4 * 128 M,那么,我们可以将这个数据集拆分为 4 个分片,每个 128 M,然后将这些分片和分片的副本分布到这 4 台机器上
首先,系统中的每一台机器都可以负责一部分主副本,提升了系统的写入性能和可用性。其次,可以让主从复制的副本数量不再和机器数量强绑定。在前面讨论的每一个副本都有全量数据集的方案中,每增加一台机器,都会导致副本集的数目增加 1 个,给系统带来了更多数据副本的性能开销。但是,当主从复制的副本数量不再和机器数量强绑定,比如指定副本数量为 3 个,那么我们需要同步的从副本数量就是 2 个,不论集群的机器的数量如何增加,副本的数量都不会改变,这样我们就可以通过增加机器,来提升整个系统整体的读写性能。
2.多主复制
如果主副本之间的复制是同步的,那么一个主副本的写入,需要等待复制到其他的主副本成功后,才能返回给用户,这样当写入出现冲突时,可以返回失败或由用户来解决冲突。但是,它却失去了多主复制最重要的一个优点,即多个主副本都可以独立处理写入,这就导致整个模式退化为主从复制的形式。所以一般来说,多主复制的主副本之间,大多采用异步模式,我们本课中讨论的多主复制也都是异步模式。
如果主副本之间的复制是异步的,那么一个主副本待自己写入成功后,就立即返回给用户,然后再异步地将修改复制给其他的主副本。这时也会出现一个问题,如果多个主副本同时成功修改一个数据,当主副本之间复制这个数据的修改时,会出现冲突,我们就不知道以哪一个主副本的写入结果为准了。所以接下来,我们就一起讨论对于异步模式的多主复制,如何解决多个主副本写入冲突的问题。
避免冲突
基于冲突的定义,我们应该怎么解决呢?有一个很自然的思路是,既然冲突是多个主副本同时修改了一个数据,或者破坏了数据的唯一性约束导致的,那么我们就对数据进行分片,让不同的主数据负责不同的数据分片,这个方式确实可以在一定程度上避免冲突,但是会出现两个问题。首先,一个修改操作可能会修改多个分片数据,这样我们就没有办法通过分片来隔离修改了。比如,我们将修改用户余额的操作进行水平分片, ID 为 0-10 的用户在主副本 1 写入, ID 为 11-20 的用户在主副本 2 写入。当 ID 6 的用户给 ID 16 的用户转账时,如果在主副本 1 上执行,那么同一时间, ID 16 的用户在主副本 2 上也有修改时,就会出现写入冲突。
其次,由于就近接入和故障等原因,我们会将出现故障的主副本流量切换到其他的主副本,这时也会出现写入冲突的情况。我们继续按刚才的例子分析,ID 为 0-10 的用户在主副本 1 写入,ID 为 11-20 的用户在主副本 2 写入。假设 ID 8 的用户在主副本 1 写入成功,但是数据的变更还没有同步到主副本 2 ,这时如果 ID 8 的用户到主副本 1 的网络出现问题,我们会立即将 ID 为 0-10 的用户的写入流量切换到主副本 2 ,那么在主副本 2 上,再对 ID 8 的数据进行修改就会导致冲突发生。
读时解决冲突读
时解决冲突的思路和写时解决冲突的思路正好相反,即在写入数据时,如果检测到冲突,不用立即进行处理,只需要将所有冲突的写入版本都记录下来。当下一次读取数据时,会将所有的数据版本都返回给业务层,在业务层解决冲突,那么读时解决冲突的方式有下面两种。第一种方式是由用户来解决冲突。毕竟用户才是最知道如何处理冲突的人,业务层将冲突提示给用户,让用户来解决。另一个方式是自定义解决冲突。业务层先依据业务情况,自定义好解决冲突的处理程序,当检测到冲突时,直接调用处理程序来解决,你会发现它和写时解决冲突的第二种实现方式一样,只不过一个在写入时解决冲突,一个在读取时解决冲突。
虽然异步模式的多主复制有多个主副本可以独立写入的优点,但是也会在一定程度上降低系统的一致性,所以我们在使用时,需要评估业务特点,对一致性要求容忍度高的业务,可以使用多主复制,而对于一致性要求高的业务,则需要慎重考虑。
无主复制
主从复制和无主复制有一个非常大的区别,主从复制先写主节点,然后由主节点将数据变更同步到所有的从副本,从副本数据的变更顺序由主节点的写入顺序决定;但是,无主复制是由客户端或代理程序,直接负责将数据写入多个存储节点,这些存储节点之间是不会直接进行数据同步的。
无主复制写入数据时,为了数据的高可用,会向多个节点写入多份数据,那么它是等所有的节点都写入成功,客户端才返回成功呢?还是有一个节点写入成功,客户端就返回成功呢?
假设对于每一份数据,我们保存 n 个副本,客户端写入成功的副本数为 w ,读取成功的副本数为 r ,那么只需要满足仲裁条件 w + r > n 成立,读副本和写副本之间的交集就一定不为空,即一定能读取到最新的写入。我们将满足仲裁条件 w + r > n 的 w 和 r 称之为法定票数写和读,这就是 Quorum 机制,你也一定能发现它其实就是抽屉原理的应用。那么对于 w、r 和 n 的值,通常是可以配置的,一个常见的配置选择为,设置 n 为奇数(通常为 3 或 5 ),w = r = (n + 1)/2 向上取整。这个配置的读写比较均衡,比如 n = 5,那么 w = r = 3,读和写都保证 3 个副本成功即可,能容忍 2 个节点故障。
读修复和反熵
主从复制和多主复制是通过主节点接受数据写入,并且由主节点负责将数据副本,成功复制到所有的从副本来保证的。但是在上文“数据读写”的讨论中,我们了解了当 w < n 时,并不能保证数据成功写入所有的副本中,那么无主复制的这个问题应该如何解决呢?一般来说,有如下的两种方式来实现数据的修复。
首先,是读修复。当客户端并行读取多个副本时,如果检测到某一副本上的数据是过期的,那么在读取数据成功后,就异步将新值写入到所有过期的副本上,进行数据修复
其次,是反熵过程。由存储系统启动后台进程,不断去查找副本之间数据的差异,将数据从新的副本上复制到旧的副本上。这里要注意,反熵过程在同步数据的时候,不能保证以数据写入的顺序复制到其他的副本,这和主从复制有着非常大的差异,同时由于数据同步是后台异步复制的,会有明显的同步滞后。总体来看,读修复对于读取频繁的数据,修复会非常及时,但它只有在数据被读取时才会发生,那么如果系统只支持读修复,不支持反熵过程的话,有一些很少访问的数据,在还没有发生读修复时,会因为副本节点的不可用而更新丢失,影响系统的持久性。所以,将读修复和反熵过程结合是一种更全面的策略。
Sloppy Quorum
在数据读写时,当我们在规定的 n 个节点的集合内,无法达到 w 或 r 时,就按照一定的规则再读写一定的节点。这些法定集合之外的数据读写的节点,可以设置一些简单的规则,比如对于一致性 Hash 环来说,可以将读写顺延到下一个节点,作为临时节点进行读写。当故障恢复时,临时节点需要将这些接收到的数据,全部复制到原来的节点上,即进行数据的回传。通过这个方式,我们可以确保在数据读写时,系统只需要有任意 w 或 r 个节点可用,就能读写成功,这将大大提升系统的可用性。但是这也说明,即使系统的读写能满足仲裁条件 w + r > n ,我们依然无法保证,一定能读取到最新的值,因为新值写入的节点并不包含在这 n 个节点之中。那么这个方案叫 Sloppy Quorum ,相比于传统的 Quorum ,它为了系统的可用性而牺牲了数据的一致性。目前,几乎所有无主复制的存储系统都支持 Sloppy Quorum,但是它在 Cassandra 中是默认关闭的,而在 Riak 中则是默认启用的,所以我们在使用时,可以根据业务情况进行选择。
网友评论