StateSynchronizer是开源分布式流存储平台Pravega的核心组件。StateSynchronizer组件以stream为基础,对外提供一致性状态共享服务。StateSynchronizer允许一组进程同时读写同一共享状态而不必担心一致性问题。本文将从共享状态和一致性的角度出发,详细描述StateSynchronizer的整体架构、工作机制和实现细节。利用stream的天然特性,StateSynchronizer可以高效地确定出更新操作的全局顺序,并且从逻辑上实现了对共享状态的一致性更新与存储。由于stream访问的高效与轻量,StateSynchronizer特别适用于高并发(\u0026gt;= 10000 clients) 的场景,并在此场景下可以作为替代ZooKeeper和etcd的解决方案。

StateSynchronizer设计者之一Flavio是著名开源组件ZooKeeper的最早作者,他同时也是《ZooKeeper:分布式过程协同技术详解》这本书的作者。

StateSynchronizer不仅是Pravega公共API的一部分,许多Pravega内部组件也大量依赖StateSynchronizer共享状态,如ReaderGroup的元信息管理。并且我们可以基于StateSynchronizer实现更高级的一致性原语,例如跨stream的事务。

1 背景简介
1.1 什么是StateSynchronizer(状态同步器)
Pravega [1]既可以被想象成是一组流存储相关的原语,因为它是实现数据持久化的一种方式,Pravega也可以被想象成是一个消息订阅-发布系统,因为通过使用reader,writer和ReaderGroup它可以自适应地进行消息传递。本文假设读者已经熟悉Pravega的有关概念,否则可以参考相应的官方文档 [2]和已发布的4篇专栏文章(见文末链接)。

Pravega实现了各种不同的构建模块用以实现stream相关原语,StateSynchronizer [2]就是其中之一,目的在于协调分布式的环境中的各个进程^2。从功能上看,StateSynchronizer为一组进程提供可靠的共享的状态存储服务:允许多个客户端同时读取和更新同一共享状态并保证一致性语义,同时提供数据的冗余和容错。从实现上看,StateSynchronizer使用一个stream为集群中运行的多个进程提供了共享状态的同步机制,这使得构建分布式应用变得更加简单。使用StateSynchronizer,多个进程可以同时对同一个共享状态进行读取和修改,而不必担心一致性问题 [3]。

StateSynchronizer的最大贡献在于它提供了一种stream原生的一致性存储方案。由于stream具有只允许追加(Append-Only)的特性,这使得大部分现有的存储服务都无法很好地应用于stream存储的场景。相比于传统的状态存储方案,stream原生的存储使得StateSynchronizer具有以下优点:

与常见的键值存储(Key/Value Store)不同,StateSynchronizer支持任意抽象的共享状态,而不仅仅局限于维护键值集合。

与常见的数据存储不同,StateSynchronizer以增量的方式维护了共享状态的整个变更历史,而不仅仅是维护共享状态的最新快照。这一特性不仅大大减少了网络传输开销,还使得客户端可以随时将共享状态回滚到任意历史时刻。

与常见的状态存储不同,StateSynchronizer的服务端既不存储共享状态本身也不负责对共享状态进行修改,所有共享状态的存储和计算都只发生在客户端本地。这一特性不仅节约了服务端的计算资源,还增加了状态计算的灵活性,例如:除了基本的CAS(Compare-And-Swap)语义,还支持高隔离级别的复杂事务^3。

与现有的基于乐观并发控制(Optimistic Concurrent Control, OCC) [4] [5]的存储系统不同,StateSynchronizer可以不依赖多版本控制机制(Multi Version Concurrent Control, MVCC) [6] [7]。这意味着即使在极端高并发的场景下,状态更新的提交也永远不会因版本冲突而需要反复重试。

StateSynchronizer无意于也不可能在所有场景中替代传统的分布式键值存储组件,因为它的运行机制大量依赖stream的特性。但是,在具有stream原生存储和较强一致性需求的场景下,StateSynchronizer可能是一种比其它传统键值存储服务更为高效的选择。

1.2 “一致性”的不同语义
在不同的上下文环境中,“一致性”一词往往有着不同的语义 [8] [9]。在分布式存储和数据高可用(High Availability)相关的语境下,一致性通常指数据副本(Replica)的一致性 [8]:如何保证分布在不同机器上的数据副本内容不存在冲突,以及如何让客户端看起来就像在以原子的方式操作唯一的数据副本,即线性化(Linearizability) [10]。常见的分布式存储组件往往依赖单一的Leader(主节点)确定出特定操作的全局顺序,例如:ZooKeeper [11]和etcd [12]都要求所有的写操作必须由Leader转发给其它数据副本。数据副本的一致性是分布式系统的难点,但却并不是一致性问题的全部。

