ES 5.4 bulk update 5000数据,这批数据里面很多更新是重复的文档id,导致性能下降(不停创建segment ),请问有什么办法解决这个问题?
比如我的数据格式是
{id:1,name:aaa}
{id:1,name:bbb}
{id:1,name:ccc}
{id:2,name:aaa}
{id:2,name:bbb}
{id:2,name:ccc}
比如我的数据格式是
{id:1,name:aaa}
{id:1,name:bbb}
{id:1,name:ccc}
{id:2,name:aaa}
{id:2,name:bbb}
{id:2,name:ccc}
10 个回复
kennywu76 - Wood
赞同来自: famoss 、Cheetah 、240475588 、ningbohezhijun 、Xiaoming 、cap_ljf 、zsw1234更多 »
update操作是分为两个步骤进行,即先根据文档ID做一次GET,得到旧版本的文档,然后在其基础上做更新再写回去,问题就出在这个GET上面。
在 core/src/main/java/org/elasticsearch/index/engine/InternalEngine.java 这个类里面,get函数会根据一个realtime参数(默认是true),决定如何拿到文档。
可以看到realtime参数决定了GET到的数据的实施性。 如果设置为false,就从searcher里面拿,而searcher只能访问refresh过的数据,即刚写入的数据存在于index writter buffer里,暂时无法搜索到,所以这种方式拿到的数据是准实时的。 而默认的realtime:true,则决定了获取到的数据需要是实时的,也就是说需要也能够访问到writter buffer里的数据。 所以,中间有一个 refresh("realtime_get"); 的函数调用,这个函数调用会检查,GET的doc id是否都是可以被搜索到。 如果已经写入了但无法搜索,也就是写入到writter buffer里这种情况,就会强制执行一次refresh操作,让数据可以被searcher检索到,保证之后从getFromSearcher调用拿的是完全实时的数据。
实际上测试下来,也是这样,关闭自动刷新,写入一条文档,然后对该文档ID执行一个GET操作,就会看到有一个新的segment生成。 默认的realtime GET 强制刷新后,生成了新的segment file。
查了下文档,GET api调用时,url里可以带可选参数realtime=false,关闭实时功能: https://www.elastic.co/guide/e ... ltime
然而,不幸的是,update API的文档和源码都没有提供一个“禁用”实时性的参数。 update对GET的调用,传入的realtime是写死为true的。
至于为什么update一定需要实时GET,想了一下,是因为update允许对文档做部分字段更新。如果有2个请求分别更新了不同的字段, 可能先更新的数据只在writter buffer里,searcher里看不到,那后面的更新还是在老版本文档上做的,造成部分更新丢失。
另外一个问题,为啥之前的版本不是这种行为? 看了下2.4的GET方法源码,其没有采用refresh的方式让数据实时,而是直接访问的translog来保证GET的实时性。官方在这个变更里 https://github.com/elastic/ela ... 20102 将其更新方式改为了refresh。理由是之前ES里有很多地方用translog维护数据的位置,使得很多操作变得很慢,去掉对translog的依赖可以提高性能。
但是很不幸,这个更改,对于短时间反复大量更新相同doc id的操作,会因为过于频繁的refresh短时间生成很多小segment,继而不断做短合产生性能损耗。 从上面链接里的讨论看,官方认为,在提升大多数应用场景性能情况下,对于这种较少见的场景下的性能损失是值得的,应该在应用层面解决。
因此,对于你的特定问题,要么优化应用数据架构,在应用层面合并相同doc id的更新,要么只能使用ES 2.x了。
kennywu76 - Wood
赞同来自: 240475588 、geekLhl
测试步骤:
1. 设置一个空的索引,设置一个shard,关闭refresh, 即设置refresh_interval=-1
2. 用python脚本写入了1万条测试数据, refresh过后,看到生成了一个包含1万条数据的segment: 3. 用如下python脚本,循环10次,每次更新1000条同样的数据: 写入速度非常快: 观察到这时候,即使refresh是关闭的,上面的bulk update操作仍然生成了新的segment
其他观察到的重要信息:
1) 之前包含1万条文档的segment, 现在docs.count变成了9000, 其中1000被标记为delete。而新生成的segment docs.count是1000,也就是更新操作都写到了这个segment里面。这个符合预期。
2) 新生成的segment, generation是9, 意味着程序里10次bulk操作,每次都生成了一个新的segment,但因为每次生成的中间segment文件内容完全相同,只有最新的generation保留下来,中间状态的删除掉了。
现在模拟产生问题的操作,也就是拼接bulk操作的时候,同一个action追加2次
复现了更新操作非常缓慢:
同时观察到,不断有新的只包含一个文档的segment生成,generation顺序加1.
反复查看segment生成情况,会看到这些包含1个文档的segment, docs.deleted也是1,都只是中间状态,随着不断写入,会被合并掉。 另外,这些segment的commit状态都还是false,也就是并未fsync到磁盘,只是存在于文件缓冲区里。 上面测试过程中,也能观察到cpu利用率明显增高。
问题看起来和测试方法有关,因为在一个bulk request里加入了1000对同样doc_id的文档,并且都是相邻的。而看起来ES处理update的方式是将已经包含该doc id的segment seal起来,并将其中旧版本的doc标记为deleted。 这样包含大量两两doc id相同的bulk request就造成不断的生成包含2个doc的新segment,然后并且标记其中一个doc为deleted,反复循环。猜测这是ES解决并发更新冲突的一种机制,具体是否是这样还需要再研究底层才能知晓。
实际的应用场景里,大量短时间并发对同一个doc_id更新的情况应该不是很常见,应该不会有什么影响。其实想想如果是数据库应用,大量并发更新同一条记录,也会导致大量的行级锁产生,性能也会比较差。
huzhoahui168
赞同来自: zsw1234
kennywu76 - Wood
赞同来自: huzhoahui168
huzhoahui168
赞同来自:
Cheetah
赞同来自:
huzhoahui168
赞同来自:
huzhoahui168
赞同来自:
huzhoahui168
赞同来自:
白衬衣 - 金桥
赞同来自: