Elasticsearch系列---搜索執行過程及scroll遊標查詢

概要

本篇主要介紹一下分佈式環境中搜索的兩階段執行過程。

兩階段搜索過程

回顧我們之前的CRUD操作,因爲只對單個文檔進行處理,文檔的唯一性很容易確定,並且很容易知道是此文檔在哪個node,哪個shard中。

但搜索比CRUD複雜,符合搜索條件的文檔,可能散落在各個node、各個shard中,我們需要找到匹配的文檔,並且把從各個node,各個shard返回的結果進行彙總、排序,組成一個最終的結果排序列表,纔算完成一個搜索過程。我們將按兩階段的方式對這個過程進行講解。

查詢階段

假定我們的ES集羣有三個node,number_of_primary_shards爲3,replica shard爲1,我們執行一個這樣的查詢請求:

GET /music/children/_search
{
  "from": 980,
  "size": 20
}

查詢階段的過程示意圖如下:

  1. Java客戶端發起查詢請求,接受請求的node-1成爲Coordinate Node(協調者),該node會創建一個priority queue,長度爲from + size即1000。
  2. Coordinate Node將請求分發到所有的primary shard或replica shard中,每個shard在本地創建一個同樣大小的priority queue,長度也爲from + size,用於存儲該shard執行查詢的結果。
  3. 每個shard將各自priority queue的元素返回給Coordinate Node,元素內只包含文檔的ID和排序值(如_score),Coordinate Node將合併所有的元素到自己的priority queue中,並完成排序動作,最終根據from、size值對結果進行截取。

補充說明:

  1. 哪個node接收客戶端的請求,該node就會成爲Coordinate Node。
  2. Coordinate Node轉發請求時,會根據負載均衡算法分配到同一分片的primary shard或replica shard上,爲什麼說replica值設置得大一些可以增加系統吞吐量的原理就在這裏,Coordinate Node的查詢請求負載均衡算法會輪詢所有的可用shard,併發場景時就會有更多的硬件資源(CPU、內存,IO)會參與其中,系統整體的吞吐量就能提升。
  3. 此查詢過程Coordinate Node得到是輕量級的元素信息,只包含文檔ID和_score這些信息,這樣可以減輕網絡負載,因爲分頁過程中,大部分的數據是會丟棄掉的。

取回階段

在完成了查詢階段後,此時Coordinate Node已經得到查詢的列表,但列表內的元素只有文檔ID和_score信息,並無實際的_source內容,取回階段就是根據文檔ID,取到完整的文檔對象的過程。如下圖所示:

  1. Coordinate Node根據from、size信息截取要取回文檔的ID,如{"from": 980, "size": 20},則取第981到第1000這20條數據,其餘丟棄,from/size爲空則默認取前10條,向其他shard發出mget請求。
  2. shard接收到請求後,根據_source參數(可選)加載文檔信息,返回給Coordinate Node。
  3. 一旦所有的shard都返回了結果,Coordinate Node將結果返回給客戶端。

前面幾篇有提到deep paging的問題,我們在這裏又複習一遍,使用from和size進行分頁時,傳遞信息給Coordinate Node的每個shard,都創建了一個from + size長度的隊列,並且Coordinate Node需要對所有傳過來的數據進行排序,工作量爲number_of_shards * (from + size),然後從裏面挑出size數量的文檔,如果from值特別大,那麼會帶來極大的硬件資源浪費,鑑於此原因,強烈建議不要使用深分頁。

不過深分頁操作很少符合人的行爲,翻幾頁還看不到想要的結果,人的第一反應是換一個搜索條件,只有機器人或爬蟲才這麼不知疲倦地一直翻頁直到服務器崩潰。

preference設置

查詢時使用preference參數,可以影響哪些shard可以用來執行搜索操作,6.1.0版本後,許多參數值已聲明爲棄用,我們挑幾個目前還在使用的簡單介紹一下:

  • _only_local:只搜索當前node中的shard
  • _local:優先搜索當前node中的shard,搜不到再去其他的shard
  • _prefer_nodes:abc,xyz:優先從指定的abc/xyz節點上搜索,如果兩個節點都有存在數據的shard,隨機從裏面挑一個節點執行搜索
  • _only_nodes:abc,xyz,...:只在符合通配abc、xyz名稱的節點上搜索,如果多個節點都有存在數據的shard,隨機從裏面挑一個節點執行搜索
  • _shards:2,3:指定shard進行搜索,這個條件如與其他條件搭配使用,此條件要寫在前面,如_shards:2,3|_local
  • 自定義字符串:一般用sessionid或userid

bouncing results問題

假如兩個文檔有相同的字段值,並且時間戳也一樣,如果按時間戳字段來排序,由於請求是在所有可用的shard上輪詢的,可能存在一種情況:這兩個文檔記錄在不同的shard之間保存的順序不相同。結果就是同一個條件的查詢,如果執行多次,分配在primary shard得到的是一種順序,分配在replica shard又是另一個順序,這個就是所謂的bouncing results問題。

如何避免:讓同一個用戶始終使用同一個shard,就可以避免這種問題,常見的做法是preference設置爲sessionid或userid,如:

