最终一致的存储后端
在对最终一致的存储后端运行 JanusGraph 时,必须使用特殊的 JanusGraph 功能来确保数据一致性,并且必须对数据退化进行特殊考虑。
本页总结了在 Apache Cassandra 或 Apache HBase 等最终一致的存储后端上运行 JanusGraph 时需要考虑的一些方面。
数据一致性
在最终一致的存储后端上,JanusGraph 必须获取锁才能确保一致性,因为底层存储后端不提供事务隔离。为了效率起见,JanusGraph 默认不使用锁定。因此,用户必须决定是否对每个定义一致性约束的模式元素使用锁定。使用 JanusGraphManagement.setConsistency(element, ConsistencyModifier.LOCK) 显式启用模式元素上的锁定,如以下示例所示。
mgmt = graph.openManagement()
name = mgmt.makePropertyKey('consistentName').dataType(String.class).make()
index = mgmt.buildIndex('byConsistentName', Vertex.class).addKey(name).unique().buildCompositeIndex()
mgmt.setConsistency(name, ConsistencyModifier.LOCK) // Ensures only one name per vertex
mgmt.setConsistency(index, ConsistencyModifier.LOCK) // Ensures name uniqueness in the graph
mgmt.commit()
当更新一个由唯一性约束保护的元素时,JanusGraph 在事务结束时调用 tx.commit() 时使用以下协议
- 获取所有具有一致性约束的元素的锁
- 从存储后端重新读取这些元素,并验证它们是否与当前事务中修改前元素的状态匹配。如果不匹配,则元素被并发修改,并抛出 PermanentLocking 异常。
- 将事务状态持久化到存储后端。
- 释放所有锁。
这是锁定协议的简要描述,其中省略了优化(例如本地冲突检测)和故障场景检测(例如过期锁)。
实际的锁应用程序机制是抽象的,因此 JanusGraph 可以使用锁定提供程序的多种实现。目前,JanusGraph 发行版中只包含一个锁定提供程序
一种基于键一致的读写操作的锁定实现,它与底层存储后端无关,只要它支持键一致操作(包括 Cassandra 和 HBase)。这是默认实现,并使用基于时间戳的锁应用程序来确定哪个事务持有锁。它要求集群中所有机器的时钟同步。
警告
锁定实现并非对所有故障场景都稳健。例如,当 Cassandra 集群低于仲裁时,一致性将不再得到保证。因此,建议在最终一致的存储后端上谨慎使用基于锁定的一致性约束。对于需要严格或频繁强制一致性约束的用例,建议使用提供事务隔离的存储后端。
无锁数据一致性
由于提交修改事务时需要获取锁的额外步骤,锁定是一种相当昂贵的方式来确保一致性,并且当大量并发事务尝试修改图中的相同元素时可能导致死锁。因此,锁定应在一致性比写入延迟更重要且冲突事务数量较少的情况下使用。
在其他情况下,最好允许冲突事务继续执行并在读取时解决不一致问题。这是一种在大规模数据系统中常用的设计模式,当实际冲突的可能性很小时最有效。因此,写入事务不会产生额外的开销,任何(不太可能)发生的冲突都会在读取时检测和解决,然后进行清理。JanusGraph 通过以下功能使使用此策略变得容易。
分叉边
由于边在底层存储后端中存储为单个记录,并发修改单个边将导致冲突。代替锁定,可以将边标签配置为使用 ConsistencyModifier.FORK。以下示例创建一个新的边标签 related 并将其一致性定义为 FORK。
mgmt = graph.openManagement()
related = mgmt.makeEdgeLabel('related').make()
mgmt.setConsistency(related, ConsistencyModifier.FORK)
mgmt.commit()
当修改其标签配置为 FORK 的边时,该边将被删除,并且修改后的边将作为新边添加。因此,如果两个并发事务修改同一条边,则提交时将存在该边的两个修改副本,如果需要,可以在查询遍历期间解决。
注意
边分叉仅适用于 MULTI 边。具有多重性约束的边标签不能使用此策略,因为约束内置在边标签定义中,这需要显式锁定或使用底层存储后端的冲突解决机制。
多属性
并发修改顶点上的单值属性可能会导致冲突。与边类似,可以允许顶点上任意数量的属性,其属性键定义为基数 LIST 并在修改时 FORK。因此,不是冲突,而是读取多个属性。由于 JanusGraph 允许属性上的属性,因此可以将 author 等来源信息添加到属性中,以便在读取时进行解析。
请参阅 多属性 了解如何定义这些属性。
数据不一致性
临时不一致性
在最终一致的存储后端上,写入可能不会立即对整个集群可见,从而导致图中的临时不一致。这是最终一致性的固有属性,即已接受的更新必须传播到集群中的其他实例,并且为了性能起见,不保证读取原子性。
从 JanusGraph 的角度来看,除了事务的某些部分可见而其他部分尚未可见的普遍不一致之外,最终一致性还可能导致以下临时图不一致。
陈旧索引条目
索引条目可能指向不存在的顶点或边。同样,顶点或边出现在图中但尚未索引,因此被全局图查询忽略。
在某些情况下,由于服务器故障,可能会发生永久性索引不一致。请参阅 此处 如何处理永久性陈旧索引条目。
半边
只有一条边的方向被持久化或删除,这可能导致该边无法或不正确地检索。
注意
为了避免写入失败导致图中永久不一致,建议使用支持批处理写入原子性的存储后端,并确保启用写入原子性。为了获得写入原子性的好处,单个事务中进行的修改数量必须小于 配置参考 中记录的配置 buffer-size 选项。缓冲区大小定义了 JanusGraph 将在单个批处理中持久化的最大修改数量。如果一个事务有更多修改,持久化将分成多个批处理,这些批处理是单独持久化的,这对于批处理加载很有用,但会使写入原子性失效。
幽灵顶点
在最终一致的存储后端上运行 JanusGraph 时可能出现的一种永久不一致现象是 幽灵顶点。如果一个顶点在并发修改时被删除,该顶点可能会重新出现为 幽灵。
可以使用以下策略来缓解此问题
存在性检查
配置事务以在返回顶点之前(双重)检查顶点的存在。有关更多信息,请参阅 事务配置,并注意这可能会显著降低性能。请注意,这并不能修复不一致,但会向用户隐藏其中一些不一致。
定期清理
使用 带有 TinkerPop 的 Hadoop-Gremlin 的 JanusGraph 运行定期批处理作业以修复图中的不一致。这是唯一可以解决所有不一致并有效修复它们的策略。
软删除
顶点不是被删除,而是被标记为已删除,这使得它们保留在图中以供将来分析,但将其隐藏在面向用户的事务中。