1. 程式人生 > >大資料學習[16]--使用scroll實現Elasticsearch資料遍歷和深度分頁[轉]

大資料學習[16]--使用scroll實現Elasticsearch資料遍歷和深度分頁[轉]

題目:使用scroll實現Elasticsearch資料遍歷和深度分頁
作者:星爺
出處:
http://lxWei.github.io/posts/%E4%BD%BF%E7%94%A8scroll%E5%AE%9E%E7%8E%B0Elasticsearch%E6%95%B0%E6%8D%AE%E9%81%8D%E5%8E%86%E5%92%8C%E6%B7%B1%E5%BA%A6%E5%88%86%E9%A1%B5.html

背景

Elasticsearch 是一個實時的分散式搜尋與分析引擎,被廣泛用來做全文搜尋、結構化搜尋、分析。在使用過程中,有一些典型的使用場景,比如分頁、遍歷等。在使用關係型資料庫中,我們被告知要注意甚至被明確禁止使用深度分頁,同理,在 Elasticsearch 中,也應該儘量避免使用深度分頁。這篇文章主要介紹 Elasticsearch 中使用分頁的方式、Elasticsearch 搜尋執行過程以及為什麼深度分頁應該被禁止,最後再介紹使用 scroll 的方式遍歷資料。

Elasticsearch 搜尋內部執行原理

一個最基本的 Elasticsearch 查詢語句是這樣的:

POST /my_index/my_type/_search
{
    "query": { "match_all": {}},
    "from": 100,
    "size":  10
}

上面的查詢表示從搜尋結果中取第100條開始的10條資料。下面講解搜尋過程時也以這個請求為例。

那麼,這個查詢語句在 Elasticsearch 叢集內部是怎麼執行的呢?為了方便描述,我們假設該 index 只有primary shards,沒有 replica shards。

在 Elasticsearch 中,搜尋一般包括兩個階段,query 和 fetch 階段,可以簡單的理解,query 階段確定要取哪些doc,fetch 階段取出具體的 doc。

Query 階段

如上圖所示,描述了一次搜尋請求的 query 階段。

  1. Client 傳送一次搜尋請求,node1 接收到請求,然後,node1 建立一個大小為 from + size 的優先順序佇列用來存結果,我們管 node1 叫 coordinating node。
  2. coordinating node將請求廣播到涉及到的 shards,每個 shard 在內部執行搜尋請求,然後,將結果存到內部的大小同樣為 from + size 的優先順序佇列裡,可以把優先順序佇列理解為一個包含 top N 結果的列表。
  3. 每個 shard 把暫存在自身優先順序佇列裡的資料返回給 coordinating node,coordinating node 拿到各個 shards 返回的結果後對結果進行一次合併,產生一個全域性的優先順序佇列,存到自身的優先順序佇列裡。

在上面的例子中,coordinating node 拿到 (from + size) * 6 條資料,然後合併並排序後選擇前面的 from + size 條資料存到優先順序佇列,以便 fetch 階段使用。另外,各個分片返回給 coordinating node 的資料用於選出前 from + size 條資料,所以,只需要返回唯一標記 doc 的 _id 以及用於排序的 _score 即可,這樣也可以保證返回的資料量足夠小。

coordinating node 計算好自己的優先順序佇列後,query 階段結束,進入 fetch 階段。

Fetch 階段

query 階段知道了要取哪些資料,但是並沒有取具體的資料,這就是 fetch 階段要做的。

上圖展示了 fetch 過程:

  1. coordinating node 傳送 GET 請求到相關shards。
  2. shard 根據 doc 的 _id 取到資料詳情,然後返回給 coordinating node。
  3. coordinating node 返回資料給 Client。

coordinating node 的優先順序佇列裡有 from + size 個 _doc _id,但是,在 fetch 階段,並不需要取回所有資料,在上面的例子中,前100條資料是不需要取的,只需要取優先順序佇列裡的第101到110條資料即可。

需要取的資料可能在不同分片,也可能在同一分片,coordinating node 使用 multi-get 來避免多次去同一分片取資料,從而提高效能。

深度分頁的問題

Elasticsearch 的這種方式提供了分頁的功能,同時,也有相應的限制。舉個例子,一個索引,有10億資料,分10個 shards,然後,一個搜尋請求,from=1,000,000,size=100,這時候,會帶來嚴重的效能問題:

  • CPU
  • 記憶體
  • IO
  • 網路頻寬

CPU、記憶體和IO消耗容易理解,網路頻寬問題稍難理解一點。在 query 階段,每個shards需要返回 1,000,100 條資料給 coordinating node,而 coordinating node 需要接收 10 * 1,000,100 條資料,即使每條資料只有 _doc _id 和 _score,這資料量也很大了,而且,這才一個查詢請求,那如果再乘以100呢?

在另一方面,我們意識到,這種深度分頁的請求並不合理,因為我們是很少人為的看很後面的請求的,在很多的業務場景中,都直接限制分頁,比如只能看前100頁。

