概要
本篇從介紹搜索分頁為起點,簡單闡述分頁式數據搜索與原有集中式數據搜索思維方式的差異,就分頁問題對deep paging問題的現象進行分析,最后介紹分頁式系統top N的案例。
搜索分頁語法
Elasticsearch中search語法有from和size兩個參數用來實現分頁的效果:
- size:顯示應該返回的結果數量,默認是10。
- from:顯示查詢數據的偏移量,即應該跳過的初始結果數量,默認是0。
from和size這兩個參數的含義和MySql使用limit關鍵字分頁的參數含義是一樣的。
舉幾個示例,查詢第1-3頁的請求:
GET /music/children/_search?size=10
GET /music/children/_search?size=10&from=10
GET /music/children/_search?size=10&from=20
分布式數據與集中式數據的差異
集中式數據存儲方式,從最早的單體應用模式,到早期的SOA服務模式,那時存儲大多數都是采用集中式數據存儲,數據落地到mysql等關系型數據中,有支持讀寫分離,部署了多台數據庫實例實現主-從結構的本質上也還是集中式存儲。
在單數據庫或主從數據庫中,執行分頁查詢,統計排序等思路相對清晰,畢竟數據都完完整整地放在一起,直接挑一台實例搞就是了,可能就是容量有上限,出結果慢一些而已。
關系型數據庫使用分布式數據存儲的經典方案是分庫分表,同一張表的數據,用一定的路由邏輯,拆分在不同的數據庫實例里存儲,此時做數據統計,就不能只關注一個實例了。
分布式數據存儲方式,搜索思路就開始有了細微的改變,比如說Elasticsearch,索引的數據是拆分存儲在各個shard里的,每個shard可能散布在ES集群的各個node上,這種情況下,做查詢,統計分析等操作,雖然ES已經封裝好了技術細節,我們仍然需要明白這是一個分布式儲存的查詢方案。
個人認為,分布式數據與集中式數據的處理差異,雖然在關系型數據庫或ES方面,已經有成熟的框架對其進行封裝,但使用者還是需要從思維上去理解分布式帶來的改變,這樣才能得到正確的結果。
deep paging問題
deep paging簡單來說叫深度分頁,就是搜索得特別深,顯示第好幾百頁的數據。為什么說deep paging是有問題的?
我們假定索引內有20000條數據,存儲在5個shard里,發送一個有條件和指定排序字段的查詢請求,如果我要取第1頁的數據,那么每個shard都取10條數據,匯總到Coordinate Node里,共50條,Coordinate Node對這50條數據再進行排序,濾掉后面的40條數據,只取最前面的
10條,返回給客戶端。
如果是第1000頁呢?
按老套路每個shard取10000-10010條,匯總到Coordinate Node里,還是50條,最后返回給客戶端?
這么做就錯啦,分布式數據不是這么查的,第1000頁,在每個shard中不是取第10000-10010條,而是取前面10010,5個shard共取50050條給Coordinate Node,Coordinate Node匯總數據完成排序等操作后再取第10000-10010條,返回客戶端10條數據。
費了這么大勁收集到50050條數據,實際給客戶端的就10條,丟掉50040條數據,好費內存。
如果第10000頁呢?這結果不忍直視
如果一個索引的分片數越多,需要匯總的數據就會成倍增長 ,可以看到分布式系統對結果排序分布的成本隨深度呈指數上升,最重要的兩個影響維度是分頁深度和shard數量。這種重量級的查詢,極有可能拖垮整個Elasticsearch集群,所以說搜索引擎對任何查詢都不要返回超過1000個結果。
引申top N問題模型
deep paging的問題,通過優化搜索關鍵詞,控制分頁深度,問題能得到一定的改善,那top N問題如如何解決呢?
聚合查詢中,經常能遇到查詢最XX的10條記錄這種分析需求,這種就是top N問題模型。
完美解決的場景
我們先舉個熟悉的案例:統計播放量最高的10首的英文兒歌。
document數據結構:
{
"_index": "music",
"_type": "children",
"_id": "2",
"_version": 6,
"found": true,
"_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
}
}
這個需求ES處理起來得心應手,有下面幾個原因:
- 一個document只會存在於一個shard中
- 每個document數據里中有播放數量的統計值
有這上面幾點的保證,查詢時ES就可以放心大膽地在每個shard取播放數最高的前10條數據,Coordinate Node匯總的數據也就50條,此時性能非常高。
不宜直接查詢的場景
上一小節依賴document的預先設計和shard存儲數據的特性,避免了全索引掃描,性能特別高,假設系統中針對每天的播放點擊,都有一個播放日志記錄,記錄着歌曲ID,點擊人,點擊時間,收聽時長,時長百分比(與完整歌曲的百分比,有聽到一半就退出不聽了的,這個值就是50%),該document的數據示例:
{
"_index": "playlog-20191121",
"_type": "music",
"_id": "1",
"_version": 1,
"found": true,
"_source": {
"music_id": 1,
"listener": "tony wang",
"listen_date": "2019-11-21 15:35:00",
"music_length": 52,
"isten_percentage": 0.95
}
}
假設歌曲總量200萬條,每天的播放日志1億條,日志索引每天建立一個,primary shard數量為10,命名格式playlog-yyyyMMdd,需求是搜索當天的播放排行榜,取排名前10的記錄。
如果直接統計,就只能硬扛了,基本過程如下:
- 每個shard根據music_id做分組統計,理論上單shard數量量最多200萬條。
- Coordinate Node收集10個shard的數據,進行合並,數據處理量上限2000萬條,最后合並成200萬條。
- 從這200萬條數據取前面10條,返回給客戶端。
這個過程絕對是重量級,如果每次都實時統計的話,ES集群的壓力可想而知。
改進方案
-
播放功能增加數據更新邏輯
預先增加按日期統計的索引數據結構,每次有用戶點擊播放時,額外發送一條更新消息將其數據更新,查詢時直接從統計的索引里出結果,避免每次查詢。 -
定時任務統計數據
數據統計的需求,可以用定時任務進行計算,將計算結果存儲起來,通過降低實時性,來避免全索引掃描計算的壓力。
簡單對比:
- 相同點:都是以空間換時間的做法,避免全索引掃描。
- 不同點:前者通過更改業務實現邏輯,增加數據級聯更新,有一定的業務耦合性;后者將實時計算變定時任務,靈活性較高,與業務耦合性低,但實時性差。
補充一點
良好的數據結構設計可以很大程度地降低ES查詢壓力,提高實時查詢的性能,但有一點需要接受:考慮得再周全的設計,也難適應千變萬化的需求;需求變更是無法避免的,沒有一勞永逸的方案。
小結
本篇從分頁查詢入手,闡述了deep paging的問題原因,並順帶將自己對分布式系統與集中式系統處理的思維差異做了簡單描述,最后引申了top N場景的問題,上面提到的改進方案,只是針對比較簡單的場景,實際生產要面臨的情況肯定更復雜,比如采用分布式計算組件storm來解決top N 問題的,這里當做是拋磚引玉,歡迎各位分享自己的看法。
專注Java高並發、分布式架構,更多技術干貨分享與心得,請關注公眾號:Java架構社區