ElasticSearch 深度分頁解決方案 {"index":{"number_of_replicas":0}}


 

 

 

 

常見深度分頁方式 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", .... } } } }

1

 

 

最開始的時候是線上客戶的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":{}}'
  • 1
  • 2

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 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM