Fix Elasticsearch performance
书接这上回,我们继续说。这次我们说:在第一次经历了2k-qps之后。踩的第一个坑,跟大家分享下。关于es的性能问题。
Introduce this business
在一个悠闲的下午,喝着下午茶。虽然领导提前通知要留意下服务器,但是没有想到居然那么猛。自己写的代码稳妥运行在2k-qps下,平常自己写代码也会注意监控这块怎么做。立马看监控系统。
一切运行都很平稳,但是发现有一些dubbo超时,引起告警。我们要有项目的own精神,立刻排查问题,请听我细细道来。
开局一段代码:
@SneakyThrows @Test public void save() throws IOException {
int anInt = RandomUtils.nextInt(0, 10000);
for (int i = 0; i < 10000; i++) {
List<ArtProductEsData> artProductEsDataList = new ArrayList<>(); ArtProductEsData artProductEsData = new ArtProductEsData(); artProductEsData.setItemName("test001"); artProductEsData.setId((long) anInt + i); artProductEsDataList.add(artProductEsData); artProductSearchClient.saveOrUpdateData(artProductEsDataList);
TimeUnit.SECONDS.sleep(1); }
}
|
这是模拟业务写的最简模型。业务逻辑:新建一个esData对象保存到es中,是的业务逻辑非常简单。就是一个批量保存的逻辑。为了便于测试,我for的次数多点,并且每次sleep 1秒。
artProductSearchClient.saveOrUpdateData
的核心逻辑是调用:SimpleElasticsearchRepository#saveAll
。但是这就是性能问题所在
- dubbo设置的调用超时时间1s
这里展示下源码:
@Override public <S extends T> Iterable<S> saveAll(Iterable<S> entities) {
Assert.notNull(entities, "Cannot insert 'null' as a List.");
IndexCoordinates indexCoordinates = getIndexCoordinates(); executeAndRefresh(operations -> operations.save(entities, indexCoordinates));
return entities; }
|
How to locate bug
我们前面介绍业务,也提出了我们要解决的问题:调用artProductSearchClient.saveOrUpdateData
超时。
这时候面试官问你,线上大量这些超时告警要怎么办呢?
思路很多。最容易想到的是把超时时间调大点,并且部署上线。
- 那超时时间调多大才不超时呢?
- 每次都要修改代码、部署代码吗?
上面介绍的方式很低效,不建议采取。这里介绍dubbo调用超时的解决思路。
dubbo-admin:目前具备了服务查询,服务治理的能力。
最常用的功能,就是配置类全限定名,配置消费端的超时时间。并且在dubbo-admin配置的优先级是最高的~
此时,我们通过dubbo打印的超时时间,我们可以大概能够得到一个超时时间,配置上去。再继续查问题。既不需要部署代码,又能快速fix问题。
到此,我们通过dubbo-admin,先快速的fix了问题了。但是没有解决问题。
这时候,面试官问你:你一般是怎么定位线上问题的呢?
线上定位问题的办法很多,这次我们讲,我们最常用的Arthas,官网:https://arthas.aliyun.com/
本次主要介绍Trace指令:
trace -E --skipJDKMethod false com.netease.gcom.search.server.service.impl.ArtProductServiceImpl saveOrUpdateData '#cost > 100' -n 5
|
上面的指令是一个sample指令:查看类名是ArtProductServiceImpl,方法名是saveOrUpdateData,统计耗时时间大于100ms的,统计五次。
[arthas@63296]$ trace -E --skipJDKMethod false com.netease.gcom.search.server.service.impl.ArtProductServiceImpl saveOrUpdateData '#cost > 100' Press Q or Ctrl+C to abort. Affect(class count: 1 , method count: 1) cost in 381 ms, listenerId: 1 `---ts=2023-03-04 11:47:51;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@18b4aac2 `---[296.402959ms] com.netease.gcom.search.server.service.impl.ArtProductServiceImpl:saveOrUpdateData() +---[0.04% 0.107417ms ] com.netease.gcom.search.server.mapstruct.ArtProductMapper:toDocs() #743 +---[0.01% 0.020334ms ] org.apache.commons.collections4.CollectionUtils:isEmpty() #744 +---[0.04% 0.120792ms ] com.netease.gcom.search.server.manager.RecHotItemManager:lambdaQuery() #749 +---[0.03% min=0.0175ms,max=0.073042ms,total=0.090542ms,count=2] java.util.List:stream() #749 +---[0.01% 0.028541ms ] java.util.stream.Stream:map() #749 +---[0.01% 0.038959ms ] java.util.stream.Collectors:toList() #749 +---[0.04% min=0.014542ms,max=0.092125ms,total=0.106667ms,count=2] java.util.stream.Stream:collect() #749 +---[0.07% 0.196292ms ] com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper:in() #749 +---[26.58% 78.792292ms ] com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper:list() #749 +---[0.02% 0.054083ms ] java.util.stream.Collectors:toMap() #749 +---[0.00% 0.014334ms ] java.util.List:iterator() #750 +---[0.04% min=0.005125ms,max=0.015292ms,total=0.10525ms,count=17] java.util.Iterator:hasNext() #750 +---[0.04% min=0.00525ms,max=0.014917ms,total=0.104956ms,count=16] java.util.Iterator:next() #750 +---[0.04% min=0.005375ms,max=0.026875ms,total=0.133375ms,count=16] org.apache.commons.collections4.MapUtils:isNotEmpty() #751 +---[0.01% 0.02475ms ] java.lang.System:currentTimeMillis() #755 +---[0.06% 0.185166ms ] java.io.PrintStream:println() #769 +---[68.23% 202.243667ms ] com.netease.gcom.search.server.repository.ArtProductRepository:saveAll() #770 +---[0.01% 0.019708ms ] java.lang.System:currentTimeMillis() #773 +---[0.02% 0.049166ms ] java.lang.StringBuilder:<init>() #776 +---[0.40% min=0.013458ms,max=0.816792ms,total=1.189083ms,count=3] java.lang.StringBuilder:append() #776 +---[0.15% 0.432ms ] java.lang.StringBuilder:toString() #776 `---[0.03% 0.091125ms ] java.io.PrintStream:println() #776
|
从上面的数据我们可以知道ArtProductRepository:saveAll()
耗时都在这个方法,我们继续trace。
其实到这里,我们可以知道耗时为什么那么长时间了。不知道老板你有没有想到呢。
我第一次trace到这里的时候,是没有看出来的。继续trace进去到了最底层future.get方法才回头到这里,看到exectueAndRefresh方法
直接说结论:当我们调用SimpleElasticsearchRepository#saveAll
方法的时候,是同步刷盘的,是一个非常耗时的动作。
How to fix bug
通过上文,我们已经定位到耗时长的问题所在了。那我们要怎么fix这个问题呢?
一般我会到Github去逛下,看下有没有类似相关的issue:https://github.com/spring-projects/spring-data-elasticsearch/issues/1266
老板你看,这不就找到了吗?可以看下大佬是怎么说的。
大佬,直接给出了fix 问题的解决方案,就是自己自定义一个CustomRepository
接口。调用execute
方法,不调用executeAndRefresh
方法即可。
说到这里其实还有其他的解决方案,我这里就点一下:org.elasticsearch.client.RestHighLevelClient#bulk
这个api也可以看下。
Extension
如果你不熟悉Arthas的指令的话。可以输入强带的-h指令,会给我们展示常用的命令。比如
好了,到此完结撒花~~