不過,這種深度分頁確實存在,比如,被爬蟲了,這個時候,直接幹掉深度分頁就好;又或者,業務上有遍歷資料的需要,比如,有1千萬粉絲的微信大V,要給所有粉絲群發訊息,或者給某省粉絲群發,這時候就需要取得所有符合條件的粉絲,而最容易想到的就是利用 from + size 來實現,不過,這個是不現實的,這時,可以採用 Elasticsearch 提供的 scroll 方式來實現遍歷。

利用 scroll 遍歷資料

可以把 scroll 理解為關係型資料庫裡的 cursor,因此,scroll 並不適合用來做實時搜尋,而更適用於後臺批處理任務,比如群發。

可以把 scroll 分為初始化和遍歷兩步,初始化時將所有符合搜尋條件的搜尋結果快取起來,可以想象成快照,在遍歷時,從這個快照裡取資料,也就是說,在初始化後對索引插入、刪除、更新資料都不會影響遍歷結果。

使用介紹

下面介紹下scroll的使用,可以通過 Elasticsearch 的 HTTP 介面做試驗下,包括初始化和遍歷兩個部分。

初始化

POST ip:port/my_index/my_type/_search?scroll=1m
{
    "query": { "match_all": {}}
}

初始化時需要像普通 search 一樣,指明 index 和 type (當然,search 是可以不指明 index 和 type 的),然後,加上引數 scroll,表示暫存搜尋結果的時間,其它就像一個普通的search請求一樣。

初始化返回一個 _scroll_id,_scroll_id 用來下次取資料用。

遍歷

POST /_search?scroll=1m
{
    "scroll_id":"XXXXXXXXXXXXXXXXXXXXXXX I am scroll id XXXXXXXXXXXXXXX"
}

這裡的 scroll_id 即 上一次遍歷取回的 _scroll_id 或者是初始化返回的 _scroll_id,同樣的,需要帶 scroll 引數。 重複這一步驟,直到返回的資料為空,即遍歷完成。注意,每次都要傳引數 scroll,重新整理搜尋結果的快取時間。另外,不需要指定 index 和 type

設定scroll的時候,需要使搜尋結果快取到下一次遍歷完成,同時,也不能太長,畢竟空間有限。

Scroll-Scan

Elasticsearch 提供了 Scroll-Scan 方式進一步提高遍歷效能。還是上面的例子,微信大V要給粉絲群發這種後臺任務,是不需要關注順序的,只要能遍歷所有資料即可,這時候,就可以用Scroll-Scan。

Scroll-Scan 的遍歷與普通 Scroll 一樣,初始化存在一點差別。

POST ip:port/my_index/my_type/_search?search_type=scan&scroll=1m&size=50
{
    "query": { "match_all": {}}
}

需要指明引數:

  • search_type。賦值為scan,表示採用 Scroll-Scan 的方式遍歷,同時告訴 Elasticsearch 搜尋結果不需要排序。
  • scroll。同上,傳時間。
  • size。與普通的 size 不同,這個 size 表示的是每個 shard 返回的 size 數,最終結果最大為 number_of_shards * size。

Scroll-Scan 方式與普通 scroll 有幾點不同:

  1. Scroll-Scan 結果沒有排序,按 index 順序返回,沒有排序,可以提高取資料效能。
  2. 初始化時只返回 _scroll_id,沒有具體的 hits 結果。
  3. size 控制的是每個分片的返回的資料量而不是整個請求返回的資料量。

Java 實現

用 Java 舉個例子。

初始化

try {
    response = esClient.prepareSearch(index)
            .setTypes(type)
            .setSearchType(SearchType.SCAN)
            .setQuery(query)
            .setScroll(new TimeValue(timeout))
            .setSize(size)
            .execute()
            .actionGet();
} catch (ElasticsearchException e) {
    // handle Exception
}  

初始化返回 _scroll_id,然後,用 _scroll_id 去遍歷,注意,上面的query是一個JSONObject,不過這裡很多種實現方式,我這兒只是個例子。

遍歷

try {
    response = esClient.prepareSearchScroll(scrollId)
            .setScroll(new TimeValue(timeout))
            .execute()
            .actionGet();
} catch (ElasticsearchException e) {
    // handle Exception
}

總結

  1. 深度分頁不管是關係型資料庫還是Elasticsearch還是其他搜尋引擎,都會帶來巨大效能開銷,特別是在分散式情況下。
  2. 有些問題可以考業務解決而不是靠技術解決,比如很多業務都對頁碼有限制,google 搜尋,往後翻到一定頁碼就不行了。
  3. Elasticsearch 提供的 Scroll 介面專門用來獲取大量資料甚至全部資料,在順序無關情況下,首推Scroll-Scan。
  4. 描述搜尋過程時,為了簡化描述,假設 index 沒有備份,實際上,index 肯定會有備份,這時候,就涉及到選擇 shard。

PS:Elasticsearch 各個版本可能有區別,但原理基本相同,本文包括文末的程式碼都基於Elasticsearch 1.3。