1. 程式人生 > >ElasticSearch 深度分頁解決方案

ElasticSearch 深度分頁解決方案

服務端 使用場景 last 上一頁 var field 大量 node let

      • 常見深度分頁方式 from+size
      • 另一種分頁方式 scroll
        • scroll + scan
      • search_after 的方式
      • es 庫 scroll search 的實現

常見深度分頁方式 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。

[[email protected] ~]# 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 自動清理。如果文檔不需要特定排序,可以指定按照文檔創建的時間返回會使叠代更高效。
[[email protected] ~]# 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 的排序,使遍歷更加高效
[[email protected] ~]# 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 

ElasticSearch 深度分頁解決方案