問題引入
我們在平常使用ElasticSearch構(gòu)建查詢條件的時候一般用的都是from+size的方式進行分頁查詢,但是如果我們的頁數(shù)太深/頁面大小太大(from*size)>10000就會引發(fā)一個錯誤,我們將會得到一個錯誤
這是為什么呢?
因為ES的分頁查詢其實是這樣來的
因為ElasticSeach的天生分布式的原因,我們的數(shù)據(jù)是分散在幾個分片中的,而我們設(shè)置了from+size需要對全部數(shù)據(jù)進行查詢,ES就以下面這種方式進行了查詢
Query階段
- Client 發(fā)送一次搜索請求,node1 接收到請求,然后,node1 創(chuàng)建一個大小為 from + size的優(yōu)先級隊列用來存結(jié)果,我們管 node1 叫 coordinating node。
- coordinating node將請求廣播到涉及到的 shards,每個 shard 在內(nèi)部執(zhí)行搜索請求,然后,將結(jié)果存到內(nèi)部的大小同樣為 from + size 的優(yōu)先級隊列里,可以把優(yōu)先級隊列理解為一個包含 top N結(jié)果的列表。
- 每個 shard 把暫存在自身優(yōu)先級隊列里的數(shù)據(jù)返回給 coordinating node,coordinating node 拿到各個 shards 返回的結(jié)果后對結(jié)果進行一次合并,產(chǎn)生一個全局的優(yōu)先級隊列,存到自身的優(yōu)先級隊列里。
在上面的例子中,coordinating node 拿到(from + size) * 6條數(shù)據(jù),然后合并并排序后選擇前面的from + size條數(shù)據(jù)存到優(yōu)先級隊列,以便 fetch 階段使用。
Fetch階段
query 階段知道了要取哪些數(shù)據(jù),但是并沒有取具體的數(shù)據(jù),這就是 fetch 階段要做的。
-
coordinating node 發(fā)送 GET 請求到相關(guān)shards。
-
shard 根據(jù) doc 的 _id取到數(shù)據(jù)詳情,然后返回給 coordinating node。
-
coordinating node 返回數(shù)據(jù)給 Client。
coordinating node 的優(yōu)先級隊列里有from + size 個_doc _id,但是,在 fetch 階段,并不需要取回所有數(shù)據(jù),在上面的例子中,前100條數(shù)據(jù)是不需要取的,只需要取優(yōu)先級隊列里的第101到110條數(shù)據(jù)即可。
需要取的數(shù)據(jù)可能在不同分片,也可能在同一分片,coordinating node 使用 「multi-get」來避免多次去同一分片取數(shù)據(jù),從而提高性能。
這種方式請求深度分頁是有問題的:
我們可以假設(shè)在一個有 5 個主分片的索引中搜索。當我們請求結(jié)果的第一頁(結(jié)果從 1 到 10 ),每一個分片產(chǎn)生前 10 的結(jié)果,并且返回給 協(xié)調(diào)節(jié)點,協(xié)調(diào)節(jié)點對 50 個結(jié)果排序得到全部結(jié)果的前 10 個。
現(xiàn)在假設(shè)我們請求第 1000 頁—結(jié)果從 10001 到 10010 。所有都以相同的方式工作除了每個分片不得不產(chǎn)生前10010個結(jié)果以外。然后協(xié)調(diào)節(jié)點對全部 50050 個結(jié)果排序最后丟棄掉這些結(jié)果中的 50040 個結(jié)果。
對結(jié)果排序的成本隨分頁的深度成指數(shù)上升。
如何解決深度分頁
分頁方式 scorll
es 提供了 scroll 的方式進行分頁讀取。原理上是對某次查詢生成一個游標 scroll_id , 后續(xù)的查詢只需要根據(jù)這個游標去取數(shù)據(jù),直到結(jié)果集中返回的 hits 字段為空,就表示遍歷結(jié)束。scroll_id 的生成可以理解為建立了一個臨時的歷史快照,在此之后的增刪改查等操作不會影響到這個快照的結(jié)果。
使用 curl 進行分頁讀取過程如下:
- 先獲取第一個 scroll_id,url 參數(shù)包括 /index/_type/ 和 scroll,scroll 字段指定了scroll_id 的有效生存期,以分鐘為單位,過期之后會被es 自動清理。如果文檔不需要特定排序,可以指定按照文檔創(chuàng)建的時間返回會使迭代更高效。
#返回結(jié)果
{
"_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": {
....
}
}
]
}
}
-
后續(xù)的文檔讀取上一次查詢返回的scroll_id 來不斷的取下一頁,如果srcoll_id 的生存期很長,那么每次返回的 scroll_id 都是一樣的,直到該 scroll_id 過期,才會返回一個新的 scroll_id。請求指定的 scroll_id 時就不需要 /index/_type 等信息了。每讀取一頁都會重新設(shè)置 scroll_id 的生存時間,所以這個時間只需要滿足讀取當前頁就可以,不需要滿足讀取所有的數(shù)據(jù)的時間,1 分鐘就夠了
-
所有文檔獲取完畢之后,需要手動清理掉 scroll_id 。雖然es 會有自動清理機制,但是 srcoll_id 的存在會耗費大量的資源來保存一份當前查詢結(jié)果集映像,并且會占用文件描述符。所以用完之后要及時清理。使用 es 提供的 CLEAR_API 來刪除指定的 scroll_id
scroll + scan
當 scroll 的文檔不需要排序時,es 為了提高檢索的效率,在 2.0 版本提供了 scroll + scan 的方式。隨后又在 2.1.0 版本去掉了 scan 的使用,直接將該優(yōu)化合入了 scroll 中。由于moa 線上的 es 版本是2.3 的,所以只簡單提一下。使用的 scan 的方式是指定 search_type=scan
# 2.0-beta 版本禁用 scroll 的排序,使遍歷更加高效
[root@dnsserver ~]# curl 'xxx.xxx.xxx.xxx:9200/order/info/_search?scroll=1m&search_type=scan' -d '{"query":{"match_all":{}}'
缺點:滾動aapi的方式不適合實時查詢,其根本還是因為他的快照機制限制了實時性,你可以理解成他是mysql某一時刻的快照,后續(xù)的改動在這個scroll存活的時間里是不可見的
search_after 的方式
上述的 scroll search 的方式,官方的建議并不是用于實時的請求,因為每一個 scroll_id 不僅會占用大量的資源(特別是排序的請求),而且是生成的歷史快照,對于數(shù)據(jù)的變更不會反映到快照上。這種方式往往用于非實時處理大量數(shù)據(jù)的情況,比如要進行數(shù)據(jù)遷移或者索引變更之類的。那么在實時情況下如果處理深度分頁的問題呢?es 給出了 search_after 的方式,這是在 >= 5.0 版本才提供的功能。
search_after 分頁的方式和 scroll 有一些顯著的區(qū)別,首先它是根據(jù)上一頁的最后一條數(shù)據(jù)來確定下一頁的位置,同時在分頁請求的過程中,如果有索引數(shù)據(jù)的增刪改查,這些變更也會實時的反映到游標上。
為了找到每一頁最后一條數(shù)據(jù),每個文檔必須有一個全局唯一值,這種分頁方式其實和目前 moa 內(nèi)存中使用rbtree 分頁的原理一樣,官方推薦使用 _uid 作為全局唯一值,其實使用業(yè)務(wù)層的 id 也可以。
- 第一頁的請求和正常的請求一樣,
curl -XGET xxx.xxx.xxx.xxx:9200/order/info/_search
{
"size": 10,
"query": {
"term" : {
"did" : 519390
}
},
"sort": [
{"uid": "desc"}
]
}
- 第二頁的請求,使用第一頁返回結(jié)果的最后一個數(shù)據(jù)的值,加上 search_after 字段來取下一頁。注意,使用 search_after 的時候要將 from 置為 0 或 -1
curl -XGET xxx.xxx.xxx.xxx:9200/order/info/_search
{
"size": 10,
"query": {
"term" : {
"did" : 519390
}
},
"search_after": [1463538857], //加上了這個
"sort": [
{"_uid": "desc"}
]
}
優(yōu)點:實時性高,但是無法跳頁,較為復雜
是否能用Redis來維護這個uid
存在什么問題
1.多人操作下導致的并發(fā)寫問題:
2. 無法進行跳頁
如何解決
多人操作下導致的并發(fā)寫問題:我們可以靜態(tài)維護/加一個獨立字段/使用主鍵id,不在redis里面維護
跳頁問題:如果我們的數(shù)據(jù)的順序不會改變的話,其實我們是可以根據(jù)分頁大小和分頁數(shù)去計算出來我們的上一頁的最后一條數(shù)據(jù)的uid/主鍵id的
但是這種情況基本沒有可能出現(xiàn),因為我們現(xiàn)在的大多數(shù)業(yè)務(wù)都是經(jīng)過不同的查詢條件(名字是否包含)/(修改時間倒序)/(按照男女篩選)因為各種篩選機制,或者如果你是Sass服務(wù)的話,不同公司的數(shù)據(jù)其實是放在一張表的,數(shù)據(jù)的id可能是交叉的,因此你不能動態(tài)的去計算這個uid/主鍵id作為下一頁申請的srot字段,這就是深度分頁中跳頁的問題,也是search_after方法無法解決的問題
請教大佬
針對這種情況,是否有大佬能給一個解決思路呢,跪求!?。?!
如果有什么方案,記得評論扣我,或者私聊哦
資料參考:文章來源:http://www.zghlxwxcb.cn/news/detail-426411.html
京東面試題:ElasticSearch深度分頁解決方案 文章來源地址http://www.zghlxwxcb.cn/news/detail-426411.html
到了這里,關(guān)于實習成長之路:關(guān)于ElasticSearch深度分頁帶來的思考,如何解決深度分頁和跳頁的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!