关于java:京东面试题ElasticSearch深度分页解决方案

35次阅读

共计 7779 个字符,预计需要花费 20 分钟才能阅读完成。

大家好,我是不才陈某~

Elasticsearch 是一个实时的分布式搜寻与剖析引擎,在应用过程中,有一些典型的应用场景,比方分页、遍历等。

在应用关系型数据库中,咱们被告知要留神甚至被明确禁止应用深度分页,同理,在 Elasticsearch 中,也应该尽量避免应用深度分页。

这篇文章次要介绍 Elasticsearch 中分页相干内容!

关注公众号:码猿技术专栏,回复关键词:1111 获取阿里外部 Java 性能调优手册!

From/Size 参数

在 ES 中,分页查问默认返回最顶端的 10 条匹配 hits。

如果须要分页,须要应用 from 和 size 参数。

  • from 参数定义了须要跳过的 hits 数,默认为 0;
  • size 参数定义了须要返回的 hits 数目的最大值。

一个根本的 ES 查问语句是这样的:

POST /my_index/my_type/_search
{"query": { "match_all": {}},
    "from": 100,
    "size":  10
}

下面的查问示意从搜寻后果中取第 100 条开始的 10 条数据。

那么,这个查问语句在 ES 集群外部是怎么执行的呢?

在 ES 中,搜寻个别包含两个阶段,query 和 fetch 阶段,能够简略的了解,query 阶段确定要取哪些 doc,fetch 阶段取出具体的 doc。

Query 阶段

如上图所示,形容了一次搜寻申请的 query 阶段:·

  1. Client 发送一次搜寻申请,node1 接管到申请,而后,node1 创立一个大小为 from + size 的优先级队列用来存后果,咱们管 node1 叫 coordinating node。
  2. coordinating node 将申请播送到波及到的 shards,每个 shard 在外部执行搜寻申请,而后,将后果存到外部的大小同样为 from + size 的优先级队列里,能够把优先级队列了解为一个蕴含top N 后果的列表。
  3. 每个 shard 把暂存在本身优先级队列里的数据返回给 coordinating node,coordinating node 拿到各个 shards 返回的后果后对后果进行一次合并,产生一个全局的优先级队列,存到本身的优先级队列里。

在下面的例子中,coordinating node 拿到 (from + size) * 6 条数据,而后合并并排序后抉择后面的 from + size 条数据存到优先级队列,以便 fetch 阶段应用。

另外,各个分片返回给 coordinating node 的数据用于选出前 from + size 条数据,所以,只须要返回惟一标记 doc 的 _id 以及用于排序的 _score 即可,这样也能够保障返回的数据量足够小。

coordinating node 计算好本人的优先级队列后,query 阶段完结,进入 fetch 阶段。

Fetch 阶段

query 阶段晓得了要取哪些数据,然而并没有取具体的数据,这就是 fetch 阶段要做的。

上图展现了 fetch 过程:

  1. coordinating node 发送 GET 申请到相干 shards。
  2. shard 依据 doc 的 _id 取到数据详情,而后返回给 coordinating node。
  3. coordinating node 返回数据给 Client。

coordinating node 的优先级队列里有from + size_doc _id,然而,在 fetch 阶段,并不需要取回所有数据,在下面的例子中,前 100 条数据是不须要取的,只须要取优先级队列里的第 101 到 110 条数据即可。

须要取的数据可能在不同分片,也可能在同一分片,coordinating node 应用 multi-get 来防止屡次去同一分片取数据,从而进步性能。

这种形式申请深度分页是有问题的:

咱们能够假如在一个有 5 个主分片的索引中搜寻。当咱们申请后果的第一页(后果从 1 到 10),每一个分片产生前 10 的后果,并且返回给 协调节点,协调节点对 50 个后果排序失去全副后果的前 10 个。

当初假如咱们申请第 1000 页—后果从 10001 到 10010。所有都以雷同的形式工作除了每个分片不得不产生前 10010 个后果以外。而后协调节点对全副 50050 个后果排序最初抛弃掉这些后果中的 50040 个后果。

对后果排序的老本随分页的深度成指数回升。

留神 1:

size 的大小不能超过 index.max_result_window 这个参数的设置,默认为 10000。

如果搜寻 size 大于 10000,须要设置 index.max_result_window 参数

PUT _settings
{
    "index": {"max_result_window": "10000000"}
}  

