ElasticSearch 深度分頁解決方案
- 常見深度分頁方式 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 進行分頁讀取過程如下:
- 先獲取第一個 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":{...}
}
- 後續的文檔讀取上一次查詢返回的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": {
....
}
}
]
}
}
- 所有文檔獲取完畢之後,需要手動清理掉 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 也可以。
- 第一頁的請求和正常的請求一樣,
curl -XGET 127.0.0.1:9200/order/info/_search
{
"size": 10,
"query": {
"term" : {
"did" : 519390
}
},
"sort": [
{"date": "asc"},
{"_uid": "desc"}
]
}
- 第二頁的請求,使用第一頁返回結果的最後一個數據的值,加上 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 深度分頁解決方案