ElasticSearch第3篇 大数据处理3大问题(“10000条”问题解决方案、hits total值统计总数不精确解决方案、大数据深度分页性能问题3种优化方案)
“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());