留神 2:

_doc将在将来的版本移除,详见:

  • https://www.elastic.co/cn/blog/moving-from-types-to-typeless-…
  • https://elasticsearch.cn/article/158

深度分页问题

Elasticsearch 的 From/Size 形式提供了分页的性能,同时,也有相应的限度。

举个例子,一个索引,有 10 亿数据,分 10 个 shards,而后,一个搜寻申请,from=1000000,size=100,这时候,会带来重大的性能问题:CPU,内存,IO,网络带宽。

在 query 阶段,每个 shards 须要返回 1000100 条数据给 coordinating node,而 coordinating node 须要接管10 * 1000,100 条数据,即便每条数据只有 _doc _id_score,这数据量也很大了?

在另一方面,咱们意识到,这种深度分页的申请并不合理,因为咱们是很少人为的看很前面的申请的,在很多的业务场景中,都间接限度分页,比方只能看前 100 页。

比方,有 1 千万粉丝的微信大 V,要给所有粉丝群发音讯,或者给某省粉丝群发,这时候就须要获得所有符合条件的粉丝,而最容易想到的就是利用 from + size 来实现,不过,这个是不事实的,这时,能够采纳 Elasticsearch 提供的其余形式来实现遍历。

深度分页问题大抵能够分为两类:

  • 随机深度分页:随机跳转页面
  • 滚动深度分页:只能一页一页往下查问

上面介绍几个官网提供的深度分页办法

Scroll

Scroll 遍历数据

咱们能够把 scroll 了解为关系型数据库里的 cursor,因而,scroll 并不适宜用来做实时搜寻,而更适宜用于后盾批处理工作,比方群发。

这个分页的用法,不是为了实时查问数据 ,而是为了 一次性查问大量的数据(甚至是全副的数据)。

因为这个 scroll 相当于保护了一份以后索引段的快照信息,这个快照信息是你执行这个 scroll 查问时的快照。在这个查问后的任何新索引进来的数据,都不会在这个快照中查问到。

然而它绝对于 from 和 size,不是查问所有数据而后剔除不要的局部,而是记录一个读取的地位,保障下一次疾速持续读取。

不思考排序的时候,能够联合 SearchType.SCAN 应用。

scroll 能够分为初始化和遍历两部,初始化时将 所有合乎搜寻条件的搜寻后果缓存起来(留神,这里只是缓存的 doc_id,而并不是真的缓存了所有的文档数据,取数据是在 fetch 阶段实现的),能够设想成快照。

在遍历时,从这个快照里取数据,也就是说,在初始化后,对索引插入、删除、更新数据都不会影响遍历后果。

根本应用

POST /twitter/tweet/_search?scroll=1m
{
    "size": 100,
    "query": {
        "match" : {"title" : "elasticsearch"}
    }
}

初始化指明 index 和 type,而后,加上参数 scroll,示意暂存搜寻后果的工夫,其它就像一个一般的 search 申请一样。

会返回一个 _scroll_id_scroll_id 用来下次取数据用。

遍历

POST /_search?scroll=1m
{"scroll_id":"XXXXXXXXXXXXXXXXXXXXXXX I am scroll id XXXXXXXXXXXXXXX"}

这里的 scroll_id 即 上一次遍历取回的 _scroll_id 或者是初始化返回的_scroll_id,同样的,须要带 scroll 参数。

反复这一步骤,直到返回的数据为空,即遍历实现。

留神,每次都要传参数 scroll,刷新搜寻后果的缓存工夫 。另外, 不须要指定 index 和 type

设置 scroll 的时候,须要使搜寻后果缓存到下一次遍历实现,同时,也不能太长,毕竟空间无限。

优缺点

毛病:

  1. scroll_id 会占用大量的资源(特地是排序的申请)
  2. 同样的,scroll 后接超时工夫,频繁的发动 scroll 申请,会呈现一些列问题。
  3. 是生成的历史快照,对于数据的变更不会反映到快照上。

长处:

实用于非实时处理大量数据的状况,比方要进行数据迁徙或者索引变更之类的。

Scroll Scan

ES 提供了 scroll scan 形式进一步提高遍历性能,然而 scroll scan 不反对排序,因而 scroll scan 适宜不须要排序的场景

根本应用

Scroll Scan 的遍历与一般 Scroll 一样,初始化存在一点差异。

