第 8 章 设置分区处理
8.1. 分区处理
Data Grid 集群基于存储数据的多个节点进行构建。为了在出现节点故障时不会丢失数据,Data Grid 在 Data Grid parlancemvapich-osgiover 多个节点中复制相同的 data iwl-setuptoolscache 条目。这种数据冗余级别通过 numOwners
配置属性配置,并确保只要同时超过 numOwners
节点崩溃,Data Grid 具有可用数据的副本。
但是,可能会出现灾难性情况,在这种情况下,超过 numOwners
节点会从集群中消失:
- 脑裂
- 这会导致路由器崩溃,这会将集群分成两个或者多个分区,或者独立操作的子集群。在这些情况下,多个从不同分区读取/写入的客户端可以看到同一缓存条目的不同版本,对于许多应用程序来说,这个版本有问题。请注意,有方法可以缓解发生脑裂的可能性,如冗余网络或 IP 绑定。它们只减少问题发生的时间窗口。
numOwners
节点按顺序崩溃-
当至少有
numOwners
节点在快速连续崩溃时,Data Grid 没有时间来在崩溃之间正确重新平衡其状态,则结果会部分数据丢失。
本节中讨论的分区处理功能允许用户配置在出现脑裂时可在缓存上执行哪些操作。Data Grid 提供多个分区处理策略,该策略在 Brewer 的 CAP The orem CAP theorem 决定在存在分区时是否牺牲可用性或一致性。以下是所提供的策略列表:
策略 | 描述 | CAP |
---|---|---|
DENY_READ_WRITES | 如果分区没有给定段的所有所有者,则针对该网段中的所有键,读取和写入都将被拒绝。 | 一致性 |
ALLOW_READS | 如果这个分区中存在给定密钥,则允许读取它,但仅在此分区包含网段的所有所有者时才允许写入。这仍然是一种一致的方法,因为某些条目在此分区中可用,但从客户端应用程序的角度来看,它不是确定的。 | 一致性 |
ALLOW_READ_WRITES | 允许每个分区中的条目被分离,在分区合并时尝试冲突解析。 | 可用性 |
应用的要求应确定适合哪些策略。例如,DENY_READ_WRITES 更适合具有高一致性要求的应用程序;例如,从系统读取的数据必须准确时。虽然如果将 Data Grid 用作最佳缓存,分区可能非常可容忍,并且 ALLOW_READ_WRITES 可能更适合,因为它通过一致性来实现可用性。
以下小节描述了 Data Grid 如何为每个分区处理策略处理 脑裂 和 连续故障。这后接一个部分,说明 Data Grid 如何通过合并 策略 在分区合并时允许自动冲突解决。最后,我们提供了一个 描述如何配置分区处理策略和合并策略的部分。
8.1.1. 脑裂
在脑裂情形中,每个网络分区将安装自己的 JGroups 视图,从其他分区中删除节点。我们没有一种直接的方法来确定是否被分成两个或者多个分区,因为分区不能相互识别。相反,我们假设,当一个或多个节点从 JGroups 集群消失时,我们假设集群被分割,而无需发送显式保留消息。
8.1.1.1. 分割策略
在本节中,我们将详细介绍每个分区处理策略在发生脑裂时的行为。
8.1.1.1.1. ALLOW_READ_WRITES
每个分区继续作为独立集群运行,所有分区都保留在 AVAILABLE 模式中。这意味着每个分区只能看到数据的一部分,每个分区都可以在缓存中写入冲突的更新。在分区合并过程中,利用 ConflictManager 和配置的 EntryMergePolicy 自动解决这些冲突。
8.1.1.1.2. DENY_READ_WRITES
当检测到分割时,每个分区不会立即开始重新平衡,而是首先检查它是否应该进入 DEGRADED 模式:
- 如果至少有一个片段丢失了其所有所有者(至少在最后一次重新平衡以来的 numOwners 节点保留),则分区进入 DEGRADED 模式。
- 如果分区没有在 最新的稳定拓扑中 包含简单的大多数节点(floor (numNodes/2)+ 1),分区还会进入 DEGRADED 模式。
- 否则,分区会保持正常运行,它会开始重新平衡。
每次重新平衡操作时都会更新 stable 拓扑,协调器决定另一个重新平衡不需要。
这些规则确保最多一个分区处于 AVAILABLE 模式,其他分区进入 DEGRADED 模式。
当分区处于 DEGRADED 模式时,它只允许访问所拥有的密钥:
- 对于在这个分区内节点上所有副本的条目,请求(读和写)是隐藏的。
-
对部分或完全归消失的节点拥有的条目的请求将被拒绝,并显示
AvailabilityException
。
这样可保证分区不能为同一键(缓存是一致的)写入不同的值,并且一个分区无法读取在其他分区中更新的密钥(没有过时的数据)。
要说明,请考虑初始集群 M = {A, B, C, D}
,使用 numOwners = 2
配置。此外,请考虑三个键 k1
、k2
和 k3
(可能存在于缓存中或不存在),例如 owners (k1)= {A,B}
, owners (k2)= {B,C}
和 owners (k3)= {C,D}
.然后,两个分区中的网络分割: N1 = {A, B}
和 N2 = {C, D}
,它们进入 DEGRADED 模式并的行为如下:
-
在
N1
上,k1
可用于读/写、k2
(完全拥有)和k3
(非拥有)不可用,并且访问它们的结果是AvailabilityException
-
在
N2
上,k1
和k2
不适用于读/写,k3
可用
分区处理流程的相关方面是,当脑裂发生时,生成的分区依赖于原始片段映射(在脑裂之前存在的分区)来计算密钥所有权。因此,如果 k1、
或 k
2k3
已存在缓存或者其可用性不同,则这并不重要。
如果稍后,如果网络修复和 N1
和 N2
分区合并回初始集群 M
,则 M
会退出降级模式,然后再次可用。在这个合并操作中,因为 M
已再次变为可用,所以 ConflictManager 和配置的 EntryMergePolicy 会被用来检查在脑裂发生和被检测之间可能发生的任何冲突。
再如,集群可以在两个分区 O1 中分割 = {A, B, C}
和 O2 = {D}
= {D},分区 O1
将保持完全可用(在剩余成员上重新平衡缓存条目)。但是,分区 O2
将检测到分割并进入降级模式。由于它没有任何完全拥有的密钥,因此它将拒绝任何具有 AvailabilityException
的读取或写入操作。
如果随后将 O1
和 O2
合并合并回 M
,则 ConflictManager 会尝试解决任何冲突,D
会再次变为完全可用。
8.1.1.1.3. ALLOW_READS
分区以与 DENY_READ_WRITES 相同的方式处理,除非当分区处于 DEGRADED 模式时,对部分拥有的密钥 WILL 不会抛出 AvailabilityException。
8.1.1.2. 当前限制
两个分区可以启动隔离,只要它们无法合并,就可以读取和写入不一致的数据。未来,我们将允许自定义可用性策略(例如,检查某个节点是否是集群的一部分),或者检查是否可以访问外部计算机,从而可以处理该情况。
8.1.2. 成功节点停止
如上一节所述,Data Grid 无法检测节点是否因为进程/机器崩溃而离开 JGroups 视图,或者由于网络故障:每当节点离开 JGroups 集群时,它假定是因为网络问题而离开的。
如果配置的副本数(numOwners
)大于 1,集群可以保持可用,并将尝试在崩溃的节点上生成新数据副本。但是,其他节点可能会在重新平衡过程中崩溃。如果超过 numOwners
节点在较短的时间内崩溃,则一些缓存条目可能会完全从集群消失。在这种情况下,在启用了 DENY_READ_WRITES 或 ALLOW_READS 策略的情况下,Data Grid 假设(正确)有脑裂并进入 DEGRADED 模式,如 split-brain 部分所述。
管理员也可以快速关闭多个 numOwners
节点,从而导致只存储在这些节点上的数据丢失。当管理员正常关闭节点时,Data Grid 知道节点无法返回。但是,如果这些节点崩溃,集群不会跟踪每个节点的保留方式,缓存仍然进入 DEGRADED 模式。
在这个阶段,集群无法恢复其状态,除了在使用外部源的数据重启时停止和禁止它。集群应该配置有适当的 numOwners
,以避免 numOwners
成功节点失败,因此这种情况应该非常罕见。如果应用程序可以在缓存中处理一些数据,管理员可以通过 JMX 将可用性模式强制返回到 AVAILABLE。
8.1.3. 冲突管理器
冲突管理器是一个允许用户检索给定键的所有存储副本值的工具。除了允许用户处理存储副本有冲突值的缓存条目的流外。另外,通过利用 EntryMergePolicy 接口的实现,可以自动解析冲突。
8.1.3.1. 检测冲突
通过检索给定键的每个存储值来检测到冲突。冲突管理器从当前一致的哈希定义的每个键的写入所有者中检索值。然后,使用 .equals 方法来确定所有值是否相等。如果所有值都相等,则键没有冲突,否则会发生冲突。请注意,如果给定节点上没有条目,则返回 null 值,因此如果给定键存在 null 和 non-null 值,则会导致冲突。
8.1.3.2. 合并策略
如果给定 CacheEntry 的一个或多个副本之间出现冲突,则需要定义冲突解析算法,因此我们提供 EntryMergePolicy 接口。这个接口由单个方法"merge"组成,返回的 CacheEntry 被耗尽为给定键的"resolved"条目。当返回非null CacheEntry 时,此条目值为"put"到缓存中的所有副本。但是,当合并实现返回 null 值时,与冲突键关联的所有副本都会从缓存中移除。
合并方法采用两个参数:"preferredEntry"和"otherEntries"。在分区合并的上下文中,preferredEntry 是存储在分区中的 CacheEntry 的主副本,其中包含最多节点,或者如果分区等于具有最大 topologyId 的分区。如果重叠的分区(例如,节点 A 存在于两个分区 {A}, {A,B,C})的拓扑中,我们选择了 {A} 作为首选分区,因为它将具有更高的 topologId,因为其他分区的拓扑位于其他分区的拓扑后面。如果没有发生分区合并,"preferredEntry"只是 CacheEntry 的主副本。第二个参数 "otherEntries" 只是与检测到冲突的密钥相关联的所有其他条目的列表。
只有在检测到冲突时,才会调用 EntryMergePolicy::merge,如果所有 CacheEntrys 都相同,则不会调用它。
目前,Data Grid 提供以下 EntryMergePolicy 实现:
policy | 描述 |
---|---|
MergePolicy.NONE (默认) | 不会尝试解决冲突。在次版本分区上托管的条目会被删除,且此分区中的节点不会保存任何数据,直到重新平衡启动为止。请注意,这个行为等同于不支持冲突解析的 Infinispan 版本。请注意,在这种情况下,对在次版本分区上托管的条目进行的所有更改都会丢失,但在重新平衡完成后,所有条目都将一致。 |
MergePolicy.PREFERRED_ALWAYS | 始终使用 "preferredEntry"。MergePolicy.NONE 几乎相当于 PREFERRED_ALWAYS,没有执行冲突解析的性能影响,因此应选择 MergePolicy.NONE,除非以下场景是问题。当使用 DENY_READ_WRITES 或 DENY_READ 策略时,写入操作只能在分区进入 DEGRADED 模式时部分完成,从而导致包含不一致值的副本。MergePolicy.PREFERRED_ALWAYS 将检测到不一致并解决,而 MergePolicy.NONE 在集群重新平衡后,CacheEntry 副本将保持不一致的情况。 |
MergePolicy.PREFERRED_NON_NULL | 使用"preferredEntry" (如果不是 null),否则会使用 "otherEntries" 的第一个条目。 |
MergePolicy.REMOVE_ALL | 当检测到冲突时,始终从缓存中删除密钥。 |
完全限定类名称 | 合并的自定义实现将使用 自定义合并策略 |
8.1.4. 使用方法
在分区合并过程中,ConflictManager 会自动尝试解决配置的 EntryMergePolicy 冲突,但也可以手动搜索应用程序所需的/解析冲突。
以下代码演示了如何检索 EmbeddedCacheManager 的 ConflictManager,如何检索给定密钥的所有版本以及如何检查给定缓存中的冲突。
EmbeddedCacheManager manager = new DefaultCacheManager("example-config.xml"); Cache<Integer, String> cache = manager.getCache("testCache"); ConflictManager<Integer, String> crm = ConflictManagerFactory.get(cache.getAdvancedCache()); // Get All Versions of Key Map<Address, InternalCacheValue<String>> versions = crm.getAllVersions(1); // Process conflicts stream and perform some operation on the cache Stream<Map<Address, CacheEntry<Integer, String>>> conflicts = crm.getConflicts(); conflicts.forEach(map -> { CacheEntry<Integer, String> entry = map.values().iterator().next(); Object conflictKey = entry.getKey(); cache.remove(conflictKey); }); // Detect and then resolve conflicts using the configured EntryMergePolicy crm.resolveConflicts(); // Detect and then resolve conflicts using the passed EntryMergePolicy instance crm.resolveConflicts((preferredEntry, otherEntries) -> preferredEntry);
虽然每个条目都处理 ConflictManager::getConflicts
流,但底层分割器器位于每个片段上事实上 lazily-loading 缓存条目。
8.1.5. 配置分区处理
除非缓存是分布式或复制的,否则分区处理配置将被忽略。默认分区处理策略是 ALLOW_READ_WRITES,默认的 EntryMergePolicy 是 MergePolicies::PREFERRED_ALWAYS。
<distributed-cache name="the-default-cache"> <partition-handling when-split="ALLOW_READ_WRITES" merge-policy="PREFERRED_NON_NULL"/> </distributed-cache>
以编程方式实现相同的:
ConfigurationBuilder dcc = new ConfigurationBuilder(); dcc.clustering().partitionHandling() .whenSplit(PartitionHandling.ALLOW_READ_WRITES) .mergePolicy(MergePolicy.PREFERRED_ALWAYS);
8.1.5.1. 实施自定义合并策略
也可以提供 EntryMergePolicy 的自定义实现
<distributed-cache name="mycache"> <partition-handling when-split="ALLOW_READ_WRITES" merge-policy="org.example.CustomMergePolicy"/> </distributed-cache>
ConfigurationBuilder dcc = new ConfigurationBuilder(); dcc.clustering().partitionHandling() .whenSplit(PartitionHandling.ALLOW_READ_WRITES) .mergePolicy(new CustomMergePolicy());
public class CustomMergePolicy implements EntryMergePolicy<String, String> { @Override public CacheEntry<String, String> merge(CacheEntry<String, String> preferredEntry, List<CacheEntry<String, String>> otherEntries) { // decide which entry should be used return the_solved_CacheEntry; }
8.1.5.2. 将自定义合并策略部署到 Infinispan 服务器实例
要在服务器上使用自定义 EntryMergePolicy 实现,需要把实施类部署到服务器。这可以通过利用 java service-provider 惯例并将类文件打包在 jar 中,其具有 META-INF/services/org.infinispan.conflict.EntryMergePolicy 文件,其中包含 EntryMergePolicy 实施的完全限定类名称。
# list all necessary implementations of EntryMergePolicy with the full qualified name org.example.CustomMergePolicy
要使自定义合并策略在服务器上使用,您应该启用对象存储,如果您的策略语义需要访问存储的 Key/Value 对象。这是因为服务器中缓存条目可能会以 marshalled 格式存储,返回到您的策略的 Key/Value 对象将是 WrappedByteArray 的实例。但是,如果自定义策略只依赖于与缓存条目关联的元数据,则不需要对象存储,应该避免(除非因其他原因而需要)因为每个请求的额外性能成本增加。最后,如果使用其中一个提供的合并策略,则无需对象存储。
8.1.6. 监控和管理
缓存的可用性模式在 JMX 中作为 Cache MBean 的属性公开。该属性是可写的,允许管理员强制将缓存从 DEGRADED 模式强制迁移到 AVAILABLE (一致性成本)。
可用性模式也可以通过 AdvancedCache 接口访问:
AdvancedCache ac = cache.getAdvancedCache(); // Read the availability boolean available = ac.getAvailability() == AvailabilityMode.AVAILABLE; // Change the availability if (!available) { ac.setAvailability(AvailabilityMode.AVAILABLE); }