背景
公司使用drools规则引擎过程中,一个规则文件中差不多10个rule,每一次访问都需要耗费800M内存,导致频繁GC,同时classloader实例数每构建一次都会增加,导致nonHeap区内存占用量跟着递增,导致比较频繁的fullGC(此时老年代使用率还不到10%)。当并发量稍微大一点服务就会挂掉。
原因
规则文件加入drools的时候需要解析编译,每访问一次接口都导致规则解析编译一次,经过监控测试发现800M内存和classloader递增问题就是在解析过程中产生的
解决思路
解决问题的本质是缓存已经解析的规则对象,避免重复加载。
缓存以后注意点
- 缓存淘汰策略
- 当规则文件发生变化的时候,要及时更新缓存
用到的技术:drools+Guava
建议用Caffeine替代Guava,我这里是公司规定使用Guava,所以代码实例使用的Guava
代码
// 建议使用Caffeine代替guava
private static Cache<String, Entity> guavaCache;
@Value("${guava.drools.maximumSize:1000}")
private Integer maximumSize;
// 默认一周
@Value("${guava.drools.expireAfterAccess:168}")
private int expireAfterAccess;
/**
* 构建guava
**/
@PostConstruct
public void init() {
guavaCache = CacheBuilder.newBuilder()
.maximumSize(maximumSize)
.expireAfterAccess(expireAfterAccess, TimeUnit.HOURS)
.build();
}
/**
* 执行规则
*
* @param drl 规则字符串
* @param fact 规则fact
* @param agendaStr agenda
* @param lastTime 规则最后修改时间
*/
public void excute(String drl, String fact, String agendaStr, Long lastTime) {
KieSession kSession = null;
try {
kSession = getSession(agendaStr, drl, lastTime);
// 新增议程组
Agenda agenda = kSession.getAgenda();
agenda.getAgendaGroup(agendaStr).setFocus();
kSession.insert(log);
kSession.insert(fact);
kSession.fireAllRules();
} catch (Exception e) {
log.error("drools rule excute ex.", e);
} finally {
if (Objects.nonNull(kSession)) {
kSession.dispose();
}
}
}
public KieSession getSession(String agenda, String drl, Long lastTime) {
Entity entity = guavaCache.getIfPresent(agenda);
KnowledgeBaseImpl kieBase;
if (Objects.isNull(entity)) {
kieBase = buildKieBase(agenda, drl, lastTime);
} else {
if (lastTime.equals(entity.getLastTime())) {
kieBase = entity.getKnowledgeBase();
} else {
// 此处无需删除,直接覆盖即可
kieBase = buildKieBase(agenda, drl, lastTime);
}
}
return kieBase.newKieSession();
}
/**
* 构建 {@link KnowledgeBaseImpl}
*
* @param agenda 规则文件唯一key
* @param drl 规则内容
* @param lastTime 规则最后修改时间
* @return
*/
private KnowledgeBaseImpl buildKieBase(String agenda, String drl, Long lastTime) {
log.info("drools create kieBase. agenda:{}", agenda);
KieHelper helper = new KieHelper();
helper.addContent(drl, ResourceType.DRL);
KnowledgeBaseImpl kieBase = (KnowledgeBaseImpl) helper.build();
// 解除严格模式
kieBase.getConfiguration().setProperty("drools.dialect.mvel.strict", "false");
guavaCache.put(agenda, new Entity(kieBase, lastTime));
return kieBase;
}
public void removeKieBase(String agenda) {
guavaCache.invalidate(agenda);
}
class Entity {
KnowledgeBaseImpl knowledgeBase;
/**
* 规则最后修改时间
*/
Long lastTime;
Entity(KnowledgeBaseImpl base, Long lastTime) {
this.knowledgeBase = base;
this.lastTime = lastTime;
}
public KnowledgeBaseImpl getKnowledgeBase() {
return knowledgeBase;
}
public Long getLastTime() {
return lastTime;
}
}
备注:
1、入口方法excute,其中drl,我们的规则是以字符串的方式存储在数据库中,业务方动态查询出来并传进来
2、lastTime参数,是随drl存储在库中的,每次drl修改都会同步更新lastTime,并作为规则的版本标记
3、agenda参数,这个参数在我们业务中是能唯一确定一条规则的,所以以他作为缓存的key
重点关注
代码的关键点在于把构建的KnowledgeBaseImpl对象要做一个缓存, 实例中的代码是结合我们系统的业务,而且是兼容整合旧代码改过来的,在设计上存在缺陷,强烈建议根据这个思路进行重写