我使用来自guava库的CacheBuilder和LocalCache,但是对于getAllPresent,p99.9的延迟大约在300-400 ms之间。 请求的延迟几乎在p99和p99.9之间翻倍(p99大约为150毫秒)
使用以下配置: 对于refreshAfterWrite,120秒,maxsize设置为2e6,到期24小时,初始容量为1e6。没有使用removeListener,也没有expireAfterWrite。 ConcurrencyLevel 256(尝试不同的值)。机器有12个核心。 当缓存正在使用时,它具有8e5到1.2e6之间的条目。 对于p99.9和大约100 qps的约3k键,使用模式为getAllPresent。
Key是hashCode的复杂对象,Objects.hash方法与其中提供的所有字段一起使用。我尝试了不同的哈希函数来确保分布是均匀的(murmur3显示了类似的结果)。所以,问题不在于碰撞。
关于如何调整它以获得更高性能的任何指示?
答案 0 :(得分:3)
我认为在Java中效率很高,99%的瓷砖是90%瓷砖的两倍,而99.9%的瓷砖是99%瓷砖的两倍。如果你看到这种模式,你将需要降低所有操作的成本,以减少延迟,即不太可能有一些快速的胜利可以帮助你。
注意:当您有一个大缓存并扫描它时,您可以预期每个条目至少涉及一个或两个L3缓存未命中。这将是昂贵的。对于适合CPU缓存的小缓存,这将快许多倍。
我会使用分析器来减少此操作的CPU和内存分配,或者更改调用缓存的方式来执行所需操作,这也会降低99.9%的分区。
答案 1 :(得分:3)
在不同的请求时间/"请求时间在p99和p99.9之间翻倍"
在getAllPresent调用期间,这可能只是一个偶然的GC。要真正研究这一点,你应该做一个精简的基准测试,跟踪GC活动(只是计数器)。
另一个麻烦来源可能是锁定争用。我在你的问题陈述中缺少确切的访问模式。并行完成了多少个请求?密钥空间如何重叠? Guava在内部对缓存哈希表进行分区,并使用concurrencyLevel作为提示。由于需要更新LRU列表,因此读访问不完全无锁。为了从不同的线程访问相同的密钥,这是锁争用的来源。这是对nitro cache performance的(过时的)评估,显示了这种效果。 (更新:guava缓存有一些策略可以避免读取锁定;这需要进一步调查)
如何获得(15次?)更快
访问缓存时最昂贵的事情是逐出算法更新其数据结构。但是,最大高速缓存大小(2E6)高于最大经验大小(1.2E6)。这意味着不会进行驱逐,因为永远不会达到容量限制。这意味着Guava Cache中LRU列表的所有更新都是毫无意义的。我已经在cache2k benchmarks为Google Guava,EHCache,infinispan和不同的驱逐策略对缓存运行时进行了基准测试,请参阅"运行时比较命中"。多线程访问的基准测试尚未完成,这将在8月份出现。
根据我的理解,没有选择在Guava Cache中更改或切换驱逐策略(任何人都可以这么做吗?)。
在cache2k中,我尝试了一种允许无锁读取访问的替代驱逐策略。在你的场景中,你可以简单地选择"随机驱逐",我希望加速大约15倍.BTW:cache2k缓存还打印哈希表统计信息和hashCode()实现的质量度量标准请参阅cache2k statistics上的注释。
应该可以进行快速评估。这里有一些代码片段可以帮助您快速入门:
<dependency>
<groupId>org.cache2k</groupId>
<artifactId>cache2k-core</artifactId>
<version>0.19.1</version>
</dependency>
<dependency>
<groupId>org.cache2k</groupId>
<artifactId>cache2k-api</artifactId>
<version>0.19.1</version>
</dependency>
备注:缓存实现未在API模块中公开,这就是我们在编译范围内需要核心模块的原因。缓存初始化:
// optional data source (similar to CacheLoader)
CacheSource<Integer, String> source =
new CacheSource<Integer, String>() {
public String get(Integer o) {
return o + "hello";
}
};
Cache<Integer, String> cache =
CacheBuilder.newCache(Integer.class, String.class)
.implementation(RandomCache.class)
.maxSize(3000000)
.expiryMillis(120 * 1000)
/* optional, if cache should do the refresh itself
.source(source)
.backgroundRefresh(true)
*/
.build();
您可以通过更改implementation
选项来试验其他驱逐算法。
cache {2}中没有getAllPresent
,您可以自行编写代码:
public Map<Integer, String> getAllPresent(Iterator<Integer> it) {
HashMap<Integer, String> hash = new HashMap<>();
while(it.hasNext()) {
int k = it.next();
String v = cache.peek(k);
if (v != null) {
hash.put(k, v);
}
}
return hash;
}
在cache2k中cache.peek()
返回一个映射元素而不调用缓存源,这正是getAllPresent
的预期语义。构建哈希映射实际上会产生很多GC负载。使用getAll
或getAllPresent
等批量操作应该是一个谨慎的决定。由于cache2k中的访问时间类似于哈希表访问时间,因此批量操作可能不会加快速度。
有关getAllPresent()的说明
在cache2k中,有一个JSR107兼容的getAll()
方法,它的用途大致相同。从API设计者的角度来看,这些方法是邪恶的,因为它与缓存控制资源的想法相矛盾。刚刚使用cache.get()或cache.peek()。如果有一个CacheSource(也称为CacheLoader),请使用cache.prefetch(keys)
&#34;对缓存&#34;您希望下次使用这些键....抱歉,有点偏离。