ChronicleMap中的多重映射

ChronicleMap的GitHub肯定有关于ChronicleMap中 Multimaps的免责声明:

纪事地图不是……

……没有二级索引。

多图。 使用ChronicleMap<K, Collection>作为multimap在技术上是可行的,但经常导致问题……

不幸的是,这是我的一个使用案例,并使用堆外存储(使用ChronicleMap)肯定是最简单的方法。

让我试着用比萨饼来解释我的问题。 我有10万种不同的比萨饼。 每个披萨都有一个ID和许多不同的浇头和面包皮。 我有三种访问模式:

  • 通过ID给我披萨。
  • 给我所有有特别馅料的比萨饼。
  • 给我所有有特殊shell的比萨饼。

我可以使用ChronicleMap轻松存储比萨饼。 但这只是一种访问模式。 我不想遍历每一个披萨,找到具有匹配的顶部或shell的披萨。 所以,我想存储像ChronicleMap<Topping,Collection>ChronicleMap<Crust,Collection>

然后,如果有人问我所有的意大利辣香肠披萨,我会在顶部的ChronicleMap中查找匹配的比萨饼的UUID,然后在主披萨地图中。

但上面引用的文件让我感到害怕。 有谁知道这些事情经常导致的“问题”是什么? 我为什么不这样做,即使它似乎对我有用? 它是否与ChronicleMap如何存储序列化对象(特别是集合)有关?

针对潜在问题的一些补充说明:

  1. 我们稍后可能会添加比萨饼,这也需要更新collections品。
  2. 许多进程正在尝试执行这些操作,因此需要通过ChronicleMap而不仅仅是基本的ConcurrentMap来共享地图。

如果实际数据确实类似于比萨饼,浇头和面包皮,即只有少数不同的浇头/面包皮,并且成千上万的披萨都包含它们,我会说拥有一个合适的多重贴图对于这种情况来说是过度的,你最好onions_pizzas.datonions_pizzas.dat ,…与UUID不同的可附加共享列表,您可以使用Chronicle Queue进行访问,并方便地从多个进程更新它们。

如果有成千上万的浇头/面包10s-100s,平均只有10s-100s的比萨饼具有特定的浇头,你应该确实使用multimap。

基本上,Chronicle-Maps-as-multimaps有3种“问题”:

每个查询都有过多的垃圾分配

如果使用ListSet类型的值创建Chronicle Map而不指定自定义值序列化程序,它将起作用,但它将完全没有效率,因为它将默认为内置Java序列化以进行序列化和反序列化每个请求的整个值集合,既不重用集合堆对象,也不重用元素的单个UUID堆对象。 因此,每次对ChronicleMap的请求都会产生大量垃圾。

解决方案但是,如果将值序列化程序指定为ListMarshallerSetMarshaller (或您可以基于ListMarshallerSetMarshaller实现编写的自定义集合编组器)以及可重用的UUID堆对象,它将解决此垃圾问题:

 ListMarshaller valueMarshaller = ListMarshaller.of( ReusableUuidReader.INSTANCE, ReusableUuidWriter.INSTANCE); List averageValue = Stream .generate(() -> ReusableUuid.random()) .limit(averagePizzasForTopping) .collect(Collectors.toList()); ChronicleMap> map = ChronicleMap .of(Topping.class, (Class>) (Class) List.class) .averageKey(pepperoni) .valueMarshaller(valueMarshaller) .averageValue(averageValue) .entries(numberOfToppings) .createPersistedTo(new File("toppings_to_pizza_ids.dat")); 

低效的值更新和复制

当您将另一个披萨UUID附加到100个UUID的列表中,并将新值插回到Chronicle Map时,Chronicle Map将重新写入整个列表,而不是将一个UUID附加到堆外内存块的末尾。 如果您使用复制,它会将100个UUID的完整列表作为更新值发送到其他节点,而不是仅发送一个添加的UUID。

两者(值更新和复制)都可以通过可怕的黑客进行优化,但它需要非常深入的Chronicle Map实现知识,并且将非常脆弱。

Chronicle-Map记忆的碎片化

如果您计划在数据存储生命周期中添加新的比萨,最初为entires分配的内存区域将变得太小而无法容纳具有更多UUID的新值,因此将重新分配内存区域(对于每个UUID列表可能多次)。 Chronicle Map的数据结构设计意味着简化的内存分配方案,如果条目被重新分配多次,则会因碎片而受到严重影响。

如果列表中有很多UUID,并且您在Linux上运行应用程序,则可以通过为每个条目预先分配大量内存(任何列表实际需要的内容)来缓解此问题(通过指定.actualChunkSize()ChronicleMapBuilder配置中)并依赖于Linux的惰性映射内存分配function(根据需要逐页)。 因此,对于每个UUID列表,您将丢失最多4KB的内存,如果列表的大小为多KB,则可能没问题。

另一方面,如果您的列表太长(并且它们是UUID列表,即小结构),并且您总共只有10万个比萨饼,那么您首先不需要多重映射,请参阅此答案的开头。

内存过量使用并依赖于Linux中的延迟映射内存分配的技巧也适用于值的短列表(集合),但仅当元素本身很大时才使用,因此平均总值大小为多KB。

当你可以避免任何其他方式的入口内存重新分配时碎片也不是问题,即新的披萨UUID及时添加但也被删除,因此顶部到uuids列表大小浮动一些平均值并且重新分配很少hitted。

如果在将条目插入Chronicle Map之后永远不会更新(或永远不会改变大小),则内存碎片永远不会成为问题。

结论

在某些用例中,如果配置正确,Chronicle Map可以充当多图。 在其他情况下,作为多图的Chronicle Map本质上是低效的。

重要因素:

  • 多图中键 – > List条目的总数
  • 值的总数
  • 密钥大小的平均值和分布
  • 不同值大小的平均值和分布
  • 价值表大小的平均值和分布
  • 值列出Chronicle Map生命周期的动态(从不更新,仅追加,删除和追加。从列表的开头和中间删除更昂贵。)
  • 是否复制了Chronicle Map
Interesting Posts