发布于 

Fix Elasticsearch performance

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秒。

  1. artProductSearchClient.saveOrUpdateData的核心逻辑是调用:SimpleElasticsearchRepository#saveAll。但是这就是性能问题所在
  2. 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调用超时的解决思路。

image-20230304103309865

dubbo-admin:目前具备了服务查询,服务治理的能力。

image-20230304104217726

最常用的功能,就是配置类全限定名,配置消费端的超时时间。并且在dubbo-admin配置的优先级是最高的~

此时,我们通过dubbo打印的超时时间,我们可以大概能够得到一个超时时间,配置上去。再继续查问题。既不需要部署代码,又能快速fix问题。

到此,我们通过dubbo-admin,先快速的fix了问题了。但是没有解决问题。

这时候,面试官问你:你一般是怎么定位线上问题的呢?

线上定位问题的办法很多,这次我们讲,我们最常用的Arthas,官网:https://arthas.aliyun.com/

本次主要介绍Trace指令:

image-20230304160503768

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。

image-20230304182713427

其实到这里,我们可以知道耗时为什么那么长时间了。不知道老板你有没有想到呢。

我第一次trace到这里的时候,是没有看出来的。继续trace进去到了最底层future.get方法才回头到这里,看到exectueAndRefresh方法

直接说结论:当我们调用SimpleElasticsearchRepository#saveAll方法的时候,是同步刷盘的,是一个非常耗时的动作。

How to fix bug

通过上文,我们已经定位到耗时长的问题所在了。那我们要怎么fix这个问题呢?

一般我会到Github去逛下,看下有没有类似相关的issue:https://github.com/spring-projects/spring-data-elasticsearch/issues/1266

老板你看,这不就找到了吗?可以看下大佬是怎么说的。

image-20230304184352818

大佬,直接给出了fix 问题的解决方案,就是自己自定义一个CustomRepository接口。调用execute方法,不调用executeAndRefresh方法即可。

说到这里其实还有其他的解决方案,我这里就点一下:org.elasticsearch.client.RestHighLevelClient#bulk这个api也可以看下。

Extension

如果你不熟悉Arthas的指令的话。可以输入强带的-h指令,会给我们展示常用的命令。比如

image-20230304184802343

好了,到此完结撒花~~