POST /my_index/my_type/_search?search_type=scan&scroll=1m&size=50
{"query": { "match_all": {}}
}

须要指明参数:

  • search_type:赋值为 scan,示意采纳 Scroll Scan 的形式遍历,同时通知 Elasticsearch 搜寻后果不须要排序。
  • scroll:同上,传工夫。
  • size:与一般的 size 不同,这个 size 示意的是每个 shard 返回的 size 数,最终后果最大为 number_of_shards * size

Scroll Scan 与 Scroll 的区别

  1. Scroll-Scan 后果 没有排序,按 index 程序返回,没有排序,能够进步取数据性能。
  2. 初始化时只返回 _scroll_id,没有具体的 hits 后果
  3. size 管制的是每个分片的返回的数据量,而不是整个申请返回的数据量。

Sliced Scroll

如果你数据量很大,用 Scroll 遍历数据那的确是承受不了,当初 Scroll 接口能够并发来进行数据遍历了。

每个 Scroll 申请,能够分成多个 Slice 申请,能够了解为切片,各 Slice 独立并行,比用 Scroll 遍历要快很多倍。

POST /index/type/_search?scroll=1m
{"query": { "match_all": {}},
    "slice": {
        "id": 0,
        "max": 5
    }   
}
 
POST ip:port/index/type/_search?scroll=1m
{"query": { "match_all": {}},
    "slice": {
        "id": 1,
        "max": 5
    }   
}

上边的示例能够独自申请两块数据,最终五块数据合并的后果与间接 scroll scan 雷同。

其中 max 是分块数,id 是第几块。

官网文档中倡议 max 的值不要超过 shard 的数量,否则可能会导致内存爆炸。

Search After

Search_after是 ES 5 新引入的一种分页查问机制,其原理简直就是和 scroll 一样,因而代码也简直是一样的。

根本应用:

第一步:

POST twitter/_search
{
    "size": 10,
    "query": {
        "match" : {"title" : "es"}
    },
    "sort": [{"date": "asc"},
        {"_id": "desc"}
    ]
}

返回出的后果信息:

{
      "took" : 29,
      "timed_out" : false,
      "_shards" : {
        "total" : 1,
        "successful" : 1,
        "skipped" : 0,
        "failed" : 0
      },
      "hits" : {
        "total" : {
          "value" : 5,
          "relation" : "eq"
        },
        "max_score" : null,
        "hits" : [
          {...},
            "sort" : [...]
          },
          {...},
            "sort" : [
              124648691,
              "624812"
            ]
          }
        ]
      }
    }

下面的申请会为每一个文档返回一个蕴含 sort 排序值的数组。

这些 sort 排序值能够被用于 search_after 参数里以便抓取下一页的数据。

比方,咱们能够应用最初的一个文档的 sort 排序值,将它传递给 search_after 参数:

GET twitter/_search
{
    "size": 10,
    "query": {
        "match" : {"title" : "es"}
    },
    "search_after": [124648691, "624812"],
    "sort": [{"date": "asc"},
        {"_id": "desc"}
    ]
}

若咱们想接着上次读取的后果进行读取下一页数据,第二次查问在第一次查问时的语句根底上增加search_after,并指明从哪个数据后开始读取。

基本原理

es 保护一个实时游标,它以上一次查问的最初一条记录为游标,不便对下一页的查问,它是一个无状态的查问,因而每次查问的都是最新的数据。

因为它采纳记录作为游标,因而SearchAfter 要求 doc 中至多有一条全局惟一变量(每个文档具备一个惟一值的字段应该用作排序标准)

优缺点

长处:

  1. 无状态查问,能够避免在查问过程中,数据的变更无奈及时反映到查问中。
  2. 不须要保护scroll_id,不须要保护快照,因而能够防止耗费大量的资源。

毛病:

  1. 因为无状态查问,因而在查问期间的变更可能会导致跨页面的不一值。
  2. 排序程序可能会在执行期间发生变化,具体取决于索引的更新和删除。
  3. 至多须要制订一个惟一的不反复字段来排序。
  4. 它不适用于大幅度跳页查问,或者全量导出,对第 N 页的跳转查问相当于对 es 一直反复的执行 N 次 search after,而全量导出则是在短时间内执行大量的反复查问。

SEARCH_AFTER不是自在跳转到任意页面的解决方案,而是并行滚动多个查问的解决方案。