脱离数据副本,在应用层的语境下,一致性通常指数据满足某种约束条件的不变性(Invariant)[13],即:指的是从应用程序特定的视角出发,保证多个进程无论以怎样的顺序对共享状态进行修改,共享状态始终处于一种“正确的状态”,而这种正确性是由应用程序或业务自身定义的。例如,对于一个交易系统而言,无论同时有多少个交易在进行,所有账户的收入与支出总和始终都应该是平衡的;又如,多进程操作(读/写)一个共享的计数器时,无论各进程以怎样的顺序读写计数器,计数器的终值应该始终与所有进程顺序依次读写计数器所得到的值相同。参考文献 [8]将这种一致性归类为“事务性的一致性(Transactional Consistency)”,而参考文献 [9]则将此类一致性简单称为“涉及多对象和多操作的一致性”。应用层的数据一致性语义与数据副本的一致性语义完全不同,即使是一个满足线性化的分布式系统,也需要考虑应用层的数据一致性问题^4。

1.3\tStateSynchronizer与现有的一致性存储产品
目前常用的分布式键值存储服务,例如ZooKeeper和etcd,都可以看作是一种对共享状态进行存储和维护的组件,即所有键值所组成的集合构成了当前的共享状态。在数据副本层面,ZooKeeper和etcd都依赖共识(Consensus)算法提供一致性保证。ZooKeeper使用ZAB(ZooKeeper’s Atomic Broadcast)协议 [14]在各节点间对写操作的提交顺序达成共识。在广播阶段,ZAB协议的行为非常类似传统的两阶段提交协议。etcd则使用Raft协议 [15]在所有节点上确定出唯一的写操作序列。与ZAB协议不同,Raft协议每次可以确认出一段一致的提交序列,并且所有的提交动作都是隐式的。在应用层数据层面,ZooKeeper和etcd都使用基于多版本控制机制的乐观并发控制提供最基础的一致性保证。一方面,虽然多版本控制机制提供了基本的CAS语义,但是在极端的高并发场景下仍因竞争而存在性能问题。另一方面,仅仅依靠多版本控制机制无法提供更加复杂的一致性语义,例如事务。尽管在数据副本层面,ZooKeeper和etcd都提供很强的一致性语义,但对于应用层面的数据一致性却还有很大的提升空间:ZooKeeper无法以原子的方式执行一组相关操作,而etcd的事务仅支持有限的简单操作(简单逻辑判断,简单状态获取,但不允许对同一个键进行多次写操作)。

在应用层数据层面,ZooKeeper和etcd都使用多版本控制机制提供最基础的一致性保证。例如,ZooKeeper的所有写操作都支持乐观并发控制:只有当目标节点的当前版本与期望版本相同时,写操作才允许成功;而etcd则更进一步,还支持非常有限的简单事务操作。一方面,虽然多版本控制机制提供了基本的CAS语义,但是在极端的高并发场景下仍因竞争而存在性能问题。另一方面,仅仅依靠多版本控制机制无法提供更加复杂的一致性语义,例如事务。尽管在数据副本层面,ZooKeeper和etcd都提供很强的一致性语义,但对于应用层面的数据一致性却还有很大的提升空间:ZooKeeper无法以原子的方式执行一组相关操作,尤其是同时操纵多个键;而etcd的事务仅支持非常有限的简单操作(简单逻辑判断,简单状态获取,但不允许对同一个键进行多次写操作)。为应用层数据提供比现有的分布式存储组件更强的一致性语义(复杂事务)和更高的并发度是StateSynchronizer的主要目标,尤其是在stream原生场景下,因为传统的以随机访问为主的存储组件很难适配stream存储的顺序特性。得益于stream的自身特性,StateSynchronizer可以不依赖乐观并发控制和CAS语义,这意味着不会出现版本冲突也无需重试,从而更加适用于高并发的场景(2.2.4小节)。在“无条件写”模式下,StateSynchronizer的理论更新提交速度等价于stream的写入速度。

与现有的绝大多数存储服务不同,StateSynchronizer反转了传统的数据存储模型(2.2.3小节):它并不存储共享状态本身,转而存储所有作用在共享状态上的更新操作。一方面,这一反转的数据模型直接抽象出了共享状态,使得共享状态不再局限于简单的键值存储,而可以推广到任意需要一致性语义的状态。另一方面,反转数据存储的同时还不可避免地反转了数据相关的操作,使得原本大量的服务端状态计算可以直接在客户端本地完成(2.2.1小节)。这一特性不仅大大降低了服务端的资源消耗,同时也使得StateSynchronizer可以提供更灵活的更新操作和更强一致性语义:复杂事务。在StateSynchronizer的框架中,客户端提交的所有更新操作都是以原子的方式顺序执行的,并且所有更新操作的执行都发生在本地。从逻辑上看,每一个更新操作都等价于一个本地事务操作。这也意味着客户端可以在更新操作中使用复杂的业务逻辑(几乎是不受限的操作,只要操作本身的作用是确定性的)而无需担心一致性问题。