我们都知道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对象证实了刚才的猜想:
很清楚这几个大对象都指向了线程池中的ThreadLocal类,即自定义缓存的持有者。
故障解决
自己阅读自定义缓存清理的MyFilter类,怀疑Filter没有激活,于是又查看dubbo的官方文献,发现了端倪:
我们的激活是根据dubbo的invoker对象里的url参数中的side来区分的,即provider还是consumer,但是最早的写法对于@Activate注解的使用有个重要bug就是没有正确指定group参数,而是默认设定了value,导致value并不是provider所以MyFilter没有激活。修正之后再次打包发布,故障不再复现。