多级优先缓存

4
我们对缓存(Java)有以下要求:
- 缓存项根据淘汰优先级不同具有不同的优先级 - 下列条目的属性将影响此优先级: - 插入或上次使用时间(LRU) - 计算条目所需的资源(在清除时)。该因素会发生变化,因为我们不时地从缓存中获取一个条目并“向其添加信息”,这使得在清除后重新计算它需要更多资源。在此参数上,条目的驱逐优先级非常离散-例如不必支持任何可能的长/双精度值。 我们只需说任何条目在自然数1-10范围内都有一个优先级(仅有10个可能的淘汰优先级值)。 看起来可以使用支持驱逐策略插件的缓存实现来完成。 EHCache似乎支持此功能。 不幸的是,Guava缓存不支持。
但是如果实现实际上只是使用一个内部缓存,在必须进行淘汰时尝试找到停留优先级最低的条目,那么我担心性能和灵活性。如果实现方式是在条目(与缓存一起)注册其新优先级的情况下,当其优先级发生更改时,并且缓存维护一个优先级队列,那么我就不会那么担心。是否有人知道采用此方式的缓存实现?有人知道EHCache实际上在做什么吗?
对于我们实际计算上述两个因素的组合优先级也很困难。很难在“最近使用时间”和“重新计算它所需的资源消耗”之间找到公平的平衡。
目前,我们制作了自己的缓存实现,其中包含一组内部缓存(Guava缓存)。每个内部缓存仅使用LRU淘汰策略。当条目更改为“重新计算其所需的资源消耗”时,它将移动到“下一个”内部缓存。这样,我们就不必计算组合淘汰优先级值,而是可以在每个内部缓存上具有不同的最大大小等。 实际上,我们喜欢这种灵活性,但我们宁愿不自己制作和维护缓存实现。 我们宁愿使用一些开源项目的缓存实现。 有人知道支持此多级内部缓存功能的开源缓存实现吗?或者有没有开源项目想采用我们的实现?

如果“重新计算”只是在消耗 CPU 循环(没有 I/O),我不会太在意这个。 - laune
长话短说:您似乎想要一个带有可变“权重”的缓存。Guava没有这个功能,除了可能是LoadingCache#refresh(key) - Markus Kull
重新计算也需要I/O。我们知道缓存对我们来说有很大的影响,所以这不是问题。只是我们宁愿使用一个满足我们需求的第三方缓存,而不是自己实现和维护一个。 - Per Steffensen
1个回答

0

你的问题非常专业化,可能属于XYProblem类别。你所询问的技术方法可能不是解决问题的正确方案,或者它会给系统增加复杂性,但与此相比却没有更多的好处。

建立分段缓存并根据再现成本分配段的解决方案似乎是解决你的问题陈述的合理想法。然而,它可能无法产生良好的整体优化。

你所要求的是缓存应该考虑最近和优先级来进行驱逐决策。这可能会产生负面影响,因为它忽略了频率的影响。例如,一个再现成本为99的条目可能被驱逐,而成本为100的条目可能被保留。但是如果成本为99的条目访问次数比成本为100的条目多三倍呢?聚合再现成本就变得更糟了。

本质上的问题是:你想保留一个条目,因为它很重要且重新生成的成本很高。 "重要" 意味着应用程序经常使用它,这意味着它正在被访问。那么为什么缓存要将其驱逐呢?

以下是我脑海中的一些随机想法,以替代解决问题:

简单地增加缓存大小。使用Guava时,你受到堆的限制。也许使用具有持久性的缓存来克服这个问题,例如infinispan或hazelcast。

为什么缓存会清除重要的条目呢?请检查应用程序是否真正访问了缓存,而不是持有引用或在其上施加额外的缓存。也许您的应用程序的访问模式不太友好,不适合LRU算法。还有比LRU更好的驱逐算法,例如ARC、LIRS或Clock-Pro。也许现代的驱逐算法可以保留您昂贵的条目。
难道不能从缓存键中推导出合理的成本估计吗?也许可以,并且基于关键字对缓存进行分段可能是“足够好”的。这样做也更透明,可以清楚地知道哪些条目进入了哪个段。
最后一点:
我喜欢您对此的想法,因为我认为缓存是调整典型的时间与空间设计决策的好地方。但是,如果您开始“改进”并添加优先驱逐功能,请走到底:评估不同的方法,并确保使用的资源确实降低了。

但是如果花费为99的条目被访问的次数增加了三倍呢?聚合再现成本会变得更糟。 这是正确设置内部缓存大小的问题。 “重要”意味着应用程序经常使用它,这意味着正在访问它。为什么缓存要将其删除? 不重要取决于两个因素。在实践中,我们每天将数百万条目放入缓存中。缓存条目被使用一两次的可能性相对较高... - Per Steffensen
在插入数据后不久(而且从未再次使用) - 假设10%的条目在前10分钟内使用。我们需要将遇到的“所有数据”都插入到缓存中,因为我们不知道这些10%在这些10分钟内被获取。假设我们的缓存大小和插入频率使得条目在30分钟后被驱逐。然后有那些“额外重要”的条目,它们像整个星期或一个月中使用了数百次。每次使用它们,我们都会添加信息到条目中,使得在驱逐时“重新计算”变得更加昂贵。 - Per Steffensen
...我们确实需要将它们保存在缓存中,但很可能它们没有被足够频繁地使用(每30分钟)以自动保留在缓存中。当我们第一次遇到这些条目时,我们无法识别它们 - 起初它们就像任何其他缓存条目一样。
使用具有持久性的缓存 并不值多少。重新计算成本昂贵的原因是需要从磁盘中读取信息。 难道不能从缓存键派生出合理的成本估算吗? 不行。当我们第一次看到这些“重要条目”时,我们对它们一无所知。
- Per Steffensen
评估不同的方法,并确保使用的资源真正降低。我们对此进行了大量测量。我们最初只有一个缓存,但我们记录了我们必须重新计算多少个“昂贵”的条目以及它们的成本。实施多级缓存在这方面有所改善,我们有测量数据表明现在我们几乎是“完美”的。 - Per Steffensen
嗨Per,感谢您更深入的阐述!您能提供一个访问跟踪吗?在github上的cache2k基准测试中,我有一组访问跟踪并比较驱逐算法。访问跟踪只是一个包含按行访问的资源ID的文件。 - cruftex
实际上,你至少在部分地重新发明了驱逐算法领域。现代的驱逐算法通过将缓存分段,并将访问次数更多的条目与其他条目分开保留来工作。我认为最早的一个是2Q算法,它于1994年推出。我在答案中提到的新算法都使用两个“段”来工作。 - cruftex

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接