使用 Terracotta 的 EHCache 如何处理分布式堆的逐出?

发布于 2024-10-21 00:33:10 字数 545 浏览 2 评论 0原文

我们最近开始将 EHCache 与 Terracotta 结合使用,为应用程序数据运行分布式数据缓存。假设任何客户端节点的堆大小约为 2GB,而服务器节点的堆大小为 8GB。我们生成大量数据,每天大约 1.5GB。

一般来说,任何一个客户端都会使用特定一天的数据集(大约 1.5GB),但服务器显然必须保存所有数据集。

当堆变大时,我希望过期的工作方式是基于 LRU 的。因此,如果任何特定的 L1 客户端缓存变得太大(例如,从 day1 切换到 day2),我希望它从 L1 中逐出所有 day1 数据。如果在我们获得第 6 个数据集时 L2 变得太大,那么最旧的数据集将完全过期。对于生存时间或空闲时间值应该是什么,我真的没有任何意见,所以我没有设置它们。

经过几天的观察,我认为这并没有达到我的预期。例如,我运行了一个 L2 最大元素为 4 的测试。我用四个元素填充它。然后我去放第五个元素。 Cache.put() 没有异常地返回,但紧随其后的具有相同键的 Cache.get() 返回 null!

所以我的问题是,如何让 EHCache+Terracotta 做我想做的事情,或者至少做一些接近的事情?

We've recently started using EHCache with Terracotta to run a distributed data cache for application data. Let's say that any client node has about 2gb for their heaps, whereas server nodes have 8gb. We generate a lot of data, about 1.5gb a day.

Generally, any one client will be using a specific day's dataset (of about 1.5gb), but the server obviously has to hold all of them.

The way I'd like expiration to work is on an LRU basis, when the heap grows to big. So if any particular L1 client side cache gets too big (say, switching from day1 to day2) I'd expect it to evict from L1 all of the day1 data. If L2 gets too big as we get the 6th dataset, then the oldest dataset gets expired completely. I don't really have any opinions about what time-to-live or time-to-idle values should be, so I'm leaving them unset.

After a few days of looking at it, I don't think this is working as I'd expect. For instance, I ran a test with a L2 max elements of 4. I populated it with four elements. Then I went to put a fifth element. Cache.put() returned without exceptions, but an immediately following Cache.get() with the same key returned null!

So my question is, how do I get EHCache+Terracotta to do what I want, or at least something close?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

不离久伴 2024-10-28 00:33:11

经过一番折腾后,我们发现这种神秘的行为是由 L1 缓存是否了解 L2 缓存中的内容决定的。附加的单元测试旨在成对执行,该对的每个元素在单独的 JVM 中运行,以避免 EHCache 单例性。

我认为良好的 L1->L2 关系应该起作用的方式是,如果您执行 .put() 没有错误,您应该能够在没有问题后立即执行相同密钥的 get() (假设没有其他并发运行的线程正在搞乱东西)。然而,对于 Terracotta EHCache,如果 .put() 需要驱逐某些内容,则驱逐不会发生,并且 put() 会被默默地忽略,除非 L1 客户端“知道”可以驱逐的键。在 *JVM2 测试中,它通过使用 .getAllWithLoader() 查找其他键,然后 *JVM2 测试按预期工作。

因此,我们为解决原始问题所做的就是让客户端定期执行 .getAllWithLoader()。然后我们就可以确保所有驱逐规则都得到遵守。

package com.mine;

import java.io.Serializable;

import junit.framework.TestCase;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
import net.sf.ehcache.config.CacheConfiguration;
import net.sf.ehcache.config.Configuration;
import net.sf.ehcache.config.TerracottaClientConfiguration;
import net.sf.ehcache.config.TerracottaConfiguration;

public class CacheEvictionTest extends TestCase {

    private static final String SERVER = "localhost:9510";
    private CacheManager cacheManager;
    private Cache cache;
    private final Serializable keyA = "a";
    private final Serializable keyB = "b";
    private final Serializable keyC = "c";

    @Override
    protected void setUp() throws Exception {
        Configuration configuration = new Configuration();
        TerracottaClientConfiguration terracottaConfig = new TerracottaClientConfiguration();
        terracottaConfig.setUrl(SERVER);
        configuration.addTerracottaConfig(terracottaConfig);

        int maxElementsInMemory = 1;
        int maxElementsOnDisk = 2;
        long timeToIdleSeconds = 15;
        long timeToLiveSeconds = 15;
        String cacheName = "TEST_CACHE";
        CacheConfiguration amoebaCache = new CacheConfiguration(cacheName, maxElementsInMemory).statistics(true)
                        .terracotta(new TerracottaConfiguration())
                        .logging(true)
                        .maxElementsOnDisk(maxElementsOnDisk)
                        .timeToIdleSeconds(timeToIdleSeconds)
                        .timeToLiveSeconds(timeToLiveSeconds);
        configuration.addCache(amoebaCache);
        configuration.addDefaultCache(new CacheConfiguration("default", 0));

        cacheManager = new CacheManager(configuration);
        cache = cacheManager.getCache(cacheName);
    }

    @Override
    protected void tearDown() throws Exception {
        if (cache != null) {
            cache.removeAll();
            cache.clearStatistics();
        }
    }

