项目中使用ehcache遇到的问题

背景

最近在做业务逻辑重构,pc、app等有类似的逻辑都写在了web层,导致新增各端保持一致的需求需要更改各自站点,增加了开发成本和维护成本,所以需要把通用逻辑抽离到业务逻辑层,web层通过rpc调用实现解耦。

问题现象

抽离出来的服务上线两周必现调用方超时,服务内部逻辑处理时间急剧增加,等待处理的请求等待时间急剧增加。如下图


image.png

正常处理时间在100ms左右。查看日志,只能看到框架组件报待处理的请求等待超时。


image.png

因为报的时间点是早高峰,又有大量客户反馈,情急之下立即重启机器,只留一台机器用于验证。
服务4台机器,重启三台后,耗时下降,抓取留下那台的线程堆栈,内存dump未发现异常信息。查看当时的调用量,确实有增长,但不是很多,考虑到服务内部调用了一个很耗时的外部接口,又没有分析到问题,增加了几台机器增加下服务整体的处理能力。
但是这件事由于没有找到具体原因,像是扎在胸口的一根刺,每天早高峰的时候都盯着屏幕,观察服务状态,随时准备见状不好立即重启。
就这样过了两周后,发现服务异常复现,由于一直盯着,立即抓取其中一台的内存dump,别的机器同时进行重启,等把这台机器的dump下载下来后,对这台也进行了重启。重启后服务正常,没有客诉,于是安下心来分析dump。
用Jvisual查看堆内存看不出什么问题,于是通过Jvisual通过dump得出的线程堆栈发现成堆的BLOCKED

image.png

可以看到是获取缓存时ehcache内部锁等待。【ehcache版本1.6.2】那么为什么大家都在等呢,我们进去看MemoryStore 148行。
image.png

这个方法被synchronized修饰表示调用该对象方法的所有调用方都需要竞争该锁。【我们都知道使用被它修饰的方法在多线程下效率很低,此时我就想看看是否ehcache的高版本优化了这部分】那么我们put的时候有几个调用方呢。看Cache 1306行
image.png

这段代码的逻辑就是从磁盘中读出数据,不为空、未过期就放回内存,memoryStore是Cache中的属性,他俩是一对一的。那么Cache有几个呢,用过ehcache大家就知道了每个cache对应配置文件中的一个cache,也就是说一个name一个cache。
image.png

巧了,我的服务里面大部分都用了longCache,那么put那块锁竞争就很激烈。
看了代码之后我们就清楚原因了
由于内存中存储的数据超过设置的maxElementsInMemory,且配置了overflowToDisk,则新增的数据会存储到磁盘,看BLOCK处都出现在从内存中获取不到数据在磁盘中获取到了,获取到之后再放到磁盘中。这就导致获取缓存和缓存数据都会竞争同一个锁。

DEMO复现

import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;

