“10000條”問題(個人稱謂) 癥狀: 在數據量不大的情況下,可能還會使用from + size的傳統分頁方式,但是數量受限,只能取前10000條的數據。 緣由:ES限值10000條,是ES團隊挑選一個不大不小的數作為閾值,為了避免深度分頁的策略。 調整:max_result_window 用於控 ...
“10000條”問題(個人稱謂)
- 癥狀: 在數據量不大的情況下,可能還會使用from + size的傳統分頁方式,但是數量受限,只能取前10000條的數據。
- 緣由:ES限值10000條,是ES團隊挑選一個不大不小的數作為閾值,為了避免深度分頁的策略。
- 調整:max_result_window 用於控制在搜索查詢中可以檢索到的最大文檔數,是有符號int類型,最大可設置231 - 1,調大可以,但隨著數據量(不是max_result_window 值)增加會有性能問題。
返回bool
$params = [
'index' => 'performance_test',
'body' => [
'index' => [
'max_result_window' => 2147483647 //用於控制在搜索查詢中可以檢索到的最大文檔數,有符號int類型,最大可設置2^31 - 1,但隨著數據量增加會有性能問題。
]
]
];
$response = $client->indices()->putSettings($params);
dd($response->asBool());
hits total值統計總數不精確解決方案(另一種“10000條”問題)
- 癥狀:調用search()方法,可能查詢到的結果有20000條(count()方法統計得出),ES也返回有10000條數據匹配。
{
"took": 101,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 10000, //這裡的統計結果不准,解決的就是這裡的問題
"relation": "gte"
},
"max_score": 1.0342529,
"hits": [
......
}
}
- 緣由:ES團隊挑選一個不大不小的數作為閾值,處於性能和業務考慮,畢竟用戶基本沒耐心看數萬條之後的數據。
- 解決:添加track_total_hits為true即可。
$params = [
'index' => 'performance_test',
'body' => [
'query' => [
'match' => [
'content' => '的'
]
],
'track_total_hits' => true
]
];
$response = $client->search($params);
dd($response->asArray());
大數據深度分頁性能問題
- 極簡概括:在大數據情況下,查詢很多頁後的數據,ES響應速度會變慢甚至崩潰。
- 問題由來:假設億級數據,要查詢某頁(頁數很大)之後的數據,ES底層就需要把前面很多頁的數據都要過一遍才能定位到指定頁,這是一個巨大的開銷。所以大數據深度分頁一般不推薦使用from + size的傳統分頁方式。也算是一類業界難題。
- 測試:花了3個小時向ES插入了1.09億條中文數據,實測每頁顯示2條數據,翻頁到2000萬頁,效果如下:
深度分頁會把記憶體乾爆,導致java程式OutOfMemoryError,ES進程被終止。
[2024-07-27T09:01:39,206][INFO ][o.e.c.r.a.AllocationService] [localhost.localdomain] current.health="YELLOW" message="Cluster health status changed from [RED] to [YELLOW] (reason: [shards started [[performance_test][0]]])." previous.health="RED" reason="shards started [[performance_test][0]]"
[2024-07-27T09:01:58,487][INFO ][o.e.m.j.JvmGcMonitorService] [localhost.localdomain] [gc][21] overhead, spent [331ms] collecting in the last [1.1s]
[2024-07-27T09:02:00,495][INFO ][o.e.m.j.JvmGcMonitorService] [localhost.localdomain] [gc][23] overhead, spent [334ms] collecting in the last [1s]
[2024-07-27T09:02:02,674][WARN ][o.e.m.j.JvmGcMonitorService] [localhost.localdomain] [gc][24] overhead, spent [2s] collecting in the last [2.1s]
[2024-07-27T09:02:04,181][WARN ][o.e.m.j.JvmGcMonitorService] [localhost.localdomain] [gc][25] overhead, spent [1.2s] collecting in the last [1.5s]
[2024-07-27T09:02:07,837][WARN ][o.e.m.j.JvmGcMonitorService] [localhost.localdomain] [gc][26] overhead, spent [3.5s] collecting in the last [1.8s]
java.lang.OutOfMemoryError: Java heap space
Dumping heap to data/java_pid20168.hprof ...
索引與映射結構(下文要用)
$params = [
'index' => 'performance_test',
'body' => [
'settings' => [
'analysis' => [
'analyzer' => [
'ik_analyzer' => [
'type' => 'ik_max_word',
],
],
],
],
'mappings' => [
'properties' => [
'id' => [
'type' => 'integer',
],
'content' => [
'type' => 'text',
'analyzer' => 'ik_analyzer',
],
],
],
],
];
方案1(業務需求優化)
從業務需求源頭限制深度分頁。
例如某度搜索,深度分頁也就50~76頁是最後一頁(甚至不讓用戶從小頁碼直接跳轉大頁碼),某寶搜索最大100頁。
這個信息自然無法全部彙集。它們的定位不是要匯聚更全的信息,而是找出最精準的(或者金主爸爸打錢)數據。
人為的也幾乎沒有耐心看完那麼多頁的詞條。
可以說深度分頁並非適合所有業務場景,而是要做權衡(說人話就是要和產品經理溝通以上文案)。
方案2 (Scroll API)
- 輔助理解:類比愚公移山。操作對象太大,那就每次少量的處理,然後不停的分批迭代。這和分頁的分批處理思想相似,不過不用手動處理分頁問題。
- 優點:允許處理超大規模的數據集。
- 缺點:不支持頁跳轉(第M頁跳轉N頁),只支持遞增頁和遞減頁。scroll查詢的相應數據是非實時的,如果遍歷過程是,其它請求要返回的數據進行了寫操作,是查詢不到最新的結果的。
$params = [
'index' => 'performance_test',
'scroll' => '1m', //用於設置滾動上下文的存活時間,1m是一分鐘,過了這個時間,scroll_id參數就失效了,有這個id,好比創建了一個專門的任務,讓ES去處理這件事
'size' => 2,
'body' => [
'query' => [
'match' => [
'content' => '袁隆平' //要搜索整個文檔中,包含袁隆平的
]
]
]
];
$response = $client->search($params);
dump($response->asArray()); //常規的處理
$scroll_id = $response['_scroll_id'];
while (true) {
$response = $client->scroll([
'scroll_id' => $scroll_id,
'scroll' => '1m'
]);
// 檢查是否有數據返回
if (empty($response['hits']['hits'])) {
break;
}
// 處理數據,這裡只做列印
foreach ($response['hits']['hits'] as $hit) {
dump($hit['_source']);
}
// 更新 scroll ID
$scroll_id = $response['_scroll_id'];
}
// 清理Scroll請求,釋放資源
$client->clearScroll([
'scroll_id' => $scroll_id
]);
方案3 (Search After 推薦方案)
- 輔助理解:上一頁的最後一條數據,作為當前頁的起始偏移量。操作對象太大,那就每次少量的處理,然後不停的迭代,這和分頁的分批處理思想相似。
- 優點:ES針對深度分頁的新解決方案,不會有太多的性能問題。
- 缺點:必須指定排序欄位,否則起始偏移量參考系參數無法使用。
$params = [
'index' => 'performance_test',
'body' => [
'query' => [
'match_all' => new stdClass()
],
'sort' => [
['id' => 'asc']
],
'size' => 2 //這個排序是必須的,不一定是id,但通常是id,排序是為了去上一條數據的時候定位不會錯亂
]
];
//發起第一次查詢,獲取第一頁結果
$response = $client->search($params);
//模擬處理第一頁的結果
foreach ($response['hits']['hits'] as $hit) {
//模擬處理
dump($hit['_source']['content']);
}
/**
* @function SearchAfter方法封裝
* @param $client object ElasticSearch對象
* @param $params array search()所需要的參數
* @param $response object|null search()方法查詢出來的結果
* @return object
*/
function searchAfter($client, $params, $response) {
if(is_object($response)) {
$response = $response->asArray();
}
if(empty($response['hits']['hits'])) {
return null;
}
$last_data = end($response['hits']['hits']);
//在其餘查詢條件不變的情況下,只修改search_after值。
$params['body']['search_after'] = $last_data['sort'];
//然後再次查詢
$response = $client->search($params);
return $response;
}
//模擬處理第二頁的數據
$response = searchAfter($client, $params, $response);
dd($response->asArray());