嵌入的 JCache Hazelcast 无法扩展

JCache Hazelcast embedded does not scale

您好,Whosebug 社区。

我有一个 Spring 启动应用程序,它使用带有 Hazelcast 实现的 Jcache 作为缓存框架。

每个 Hazelcast 节点有 5 个缓存,每个缓存大小为 50000 个元素。有 4 个 Hazelcast 实例组成一个集群。

我遇到的问题如下:

我有一个非常繁重的调用,它从所有四个缓存中读取数据。在初始启动时,当所有缓存都为空时,此调用最多需要 600 秒。

当有一个Hazelcast实例运行并且所有5个缓存都充满了数据时,这个调用发生得比较快,平均只需要4秒。

当我启动 2 个 Hazelcast 实例并形成一个集群时,响应时间变得更糟,同一个调用平均需要 25 秒。

我在集群中添加的 Hazelcast 实例越多,响应时间就越长。当然,当数据在集群中的 Hazelcast 节点之间进行分区时,我预计会看到更糟糕的交付时间。但是没想到仅仅多加一个hazelcast实例,响应时间就慢了6-7倍...

请注意,出于简单原因和测试目的,我只在 一台 机器上启动了四个 Spring 启动实例,每个 Hazelcast 嵌入式节点都嵌入其中。因此,这种糟糕的性能不能用网络延迟来解释。我认为即使使用 Hazelcast,这个 API 调用也很慢,因为在 Hazelcast 集群节点之间发送时需要 serialized/deserialized 很多数据。如有不妥请指正

缓存数据在所有节点之间平均分配。我正在考虑添加近缓存以减少延迟,但是,根据 Hazelcast 文档,Jcache 成员无法使用近缓存。就我而言,由于某些项目要求,我无法切换到 Jcache 客户端来使用 Near Cache。在这种情况下,是否有一些关于如何减少延迟的建议?

提前致谢。


演示问题的虚拟代码示例:

  1. Hazelcast 配置:保持默认,没有任何改变
  2. 缓存:
private void createCaches() {

      CacheConfiguration<?, ?> cacheConfig = new CacheConfig<>()
              .setEvictionConfig(
                      new EvictionConfig()
                              .setEvictionPolicy(EvictionPolicy.LRU)
                              .setSize(150000)
                              .setMaxSizePolicy(MaxSizePolicy.ENTRY_COUNT)
              )
              .setBackupCount(5)
              .setInMemoryFormat(InMemoryFormat.OBJECT)
              .setManagementEnabled(true)
              .setStatisticsEnabled(true);
      cacheManager.createCache("books", cacheConfig);
      cacheManager.createCache("bottles", cacheConfig);
      cacheManager.createCache("chairs", cacheConfig);
      cacheManager.createCache("tables", cacheConfig);
      cacheManager.createCache("windows", cacheConfig);
  }

  1. 虚拟控制器:
@GetMapping("/dummy_call")
    public String getExampleObjects() { // simulates a situatation where one call needs to fetch data from multiple cached sources.
        Instant start = Instant.now();
        int i = 0;
        while (i != 50000) {
            exampleService.getBook(i);
            exampleService.getBottle(i);
            exampleService.getChair(i);
            exampleService.getTable(i);
            exampleService.getWindow(i);
            i++;
        }
        Instant end = Instant.now();
        return String.format("The heavy call took: %o seconds", Duration.between(start, end).getSeconds());
    }

  1. 虚拟服务:
@Service
public class ExampleService {

    @CacheResult(cacheName = "books")
    public ExampleBooks getBook(int i) {
        try {
            Thread.sleep(1); // just to simulate slow service here!
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return new Book(Integer.toString(i), Integer.toString(i));
    }

    @CacheResult(cacheName = "bottles")
    public ExampleMooks getBottle(int i) {
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return new Bottle(Integer.toString(i), Integer.toString(i));
    }

    @CacheResult(cacheName = "chairs")
    public ExamplePooks getChair(int i) {
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return new Chair(Integer.toString(i), Integer.toString(i));
    }

    @CacheResult(cacheName = "tables")
    public ExampleRooks getTable(int i) {
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return new Table(Integer.toString(i), Integer.toString(i));
    }

    @CacheResult(cacheName = "windows")
    public ExampleTooks getWindow(int i) {
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return new Window(Integer.toString(i), Integer.toString(i));
    }
}

如果你算一下:

4s / 250 000 次查找是每次本地查找 0.016 毫秒。这似乎相当高,但让我们接受它。

当您添加单个节点时,数据将被分区,并且一半的请求将从另一个节点提供服务。如果您再添加 2 个节点(总共 4 个),那么 25% 的请求将在本地提供服务,而 75% 的请求将通过网络提供服务。这应该可以解释为什么当您添加更多节点时响应时间会增加。

即使是在本地主机上进行简单的 ping 操作也需要两倍或更多的时间。在真实网络上,我们在基准测试中看到的读取延迟是每次读取调用 0.3-0.4 毫秒。这使得:

0.25 * 250k *0.016 + 0.75 * 250k * 0.3 = ~57 秒

您根本无法通过网络(即使是本地电话)连续拨打这么多电话,您需要

  • 并行调用 - 使用 javax.cache.Cache#getAll 减少调用次数
  • 您可以尝试通过 com.hazelcast.config.MapConfig#setReadBackupData 启用 reading local backups 以减少网络请求。

读取备份数据功能仅适用于 IMap,因此您需要使用 Spring 缓存与 hazelcast-spring 模块及其 com.hazelcast.spring.cache.HazelcastCacheManager:

    @Bean
    HazelcastCacheManager cacheManager(HazelcastInstance hazelcastInstance) {
        return new HazelcastCacheManager(hazelcastInstance);
    }

有关详细信息,请参阅 documentation