我目前正在围绕一个大内存索引结构(几千兆字节)进行设计。索引实际上是一个RTree,叶子是BTrees(不要问)。它支持特殊查询并将其推送到逻辑限制。
由于这些节点是搜索节点,我问自己如何最好地使其并行。
到目前为止,我知道有六种解决方案:
在计划写入时阻止读取。树完全被阻塞,直到最后一次读取完成,然后执行写入,并且在写入之后,树可以再次用于多次读取。 (读取不需要锁定)。
克隆节点以更改和重用现有节点(包括叶子)并在两者之间切换,只需再次停止读取切换和完成。由于叶子指针必须改变,叶子指针也可能成为他们自己的集合,因此可以切换修改,并且可以将更改重做为第二个版本,以避免在每个插入上复制指针。
使用索引的独立副本,如双缓冲。更新索引的一个副本,切换它。一旦没有人读取旧索引,就以相同的方式改变该索引。这样,可以在不阻止现有读取的情况下完成更改。如果另一个插入物在合理的时间内撞击树,也可以进行这些更改。
使用串行共享无结构,因此每个搜索线程都有自己的副本。由于线程只能在执行单次读取后更改其树,因此这也将是无锁且简单的。正确读取均匀分布在每个工作线程中(绑定到某个核心),吞吐量不会受到损害。
对即将写入的每个节点使用写入/读取锁定,并且仅在写入期间阻止子树。这将涉及针对树的附加操作,因为拆分和合并将向上传播,因此需要重新插入插入(因为向上扩展锁(括号)将引入死锁的机会)。如果你有更高的页面大小,拆分和合并不是那么频繁,这也是一个好方法。实际上,目前我的BTree实现通过拆分节点并重新插入值来使用类似的机制,除非不需要拆分(这不是最佳但更简单)。
为每个节点使用双缓冲区,例如每个页面在两个版本之间切换的数据库的影子缓存。因此,每次修改节点时,都会修改一个副本,一旦发出读取,就会使用旧版本或新版本。每个节点都获得一个版本号,并选择更接近活动版本(最新更改)的版本。要在版本之间切换,只需要对根信息进行atomar更改。这样可以更改和使用树。这种切换可以每次都完成,但必须确保在覆盖新版本时没有使用旧版本的读取。该方法有可能不干扰缓存局部性以链接叶子等。但它也需要两倍的内存量,因为必须存在后备缓冲区但节省了分配时间,并且可能适用于高频率的更改。
有了这些想法什么是最好的?我知道这取决于但是在野外做了什么?如果有10个读线程(甚至更多)并被单个写操作阻塞,我想这不是我真正想要的。
L3,L2和L1缓存以及具有多个CPU的场景如何?有什么问题吗?双缓冲的美妙之处在于那些读取旧版本的内容仍然可以使用正确的缓存版本。
创建节点的新副本的版本并不吸引人。那么在今天的数据库环境中会遇到什么呢?
[更新]
通过重新阅读帖子,我想知道使用写锁定进行拆分和合并是否更适合创建替换节点,因为对于拆分和合并我需要复制一半的元素,这些操作非常罕见因此,实际上完全复制节点将通过替换父节点中的这个简单快速操作的节点来实现这一目的。这样,读取的实际块将非常有限,因为无论如何我们创建副本,阻塞只在更换新节点时发生。因为在那些访问期间叶子可能不会被改变,所以它不重要,因为信息密度没有改变。但是,这又要求节点的每次访问都需要增加和减少读锁定并检查预期的写锁定。这一切都是开销,这一切都阻止了进一步的读取。
[UPDATE2]
解决方案7.(目前受青睐)
目前我们支持内部(非叶子)节点的双缓冲区,并使用类似于行锁定的东西。
我们尝试使用这些索引结构(这都是索引)进行分解的逻辑表导致在这些信息上使用集合的代数。我注意到这个集合的代数是线性的(O(m + n)用于交集和并集)并且让我们有机会锁定每个条目是这种操作的一部分。
通过对内部节点进行双缓冲(这不是很难实现,也不是花费太多(大约<1%的内存开销)),我们可以在这个问题上免费解决,而不会阻止过多的读取操作。
由于我们以某种方式批量修改,很少看到给定的列被更新但是一旦被更新,则需要更多的时间,因为对于这个单独的条目,这些修改可能会有数千个。
因此,目标是改变用于简单地交叉稍后当前修改的列的集合的代数。由于一次只修改一列这样的操作只会阻塞一次。对于目前阅读它的每个人来说,写操作必须等待。并且猜测一下,一旦写入操作等待,它通常会让另一个列的另一个写入操作发生并不繁琐。我们计算出这种块的可行性非常低。所以我们不需要关心。
使用check for write,检查写入意图,添加读取,再次检查写入以及使用read进行检查来完成锁定机制。所以没有明确的对象锁定。我们访问固定的字节区域,如果结构清晰,那么一切关键是计划进入c ++版本以使其更快一些(2x我们猜测,这只需要一个人一到两周的时间来做,特别是如果你使用Java来C ++翻译)。
现在唯一重要的影响可能是缓存问题,因为它会使L1缓存无效,也可能是L2。因此,我们计划收集这样的表/索引上的所有修改,以便在1分钟或更长时间内分时运行,但要均匀分布,以使系统不具备性能hickhup。
如果您知道任何可以帮助我们的事情,请继续。
答案 0 :(得分:0)
由于没有人回复,我想总结一下我们(I)最终做了什么。结构现已分开。我们有一个RTree,叶子实际上是Tables。这些表甚至可以是远程的,因此我们的分发方式大部分都是透明的,这要归功于RMI和代理。
其余的很简单。 RTree可以建议一个表进行拆分,这个拆分也是一个表。这种拆分是在一台机器上完成的,如果必须是远程的,则转移到另一台机器上。合并几乎相似。
对于绑定到不同CPU的线程,此远程也适用,以避免缓存问题。
关于内存中的修改,正如我已经建议的那样。我们复制内部节点并将表格旋转90°并调整代数集算法以有效处理锁定列。对单个表的测试很简单,并且与每列的1000个条目进行比较,毕竟不是性能问题。死锁也是不可能的,因为一次使用一列,因此每个线程只有一个锁。我们尝试并行执行列,这会增加响应时间。我们还考虑将列绑定到给定的虚拟内核,因此没有再次锁定,因为列是隔离的,而且修改可以再次序列化。
通过这种方式,每个CPU可以使用20个核心,并且还可以避免缓存未命中。