在多线程读58万数据处理后写入到另外一张表中性能调优实例遇到第一个问题就是drools的性能瓶颈,对CPU消耗比加大,那么如何优化呢?
从下图看到i5-8250U
的计算能力应该不行,这个时候终于知道cpu好,会能起到什么效果了。
使用arthas可以定位到,有两个地方耗费时间Results results = kieHelper.verify();
和KieBase kieBase = kieHelper.build(config);
public KieSession decodeToSession(String... drl) {
KieHelper kieHelper = new KieHelper();
String[] var3 = drl;
int var4 = drl.length;
for(int var5 = 0; var5 < var4; ++var5) {
String s = var3[var5];
kieHelper.addContent(s, ResourceType.DRL);
}
Results results = kieHelper.verify();
if (results.hasMessages(new Level[]{Level.WARNING, Level.ERROR})) {
List<Message> messages = results.getMessages(new Level[]{Level.WARNING, Level.ERROR});
Iterator var11 = messages.iterator();
while(var11.hasNext()) {
Message message = (Message)var11.next();
this.logger.error("Error: {}", message.getText());
}
throw new IllegalStateException("Compilation errors.");
} else {
KieBaseConfiguration config = kieHelper.ks.newKieBaseConfiguration();
if (EventProcessingOption.STREAM.getMode().equalsIgnoreCase(this.getMode())) {
config.setOption(EventProcessingOption.STREAM);
} else {
config.setOption(EventProcessingOption.CLOUD);
}
KieBase kieBase = kieHelper.build(config);
KieSession kieSession = kieBase.newKieSession();
if (this.getListener() == null || !"off".equalsIgnoreCase(this.getListener())) {
kieSession.addEventListener(new DefaultRuleRuntimeEventListener());
kieSession.addEventListener(new DefaultAgendaEventListener());
kieSession.addEventListener(new DefaultProcessEventListener());
}
return kieSession;
}
}
先看KieHelper.verify
,对规则的校验并不总是需要,那么设置一个规则,校验过的不再校验,这个时间就节省了。
public KieSession getKieSession(List<String> ruleFileNames){
if (CollectionUtils.isEmpty(ruleFileNames)){
throw new NullPointerException("ruleFileNames is null");
}
List<String> rules = new ArrayList<>();
Long start = System.currentTimeMillis();
for (String ruleFileName:ruleFileNames){
// 从缓存中获取内容
String rule = cacheService.get(ruleFileName);
if (StringUtils.isBlank(rule)){
// 缓存中没有取到数据,这种情况是针对新增的文件,在cache中还没有存在,定时任务还没有刷到,则主动加载
rule = loadAppointRule(ruleFileName);
rules.add(rule);
} else {
if (Constants.ON.equals(cacheService.get(ruleFileName+Constants.FLAG))){
// 缓存没有变化,就不需要再校验了,因为loadAppointRule的时候已经verify了
rules.add(rule);
log.info("文件:{},缓存命中",ruleFileName);
} else{
log.info("文件:{},缓存没有命中",ruleFileName);
// 缓存发生变化,需要再次verify
if (verify(rule)){
// 校验通过的标志
cacheService.put(ruleFileName+Constants.FLAG,Constants.ON);
rules.add(rule);
} else{
throw new IllegalArgumentException("ruleFileName verify error");
}
}
}
}
Long end = System.currentTimeMillis();
log.info("load rule 总耗时:{}ms",end-start);
// 构建KieSession
start = System.currentTimeMillis();
KieSession kieSession = buildKieSession(rules);
end = System.currentTimeMillis();
log.info("buildKieSession 总耗时:{}ms",end-start);
return kieSession;
}
加载文件,校验通过之后,就存入缓存,并设置校验通过标志位,这个时间就节省掉了。
private String loadAppointRule(String fileName) {
List<File> fileList = getAllRuleFiles();
for (File file : fileList) {
if (fileName.equals(file.getName())) {
String rule = encodeToString(file.getPath());
// 校验加载的规则文件是否合法
if (verify(rule)){
// 将正确的规则文件写入到缓存中
cacheService.put(fileName, rule);
// 校验通过的标志
cacheService.put(fileName+Constants.FLAG,Constants.ON);
} else{
log.error("规则文件:{},存在异常,请注意核查",fileName);
}
return rule;
}
}
return null;
}
继续跟踪,发现耗时还是getKieSession
问题出现下面的代码,那么该如何优化呢?
private KieSession buildKieSession(List<String> rules){
KieHelper kieHelper = new KieHelper();
for (String rule : rules) {
kieHelper.addContent(rule, ResourceType.DRL);
}
KieBaseConfiguration config = kieHelper.ks.newKieBaseConfiguration();
if (EventProcessingOption.STREAM.getMode().equalsIgnoreCase(getMode())) {
config.setOption(EventProcessingOption.STREAM);
} else {
config.setOption(EventProcessingOption.CLOUD);
}
KieBase kieBase = kieHelper.build(config);
KieSession kieSession = kieBase.newKieSession();
if (getListener() == null || !Constants.OFF.equalsIgnoreCase(getListener())) {
kieSession.addEventListener(new DefaultRuleRuntimeEventListener());
kieSession.addEventListener(new DefaultAgendaEventListener());
kieSession.addEventListener(new DefaultProcessEventListener());
}
return kieSession;
}
换了一台i5-9400的,buildKieSession 总耗时:2350ms,相比i5-8250U,快了8倍以上,确实难以想象。
drools的线程数量,跟CPU核数有关系
我们知道有状态的kiesession构建时间要比无状态的statelesskiesession长,那么如果是统一个规则,重复支持,这个状态保持住不就可以了吗?推理逻辑就是这样。
可以看到kiesession的编译时间骤降,性能很快提升上去了。
调整代码如下:
@Override
public boolean matchByDay(LiuShiJiaZiEnum day) throws InvocationTargetException, IllegalAccessException {
log.info(String.format("task %s start", day.display()));
long start = System.currentTimeMillis();
List<BaziInfo> baziInfos = baziInfoService.getByDay(day.display());
// 获取kiesession
List<String> ruleList = new ArrayList<>();
ruleList.add("shy3.xlsx");
KieSession session = kieTemplate.getKieSession(ruleList);
// 生成
ShyGenYinDto dto = null;
ShyGenyinQr shyGenyinQr = null;
for (BaziInfo baziInfo: baziInfos){
BaZi bazi = new BaZi.Builder().setYear(baziInfo.getYear())
.setMonth(baziInfo.getMonth()).setDay(baziInfo.getDay())
.setHour(baziInfo.getHour()).build();
dto = qiangRuoService.getQiangRuo(session,bazi);
shyGenyinQr = convert2Shy(dto);
shyGenyinQr.setId(baziInfo.getFullBazi());
//
shyGenyinQrService.insertSelective(shyGenyinQr);
}
session.dispose();
long end = System.currentTimeMillis();
log.info("日柱{}:数量{},查询耗时{}", day.display(),baziInfos.size(), end-start);
return true;
}
再看CPU负载如下,降低很多了。
上图还有一个耗时的22.0% - 540 s org.apache.commons.beanutils.BeanUtils.copyProperties,这个地方用到了一个存在性能问题,参见几种copyProperties工具类性能比较
改用org.springframework.beans.BeanUtils
,住一个这个跟apache的顺序刚好相反。再次测试发现spring的BeanUtils性能真是好。
换成我的i5-9400,发现线程是Net I/O状态,
查看CPU,打log都消耗了3.2%的CPU,如果想更快,这个块的日志都可以关掉