import java.io.InputStream;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class TestEHCache1 {
    private static CacheManager cm = null;
    private static ConcurrentHashMap<String, Cache> cacheTable = new ConcurrentHashMap<String, Cache>();

    public static void init() {
        final InputStream resourceAsStream = TestEHCache1.class.getClassLoader().getResourceAsStream("ehcache1.xml");
        cm = new CacheManager(resourceAsStream);
        String[] names = cm.getCacheNames();
        for (String s : names) {
            System.out.println("put:" + s);
            cacheTable.put(s.toLowerCase(), cm.getCache(s));
        }
    }

    private static Cache getCache(String cacheName) {
        return ((Cache) cacheTable.get(cacheName.toLowerCase()));
    }

    public static Object getCacheValue(String cacheName, String key) {
        try {
            Element element = getCache(cacheName).get(key);
            if (element != null) {
                return element.getObjectValue();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public static void setCacheValue(String cacheName, String key, Object value) {
        try {
            Element element = new Element(key, value);
            getCache(cacheName).put(element);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    public static void main(String[] args) {
        init();
        setCacheValue("longCache", "test1", "value1");
        final Object cacheValue = getCacheValue("longCache", "test1");
        System.out.println(cacheValue);
        //给jvisual连接时间,获取线程dump
        try {
            Thread.sleep(5000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Executor executor = new ThreadPoolExecutor(100, 320, 1000L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(5000));
        for (int i = 0; i < 100000; i++) {
            int finalI = i;
            try {
                executor.execute(() -> {
                    long begin = System.currentTimeMillis();
                    setCacheValue("longCache", "test_INFODO_DAFASDFAFEWFEWF" + finalI, "value" + finalI);
                    System.out.println("put cost:" + (System.currentTimeMillis() - begin));
                });

            } catch (Exception e) {
                e.printStackTrace();
            }
        }


        for (int i = 0; i < 100000; i++) {
            int finalI = i;
            try {
                executor.execute(() -> {
                    long beginTime = System.currentTimeMillis();
                    final Object longCache = getCacheValue("longCache", "test_INFODO_DAFASDFAFEWFEWF" + finalI);
                    System.out.println(longCache + " cost:" + (System.currentTimeMillis() - beginTime));
                });
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

ehcache1.x配置

<?xml version="1.0" encoding="UTF-8"?>
<ehcache maxBytesLocalHeap="1K" maxBytesLocalOffHeap="0M" maxBytesLocalDisk="1M">
    <diskStore path=""/>
    <cache name="longCache"
<!--调成1,使得数据立刻存入磁盘-->
           maxElementsInMemory="1"
           eternal="false"
           timeToIdleSeconds="10"
           timeToLiveSeconds="10"
           overflowToDisk="true"
           diskPersistent="true"
           diskSpoolBufferSizeMB="200"
           diskExpiryThreadIntervalSeconds="120"
           memoryStoreEvictionPolicy="LRU"
    />
</ehcache>

打印耗时


image.png

可以看到耗时在400ms左右
查看线程dump,问题复现。


image.png

修复方案

服务在此处缓存是为了降低对第三方调用,缓存到内存中的缓存数目后对第三方服务的压力没有很大,没必要再落到磁盘,且服务也不必须启动时从磁盘中读取缓存数据,所以把overflowToDisk设置为false可以解决。但是方法的同步效率不高,在查看高版本代码后,发现它优化了这部分代码【下图】,测试后在大量数据存取效率明显高于1.x版本。

image.png

由于3.x版本改动量较大,调用方式,配置文件都有很大差别,不是向下兼容的,最后比较下使用了2.x版本相对改动较小。且我们知道同一个cache名字下的数据使用同一锁,新版本的话粒度相对于1.x粒度变得很小,但是不同业务使用同一个cache也存在效率上的问题。

最后改动方案

ehcache升级到2.10.6
新增cache用于该业务
ehcache最终配置如下

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="ehcache.xsd" updateCheck="true"
         maxBytesLocalHeap = "800M"
         dynamicConfig="true">
    <sizeOfPolicy maxDepth="100000" maxDepthExceededBehavior="abort" />
    <diskStore path="java.io.tmpdir"/>
        <defaultCache
        eternal="false"
        timeToIdleSeconds="180"
        timeToLiveSeconds="180"
        overflowToDisk="false"
        diskPersistent="false"
        diskSpoolBufferSizeMB="50"
        diskExpiryThreadIntervalSeconds="120"
        memoryStoreEvictionPolicy="LRU"
    />
    <cache name="longCache"
        eternal="false"
        timeToIdleSeconds="3600"
        timeToLiveSeconds="3600"
        overflowToDisk="false"
        diskPersistent="false"
        diskSpoolBufferSizeMB="200"
        diskExpiryThreadIntervalSeconds="120"
        memoryStoreEvictionPolicy="LRU"
    />
    <cache name="bizXXCache"
           eternal="false"
           timeToIdleSeconds="1800"
           timeToLiveSeconds="1800"
           overflowToDisk="false"
           diskPersistent="false"
           diskSpoolBufferSizeMB="200"
           diskExpiryThreadIntervalSeconds="120"
           memoryStoreEvictionPolicy="LRU"
    />
    <cache name="shortCache"
        eternal="false"
        overflowToDisk="false"
        timeToIdleSeconds="300"
        timeToLiveSeconds="300"
        diskPersistent="false"
        diskSpoolBufferSizeMB="200"
        diskExpiryThreadIntervalSeconds="120"
        memoryStoreEvictionPolicy="LFU"
      />
    <cache name="maxCache"
        eternal="false"
        timeToIdleSeconds="86400"
        timeToLiveSeconds="86400"
        overflowToDisk="false"
        diskPersistent="false"
        diskSpoolBufferSizeMB="100"
        diskExpiryThreadIntervalSeconds="120"
        memoryStoreEvictionPolicy="LRU"
    />
</ehcache>

上线后已平稳运行20多天,问题解决。

后记

其实在第一次出问题时,我们如果获取当时的线程dump就能够分析出问题,但是在报警和客诉的压力下,快速重启恢复导致问题再次出现,也告诉我们如果一个没有找到问题的问题还会再次出现问题。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351

推荐阅读更多精彩内容