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());
  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小松聊PHP进阶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值