LocalCache guava,优化吞吐量更高

时间:2014-06-24 04:16:57

标签: java performance caching guava

我使用来自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显示了类似的结果)。所以,问题不在于碰撞。

关于如何调整它以获得更高性能的任何指示?

2 个答案:

答案 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负载。使用getAllgetAllPresent等批量操作应该是一个谨慎的决定。由于cache2k中的访问时间类似于哈希表访问时间,因此批量操作可能不会加快速度。

有关getAllPresent()的说明

在cache2k中,有一个JSR107兼容的getAll()方法,它的用途大致相同。从API设计者的角度来看,这些方法是邪恶的,因为它与缓存控制资源的想法相矛盾。刚刚使用cache.get()或cache.peek()。如果有一个CacheSource(也称为CacheLoader),请使用cache.prefetch(keys)&#34;对缓存&#34;您希望下次使用这些键....抱歉,有点偏离。