首页 文章 精选 留言 我的

精选列表

搜索[Spring Framework],共10000篇文章
优秀的个人博客,低调大师

《Spring ES实战》第5招 Query要分两类

# Elasticsearch Query DSL 实战:把“相关性”与“过滤”分开 ```text 旧方案:所有条件都塞进 must,Text 字段用 term 硬匹配,模糊词用 wildcard 兜底 冲突:数据明明存在却查不到,评分被无关条件污染,通配符查询拖垮 CPU 后果:搜索结果时灵时不灵,排序忽高忽低,集群在促销高峰频繁 GC ``` 很多团队把 Elasticsearch 当“能分词的 MySQL”,把 SQL 的 WHERE 习惯直接搬进 Query DSL。 结果就是:Text 字段用 term 查不到,数字字段用 match 乱匹配,wildcard 满天飞。 我是老李,过去半年在深圳一家电商公司做搜索中台,亲眼见过一个“明明有货却搜不到”的线上事故,根因就是 Query DSL 里把全文查询和精确过滤混成了一锅粥。 为什么数据存在却查不到? 什么时候条件应该参与评分,什么时候只负责过滤? 如果把评分和过滤混在一起,线上会付出什么代价? # 01、故事 ```text 周五 23:47→电商搜索中台→大促预热搜索“华为手机”→冲突:有货却搜不到 ``` 那天是周五晚上 23:47,我还在公司。运营在群里发了一张截图:App 搜索“华为手机”,结果页空白,但商品后台明明有 300 多台库存。 任务是十分钟内恢复搜索,否则大促预热页要回滚。我打开 Kibana,先跑了一遍线上查询,确实返回 0 条。 再看商品索引,文档存在,status 是 1,keyword 字段里也有“华为手机”。我第一反应是字段映射有问题,但 mapping 显示 keyword 是 Text 类型。 时间一分一秒过去,群里消息越刷越快。我盯着那个查询体,突然意识到问题可能不在数据,而在查询本身。 > 深夜告警催人醒, > 有货无果似幽灵。 > 索引文档皆安在, > 查询深处藏祸根。 # 02、问题 ```text 需求变化:从“关键词匹配”扩展到“品牌+类目+价格+状态”多维筛选 冲突:Text 字段用 term 查询,分词后索引词与查询词不一致 后果:数据存在但查不到,过滤条件参与评分,排序被无关字段干扰 ``` 最初的搜索只做关键词匹配,一个 match 就够。后来运营要求按品牌、类目、价格、状态筛选,开发就把所有条件都塞进 bool 的 must 里。 问题出在 keyword 字段。它是 Text 类型,写入时被 standard 分词器拆成“华为”和“手机”两个词。而 term 查询不做分词,拿“华为手机”整串去倒排索引里找,当然找不到。 更隐蔽的问题是:brand、category、status 这些精确值字段也放在 must 里,它们会参与评分。一个“华为”品牌匹配的得分,可能比关键词“手机”的得分还高,排序就乱了。 可验证的验收标准是:搜索“华为手机”能返回结果;按品牌“华为”筛选后结果只含华为;价格区间 3000-6000 能正确过滤;status=1 只返回在售商品。 > 分词写入倒排里, > 整串查询空手归。 > 过滤误入评分场, > 排序错乱用户离。 # 03、原理 ```text 核心机制:Text 字段分词后建倒排索引,term 查询不做分词直接匹配索引词 ``` Elasticsearch 的查询分两大类:Full-text Query 和 Term-level Query。前者对查询词做分词,后者不做分词。 match 查询会对输入做分词,然后拿每个词去倒排索引里找。比如“华为手机”会被拆成“华为”和“手机”,分别匹配。 term 查询不做分词,拿整个输入去倒排索引里精确匹配。如果字段是 Text 类型,索引里存的是分词后的词,term 拿整串去匹配,必然失败。 一个反直觉的判断是:Text 字段上 term 查询“数据存在却查不到”不是 bug,而是符合设计预期的行为。 要精确匹配 Text 字段,应该用 keyword 子字段,或者把字段映射为 Keyword 类型。 另一个反直觉判断:filter 不参与评分,但它的性能通常比 must 更好,因为它可以利用缓存。把精确条件放进 filter,既正确又快。 > 全文查询问相关, > 词项查询问相等。 > 分词与否是分界, > 张冠李戴必成空。 # 04、架构 ```text 输入:用户关键词+品牌+类目+价格区间+状态→本模块:Query DSL 组装→输出:相关度排序的过滤后结果 ``` 查询层分两条路径:关键词走全文查询,负责评分;品牌、类目、价格、状态走过滤,只负责筛选。 bool 查询是组合器,四个子句各司其职:must 必须匹配且参与评分;should 可选匹配,匹配了加分;filter 必须匹配但不评分;must_not 必须不匹配。 收益是排序只由关键词相关性决定,过滤条件不干扰评分。代价是查询体变复杂,需要团队约定字段类型和查询选择规范。 适用条件是:有明确的关键词搜索需求,同时有多个精确筛选维度。如果只有精确筛选没有关键词,直接用 filter 就够了。 > 一层查询两条路, > 相关过滤各司职。 > 评分只问关键词, > 筛选不扰排序分。 # 05、第一版实战 ```text 环境→配置→代码→启动→验证→V1 ``` 环境:Elasticsearch 8.11.3,Kibana 8.11.3,本地 Docker 单节点。版本需按当前官方文档核验。 先创建索引,keyword 用 Text 类型并带 keyword 子字段,brand、category、status 用 Keyword 类型,price 用 double: ```json PUT /products { "mappings": { "properties": { "keyword": { "type": "text", "fields": { "raw": { "type": "keyword" } } }, "brand": { "type": "keyword" }, "category": { "type": "keyword" }, "price": { "type": "double" }, "status": { "type": "integer" } } } } ``` 写入测试数据: ```json POST /products/_bulk { "index": { "_id": 1 } } { "keyword": "华为手机 Mate60", "brand": "华为", "category": "手机", "price": 5999, "status": 1 } { "index": { "_id": 2 } } { "keyword": "华为手机 P60", "brand": "华为", "category": "手机", "price": 4388, "status": 1 } { "index": { "_id": 3 } } { "keyword": "小米手机 14", "brand": "小米", "category": "手机", "price": 3999, "status": 0 } ``` V1 查询:关键词用 multi_match 查 keyword 和 brand,品牌、类目、价格、状态全部塞进 must: ```json POST /products/_search { "query": { "bool": { "must": [ { "multi_match": { "query": "华为手机", "fields": ["keyword", "brand"] } }, { "term": { "brand": "华为" } }, { "term": { "category": "手机" } }, { "range": { "price": { "gte": 3000, "lte": 6000 } } }, { "term": { "status": 1 } } ] } } } ``` 启动验证:在 Kibana Dev Tools 执行上述请求。示例输出: ```text 示例输出: { "hits": { "total": { "value": 2 }, "hits": [ { "_id": "1", "_score": 2.5 }, { "_id": "2", "_score": 2.2 } ] } } ``` 成功标准:返回 2 条华为手机,价格都在 3000-6000,status 都是 1。V1 看起来能跑通。 > 索引建好数据入, > 查询体里五条件。 > 看似结果皆正确, > 隐患已在暗处生。 # 06、排查 ```text 诊断树:现象→怀疑→检查→证据→根因→修复 ``` 问题一:搜索“华为手机”时,如果用户输入“华为 手机”中间有空格,multi_match 能匹配。 但如果用户输入“华为手机”整串,而 keyword 字段被分词,match 依然能匹配。 真正的问题出在另一个场景。 运营反馈:筛选品牌“华为”后,搜索“华为手机”返回 0 条。我怀疑是 term 查询在 Text 字段上失效。 检查 mapping: ```json GET /products/_mapping ``` 证据:keyword 字段是 text 类型,brand 是 keyword 类型。但查询里 term 用在 brand 上,这是对的。问题不在这里。 继续排查:运营说“华为手机”搜不到,是在一个旧索引上。那个索引的 keyword 字段没有 keyword 子字段,且查询里用了 term 查 keyword。 检查旧索引查询: ```json POST /old_products/_search { "query": { "term": { "keyword": "华为手机" } } } ``` 证据:返回 0 条。根因确认:Text 字段被分词,term 拿整串匹配失败。修复:改用 match 或 keyword.raw 子字段。 **错误尝试:** 我一开始以为是 status 字段类型问题,把 status 从 integer 改成 keyword,结果查询依然时好时坏。后来才发现真正的问题是 term 查 Text 字段。 > 现象诡异查映射, > 怀疑类型先入主。 > 证据指向分词处, > 根因方显真面目。 # 07、优化 ```text V1:所有条件塞 must→证据:过滤条件参与评分,排序被干扰→V2:精确条件移入 filter ``` V1 的问题是 brand、category、status、price 都放在 must 里,它们会参与评分。 一个品牌完全匹配的文档,可能因为 brand 字段短、匹配度高,得分超过关键词更相关的文档。 优化:把精确条件全部移入 filter,只保留 multi_match 在 must 里: ```json POST /products/_search { "query": { "bool": { "must": [ { "multi_match": { "query": "华为手机", "fields": ["keyword", "brand"] } } ], "filter": [ { "term": { "brand": "华为" } }, { "term": { "category": "手机" } }, { "range": { "price": { "gte": 3000, "lte": 6000 } } }, { "term": { "status": 1 } } ] } } } ``` 这样排序只由 multi_match 的相关性决定,过滤条件不干扰评分。同时 filter 可以利用 Elasticsearch 的查询缓存,性能更好。 > 五条件里四过滤, > 何必都进评分场。 > 移入 filter 不评分, > 排序纯粹性能强。 # 08、演进 ```text V2:must 只留关键词,filter 管筛选→能力:排序纯净+缓存加速→遗留:should 场景未覆盖 ``` 用 V1 同一组请求验证:搜索“华为手机”,品牌华为,类目手机,价格 3000-6000,status=1。 V2 返回 2 条,得分只由关键词相关性决定。示例输出: ```text 示例输出: { "hits": { "total": { "value": 2 }, "hits": [ { "_id": "1", "_score": 1.8 }, { "_id": "2", "_score": 1.5 } ] } } ``` 收益是排序稳定,过滤条件变化不影响得分。代价是查询体变长,需要团队理解 bool 四个子句的语义。 遗留问题:should 子句还没用上。比如“华为”和“荣耀”两个品牌,用户可能都想要,should 可以加分但不强制匹配。 > 同组请求前后比, > 排序纯净分不虚。 > 缓存加速过滤快, > should 场景待补齐。 # 09、洞见 ```text 从现象到判断:全文查询回答“有多相关”,过滤回答“符不符合条件” ``` ## 把“相关性”和“过滤”分开是 Elasticsearch 查询设计的第一原则 ```text must 管评分,filter 管筛选,两者不可混用 ``` 全文查询回答“有多相关”,过滤回答“符不符合条件”。把两者分开,排序才纯净,缓存才生效。混在一起,既污染评分,又拖慢查询。 ## 反直觉判断:Text 字段上 term 查询查不到,不是 bug,是设计预期 ```text Text 分词建索引,term 不分词去匹配,必然错位 ``` 很多人遇到“数据存在却查不到”,第一反应是 Elasticsearch 出问题了。实际上这是 Text 字段分词机制的直接结果。要精确匹配,用 keyword 子字段或 Keyword 类型。 ## 反直觉判断:wildcard 不应替代 Analyzer ```text wildcard 是精确匹配的兜底,不是分词匹配的替代品 ``` wildcard 查询不做分词,它是对词项做通配符匹配。用它来模拟模糊搜索,会扫描大量词项,CPU 飙升。模糊搜索应该靠 Analyzer 的分词和同义词,而不是 wildcard。 ## filter 不评分,但通常比 must 更快 ```text filter 结果可缓存,must 每次都要算分 ``` 精确条件放进 filter,Elasticsearch 可以缓存过滤结果。下次相同过滤条件直接复用,跳过评分计算。这是性能优化的关键一步。 > 相关过滤本两途, > 混为一谈必迷途。 > 词项查询不分词, > 通配岂能替分词。 # 10、系统落地 ```text 入口:App 搜索框→服务:搜索中台→本模块:Query DSL 组装→存储:Elasticsearch→未实现:should 多品牌加分 ``` 原来搜索中台只有一个简单的 match 查询。本篇增加了 bool 组合查询,把关键词、品牌、类目、价格、状态分开处理。 现在能做什么:用户搜索关键词,同时按品牌、类目、价格、状态精确筛选,排序只由关键词相关性决定。 下一篇缺什么:should 子句的多品牌加分场景还没实现,比如用户搜“华为荣耀”,两个品牌都匹配的文档应该排在前面。 官方文档参考:Elasticsearch 官方文档主页 https://www.elastic.co/docs > 搜索中台初长成, > 多维筛选各归位。 > 关键词定排序分, > 过滤条件不扰分。 # 11、小结 ```text 三问三答:为什么查不到→term 不分词;何时评分→must/should;选错后果→排序乱性能差 ``` 为什么数据存在却查不到?因为 Text 字段被分词,term 查询拿整串去匹配分词后的索引词,必然失败。 什么时候条件参与评分,什么时候只负责过滤?must 和 should 参与评分,filter 和 must_not 只负责过滤。关键词放 must,精确条件放 filter。 选错会付出什么代价?排序被无关条件污染,用户看到不相关的结果排在前面;filter 缓存失效,查询性能下降,高峰时集群压力增大。 当前状态:V2 已上线,搜索“华为手机”能正确返回结果,筛选条件不干扰排序。 > 三问三答理分明, > 查询设计有章循。 > 相关过滤分开走, > 线上稳定用户欣。 # 12、作业 ```text 理解→实战→排障→架构判断 ``` ## 理解题:match 和 term 在 Text 字段上的行为有什么区别? match 会对查询词做分词,然后拿每个词去倒排索引里匹配。term 不做分词,拿整串去匹配。Text 字段写入时被分词,所以 term 查 Text 字段通常查不到,match 能查到。 ## 实战题:给商品索引增加一个 should 子句,让搜索“华为手机”时,品牌为“荣耀”的文档也能返回但得分低于华为。 ```json POST /products/_search { "query": { "bool": { "must": [ { "multi_match": { "query": "华为手机", "fields": ["keyword", "brand"] } } ], "should": [ { "term": { "brand": "荣耀" } } ], "filter": [ { "term": { "category": "手机" } }, { "range": { "price": { "gte": 3000, "lte": 6000 } } }, { "term": { "status": 1 } } ] } } } ``` 荣耀品牌文档会返回,但得分低于华为品牌文档,因为 should 只加分不强制匹配。 ## 排障题:线上搜索“苹果手机”返回 0 条,但商品后台有货。请写出排查步骤。 第一步,检查索引 mapping,确认 keyword 字段类型。 第二步,用 match 查询测试,如果能查到,说明是 term 查询在 Text 字段上失效。 第三步,检查查询体,看是否用了 term 查 Text 字段。 第四步,修复:改用 match 或 keyword.raw 子字段。 ## 架构判断题:有人说“把所有条件都放进 must,这样最安全,不会漏掉任何条件”。这个说法对吗? 不对。must 里的条件都参与评分,精确条件放进 must 会污染排序。而且 must 不能利用 filter 缓存,性能更差。正确做法是:关键词放 must,精确条件放 filter。 > 理解实战排障题, > 架构判断见真章。 > 查询设计有原则, > 相关过滤不可混。 # 13、升华 ```text 工程结论:查询设计的第一原则是把“相关性”和“过滤”分开 ``` 有依据的结论是:Full-text Query 回答“有多相关”,Filter 回答“符不符合条件”。把两者分开,排序才纯净,缓存才生效,性能才稳定。 term 查 Text 字段失效不是 bug,是分词机制的直接结果。wildcard 不应替代 Analyzer,模糊搜索应该靠分词和同义词。 下一篇可以深入 should 的多品牌加分场景,以及 function_score 如何做更精细的相关性调权。 > 查询设计第一则, > 相关过滤两分明。 > 词项通配皆有界, > 工程路上步步清。 **系列项目:** 电商搜索中台,服务 App 与运营后台 **已完成:** 关键词搜索、品牌/类目/价格/状态筛选、bool 组合查询 **本篇:** 区分 Full-text Query 与 Term-level Query,掌握 match、multi_match、term、terms、range、exists、prefix、wildcard、bool 及 must、should、filter、must_not **下一篇:** should 多品牌加分与 function_score 相关性调权

