ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Elasticsearch 查询性能实测:9000 万日志数据 9 类 DSL 延迟对比(term / match / wildcard / 深分页 / 聚合)

Elasticsearch 查询性能实测:9000 万日志数据 9 类 DSL 延迟对比(term / match / wildcard / 深分页 / 聚合) 做日志平台或者检索系统选型时Elasticsearch 到底能扛多大数据量、各类查询到底多快是最常被问到的问题。官方文档和网上的理论文章很多但真正拿一套真实规模的数据、把常用查询类型逐一跑一遍并给出延迟数字的资料并不多。本文分享一次日志检索模块上线前的性能摸底测试在 3 节点 8C16G 的 Elasticsearch 集群上用9000 万条真实日志数据把日常检索中最常用的 9 类 DSL 查询——term 精确匹配、match 全文检索、range 范围、terms 聚合、multi_match 多字段、wildcard 模糊、filter 过滤、bool 复合条件、search_after 深分页——逐项实测延迟从37ms 到 3054ms相差 80 倍以上。每类查询都附上完整 DSL、真实响应和原理分析最后给出慢查询的优化建议。环境与数据均为真实测试环境响应时间为实测值不同时刻执行会有轻微差异索引名、IP 等已做脱敏处理。一、测试环境与数据说明项目配置集群规模3 节点单节点资源8C 16G数据量约 9000 万条日志单个索引分片数3 主分片单条日志含原始报文 raw_message、主机 IP、日志来源路径、时间戳等字段测试方式通过 Kibana Dev Tools或等价的curl请求直接执行 DSL关注响应中的took字段毫秒。所有查询均在同一数据集上执行避免不同索引带来的干扰。有两个前置知识点先说明后面的结果都能用它们解释hits.total.value默认只数到 10000。注意下面多数响应里total.value是10000且relation: gte——这不是真的只有一万条命中而是 ES 默认不精确统计超过 1 万的总数track_total_hits默认为 10000。只有显式传track_total_hits: true时才会得到精确总数而这个计数本身也是有代价的后面 wildcard 一节会看到对比。查询是否算分影响性能。query上下文如match要计算相关度评分并排序filter上下文如term、range包在 filter 里只判断匹配与否、不算分且可缓存。同样的条件放在两个上下文里延迟可能差数倍。下面按实测延迟从低到高的顺序逐类展开。二、九类查询实测1. term 精确匹配37ms测试目标精确匹配某个字段为特定值的文档例如日志来源路径等于某个文件。GETlogs_app-000002/_search{query:{term:{log_source:/var/log/app/app-1.log}},size:1000}实测响应节选{took:37,timed_out:false,_shards:{total:3,successful:3,skipped:0,failed:0},hits:{total:{value:10000,relation:gte},max_score:0.43620282,hits:[]}}37ms全场最快。原理term 查询直接拿精确值去倒排索引的 term 字典里做一次查找term dictionary 是排序的FST 结构定位极快然后合并对应的 posting list。不做分词、不算复杂评分、命中后按预先算好的 score 直接取 top N。注意一个常见的坑term查询用于text类型字段时查询词不会被分词而索引时字段分过词结果往往是查不到。对 IP、路径、状态码这类精确值字段应该定义为keyword类型。2. match 单字段全文检索164ms测试目标基于单个关键字或短语的简单全文检索最接近在日志里搜一个词的场景。GETlogs_app-000002/_search{query:{match:{raw_message:systemd}},size:1000}实测响应节选{took:164,timed_out:false,_shards:{total:3,successful:3,skipped:0,failed:0},hits:{total:{value:10000,relation:gte},max_score:7.409615,hits:[]}}164ms。match 比 term 慢一个量级主要开销在两处查询词先经过分词器得到一个或多个词项每个词项都要查倒排命中集合大本例命中远超 1 万所有命中文档都要计算 BM25 相关度评分再按分数做全局归并排序取 top 1000。size越大需要跨分片归并的候选越多返回也越慢所以检索页务必控制单页返回条数。3. range 范围查询167ms测试目标基于数值或日期字段的区间过滤日志检索里最高频的条件——“最近 N 小时”。GETlogs_app-000002/_search{query:{range:{save_time:{gte:2024-03-01 17:06:17.083,lte:2024-07-31 17:06:17.083}}},size:1000}实测响应节选{took:167,timed_out:false,hits:{total:{value:10000,relation:gte},max_score:1.0,hits:[]}}167ms。时间范围命中了海量文档数月的数据但因为范围查询走的是filter语义的执行路径本例直接放在 query 里ES 对 range 无评分必要仍按 constant score 处理延迟与 match 相当。日志场景的实践建议时间范围条件尽量放进bool.filter而不是must既能利用缓存又避免无意义的评分计算。4. terms 聚合统计781ms测试目标统计分析类查询按主机 IP 统计日志量分布——日志大屏、容量统计的典型查询。GETlogs_app-000002/_search{size:0,aggs:{hostname_count:{terms:{field:host_ip}}}}实测响应节选{took:781,timed_out:false,hits:{total:{value:10000,relation:gte},max_score:null,hits:[]},aggregations:{hostname_count:{doc_count_error_upper_bound:0,sum_other_doc_count:0,buckets:[{key:192.168.10.11,doc_count:86693981},{key:192.168.10.29,doc_count:5384188},{key:192.168.30.54,doc_count:180519},{key:192.168.10.12,doc_count:87578},{key:192.168.10.30,doc_count:29454},{key:192.168.10.65,doc_count:6003},{key:192.168.10.26,doc_count:634},{key:192.168.10.35,doc_count:34},{key:192.168.10.64,doc_count:14}]}}}781ms。聚合要遍历所有命中文档的host_ip字段值global ordinal 机制会把字符串映射为整数再做统计9000 万文档的遍历无法只看 top N。数据倾斜也很明显一台主机占了 8669 万条96%说明测试数据主要来自单机日志灌入——生产上分布会更均匀但聚合的遍历成本量级不变。两个优化方向高频统计指标预聚合写入时或定时任务聚合成小结果表查询时直接读以及缩小聚合的时间范围先过滤再聚合。5. multi_match 多字段查询1076ms测试目标同一个关键字同时在多个字段中搜索比如IP 可能出现在主机字段也可能出现在日志正文里。GETlogs_app-000002/_search{query:{multi_match:{query:192.168.10.11,fields:[host_ip,raw_message]}}}实测响应节选{took:1076,timed_out:false,hits:{total:{value:10000,relation:gte},max_score:10.522722,hits:[]}}1076ms。multi_match 本质是把查询词在每个字段上各做一次 matchbest_fields 策略下取每个文档各字段评分的最高值字段数翻倍倒排查询和评分开销接近翻倍且raw_message这种大文本字段的词项命中量巨大进一步放大了排序开销。优化思路能确定字段就不要多字段盲搜确实要跨字段检索考虑写入时用copy_to把需要统一检索的字段汇聚到一个组合字段单字段 match 的成本远低于 multi_match。6. wildcard 通配符查询1635ms测试目标等价于 MySQL 的LIKE %xxx%模糊查询例如按日志文件名的片段搜来源路径。POSTlogs_app-000002/_search{track_total_hits:true,query:{wildcard:{log_source:*logtest*}}}实测响应节选{took:1635,timed_out:false,hits:{total:{value:83079669,relation:eq},max_score:1.0,hits:[]}}1635ms且这条开了track_total_hits: true精确总数 8307 万。wildcard 慢的根源以*开头的模式无法利用排好序的 term 字典做前缀定位回想二分查找需要有序前缀只能逐个扫描 term 字典里的每个词项做正则匹配字典里词项越多越慢。这也是所有搜索引擎慎用前后通配的原因。更值得记录的是它的对照同样 9000 万数据上一节的 terms 聚合精确遍历了全部文档只要 781ms而 wildcard 光在字典扫描加计数上就花了 1635ms其中相当一部分开销来自对 8307 万命中做精确计数track_total_hits: true——不需要精确总数的场景务必别开这个参数。替代方案高频的模糊检索需求应该在建模阶段解决——用ngram分词器把子串切进倒排或用 ES 7.9 的wildcard字段类型专为大值通配优化把字符串索引为二进制 surface 形式低频的临时排查可以退一步用match_phrase或限定字段前缀prefix查询。7. filter must 组合查询2304ms测试目标过滤器与查询组合——正文关键字是软条件算分日志任务 ID 是硬条件过滤这是告警排查页的典型查询。GETlogs_app-000002/_search{query:{bool:{must:[{match:{raw_message:执行输出}}],filter:[{term:{log_id:log-task-0001}}]}}}实测响应节选{took:2304,timed_out:false,hits:{total:{value:10000,relation:gte},max_score:5.110891,hits:[]}}2304ms。很多人以为加了 filter 会变快实测反而比单独的 match164ms慢了一个量级。原因在于条件组合方式must里的 match 命中量极大执行输出是测试数据的常见输出词ES 需要先对海量候选算分再用 filter 条件收敛如果倒过来——把高选择性的 term 条件放在前面收敛候选集评分开销会急剧缩小。实践建议bool组合里把选择性强能砍掉大部分数据的条件放 filter把全文 match 这类弱条件放 must并尽量让 filter 先执行ES 优化器通常会把 filter 前置但条件本身的选择性决定了收敛效果。同一条件反复出现时 filter 上下文还有 node/query 级缓存红利。8. bool must 复合条件查询2702ms测试目标多个关键字组合查询——主机 IP 加正文关键词的两条件检索。GETlogs_app-000002/_search{query:{bool:{must:[{match:{host_ip:192.168.10.11}},{match:{raw_message:执行输出}}]}}}实测响应节选{took:2702,timed_out:false,hits:{total:{value:10000,relation:gte},max_score:5.4751234,hits:[]}}2702ms。两个条件都放在must里意味着两个都要评分、结果集要做带分数的交集归并。其中host_ip是精确值却用了match会先分词再查倒排纯属浪费——这就是上一节说的精确值用 term、放 filter的反面教材。把 IP 条件改成filter term后这条查询的延迟可以压到几百毫秒级对应上一节的结构。这也是真实业务代码里最常见的性能问题图省事全部用match塞进must数据量小的时候无感上了规模之后每一处都变成慢查询。9. search_after 深分页3054ms测试目标大数据集下的翻页——跳到结果集深处取一批数据导出、对账场景常见。GETlogs_app-000002/_search{track_total_hits:true,size:1000,sort:[{save_time:{order:asc}}],search_after:[1718869087580],query:{match_all:{}}}实测响应节选{took:3054,timed_out:false,hits:{total:{value:10000,relation:gte},max_score:null,hits:[]}}3054ms九类查询中最慢。先说清楚这已经是深分页的正确姿势。如果用传统的from: 100000, size: 1000每个分片都要取回from size条数据做全局排序from越深越慢且默认max_result_window10000直接拒绝请求。search_after用上一页的排序值做游标无状态、不触发窗口限制是官方推荐的深翻页方案。即便如此它仍是本次最慢的查询原因有三个match_all命中全部 9000 万文档、按时间字段排序需要跨分片大归并、又开了track_total_hits: true做精确计数。三项叠加3000ms 不冤。生产上的导出类需求应该叠加时间范围过滤 较小的 size 关闭精确计数必要时改用 PITpoint in time配合 search_after 保证翻页一致性。三、实测结果汇总排名查询类型典型场景实测延迟主要瓶颈1term 精确匹配按来源路径/IP 精确过滤37ms几乎无瓶颈2match 全文检索日志正文搜关键字164ms分词 BM25 评分排序3range 范围查询时间范围过滤167ms大命中集 constant score4terms 聚合按主机统计日志量781ms全量文档遍历5multi_match 多字段关键字跨字段搜1076ms多字段倒排 评分翻倍6wildcard 模糊LIKE %xx% 检索1635msterm 字典全扫描 精确计数7filter must 组合硬条件 软条件排查2304ms大命中集先评分后收敛8bool must 复合多关键字组合检索2702ms双条件均评分 归并9search_after 深分页深翻页 / 导出3054ms全量排序归并 精确计数最快的 term 与最慢的深分页差82 倍。延迟排序也给出了直观的结论精确条件term/range 评分条件match 遍历类聚合 字典扫描wildcard 全量排序深分页。四、数据量级的影响1000 万 vs 9000 万同一套测试还对比了 1000 万级数据单分片指标索引约 430~530 万命中下的 wildcard 查询GETmetric_test/_search{track_total_hits:true,query:{wildcard:{component:*ens1*}},size:1000}{took:1184,_shards:{total:1,successful:1,skipped:0,failed:0},hits:{total:{value:427675,relation:eq},max_score:1.0,hits:[]}}同数据集换一个更宽泛的模式*nn*命中 53 万后took降到 163ms。同样的 wildcard9000 万数据 1635ms、1000 万数据 1184ms而把命中量从 427 万降到 53 万后直接降到 163ms——说明 wildcard 的延迟对term 字典规模和命中数量都敏感量级上升时它是最先恶化的查询类型之一。整体上 1000 万级数据的各类查询延迟区间在 100ms~1.7s与 9000 万级相比恶化幅度远小于线性ES 的分布式分片归并在这中间起了摊薄作用。五、慢查询优化清单把本次实测暴露的问题收敛成一份可落地的检查清单精确值字段用 keyword term绝不 match。IP、路径、状态码、ID 一律keyword映射查询时term进filter。本次 bool must 慢查询2702ms的主要病灶就是精确字段用了 match。强选择条件放 filter 且前置全文检索放 must。让 filter 先把候选集收敛到小再对少量文档评分。控制 size 与命中规模。检索页 size 控制在几百到 1000配合terminate_after、min_score截断不需要的深海数据。不查全量总数就不开 track_total_hits。默认 1 万上限是保护本次 wildcard 与深分页的延迟里精确计数都占了大头。wildcard 用建模解决ngram 分词、wildcard字段类型或search_as_you_type实在要临时模糊至少限定字段、避免前置*。深翻页一律 search_afterPIT导出任务叠加时间过滤与批量 size禁用 from/size 深跳页。高频统计走预聚合。大屏类聚合本次 781ms写入时或离线聚合成小表查询延迟可以从百毫秒级降到毫秒级——这也是我们在日志大屏场景最终采用的方案聚合 DSL 的写法可以参考另一篇《Elasticsearch 聚合 DSL 实战》。时间范围是日志检索的第一过滤器。所有查询都应默认带时间范围并放 filter这既是语义需要也让缓存命中最大化。六、总结这次摸底测试的价值在于把ES 快不快这个模糊问题拆成了哪类查询、什么数据量、什么写法下的具体数字3 节点 8C16G、9000 万日志规范写法的精确/范围查询在200ms 内规范的多条件组合能压在几百毫秒而 wildcard、深分页、全量聚合这类重量级操作在秒级且对数据量增长最敏感。换句话说ES 的性能下限由硬件和数据量决定但上限很大程度由 DSL 写法决定——同样的业务需求term/filter/search_after 的组合与 match/wildcard/from-size 的组合之间隔着 10 倍以上的延迟差。建议把本文的九类查询做成上线前的回归用例每次索引模板或分片调整后跑一遍延迟回归一目了然。系列文章检索 DSL 的具体写法时间范围、去重、分钟级统计、最新日志Elasticsearch 日志检索 DSL 实战日志大屏聚合 DSL 与 bucket_script 比率计算Elasticsearch 聚合 DSL 实战海量日志的索引粒度与分片规划Elasticsearch 日增 30TB 日志架构实战
返回列表