zoukankan      html  css  js  c++  java
  • ElasticSearch 深度分页解决方案

    常见深度分页方式 from+size

    es 默认采用的分页方式是 from+ size 的形式,在深度分页的情况下,这种使用方式效率是非常低的,比如

    from = 5000, size=10, es 需要在各个分片上匹配排序并得到5000*10条有效数据,然后在结果集中取最后10条

    数据返回,这种方式类似于mongo的 skip + size。

    除了效率上的问题,还有一个无法解决的问题是,es 目前支持最大的 skip 值是 max_result_window ,默认

    为 10000 。也就是当 from + size > max_result_window 时,es 将返回错误

    [root@dnsserver ~]# curl -XGET 127.0.0.1:9200/custm/_settings?pretty 
    {
      "custm" : {
        "settings" : {
          "index" : {
            "max_result_window" : "50000",
             ....
           }
        }
      }
    }

    最开始的时候是线上客户的es数据出现问题,当分页到几百页的时候,es 无法返回数据,此时为了恢复正常使用,我们采用了紧急规避方案,就是将 max_result_window 的值调至 50000。

    [root@dnsserver ~]# curl -XPUT "127.0.0.1:9200/custm/_settings" -d 
    '{ 
        "index" : { 
            "max_result_window" : 50000 
        }
    }'

    然后这种方式只能暂时解决问题,当es 的使用越来越多,数据量越来越大,深度分页的场景越来越复杂时,如何解决这种问题呢?

    另一种分页方式 scroll

    为了满足深度分页的场景,es 提供了 scroll 的方式进行分页读取。原理上是对某次查询生成一个游标 scroll_id , 后续的查询只需要根据这个游标去取数据,直到结果集中返回的 hits 字段为空,就表示遍历结束。scroll_id 的生成可以理解为建立了一个临时的历史快照,在此之后的增删改查等操作不会影响到这个快照的结果。

    使用 curl 进行分页读取过程如下:

    1. 先获取第一个 scroll_id,url 参数包括 /index/_type/ 和 scroll,scroll 字段指定了scroll_id 的有效生存期,以分钟为单位,过期之后会被es 自动清理。如果文档不需要特定排序,可以指定按照文档创建的时间返回会使迭代更高效。
    [root@dnsserver ~]# curl -XGET 200.200.107.232:9200/product/info/_search?pretty&scroll=2m -d 
    '{"query":{"match_all":{}}, "sort": ["_doc"]}'
    
    # 返回结果
    {
      "_scroll_id": "cXVlcnlBbmRGZXRjaDsxOzg3OTA4NDpTQzRmWWkwQ1Q1bUlwMjc0WmdIX2ZnOzA7",
      "took": 1,
      "timed_out": false,
      "_shards": {
      "total": 1,
      "successful": 1,
      "failed": 0
      },
      "hits":{...}
    }
    1. 后续的文档读取上一次查询返回的scroll_id 来不断的取下一页,如果srcoll_id 的生存期很长,那么每次返回的 scroll_id 都是一样的,直到该 scroll_id 过期,才会返回一个新的 scroll_id。请求指定的 scroll_id 时就不需要 /index/_type 等信息了。每读取一页都会重新设置 scroll_id 的生存时间,所以这个时间只需要满足读取当前页就可以,不需要满足读取所有的数据的时间,1 分钟足以。
    [root@dnsserver ~]# curl -XGET '200.200.107.232:9200/_search/scroll?scroll=1m&scroll_id=cXVlcnlBbmRGZXRjaDsxOzg4NDg2OTpTQzRmWWkwQ1Q1bUlwMjc0WmdIX2ZnOzA7'
    
    #返回结果
    {
        "_scroll_id": "cXVlcnlBbmRGZXRjaDsxOzk1ODg3NDpTQzRmWWkwQ1Q1bUlwMjc0WmdIX2ZnOzA7",
        "took": 106,
        "_shards": {
            "total": 1,
            "successful": 1,
            "failed": 0
        },
        "hits": {
            "total": 22424,
            "max_score": 1.0,
            "hits": [{
                    "_index": "product",
                    "_type": "info",
                    "_id": "did-519392_pdid-2010",
                    "_score": 1.0,
                    "_routing": "519392",
                    "_source": {
                        ....
                    }
                }
            ]
        }
    }
    1. 所有文档获取完毕之后,需要手动清理掉 scroll_id 。虽然es 会有自动清理机制,但是 srcoll_id 的存在会耗费大量的资源来保存一份当前查询结果集映像,并且会占用文件描述符。所以用完之后要及时清理。使用 es 提供的 CLEAR_API 来删除指定的 scroll_id
    ## 删掉指定的多个 srcoll_id 
    [root@dnsserver ~]# curl -XDELETE 127.0.0.1:9200/_search/scroll -d 
    '{"scroll_id" : ["cXVlcnlBbmRGZXRjaDsxOzg3OTA4NDpTQzRmWWkwQ1Q1bUlwMjc0WmdIX2ZnOzA7"]}'
    
    ## 删除掉所有索引上的 scroll_id 
    [root@dnsserver ~]# curl -XDELETE 127.0.0.1:9200/_search/scroll/_all
    
    ## 查询当前所有的scroll 状态
    [root@dnsserver ~]# curl -XGET 127.0.0.1:9200/_nodes/stats/indices/search?pretty
    {
      "cluster_name" : "200.200.107.232",
      "nodes" : {
        "SC4fYi0CT5mIp274ZgH_fg" : {
          "timestamp" : 1514346295736,
          "name" : "200.200.107.232",
          "transport_address" : "200.200.107.232:9300",
          "host" : "200.200.107.232",
          "ip" : [ "200.200.107.232:9300", "NONE" ],
          "indices" : {
            "search" : {
              "open_contexts" : 0,
              "query_total" : 975758,
              "query_time_in_millis" : 329850,
              "query_current" : 0,
              "fetch_total" : 217069,
              "fetch_time_in_millis" : 84699,
              "fetch_current" : 0,
              "scroll_total" : 5348,
              "scroll_time_in_millis" : 92712468,
              "scroll_current" : 0
            }
          }
        }
      }
    }
    

    scroll + scan

    当 scroll 的文档不需要排序时,es 为了提高检索的效率,在 2.0 版本提供了 scroll + scan 的方式。随后又在 2.1.0 版本去掉了 scan 的使用,直接将该优化合入了 scroll 中。由于moa 线上的 es 版本是2.3 的,所以只简单提一下。使用的 scan 的方式是指定 search_type=scan

    # 2.0-beta 版本禁用 scroll 的排序,使遍历更加高效
    [root@dnsserver ~]# curl '127.0.0.1:9200/order/info/_search?scroll=1m&search_type=scan'  -d '{"query":{"match_all":{}}'

    search_after 的方式

    上述的 scroll search 的方式,官方的建议并不是用于实时的请求,因为每一个 scroll_id 不仅会占用大量的资源(特别是排序的请求),而且是生成的历史快照,对于数据的变更不会反映到快照上。这种方式往往用于非实时处理大量数据的情况,比如要进行数据迁移或者索引变更之类的。那么在实时情况下如果处理深度分页的问题呢?es 给出了 search_after 的方式,这是在 >= 5.0 版本才提供的功能。

    search_after 分页的方式和 scroll 有一些显著的区别,首先它是根据上一页的最后一条数据来确定下一页的位置,同时在分页请求的过程中,如果有索引数据的增删改查,这些变更也会实时的反映到游标上。

    为了找到每一页最后一条数据,每个文档必须有一个全局唯一值,这种分页方式其实和目前 moa 内存中使用rbtree 分页的原理一样,官方推荐使用 _uid 作为全局唯一值,其实使用业务层的 id 也可以。

    1. 第一页的请求和正常的请求一样,
    curl -XGET 127.0.0.1:9200/order/info/_search
    {
        "size": 10,
        "query": {
            "term" : {
                "did" : 519390
            }
        },
        "sort": [
            {"date": "asc"},
            {"_uid": "desc"}
        ]
    }
    1. 第二页的请求,使用第一页返回结果的最后一个数据的值,加上 search_after 字段来取下一页。注意,使用 search_after 的时候要将 from 置为 0 或 -1
    curl -XGET 127.0.0.1:9200/order/info/_search
    {
        "size": 10,
        "query": {
            "term" : {
                "did" : 519390
            }
        },
        "search_after": [1463538857, "tweet#654323"],
        "sort": [
            {"date": "asc"},
            {"_uid": "desc"}
        ]
    }

    总结:search_after 适用于深度分页+ 排序,因为每一页的数据依赖于上一页最后一条数据,所以无法跳页请求。

    且返回的始终是最新的数据,在分页过程中数据的位置可能会有变更。这种分页方式更加符合moa的业务场景。

    es 库 scroll search 的实现

    由于当前服务端的 es 版本还局限于 2.3 ,所以无法使用的更高效的 search_after 的方式,在某些场景中为了能取得所有的数据,只能使用 scroll 的方式代替。以下基于 scroll_search 实现的 c API:

    es_cursor * co_es_scroll_search(char* esindex, char* estype, 
                        cJSON* query, cJSON* sort, cJSON* fields, int size, char* routing);
    BOOL        es_scroll_cursor_next(es_cursor* cursor);
    void        es_cursor_destroy(es_cursor* cursor);

    具体业务的使用场景如下:

    // 1. 获取第一个 scroll_id 和部分数据
    es_cursor *cursor = co_es_scroll_search((char*)index_name,(char*)type_name,
                                            queryJ, sortJ, fieldJ, size , routing);
    // 2. 迭代处理每一项数据,当前页的数据处理完毕之后会自动根据 scroll_id 去请求下一页,无需业务层关心
    while (es_scroll_cursor_next(cursor))
    {
        cJSON* data = es_cursor_json(cursor); //获取一项数据
        ....  
    }
    // 3. 销毁游标,同时会清除无效的 scroll_id ,无需业务层关心
    es_cursor_destroy(cursor);

    附:es 版本变更记录如下

    2.0 -> 2.1 -> 2.2 -> 2.3 -> 2.4 -> 5.0 -> 5.1 -> 5.2 -> 5.3 -> 5.4 -> 5.5 -> 5.6 -> 6.0 -> 6.1 
  • 相关阅读:
    Nim or not Nim? hdu3032 SG值打表找规律
    Maximum 贪心
    The Super Powers
    LCM Cardinality 暴力
    Longge's problem poj2480 欧拉函数,gcd
    GCD hdu2588
    Perfect Pth Powers poj1730
    6656 Watching the Kangaroo
    yield 小用
    wpf DropDownButton 源码
  • 原文地址:https://www.cnblogs.com/jpfss/p/10815172.html
Copyright © 2011-2022 走看看