《Elasticsearch 查询性能调优》系列,共包含以下文章:
- 查询性能调优(一):性能保护参数详解
- 查询性能调优(二):SQL LIMIT 和 terminate_after 对比
- 查询性能调优(三):track_total_hits 和 terminate_after 可能的冲突
- 查询性能调优(四):计数的精确性探讨
- 查询性能调优(五):如何确保 "最相关" 的结果
- 查询性能调优(六):track_total_hits 影响返回结果的相关性排序吗
- 查询性能调优(七):为什么计数对性能影响如此之大?
😊 如果您觉得这篇文章有用 ✔️ 的话,请给博主一个一键三连 🚀🚀🚀 吧 (点赞 🧡、关注 💛、收藏 💚)!!!您的支持 💖💖💖 将激励 🔥 博主输出更多优质内容!!!
为什么计数对性能影响如此之大?
-
- [0.核心问题:计数不是简单的 O(N) 🎯](#0.核心问题:计数不是简单的 O(N) 🎯)
- [1.Elasticsearch 计数的真实复杂度 🔍](#1.Elasticsearch 计数的真实复杂度 🔍)
-
- [1.1 不是简单的循环计数](#1.1 不是简单的循环计数)
- [2. 存储层级的开销 💾](#2. 存储层级的开销 💾)
-
- [2.1 Lucene 段文件结构](#2.1 Lucene 段文件结构)
- [2.2 内存 vs 磁盘访问](#2.2 内存 vs 磁盘访问)
- [3.分布式系统的放大效应 🌐](#3.分布式系统的放大效应 🌐)
-
- [3.1 分片并行但协调复杂](#3.1 分片并行但协调复杂)
- [3.2 网络和内存开销](#3.2 网络和内存开销)
- [4.断路器和资源限制 ⚡](#4.断路器和资源限制 ⚡)
-
- [4.1 内存断路器机制](#4.1 内存断路器机制)
- [5.对比实验:有计数 vs 无计数 📊](#5.对比实验:有计数 vs 无计数 📊)
-
- [5.1 测试环境](#5.1 测试环境)
- [5.2 测试 1:只返回结果,不计数](#5.2 测试 1:只返回结果,不计数)
- [5.3 测试 2:精确计数](#5.3 测试 2:精确计数)
- [5.4 性能对比表](#5.4 性能对比表)
- [6.查询复杂度的影响 🔧](#6.查询复杂度的影响 🔧)
-
- [6.1 简单查询 vs 复杂查询](#6.1 简单查询 vs 复杂查询)
- [6.2 聚合查询的额外开销](#6.2 聚合查询的额外开销)
- [7.为什么近似计数(track_total_hits: N)是聪明的设计 🎯](#7.为什么近似计数(track_total_hits: N)是聪明的设计 🎯)
-
- [7.1 计数过程的优化](#7.1 计数过程的优化)
- [7.2 节省的资源](#7.2 节省的资源)
- [8.实际场景中的权衡 💡](#8.实际场景中的权衡 💡)
-
- [8.1 场景分析:电商网站搜索](#8.1 场景分析:电商网站搜索)
- [8.2 什么时候需要精确计数 ?](#8.2 什么时候需要精确计数 ?)
- [9.性能优化建议 🚀](#9.性能优化建议 🚀)
-
- [9.1 分层查询策略](#9.1 分层查询策略)
- [9.2 使用专用计数 API](#9.2 使用专用计数 API)
- [9.3 预计算和缓存](#9.3 预计算和缓存)
- [10.总结:为什么 "只是O(N)" 却影响巨大 📈](#10.总结:为什么 "只是O(N)" 却影响巨大 📈)
❓ 为什么计数只有 O ( N ) O(N) O(N) 复杂度,却非常影响性能?
看起来只是简单的 O(N) 复杂度,但实际上在分布式、大数据场景下,计数远比想象中复杂和昂贵。本文将详细解释原因。
0.核心问题:计数不是简单的 O(N) 🎯
在 Elasticsearch 中,计数 = 扫描全部数据 + 复杂判断 + 分布式协调,而不仅仅是遍历。
1.Elasticsearch 计数的真实复杂度 🔍
1.1 不是简单的循环计数
python
# ❌ 你以为的计数(简单 O(N)):
def simple_count(docs, query):
count = 0
for doc in docs: # O(N)
if matches(doc, query):
count += 1
return count
# ✅ 实际的 Elasticsearch 计数:
def elasticsearch_count(shards, query, sorting, aggregations):
total = 0
for shard in shards: # S 个分片
shard_count = 0
for segment in shard.segments: # 多个段文件
for doc in segment.docs: # 段内文档
# 复杂查询判断
if complex_query_match(doc, query): # 可能涉及脚本、嵌套等
if needs_scoring: # 如果需要评分
calculate_score(doc) # 复杂计算
if has_aggregations: # 如果有聚合
update_aggregations(doc)
shard_count += 1
total += shard_count
# 分布式协调开销
if total > circuit_breaker_limit: # 断路器检查
throw_memory_exception()
# 合并所有分片结果
return merge_counts_from_all_shards(total)
实际复杂度更接近 : O ( S × S e g m e n t s × N × Q u e r y C o m p l e x i t y ) O(S × Segments × N × QueryComplexity) O(S×Segments×N×QueryComplexity)
2. 存储层级的开销 💾
2.1 Lucene 段文件结构
索引物理结构:
my-index/
├── shard0/
│ ├── _0.cfs # 复合段文件
│ ├── _0.si # 段信息
│ ├── _1.cfs # 另一个段
│ └── segments.gen
├── shard1/
│ ├── _0.cfs
│ └── ...
└── ...
每个段都需要:
- 1️⃣ 打开文件
- 2️⃣ 加载倒排索引
- 3️⃣ 遍历所有文档
2.2 内存 vs 磁盘访问
python
def count_in_segment(segment, query):
# 内存映射文件访问
if query_in_cache:
# 缓存命中:快
return cached_count
else:
# 缓存未命中:需要磁盘IO
for doc_id in range(segment.max_doc):
# 每次判断可能需要多次磁盘读取
# 1. 读取倒排列表
# 2. 读取正排数据(如果需要_source判断)
# 3. 读取doc values(如果需要字段判断)
if check_document(doc_id, query):
count += 1
return count
3.分布式系统的放大效应 🌐
3.1 分片并行但协调复杂
协调节点
分片1: 1000万文档
分片2: 1000万文档
分片3: 1000万文档
分片4: 1000万文档
扫描全部
计算分数
维护聚合
扫描全部
计算分数
维护聚合
扫描全部
计算分数
维护聚合
扫描全部
计算分数
维护聚合
返回结果到协调节点
内存合并所有结果
可能触发OOM
3.2 网络和内存开销
一个 10 10 10 分片集群的精确计数:
每个分片需要:
- CPU:扫描全部文档并判断
- 内存:维护计数器、聚合中间结果
- 磁盘 IO:读取索引文件
协调节点需要:
- 内存:合并所有分片的结果(可能很大)
- 网络:接收所有分片的数据
最坏情况:某个分片特别慢,拖累整个查询
4.断路器和资源限制 ⚡
4.1 内存断路器机制
java
// Elasticsearch 的请求断路器
public class RequestCircuitBreaker {
private final long memoryLimit;
private long currentUsage = 0;
public void checkLimit(long additionalMemory) {
currentUsage += additionalMemory;
if (currentUsage > memoryLimit) {
// ❌ 抛出异常,查询失败!
throw new CircuitBreakingException(
"Too much memory used by request"
);
}
}
}
// 计数如何触发断路器:
while (scanning_documents) {
for (each matching document) {
// 每个匹配文档都增加内存使用
circuitBreaker.checkLimit(document_size);
if (has_aggregations) {
// 聚合更耗内存
circuitBreaker.checkLimit(aggregation_bucket_size);
}
}
}
5.对比实验:有计数 vs 无计数 📊
5.1 测试环境
json
PUT /performance-test
{
"settings": {
"number_of_shards": 5,
"number_of_replicas": 0
},
"mappings": {
"properties": {
"text": { "type": "text" },
"value": { "type": "integer" },
"category": { "type": "keyword" }
}
}
}
// 插入1000万文档
5.2 测试 1:只返回结果,不计数
json
GET /performance-test/_search
{
"size": 10,
"track_total_hits": false, // 不精确计数
"query": {
"match": { "text": "test" }
},
"aggs": {
"categories": {
"terms": { "field": "category" }
}
}
}
结果:
- 响应时间: 120 m s 120ms 120ms
- 内存使用:低
- 返回:
{ "total": { "value": 10000, "relation": "gte" } }
5.3 测试 2:精确计数
json
GET /performance-test/_search
{
"size": 10,
"track_total_hits": true, // 精确计数!
"query": {
"match": { "text": "test" }
},
"aggs": {
"categories": {
"terms": { "field": "category" }
}
}
}
结果:
- 响应时间: 2500 m s 2500ms 2500ms( 20 20 20 倍!)
- 内存使用:高
- 可能触发断路器
- 返回:
{ "total": { "value": 1234567, "relation": "eq" } }
5.4 性能对比表
| 场景 | 时间 | 内存 | 网络流量 | 风险 |
|---|---|---|---|---|
| 不计数 | 100 − 200 m s 100-200ms 100−200ms | 低 | 小 | 低 |
| 计数到 1 1 1 万 | 200 − 500 m s 200-500ms 200−500ms | 中 | 中 | 中 |
| 完全精确计数 | 1 − 5 s + 1-5s+ 1−5s+ | 高 | 大 | 可能失败 |
6.查询复杂度的影响 🔧
6.1 简单查询 vs 复杂查询
json
// 简单查询:计数相对快
{
"query": {
"term": { "status": "active" } // 可以直接用倒排索引统计
}
}
// Lucene 可以:直接读取倒排列表的长度 → O(1)!
// 复杂查询:必须扫描文档
{
"query": {
"bool": {
"must": [
{ "match": { "text": "error" } },
{ "script": {
"script": "doc['value'].value > params.threshold",
"params": { "threshold": 100 }
}}
]
}
}
}
// 必须:对每个文档执行脚本判断 → O(N) × 脚本复杂度
6.2 聚合查询的额外开销
json
{
"size": 0,
"track_total_hits": true, // 计数
"aggs": {
"price_ranges": {
"range": {
"field": "price",
"ranges": [
{ "to": 100 },
{ "from": 100, "to": 500 },
{ "from": 500 }
]
},
"aggs": {
"avg_rating": { "avg": { "field": "rating" } }
}
}
}
}
不仅要计数,还要:
- 1️⃣ 维护多个范围的计数。
- 2️⃣ 计算每个范围的平均值。
- 3️⃣ 在内存中维护中间结果。
7.为什么近似计数(track_total_hits: N)是聪明的设计 🎯
7.1 计数过程的优化
python
def smart_count_with_limit(limit):
count = 0
for doc in documents:
if matches_query(doc):
count += 1
# 关键优化:达到限制就停止精确计数
if count >= limit:
# 继续扫描,但不更新计数了
# 或者直接停止扫描(如果不需要更多文档)
return CountResult(value=limit, relation="gte")
# 如果没达到限制,返回精确值
return CountResult(value=count, relation="eq")
7.2 节省的资源
精确计数到 100 100 100 万 vs 近似计数到 1 1 1 万:
- ✅ CPU 节省: 99 % 99\% 99% 的文档不需要判断
- ✅ 内存节省:不需要维护大计数器
- ✅ 磁盘 IO 节省:可能提前停止扫描
- ✅ 网络节省:协调节点压力小
结果差异:
- 精确:"共
1,234,567个结果" - 近似:"至少有
10,000个结果"
用户体验:几乎没差别!
8.实际场景中的权衡 💡
8.1 场景分析:电商网站搜索
- 用户搜索 "
手机" - 业务需求:
- 快速展示前 20 20 20 个最相关商品 ✅(需要排序)
- 显示大概有多少结果 ✅(帮助用户决策)
- 精确的
1234567个结果 ❌(用户不关心!)
javascript
// 技术实现:
const searchConfig = {
size: 20,
track_total_hits: 10000, // 精确计数到1万,够了!
query: { ... },
sort: [{ _score: 'desc' }]
};
// 返回给用户:
{
total: { value: 10000, relation: 'gte' }, // "至少1万个手机"
hits: [...] // 最相关的20个
}
- 用户看到:"
显示 1-20 条,共 10000+ 条结果" - 用户决策:"
噢,很多结果,我可以加筛选条件"
8.2 什么时候需要精确计数 ?
✅ 需要精确计数的场景:
- 1️⃣ 财务对账:"必须知道精确的交易笔数"
- 2️⃣ 报表审计:"法规要求精确数字"
- 3️⃣ 分页总数:"需要知道总页数"(但可以考虑其他方案)
- 4️⃣ 数据导出:"需要知道要导出多少条"
❌ 不需要精确计数的场景:
- 1️⃣ 用户搜索:"大概数量就够了"
- 2️⃣ 监控告警:"超过阈值就告警,具体数字不重要"
- 3️⃣ 趋势分析:"相对变化比绝对数字重要"
- 4️⃣ 推荐系统:"相关度比数量重要"
9.性能优化建议 🚀
9.1 分层查询策略
json
// 第一步:快速近似查询
GET /data/_search
{
"size": 0,
"track_total_hits": 1000, // 快速近似
"query": { ... }
}
// 第二步:如果需要精确数字,再单独计数
GET /data/_count // 专用计数API,更高效
{
"query": { ... }
}
// 第三步:获取数据(如果需要)
GET /data/_search
{
"size": 100,
"track_total_hits": false, // 不重复计数
"query": { ... }
}
9.2 使用专用计数 API
json
// _count API 比 _search 更高效
GET /data/_count
{
"query": {
"term": { "status": "active" }
}
}
// 返回:{ "count": 1234567 }
优势:
- ✅ 不计算分数
- ✅ 不排序
- ✅ 不返回文档
- ✅ 内存使用更少
9.3 预计算和缓存
json
// 对常用查询预计算总数
POST /cache/_doc
{
"query_hash": "abc123",
"total_count": 1234567,
"expire_at": "2024-01-01T00:00:00Z"
}
// 查询时先检查缓存
GET /cache/_search
{
"query": {
"term": { "query_hash": "current_query_hash" }
}
}
10.总结:为什么 "只是O(N)" 却影响巨大 📈
- N 很大:现代应用的数据量在亿、十亿级别。
- 常数因子很大:每次判断都涉及磁盘 IO、内存分配、网络通信。
- 分布式放大:每个分片都要执行,协调节点要合并。
- 资源限制:内存断路器会提前终止查询。
- 机会成本:计数占用的资源不能用于其他查询。
直观类比
- 不是 "
数 100 个苹果" 的 O ( N ) O(N) O(N) - 而是 "
数 100 个城市的每个苹果" 的 O ( N ) O(N) O(N):- 要去 100 个城市(分片)
- 每个城市打开仓库(段文件)
- 检查每个苹果是否合格(查询判断)
- 向总部报告(网络通信)
- 总部合并所有报告(协调节点)
看起来是 O ( N ) O(N) O(N),但每个 N N N 的代价都很高!
所以,track_total_hits 参数让你可以控制:
- 要精度 (设为
true/ 大数):慢,但准确 - 要速度 (设为
false/ 小数):快,但近似
这是一种明智的工程权衡,让你根据业务需求选择最合适的方案。