您的位置:首页 > 其它

ES 5.x bulk update重复的文档ID性能低下分析

2017-09-16 13:53 204 查看
目前很多公司将ES作为数据库数据的索引,将多个数据库的数据同步到ES是非常常见的应用场景。所以感觉问题可能会困扰不止一个用户,而官方的文档没有对update底层机制作了详细的说明,特将该问题整理成文章,供使用ES的用户参考。

问题描述

在ES5.x里通过bulk update将数据从数据库同步到ES,如果短时间内的一批数据里存在相同的文档ID,例如一个bulk update大量更新相同的文档ID,如下:

{id:1,name:aaa}
{id:1,name:bbb}
{id:1,name:ccc}
{id:2,name:aaa}
{id:2,name:bbb}
{id:2,name:ccc}
.......


则更新的速度非常慢。而且ES1.X和ES2.X里同样的操作则快很多。

根源追溯

update的操作则分为两个操作,即先根据文档ID进行get请求,得到旧版的文档信息,然后在此基础上做更新再写回去,问题就出在GET上面。

在 core/src/main/java/org/elasticsearch/index/engine/InternalEngine.java 这个类里面,get函数会根据一个realtime参数(默认是true),决定如何拿到文档。

public GetResult get(Get get, Function<String, Searcher> searcherFactory, LongConsumer onRefresh) throws EngineException {
assert Objects.equals(get.uid().field(), uidField) : get.uid().field();
try (ReleasableLock lock = readLock.acquire()) {
ensureOpen();
if (get.realtime()) {
VersionValue versionValue = versionMap.getUnderLock(get.uid());
if (versionValue != null) {
if (versionValue.isDelete()) {
return GetResult.NOT_EXISTS;
}
if (get.versionType().isVersionConflictForReads(versionValue.getVersion(), get.version())) {
throw new VersionConflictEngineException(shardId, get.type(), get.id(),
get.versionType().explainConflictForReads(versionValue.getVersion(), get.version()));
}
long time = System.nanoTime();
refresh("realtime_get");
onRefresh.accept(System.nanoTime() - time);
}
}

// no version, get the version from the index, we know that we refresh on flush
return getFromSearcher(get, searcherFactory);
}


可以看到realtime参数决定了GET到的数据的实时性。如果设置为false,则直接从searcher里面拿,而search只能访问refresh的数据,意味着刚写入的数据由于存在于index writer buffer里还未refresh,暂时无法搜索到,所以这种方式拿到的数据是准实时的。而默认realtime:true,则决定了获取到的数据必须是实时的,也就是说writer buffer里的函数也要被检索到。从代码中可以看到,其中存在一个refresh(“realtime_get”)的函数调用。这个函数会去检查,get的docid是否都是可以搜到。如果已经写入了但无法搜到,也就是刚刚写入到writer buffer里还为refresh这种情况,就会强制进行一次refresh操作,让数据对search可见,保证末尾的getFromSearch调用拿的是完全实时的数据。

实际测试下来,也是这样,关闭自动更新,写入一条文档,然后对文档ID执行一个GET操作,就会看到有一个新的segment生成。

查了下文档,GET api的调用时,可以选择实时和非实时,只需要在url里带参数realtime=[true/|false]。参考:https://www.elastic.co/guide/en/elasticsearch/reference/5.6/docs-get.html#realtime

然而,不幸的是,update API的文档和源码都没有提供一个“禁用”实时性的参数。 update对GET的调用,传入的realtime是写死为true的,意味着update的时候,强制执行realtime GET.

至于为什么update一定需要实时GET,想了一下,是因为update允许对文档做部分字段更新。如果有2个请求分别更新了不同的字段, 可能先更新的数据还在writter buffer里,没来得及refresh,因而对searcher不可见。那后面的更新还是在老版本文档上做的,造成部分更新丢失。

另外一个问题,为啥5.x之前的版本没有这个性能问题? 看了下2.4的GET方法源码,其没有采用refresh的方式来保障数据的实时性,而是通过访问translog来达到同样的目的。官方在这个变更里 https://github.com/elastic/elasticsearch/pull/20102 将机制从translog改为了refresh。理由是之前ES里有很多地方利用translog来维护数据的位置,使得很多操作变得很慢,去掉对translog的依赖可以全面提高性能。

但是很不幸,这个更改,对于短时间反复大量更新相同doc id的操作,会因为过于频繁的强制refresh,短时间生成很多小segment,继而不断触发segment合并,产生性能损耗。 从上面链接里的讨论看,官方认为,在提升大多数应用场景性能的前提下,对于这种较少见的场景下的性能损失是值得付出的。所以,建议从应用层面去解决。

因此,如果实际应用场景里遇到类似的数据更新问题, 只能是优化应用数据架构,在应用层面合并相同doc id的数据更新后再写入ES,或者只能使用ES 2.x这样的老版本了。

文章引用自:https://elasticsearch.cn/question/2352

https://elasticsearch.cn/article/273
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息