记录一次dubbo本地缓存清理失败的惨痛教训

我们都知道dubbo的SPI扩展模式可以对开发者的功能扩展进行友好支持。最近我们有一些业务场景,用到了需要dubbo的本地缓存的功能,来支持业务场景的需要,目前使用的是2.6.5版本,发现dubbo本身支持的本地缓存没有做清理重置操作,担心会有问题,于是自己利用SPI进行了本地缓存扩展。由于测试场景简单,不够充分,导致上线引发了相关服务的pot节点全部在启动半小时后内存和cpu使用率同时飙升,虽然没有造成生产环境的损失,仍然给笔者敲响了警钟。

业务场景

业务场景主要是对详情页预加载的一次优化,由于详情页的信息和数据往往比较丰富,调用接口比较多,因此想到通过进入列表页时进行详情页预加载的功能,提高详情页的响应速度,完善客户体验。

实现方案

通过阅读dubbo的官方文档和相关源码,笔者了解到,Dubbo 的扩展点加载从 JDK 标准的 SPI (Service Provider Interface) 扩展点发现机制加强而来。在扩展类的 jar 包内,放置扩展点配置文件 META-INF/dubbo/接口全限定名,内容为:配置名=扩展实现类全限定名,多个实现类用换行符分隔。
自定义实现本地缓存的工厂类:

public class LocalCacheFactory extends AbstractCacheFactory {
     
    @Override
    protected Cache createCache(URL url) {
     
        String key = url.toParameterString("application", "id", "interface", "method");
        return MyCacheUtil.getCache(key);
    }
}

自定义缓存实现类,继承,Cache接口:

public class LocalCache implements Cache {
     
    private final Map<Object, Object> cacheMap;
    public LocalCache() {
     
        this.cacheMap = new HashMap<>();
    }

    @Override
    public void put(Object key, Object value) {
     
        if (value != null) {
     
            cacheMap.put(key, value);
        }
    }

    @Override
    public Object get(Object key) {
     
        Object value = cacheMap.get(key);
        return value;
    }

}

自定义扩展点的全限定名com.alibaba.dubbo.cache.CacheFactory 文件,放置在META-INF/dubbo/目录下:

local=xxx.yyy.LocalCacheFactory

自定义Filter文件com.alibaba.dubbo.rpc.Filter,放置在META-INF/dubbo/目录下:

MyFilter=xxx.yyy.MyFilter

自定义MyFilter,用于日志打印和线程生命周期结束后回收缓存:

@Activate(group = Constants.PROVIDER)
public class MyFilter implements Filter {
     
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
     
        Result result;
        try {
     
            result = invoker.invoke(invocation);
        } catch (RpcException re) {
     
            throw re;
        } finally {
     
            try {
     
                CacheUtil.clear();
            } catch (Exception e) {
     
                log.error("异常", e);
            }
        }
        return result;
    }
}

故障复现和定位

在上层服务启用自定义的dubbo服务之后,起初半小时没有发现什么异常,但半小时之后,收到内存使用率超过阈值的报警,观察监控大盘,发现内存使用率和cpu使用率飙升,终端通过jstat 和 jmap 等命令查看发现,fullgc已经在短短半个多小时内发生150多次,gc时间500多毫秒,Eden区和老年代使用率分别达到100%和80%以上,一些大对象除了String、char[]和Integer、HashMap以外,还有大量的接口定义vo和dto对象,于是确定是刚发版导致的问题,于是马上利用jmap -dump命令做dump文件,将将近4G的文件拷贝后,立即回滚代码,回滚后观察cpu和内存使用率恢复。由于刚发版只是升级了对于dubbo的自定义缓存功能的依赖,自然问题也很好定位。
已经隐约猜到问题的情况下,还是决定利用工具看一下做个证实,刚开始利用jdk自带的jvisualvm,打开文件和追踪GCroot都非常缓慢,于是换用JProfiler工具,发现大对象和刚才通过命令查看的结果一样:
在这里插入图片描述
打码的对象既是dubbo接口定义的vo请求和返回对象,跟踪gcroot对象证实了刚才的猜想:
记录一次dubbo本地缓存清理失败的惨痛教训_第1张图片
很清楚这几个大对象都指向了线程池中的ThreadLocal类,即自定义缓存的持有者。

故障解决

自己阅读自定义缓存清理的MyFilter类,怀疑Filter没有激活,于是又查看dubbo的官方文献,发现了端倪:
记录一次dubbo本地缓存清理失败的惨痛教训_第2张图片
我们的激活是根据dubbo的invoker对象里的url参数中的side来区分的,即provider还是consumer,但是最早的写法对于@Activate注解的使用有个重要bug就是没有正确指定group参数,而是默认设定了value,导致value并不是provider所以MyFilter没有激活。修正之后再次打包发布,故障不再复现。

你可能感兴趣的:(dubbo)