优秀的个人博客,低调大师

《Spring ES实战》第12招 Shard不是越多越好

# Elasticsearch 索引架构实战:从分片到段,避开分片税 ```text 旧方案:单索引多分片,盲目扩容 冲突:查询变慢,Heap 飙升,文件句柄耗尽 后果:集群不稳定,协调成本剧增 ``` 很多团队把 Elasticsearch 当黑盒,数据一多就加 Shard,结果查询反而更慢。 我是老李,过去半年在深圳一家电商公司负责搜索服务,亲眼见过一个索引从 5 个 Shard 扩到 40 个 Shard 后,P99 延迟从 200ms 涨到 1.8s。 为什么增加 Shard 有时不仅不会让查询更快,反而增加 Heap、文件句柄和协调成本? 是优先控制单 Shard 大小,还是优先增加 Shard 数量来分摊写入压力? 如果选错,会不会导致集群在高峰期频繁 Full GC,甚至节点失联? # 01、故事 ```text 凌晨 02:17→深圳南山机房→商品搜索索引扩容→分片数从 5 改到 40 ``` 那天凌晨两点十七分,我在南山机房的工位上,任务是给商品搜索索引做一次扩容,因为运营说大促前数据量要翻倍。 我打开 Kibana 的 Dev Tools,把索引模板里的 number_of_shards 从 5 改成 40,然后重建索引。 数据灌到一半,监控屏上三个数据节点的 Heap 使用率同时冲破 85%,查询线程池开始堆积。 我赶紧停掉写入,但已经晚了,几个慢查询把协调节点拖住,搜索接口开始超时。 > 凌晨两点改分片, > 四十个分片压垮三节点。 > 堆内存冲上八十五, > 搜索超时告警连成线。 # 02、问题 ```text 需求变化:数据量翻倍→分片数从 5 扩到 40 冲突:查询变慢,Heap 飙升,文件句柄耗尽 后果:协调成本剧增,集群不稳定 ``` 表面看是数据量增长,实际是分片数从 5 变成 40,每个查询要协调的分片数翻了 8 倍。 每个 Shard 都是一个 Lucene Index 实例,占用独立 Heap、文件句柄和线程资源,40 个 Shard 意味着 40 份固定成本。 可验证的验收标准是:同样 100 并发查询,P99 延迟不超过 500ms,Heap 使用率低于 75%,文件句柄数低于节点上限的 60%。 > 数据翻倍本寻常, > 分片八倍惹祸殃。 > 固定成本四十份, > 协调开销把命伤。 # 03、原理 ```text Index → Primary Shard → Replica Shard → Segment → Lucene Index ``` 一个 Index 在创建时被拆成多个 Primary Shard,每个 Primary Shard 决定数据分区,通过 Routing 把文档哈希到对应分片。 Replica Shard 是 Primary 的副本,提供查询能力和故障冗余,但不承担写入主路径。 每个 Shard 内部是一个 Lucene Index,由多个 Segment 组成,Segment 是不可变的倒排索引文件。 反直觉判断:Shard 数量越多,单次查询的并行度越高,但协调节点需要合并的结果集也越大,CPU 和内存开销非线性增长。 > 索引拆成主分片, > 路由哈希定归属。 > 副本只读不主写, > 段是不可变倒排。 # 04、架构 ```text 输入:文档写入请求、查询请求 本模块:Coordinating Node → Primary/Replica Shards → Segments 输出:查询结果合并、写入确认 边界:单 Shard 建议 10-50GB,单节点 Shard 数受 Heap 限制 ``` Coordinating Node 接收查询,广播到所有相关 Shard,每个 Shard 在本地 Lucene Index 上执行查询,返回 TopN 给协调节点做全局排序。 写入路径只走 Primary Shard,Primary 写完同步转发给 Replica,等副本确认后才返回成功。 收益是水平扩展读写能力,代价是每个 Shard 都有固定内存和文件句柄开销,适用条件是单 Shard 大小在 10 到 50GB 之间。 > 协调节点广播查询, > 各分片本地取前 N。 > 全局归并再返回, > 固定成本要算清。 # 05、第一版实战 ```text 环境:Elasticsearch 8.11.3,3 数据节点,每节点 16GB Heap 配置:商品索引 5 Primary Shards,1 Replica 代码:索引创建、文档写入、查询验证 启动:docker compose 启动 3 节点集群 验证:_cat/shards 观察分片分布,查询延迟基线 V1:5 分片,P99 200ms,Heap 60% ``` 环境是 Elasticsearch 8.11.3,三个数据节点,每个节点 16GB Heap,用 Docker Compose 启动。 先创建商品索引,5 个 Primary Shard,1 个 Replica,总共 10 个 Shard。 ```bash curl -X PUT "localhost:9200/products_v1" -H 'Content-Type: application/json' -d' { "settings": { "number_of_shards": 5, "number_of_replicas": 1 }, "mappings": { "properties": { "title": { "type": "text" }, "price": { "type": "double" }, "category": { "type": "keyword" }, "created_at": { "type": "date" } } } }' ``` 写入 500 万条商品文档,用 bulk API 分批灌入。 ```bash curl -X POST "localhost:9200/_bulk" -H 'Content-Type: application/x-ndjson' --data-binary @products_bulk.json ``` 查看分片分布,确认 10 个 Shard 均匀分布在 3 个节点上。 ```bash curl -s "localhost:9200/_cat/shards/products_v1?v&h=index,shard,prirep,state,node,store" ``` ```text 示例输出: index shard prirep state node store products_v1 0 p STARTED node-1 8.2gb products_v1 0 r STARTED node-2 8.2gb products_v1 1 p STARTED node-2 8.1gb products_v1 1 r STARTED node-3 8.1gb products_v1 2 p STARTED node-3 8.3gb products_v1 2 r STARTED node-1 8.3gb products_v1 3 p STARTED node-1 8.0gb products_v1 3 r STARTED node-2 8.0gb products_v1 4 p STARTED node-3 8.2gb products_v1 4 r STARTED node-1 8.2gb ``` 执行查询验证,按分类过滤并按价格排序,取前 20 条。 ```bash curl -s "localhost:9200/products_v1/_search" -H 'Content-Type: application/json' -d' { "query": { "term": { "category": "electronics" } }, "sort": [ { "price": "desc" } ], "size": 20 }' ``` ```text 示例输出: { "took": 187, "hits": { "total": { "value": 12430 }, "max_score": null }, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 } } ``` 成功标准:查询 took 低于 200ms,_shards.total 等于 5,所有分片 successful。 > 五主五副十份片, > 三节点上均匀摊。 > 查询两百毫秒内, > 基线稳定心不慌。 # 06、排查 ```text 诊断树: 查询变慢 ├── 怀疑:分片数过多 │ ├── 检查:_cat/shards 统计分片总数 │ ├── 证据:40 个 Shard 分布在 3 节点,每节点 13+ Shard │ └── 根因:协调节点合并 40 个分片结果,CPU 和内存开销过大 ├── 怀疑:Heap 压力 │ ├── 检查:_cat/nodes?v&h=name,heap.percent,ram.percent │ ├── 证据:heap.percent 85%+,频繁 Young GC │ └── 根因:每个 Shard 的 Segment 缓存和查询缓存叠加 └── 怀疑:文件句柄耗尽 ├── 检查:节点上 lsof -p | wc -l ├── 证据:单节点文件句柄数超过 60000 └── 根因:每个 Segment 打开多个文件,Shard 越多句柄越多 ``` 第一个问题是查询变慢,我怀疑是分片数过多,用 _cat/shards 统计发现 40 个 Shard 分布在 3 个节点上,每节点超过 13 个 Shard。 协调节点每次查询要广播到 40 个 Shard,合并结果时 CPU 和内存开销远超 5 个 Shard 的场景。 第二个问题是 Heap 压力,用 _cat/nodes 查看节点状态。 ```bash curl -s "localhost:9200/_cat/nodes?v&h=name,heap.percent,ram.percent,cpu" ``` ```text 示例输出: name heap.percent ram.percent cpu node-1 87 92 78 node-2 85 90 75 node-3 88 93 80 ``` 每个 Shard 的 Segment 缓存和查询缓存叠加,导致 Heap 使用率冲到 85% 以上,频繁 Young GC。 第三个问题是文件句柄,登录节点执行 lsof 统计。 ```bash lsof -p $(pgrep -f elasticsearch) | wc -l ``` ```text 示例输出: 62341 ``` 每个 Segment 打开多个文件,Shard 越多句柄越多,单节点文件句柄数超过 60000,接近系统上限。 **错误尝试:** 我一开始以为是查询语句没优化,加了 constant_keyword 和 filter 缓存,结果延迟只降了 50ms,Heap 依然高企。 > 四十片查一次, > 协调节点累成痴。 > 堆高句柄都耗尽, > 优化查询也难支。 # 07、优化 ```text V1:5 分片,P99 200ms,Heap 60% 证据:40 分片,P99 1.8s,Heap 87%,句柄 62000+ V2:回到 5 分片,单分片 40GB,P99 210ms,Heap 62% ``` 核心改动是把分片数从 40 降回 5,让单 Shard 大小控制在 40GB 左右,而不是追求分片多。 重建索引,设置 5 个 Primary Shard,1 个 Replica,用 reindex 从旧索引迁移数据。 ```bash curl -X PUT "localhost:9200/products_v2" -H 'Content-Type: application/json' -d' { "settings": { "number_of_shards": 5, "number_of_replicas": 1, "refresh_interval": "5s" }, "mappings": { "properties": { "title": { "type": "text" }, "price": { "type": "double" }, "category": { "type": "keyword" }, "created_at": { "type": "date" } } } }' ``` ```bash curl -X POST "localhost:9200/_reindex" -H 'Content-Type: application/json' -d' { "source": { "index": "products_v1" }, "dest": { "index": "products_v2" } }' ``` 同时把 refresh_interval 从默认 1s 调到 5s,减少 Segment 生成频率,降低 Merge 压力。 > 四十片回五片, > 单分片四十吉。 > 刷新间隔调五秒, > 堆和句柄都松气。 # 08、演进 ```text V2:5 分片,单分片 40GB,P99 210ms,Heap 62% 能力:查询延迟稳定,Heap 余量充足,句柄数下降 60% 遗留:单分片 40GB 接近上限,未来数据增长需滚动索引 ``` 用 V1 同一组查询请求验证,P99 从 1.8s 降到 210ms,Heap 从 87% 降到 62%,句柄数从 62000 降到 25000 左右。 收益是协调成本大幅下降,每个查询只广播 5 个 Shard,合并结果集小,CPU 和内存开销可控。 代价是单分片 40GB 接近建议上限,如果数据继续增长,需要引入滚动索引或 ILM 来拆分。 遗留问题是当前方案没有处理 Hot Shard,如果某个分类数据量特别大,可能导致单个 Shard 过热。 > 五片查询二百一, > 堆余充足句柄低。 > 单片四十近上限, > 滚动索引待补齐。 # 09、洞见 ```text 从现象到判断:分片数增加反而变慢,说明固定成本主导了性能 ``` ## 分片是扩展单位,同时也是固定成本单位 ```text 每增加一个 Shard,就多一份 Lucene Index 的 Heap、句柄和线程开销 ``` 很多人把 Shard 当成免费的并行度,实际上每个 Shard 都有独立的 Lucene Index 实例,占用固定内存和文件句柄。 当分片数从 5 增加到 40,固定成本翻了 8 倍,而查询并行度的收益被协调合并的开销抵消。 ## 反直觉判断:分片越多,查询不一定越快 ```text 协调节点合并 N 个分片结果的开销是 O(N log N),N 越大越不划算 ``` 查询延迟由最慢的分片决定,分片越多,出现慢分片的概率越高,协调节点还要做全局排序。 在 3 节点集群里,40 个 Shard 意味着每个节点要处理 13 个 Shard 的查询,CPU 争抢严重。 ## 反直觉判断:单分片大小比分片数量更重要 ```text 10-50GB 的单分片大小是经验值,超出后 Merge 和 Recovery 成本剧增 ``` 单分片太小,固定成本占比高;单分片太大,Segment Merge 和故障恢复时间长。 规划索引时先估算数据总量,再反推分片数,而不是先定分片数再看数据量。 ## 搜索集群规划首先要避免分片税 ```text 分片税 = 分片数 × 单分片固定成本,规划时先算这笔账 ``` 分片税是每个查询都要支付的固定开销,包括广播、上下文切换、结果合并。 规划集群时先确定单分片目标大小,再根据数据总量计算分片数,最后验证节点 Heap 和句柄余量。 > 分片本是双刃剑, > 扩展成本两相兼。 > 固定开销算清楚, > 莫让查询交税钱。 # 10、系统落地 ```text 入口:搜索 API 网关 服务:商品搜索服务 本模块:Elasticsearch 索引架构(5 分片 + 1 副本 + 5s Refresh) 存储:3 数据节点,每节点 16GB Heap 未实现:ILM 滚动索引、Hot Shard 自动拆分 ``` 原来系统只有一个 products 索引,分片数随意设置,没有容量规划。 本篇增加了基于数据量反推分片数的方法,把索引控制在 5 个 Primary Shard,单分片 40GB。 现在能做什么:查询 P99 稳定在 210ms,Heap 余量充足,句柄数在安全范围内。 下一篇缺什么:ILM 滚动索引和 Hot Shard 检测,应对数据持续增长和热点分片。 官方文档可参考 Elasticsearch 官网的 Size your shards 指南:https://www.elastic.co/guide/en/elasticsearch/referenc e/current/size-your-shards.html > 入口网关到搜索, > 五片索引稳如钟。 > 滚动索引还未建, > 热点分片待追踪。 # 11、小结 ```text 三问三答: 为什么增加 Shard 反而变慢?→ 固定成本叠加,协调合并开销非线性增长 优先控制单 Shard 大小还是增加 Shard 数量?→ 先控制单 Shard 大小在 10-50GB,再反推分片数 选错后果?→ Heap 飙升、句柄耗尽、节点失联,集群不可用 ``` 增加 Shard 反而变慢,是因为每个 Shard 都有固定 Heap、句柄和线程开销,协调节点合并结果的成本随分片数非线性增长。 应该优先控制单 Shard 大小在 10 到 50GB,根据数据总量反推分片数,而不是盲目增加分片。 选错的后果是 Heap 使用率冲高、文件句柄耗尽、节点失联,最终整个集群不可用。 当前状态:商品索引稳定在 5 分片,P99 210ms,Heap 62%,句柄数安全。 > 三问三答理清楚, > 分片固定成本高。 > 先定单片再反推, > 集群稳定不告饶。 # 12、作业 ```text 理解→实战→排障→架构判断 ``` ## 理解题:为什么说 Shard 是扩展单位同时也是固定成本单位? 每个 Shard 内部是一个独立的 Lucene Index 实例,占用独立的 Heap 内存、文件句柄和线程资源。 增加 Shard 可以分摊写入压力、提升查询并行度,但每个 Shard 的固定成本不会消失,反而会叠加。 当分片数过多时,固定成本超过并行收益,查询反而变慢。 ## 实战题:创建一个 3 分片 1 副本的索引,写入 100 万条文档,观察 _cat/shards 分布和查询延迟 ```bash curl -X PUT "localhost:9200/homework_idx" -H 'Content-Type: application/json' -d' { "settings": { "number_of_shards": 3, "number_of_replicas": 1 }, "mappings": { "properties": { "content": { "type": "text" } } } }' ``` 写入 100 万条文档后,用 _cat/shards 确认 6 个 Shard 均匀分布,执行 match_all 查询记录 took 值。 对比 10 分片 1 副本的索引,观察查询延迟和节点 Heap 变化。 ## 排障题:查询突然变慢,_cat/nodes 显示 Heap 90%,_cat/shards 显示 60 个 Shard,如何定位和修复? 先确认分片数是否过多,60 个 Shard 在 3 节点上每节点 20 个,固定成本过高。 检查 _cat/shards 看是否有分片分布不均,检查 _cat/nodes 看 Heap 和句柄使用率。 修复方案是重建索引,把分片数降到合理范围,单分片大小控制在 10-50GB,同时调大 refresh_interval。 ## 架构判断题:一个日志索引每天写入 200GB,保留 7 天,应该用多少分片? 总数据量 1.4TB,按单分片 40GB 计算,需要 35 个 Primary Shard。 但 35 个分片在 3 节点集群上每节点约 12 个,固定成本偏高。 更好的方案是用 ILM 按天滚动索引,每天一个索引 5 个分片,单分片 40GB,7 天共 35 个分片但分散在不同索引,查询只命中相关索引。 > 理解实战排障题, > 架构判断要仔细。 > 分片规划先算账, > 滚动索引解难题。 # 13、升华 ```text 工程结论:分片规划是搜索集群稳定的第一道防线 ``` 有依据的结论是:分片数不是越多越好,单分片大小控制在 10-50GB,根据数据总量反推分片数,才能避免分片税。 本次实战从 40 分片回退到 5 分片,P99 从 1.8s 降到 210ms,Heap 从 87% 降到 62%,证明固定成本主导了小集群的查询性能。 下一篇可以深入 ILM 滚动索引和 Hot Shard 检测,解决数据持续增长和热点分片问题。 > 分片规划第一关, > 固定成本要算全。 > 单片大小控十到五十, > 集群稳定保平安。 **系列项目:** 电商搜索服务 **已完成:** 索引架构与分片规划 **本篇:** 从分片到段的层级理解与分片税规避 **下一篇:** ILM 滚动索引与 Hot Shard 治理

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册