GET /music/children/_search?preference=10086
{
  "from": 980,
  "size": 20
}

超時問題

我們回顧查詢階段和取回階段,必須所有的操作都完成了,纔給客戶端返回結果,如果中途有shard在執行特別重的任務,導致查詢很慢怎麼辦?會拖慢整個集羣嗎?

如果是高併發場景,那極有可能,因爲某一個節點慢,整個查詢請求堆積,拖死集羣都有可能。

爲了防止這一情況,我們使用timeout參數,告訴shard允許處理數據的最大時間,時間一到,執行關門動作,能有多少數據返回多少數據,剩下的不要了,這樣可以確保集羣是穩定運行的,如下圖所示:

routing

在設計大規模數據搜索時,我們爲了實現數據集中性,索引時會按一定規則將數據進行存儲,比如訂單數據,我們會按userid爲route key,每個userid的訂單數據,都放在同一個shard上,既然存儲時使用了route key,那麼搜索時同樣使用route key,可以讓查詢只搜索相關的shard,如:

GET /music/children/_search?routing=10086
{
  "from": 980,
  "size": 20
}

這樣由於精準到具體的shard,可以極大的縮小搜索範圍,數據量越大,效果越明顯。

搜索類型

默認的搜索類型是query_then_fetch,我們還可以選擇dfs_query_then_fetch,這個有預查詢階段,可以從所有相關shard中獲取詞頻來計算全局詞頻,可以提升revelance sort精準度。

scroll遊標查詢

如果我們要把大批量的數據從ES集羣中取出,用來執行一些計算,一次性取完肯定不合適,IO壓力過大,性能容易出問題,分頁查詢又容易造成deep paging的問題。一般推薦使用scroll查詢,一批一批的查,直到所有數據都查詢完。

原理

  • scroll查詢會先做查詢初始化,然後再批量地拉取結果,有點像數據庫的cursor。
  • scroll查詢會取某個時間點的快照數據,查詢初始化後索引上的數據發生了變化,快照數據還是原來的,有點像數據庫的索引視圖。
  • scroll查詢用字段_doc排序,去掉了全局排序,性能比較高。
  • scroll查詢要設置過期時間,每次搜索在這個時間內完成即可。

示例

我們假定每次取10條數據,時間窗口爲1秒
請求如下:

GET /music/children/_search?scroll=1s
{
  "size": 10
}

響應如下(結果有刪減):

{
  "_scroll_id": "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAABJQFkExczF1dXM3VHB1RFNpVDR4RkxPb1EAAAAAAAASUhZBMXMxdXVzN1RwdURTaVQ0eEZMT29RAAAAAAAAElMWQTFzMXV1czdUcHVEU2lUNHhGTE9vUQAAAAAAABJUFkExczF1dXM3VHB1RFNpVDR4RkxPb1EAAAAAAAASURZBMXMxdXVzN1RwdURTaVQ0eEZMT29R",
  "took": 2,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 4,
    "max_score": 1,
    "hits": [
      {
        "_index": "music",
        "_type": "children",
        "_id": "2",
        "_score": 1,
        "_source": {
          "name": "wake me, shark me",
          "content": "don't let me sleep too late, gonna get up brightly early in the morning",
          "language": "english",
          "length": "55",
          "likes": 0,
          "author": "John Smith"
        }
      }
    ]
  }
}

注意那個scroll_id,下次再查詢時,只要帶上這個就行了

GET /_search/scroll
{
    "scroll": "1s", 
    "scroll_id" : "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAABJQFkExczF1dXM3VHB1RFNpVDR4RkxPb1EAAAAAAAASUhZBMXMxdXVzN1RwdURTaVQ0eEZMT29RAAAAAAAAElMWQTFzMXV1czdUcHVEU2lUNHhGTE9vUQAAAAAAABJUFkExczF1dXM3VHB1RFNpVDR4RkxPb1EAAAAAAAASURZBMXMxdXVzN1RwdURTaVQ0eEZMT29R"
}

每次的查詢,都把最新的scroll_id帶上,直到數據查詢完成爲止。

scroll查詢看起來像分頁,但使用場景不一樣,分頁主要是按頁展示數據,主要受衆是人,scroll一批一批的獲取數據,主要受衆一般是數據分析的系統,是給系統用的。
性能也不同,前面我們瞭解後,分頁查詢隨着頁數的加深,壓力越來越大,而scroll是基於_doc排序的數據處理,特別適用於大批量數據的獲取分析。

小結

本篇詳細介紹了查詢的兩階段過程,以及能夠影響查詢行爲的一些參數設置,歷經多個版本迭代,有些preference參數已經不用了,瞭解一下就行,另外介紹了bouncing results產生的原理及規避辦法,最後介紹了一下大批量數據查詢利器scroll的簡單用法。

專注Java高併發、分佈式架構,更多技術乾貨分享與心得,請關注公衆號:Java架構社區
可以掃左邊二維碼添加好友,邀請你加入Java架構社區微信羣共同探討技術
Java架構社區

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章