> 文章列表 > ElasticSearch的Deep Paging 性能问题和解决方案

ElasticSearch的Deep Paging 性能问题和解决方案

ElasticSearch的Deep Paging 性能问题和解决方案

一、深度分页问题

ES默认采用的分页方式是from + size的形式,类似于MySQL的分页limit。当请求数据量比较大时,ElasticSearch会对分页做出限制,因为此时消耗会很大。举个例子,一个索引分10个shards,然后一个搜索请求,from = 990,size = 10,这个时候,会带来严重的性能问题:CPU、内存、IO、网络带宽。
ElasticSearch的Deep Paging 性能问题和解决方案
CPU、内存和IO消耗很容易理解,网络带宽问题稍微难理解一点。在query阶段,每个shard需要返回1000条数据给coordinating node,而coordinating node需要接收10 * 1000条数据,即使每条数据只有_doc_id和score,这个数据量也很大了,而且,这才一个查询请求,那如果再乘以100呢?
ES有个设置index.max_result_window,默认是10000条数据,如果分页的数据超过了1万条,就拒绝返回结果了。有时这种深度分页的请求并不合理,因为很少有人看后面的请求,在很多业务场景中,都直接限制分页,比如只能看前100页。不过这种深度分页确实存在,比如有1千万分时的微信大V,要给所有粉丝群发消息,这时候就需要取得所有符合条件的粉丝,而最容易想到的就是利用from + size来实现,但是这就会引发深度分页的问题,这就用到了接下来提到的解决方案。

二、深度分页解决方案

2.1 利用scroll遍历方式

scroll分为初始化和遍历两步,初始化时将所有符合搜索条件的搜索结果缓存起来,可以想象成快照,在遍历时,从这个快照取数据,也就是说,在初始化后对索引插入、删除、更新数据都不会影响遍历结果。因此scroll并不适合用来实时搜索,而更适合用于后台批量处理任务,比如群发。
1、初始化

POST /book/_search?scroll=1m&size=2
{"query": { "match_all": {}}
}

初始化时需要像普通search一样,指明index和type(当然,search是可以不指明index和type的),然后,加上scroll参数,表示暂存搜索结果的时间,其他就像一个普通的search请求一样。初始化返回一个scroll_id,scroll_id用来下次取数据用。
2、遍历

GET /_search/scroll
{"scroll": "1m","scroll_id" : "步骤1中查询出来的值"
}

这里scroll_id即上一次遍历取回的_scroll_id或者初始化返回的_scroll_id,同样的,需要带scroll参数。重复这一步骤,知道返回的数据为空,即遍历完成。注意,每次都要传参数scroll,刷新搜索结果的缓存时间。另外,不需要指定index和type。设置scroll的时候,需要使搜索结果缓存到下一次遍历完成,同时,也不能太长。毕竟空间有限。

2.2 search after方式

满足实时获取下一页的文档信息,search _after分页的方式是根据上一页的最后一条数据来确定下一页的文职,同时在分页请求的过程中,如果索引数据的增删改,这些变更也会实时地反映到游标上,这种方式是在es-5.x后才提供的。为了找到每一页最后一条数据,每个文档的排序字段必须有一个全局唯一值,一般使用_id就可以了。

GET /book/_search
{"query":{"match_all": {}},"size":2,"sort":[{"_id": "desc"}]
}GET /book/_search
{"query":{"match_all": {}},"size":2,"search_after":[3],"sort":[{"_id": "desc"}]
}

下一页依赖上一页的最后一条的信息,所以不能跳页。

2.3 三种分页方式比较

分页方式 性能 优点 缺点 场景
from + size 灵活性好,实现简单 深度分页问题 数据量比较小,能容忍深度分页问题
scroll 解决了深度分页问题 无法反映数据的实时性(快照版本) 维护成本高,需要维护一个 scroll_id 海量数据的导出 需要查询海量结果集的数据
search_after 性能最好 不存在深度分页问题 能够反映数据的实时变更 实现连续分页的实现会比较复杂,因为每一次查询都需要上次查询的结果 海量数据的分页

安防行业门户