关键词: 桶数限制, 分区partition, cardinality, script
最近生产上的定时任务发生了OOM异常.很遗憾的是,st环境无法重现,生产环境也由于权限的问题无法定位,所以找不到问题的原因.
不过从程序来看,代码还是有很大的优化空间.
目前业务现状(模拟的业务场景):
1,表中有约140万条不同的会员卡(卡信息表)数据,对应有大约3200万条消费记录存在es中.
2.需要对半个月/一个月都未消费的会员卡发送短信提醒,如果卡有异常(另一表记录),则不发送
3.同一张会员卡可以给多个人使用(会员与卡映射表)
现有程序设计:
1.分批遍历卡信息,剔除有异常的会员卡
2.用每张卡去ES查询最后一条消费记录的时间,然后判断是否半个月/一个月都未消费
3.符合条件的卡发短信提醒卡的所有成员
缺点:
1.需要全部卡都进行遍历,并到ES进行聚合查询,再做判断,占内存多,计算量大,耗时
2.查卡过程的SQL使用了not exist,查询时间长
3.很多会员卡不符合条件,做了不必要的筛选,耗时且浪费巨大
因为无法找到内存溢出的原因,只有通过其他方法进行代码优化.
=>改进办法:
1.设计思路: 原先是 会员卡->es查询最后一笔消费时间->符合条件的数据->发短信 (正向)
改为:es分别获取最后一笔交易是半个月前,一个月前的会员卡->数据库查会员卡信息->发短信 (反向)
2.发短信步骤改为异步
为什么原先的设计没有按这种思路来呢? 是因为当时一直找到符合要求的es脚本去筛选数据.所以只能遍历会员卡,而不是相反先从es筛选开始. 光是这一步就有巨大的区别.
参考资料:
Elasticsearch 中如何巧妙地使用聚合函数达到数据库中having的效果
ElasticSearch 聚合筛选,类似SQL里面的having
How to use Scroll on Elasticsearch aggregation?
filtering_values_with_partitions
在es使用聚合查询时,由于桶数过多超出设置会抛异常,因此需要对查询进行分区(即分批次循环查询)
先获取es总记录数
private int getRecordTotalNum(RestHihLevelclient client) {
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
CardinalityAgeregationBuilder countAee = AggregationBuilders.cardinality(CARD_CNT).field(CARD_ID);
sourceBuilder.agregation(countAgg.size(0);
SearchRequest request = new SearchRequest(INDEX_NAME).types(TYPE_NAME).source (sourceBuilder);
long total = 0;
try {
SearchResponse search = client.search(request);
Cardinality cntAgg = sesearch.getAggregations().get(CARD_CNT);
total = cntAgg.getValue();
catch (Exception e) {
log.error("查询ES异常", e);
}
return total;
}
至于每个批次数据量多少,要根据实际情况而定,如综合查询所需时间,cup消耗等.
在分区的聚合查询中,要对聚合结果进行having过滤(筛选消费时间为半个月前).
而此时又发现新的问题:查询结果会比实际的时间少8小时--原来是时区原因导致的.
所以格式化要设置时区, es脚本大致如下:
{
"size": 0,
"aggregations": {
"by_card_id": {
"terms": {
"field": "card_id",
"size": 1000000,
"min_doc_count": 1,
.
.
.
"include": {
"partition": 0,
"num_partitions": 12
}
},
"aggregations": {
"by_trx_tim": {
"max": {
"field": "trx_tim"
}
},
"having": {
"buckket_selector": {
"buckets_path": {
"trxTim": "by_trx_tim"
},
"script": {
"source": "SimpleDateFormat sdf = new SimpleDateFormat(\"yyyy-MM-dd\");
sdf.setTimeZone(TimeZone.getTimeZone(\"GMT0\"));
String tt = sdf.format(params.trxTim);
return tt.compareTo(\"2020-08-26\") == 0;",
"lang": "painless"
},
"gap_policy": "skip"
}
}
}
}
}
}
由此获取会员卡卡号,就是满足半个月无消费的条件的数据了. 再做一次满足一个月的查询,就得到全部要发短信的会员卡.
此时再单独查出消费异常的会员卡,剔除掉即可,不用和以前那样做not exist耗时的操作了.
剩下的就是组装信息发短信了.
总结:
原先对ES了解很浅,并不知道ES有类似having的查询,也不知道可以插入自定义查询脚本. 当时也试图找过相关的资料,可能是查资料不得法,没有找到这块内容.所以问题驱动开发,能不断扩充知识.
有疑问或不同的观点,欢迎提出,一起探讨.