    public void testMaxElementOnDiskEvictionJVM1() throws Exception {
        cache.clearStatistics();

        cache.put(new Element(keyA, keyA));
        cache.put(new Element(keyB, keyB));

        cache = null;
    }

    public void testMaxElementOnDiskEvictionJVM2() throws Exception {
        assertEquals(2, cache.getSize());

        for (Object key : cache.getKeys()) {
            cache.get(key;
        }

        cache.put(new Element(keyC, keyC));

        assertEquals(2, cache.getSize());
        assertNotNull(cache.get(keyC));
    }

    public void testEvictsExpiredElementsFromDiskWhenNotInMemoryAndWeNeverKnewAboutItJVM1() throws Exception {
        cache.clearStatistics();
        cache.put(new Element(keyA, keyA));

        cache = null;
        cacheManager = null;
    }

    public void testEvictsExpiredElementsFromDiskWhenNotInMemoryAndWeNeverKnewAboutItJVM2() throws Exception {
        cache.clearStatistics();

        for (Object key : cache.getKeys()) {
            cache.get(key;
        }
        assertEquals(0, cache.getStatistics().getEvictionCount());

        Thread.sleep(20000);

        assertEquals(1, cache.getStatistics().getEvictionCount());
    }
}

After much messing around, it turns out the mysterious behavior is determined by whether or not the L1 cache has any knowledge of what's in the L2 cache. The attached unit tests are meant to be executed in pairs, each element of the pair run in a separate JVM to avoid EHCache singleton-ness.

The way I think a well-behaved L1->L2 relationship should work is that the if you do a .put() without error, you should be able to do a get() of the same key immediately after without a problem (assuming no other concurrently running threads are messing with stuff). However, with Terracotta EHCache, if that .put() requires something to be evicted, the eviction does NOT happen, and the put() is silently ignored, unless the L1 client 'knows' about the keys that can be evicted. In the *JVM2 tests, it finds out about those other keys by using .getAllWithLoader(), and then the *JVM2 tests work as expected.

So what we have done to address our original is to make it so that periodically clients do a .getAllWithLoader(). Then we can be sure that all the eviction rules are followed.

package com.mine;

import java.io.Serializable;

import junit.framework.TestCase;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
import net.sf.ehcache.config.CacheConfiguration;
import net.sf.ehcache.config.Configuration;
import net.sf.ehcache.config.TerracottaClientConfiguration;
import net.sf.ehcache.config.TerracottaConfiguration;

public class CacheEvictionTest extends TestCase {

    private static final String SERVER = "localhost:9510";
    private CacheManager cacheManager;
    private Cache cache;
    private final Serializable keyA = "a";
    private final Serializable keyB = "b";
    private final Serializable keyC = "c";

    @Override
    protected void setUp() throws Exception {
        Configuration configuration = new Configuration();
        TerracottaClientConfiguration terracottaConfig = new TerracottaClientConfiguration();
        terracottaConfig.setUrl(SERVER);
        configuration.addTerracottaConfig(terracottaConfig);

        int maxElementsInMemory = 1;
        int maxElementsOnDisk = 2;
        long timeToIdleSeconds = 15;
        long timeToLiveSeconds = 15;
        String cacheName = "TEST_CACHE";
        CacheConfiguration amoebaCache = new CacheConfiguration(cacheName, maxElementsInMemory).statistics(true)
                        .terracotta(new TerracottaConfiguration())
                        .logging(true)
                        .maxElementsOnDisk(maxElementsOnDisk)
                        .timeToIdleSeconds(timeToIdleSeconds)
                        .timeToLiveSeconds(timeToLiveSeconds);
        configuration.addCache(amoebaCache);
        configuration.addDefaultCache(new CacheConfiguration("default", 0));

        cacheManager = new CacheManager(configuration);
        cache = cacheManager.getCache(cacheName);
    }

    @Override
    protected void tearDown() throws Exception {
        if (cache != null) {
            cache.removeAll();
            cache.clearStatistics();
        }
    }

    public void testMaxElementOnDiskEvictionJVM1() throws Exception {
        cache.clearStatistics();

        cache.put(new Element(keyA, keyA));
        cache.put(new Element(keyB, keyB));

        cache = null;
    }

    public void testMaxElementOnDiskEvictionJVM2() throws Exception {
        assertEquals(2, cache.getSize());

        for (Object key : cache.getKeys()) {
            cache.get(key;
        }

        cache.put(new Element(keyC, keyC));

        assertEquals(2, cache.getSize());
        assertNotNull(cache.get(keyC));
    }

    public void testEvictsExpiredElementsFromDiskWhenNotInMemoryAndWeNeverKnewAboutItJVM1() throws Exception {
        cache.clearStatistics();
        cache.put(new Element(keyA, keyA));

        cache = null;
        cacheManager = null;
    }

    public void testEvictsExpiredElementsFromDiskWhenNotInMemoryAndWeNeverKnewAboutItJVM2() throws Exception {
        cache.clearStatistics();

        for (Object key : cache.getKeys()) {
            cache.get(key;
        }
        assertEquals(0, cache.getStatistics().getEvictionCount());

        Thread.sleep(20000);

        assertEquals(1, cache.getStatistics().getEvictionCount());
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文