作者:Lars Hofhansl(这是我今年 3 月发布的 HBase 中的 ACID 帖子的后续内容)HBase 有一些特殊的原子操作:checkAndPut、checkAndDelete - 这些只是检查列的值作为前提条件,然后应用 Put或 D
作者:Lars Hofhansl(这是我今年 3 月份在 HBase 中的 ACID 帖子的后续)
HBase 有一些特殊的原子操作:
- checkAndPut、checkAndDelete - 这些只是检查列的值作为先决条件,然后如果检查成功则应用 Put 或 Delete。
- 增量、追加 - 这些允许原子添加到解释为整数,或附加到列的末尾,分别
checkAndPut 和 checkAndDelete 是幂等的,因为它们可以安全地多次应用(但请注意,当在重试)。
增量和追加不是幂等的。它们是 HBase 中唯一不可重复的操作。 Increment 和 Append 也是唯一 HBase 的 MVCC 模型提供的快照隔离不足以满足要求的操作......稍后会详细介绍。
事实证明, checkAndPut 和 checkAndDelete 并不像预期的那样原子(这个问题是由 Gregory 提出的,尽管我一开始并不相信他是对的 - 请参阅 HBASE-7051)。
看一下代码就可以很明显地看出这一点:
一些 Put 优化 (HBASE-4528) ) 允许在更改同步到 WAL 之前释放行锁。这也要求在提交 MVCC 更改之前释放锁,以便在保证更改持久之前,更改对其他事务不可见。
获取行锁以进行原子更改的另一个操作(例如 checkAndXXX)可能会事实上,尽管持有行锁,但看不到当前的情况,因为仍然可能存在未完成的 MVCC 更改,这些更改只有在行锁释放并重新获取后
才可见。因此它可能会在陈旧的数据上运行。 HBASE-4528 后,持有行锁已经不够好了。
Increment 和 Append 也有同样的问题。
这部分的修复相对简单:我们需要某种“MVCC 屏障”。我们不是在更新阶段结束时完成单个 MVCC 事务(这将等待所有先前的事务完成),而是早一点等待先前的事务完成
之前
我们开始检查或获取原子操作的阶段。这只会稍微降低并发性,因为在操作结束之前我们无论如何都必须等待所有先前的事务。 HBASE-7051 正是针对 checkAndXXX 操作执行此操作。此外 - 如上所述 - Increment 和 Append 还有另一个问题,
它们需要是可序列化的事务。快照隔离不够好
。例如:如果您从 0 开始并发出增量 1 和另一个增量 2,则结果必须始终为 3。如果两者可以以相同的起始值开始(a快照)结果可能是 1 或 2,具体取决于哪一个先完成。
增量和追加目前通过一个丑陋的“黑客”解决了这个问题:当他们将更改写入memstore时,他们将所有新KeyValues的memstoreTS设置为0!结果是它们立即对其他事务可见,违反了 HBase 的 MVCC。再次参见 HBase 中的 ACID 以获取 memstoreTS 的说明。
这保证了并发增量和追加操作的正确结果,但对并发扫描器的可见性并不是您所期望的。即使扫描仪应该看到数据的早期快照,即使对于部分行,增量和附加值也可以随时对任何扫描仪可见。
增量和附加也设计用于非常高的吞吐量,因此它们实际上操纵 HBase memstore 删除刚刚修改的列的旧版本。因此,您会丢失更改的版本历史记录,以换取避免内存存储因许多增量或追加的版本而爆炸。这在 HBase 中称为“更新插入”。 Upsert 很好,因为它可以防止内存存储被填满,如果没有人关心它们,就会有很多旧值。缺点是这是对 memstore 的特殊操作,并且很难正确执行。 MVCC。它也不适用于 mslab(请参阅此 Cloudera 博客文章以了解 mslab 的说明)。
如果您不关心可见性,这是一个简单的问题,因为您可以只查看内存存储并删除旧的价值观。不过,如果您关心 MVCC,则必须首先证明删除 KV 是安全的。
我几乎在一年前尝试修复此问题(HBASE-4583),但经过与同事的一些讨论后我们集体放弃了这一点。
几天前,我重新打开了 HBASE-4583,并开始使用一个激进的补丁,该补丁消除了所有 upsert 类型的逻辑(将 memstoreTS 设置为 0),并在开始增量/追加之前等待先前的事务。然后,我依靠对 HBASE-4241 的更改,仅在将内存存储刷新到磁盘时刷新所需列的版本。事实证明这仍然慢了很多(10-15%),因为它需要经常刷新内存存储,即使它会导致大部分空文件。不过,这仍然是一个很好的尝试,因为它摆脱了很多特殊代码,并将增量和追加变成普通的 HBase 公民。
第二个不太激进的版本使 upsert MVCC 意识到。
那就是实际上并不像看起来那么容易。为了从 memstore 中删除列的版本(KeyValue),您必须证明它不会被任何并发或未来的扫描器看到。这意味着我们必须找到任何扫描仪的最早读取点
并确保至少有一个版本的KV早于该最小读取点;然后我们就可以安全地从 memstore 中删除该 KV 的所有旧版本 - 因为任何扫描仪都保证能看到更新版本的 KV。
中的“不太激进”补丁正是这样做的。
在最后,我最终使用 HBASE-4583 提交的补丁同时执行以下两项操作:
如果要递增或追加的列的列族的版本设置为 1,我们会执行补丁添加的 MVCC 感知更新插入。如果 VERSIONS 是 > 1、我们使用通常的逻辑向memstore添加一个KeyValue。所以现在这在所有情况下都符合预期。如果请求多个版本,它们将被保留,并且即使使用增量和追加,时间范围查询也将起作用;当 VERSIONS 设置为 1 时,它还保留了(大部分)性能特征。
译文地址:HBase MVCC 和内置原子操作,感谢原作者分享。