概要
本篇主要介紹一下分佈式環境中搜索的兩階段執行過程。
兩階段搜索過程
回顧我們之前的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
}
查詢階段的過程示意圖如下:
- Java客戶端發起查詢請求,接受請求的node-1成爲Coordinate Node(協調者),該node會創建一個priority queue,長度爲from + size即1000。
- Coordinate Node將請求分發到所有的primary shard或replica shard中,每個shard在本地創建一個同樣大小的priority queue,長度也爲from + size,用於存儲該shard執行查詢的結果。
- 每個shard將各自priority queue的元素返回給Coordinate Node,元素內只包含文檔的ID和排序值(如_score),Coordinate Node將合併所有的元素到自己的priority queue中,並完成排序動作,最終根據from、size值對結果進行截取。
補充說明:
- 哪個node接收客戶端的請求,該node就會成爲Coordinate Node。
- Coordinate Node轉發請求時,會根據負載均衡算法分配到同一分片的primary shard或replica shard上,爲什麼說replica值設置得大一些可以增加系統吞吐量的原理就在這裏,Coordinate Node的查詢請求負載均衡算法會輪詢所有的可用shard,併發場景時就會有更多的硬件資源(CPU、內存,IO)會參與其中,系統整體的吞吐量就能提升。
- 此查詢過程Coordinate Node得到是輕量級的元素信息,只包含文檔ID和_score這些信息,這樣可以減輕網絡負載,因爲分頁過程中,大部分的數據是會丟棄掉的。
取回階段
在完成了查詢階段後,此時Coordinate Node已經得到查詢的列表,但列表內的元素只有文檔ID和_score信息,並無實際的_source內容,取回階段就是根據文檔ID,取到完整的文檔對象的過程。如下圖所示:
- Coordinate Node根據from、size信息截取要取回文檔的ID,如{"from": 980, "size": 20},則取第981到第1000這20條數據,其餘丟棄,from/size爲空則默認取前10條,向其他shard發出mget請求。
- shard接收到請求後,根據_source參數(可選)加載文檔信息,返回給Coordinate Node。
- 一旦所有的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架構社區微信羣共同探討技術