总结

分页形式 性能 长处 毛病 场景
from + size 灵活性好,实现简略 深度分页问题 数据量比拟小,能容忍深度分页问题
scroll 解决了深度分页问题 无奈反馈数据的实时性(快照版本)保护老本高,须要保护一个 scroll_id 海量数据的导出须要查问海量后果集的数据
search_after 性能最好不存在深度分页问题可能反映数据的实时变更 实现简单,须要有一个全局惟一的字段间断分页的实现会比较复杂,因为每一次查问都须要上次查问的后果,它不适用于大幅度跳页查问 海量数据的分页

ES7 版本变更

参照:https://www.elastic.co/guide/en/elasticsearch/reference/maste…

7.* 版本中,ES 官网不再举荐应用 Scroll 办法来进行深分页,而是举荐应用带 PIT 的 search_after 来进行查问;

7.* 版本开始,您能够应用 SEARCH_AFTER 参数通过上一页中的一组排序值检索下一页命中。

应用 SEARCH_AFTER 须要多个具备雷同查问和排序值的搜寻申请。

如果这些申请之间产生刷新,则后果的程序可能会更改,从而导致页面之间的后果不统一。

为防止出现这种状况,您能够创立一个工夫点 (PIT) 来在搜寻过程中保留以后索引状态。

POST /my-index-000001/_pit?keep_alive=1m

返回一个 PIT ID:{"id": "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA=="}

在搜寻申请中指定 PIT:

GET /_search
{
  "size": 10000,
  "query": {
    "match" : {"user.id" : "elkbee"}
  },
  "pit": {
     "id":  "46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==", 
     "keep_alive": "1m"
  },
  "sort": [{"@timestamp": {"order": "asc", "format": "strict_date_optional_time_nanos", "numeric_type" : "date_nanos"}}
  ]
}

性能比照

别离分页获取 1 - 1049000 - 4901099000 - 99010 范畴各 10 条数据(前提 10w 条),性能大抵是这样:

向前翻页

对于向前翻页,ES 中没有相应 API,然而依据官网说法(https://github.com/elastic/elasticsearch/issues/29449),ES 中的向前翻页问题能够通过翻转排序形式来实现即:

  1. 对于某一页,正序 search_after 该页的最初一条数据 id 为下一页,则逆序 search_after 该页的第一条数据 id 则为上一页。
  2. 国内论坛上,有人应用缓存来解决上一页的问题:https://elasticsearch.cn/question/7711

总结

  1. 如果数据量小(from+size 在 10000 条内),或者只关注后果集的 TopN 数据,能够应用 from/size 分页,简略粗犷
  2. 数据量大,深度翻页,后盾批处理工作(数据迁徙)之类的工作,应用 scroll 形式
  3. 数据量大,深度翻页,用户实时、高并发查问需要,应用 search after 形式

集体思考

Scroll 和 search_after 原理基本相同,他们都采纳了游标的形式来进行深分页。

这种形式尽管可能肯定水平上解决深分页问题。然而,它们并不是深分页问题的终极解决方案,深分页问题 必须防止!!

对于 Scroll,无可避免的要保护 scroll_id 和历史快照,并且,还必须保障 scroll_id 的存活工夫,这对服务器是一个微小的负荷。

对于Search_After,如果容许用户大幅度跳转页面,会导致短时间内频繁的搜寻动作,这样的效率十分低下,这也会减少服务器的负荷,同时,在查问过程中,索引的增删改会导致查问数据不统一或者排序变动,造成后果不精确。

Search_After自身就是一种业务折中计划,它不容许指定跳转到页面,而只提供下一页的性能。

Scroll 默认你会在后续将所有符合条件的数据都取出来,所以,它只是搜寻到了所有的符合条件的 doc_id(这也是为什么官网举荐用doc_id 进行排序,因为自身缓存的就是doc_id,如果用其余字段排序会减少查问量),并将它们排序后保留在协调节点(coordinate node),然而并没有将所有数据进行 fetch,而是每次 scroll,读取 size 个文档,并返回此次读取的最初一个文档以及上下文状态,用以告知下一次须要从哪个 shard 的哪个文档之后开始读取。

这也是为什么官网不举荐 scroll 用来给用户进行实时的分页查问,而是适宜于大批量的拉取数据,因为它从设计上就不是为了实时读取数据而设计的。

正文完
 0