【架构实战】Elasticsearch搜索架构:从倒排索引到集群调优
📅 2026/7/12 11:35:10
👁️ 阅读次数
📝 编程学习
【架构实战】Elasticsearch搜索架构:从倒排索引到集群调优
一、一个搜索请求,把整个集群打挂了
2019年双12,凌晨0点刚过5分钟,ES集群全部节点CPU 100%,查询超时,监控疯狂告警。
排查发现运营在后台跑了一句"查询所有包含’优惠券’关键词的订单",这条查询在3亿订单数据上做模糊匹配,瞬间打爆了整个集群。
更糟的是:运维经验不足,直接重启了全部节点——数据恢复花了6个小时,期间所有搜索功能不可用。
这就是ES运维的残酷现实:开箱即用很简单,扛住生产流量很难。
二、倒排索引——ES的核心原理
2.1 正排 vs 倒排
【正排索引(MySQL B+树)】 文档1 → "华为手机很好用" 文档2 → "小米手机性价比高" 文档3 → "iPhone也很好用" 查"手机" → 需要扫描所有文档 → O(n) 【倒排索引(Elasticsearch)】 词项(Term) → 文档列表 "华为" → [1] "手机" → [1, 2] "很好用" → [1, 3] "小米" → [2] "性价比" → [2] "iPhone" → [3] 查"手机" → 直接定位Term → [1, 2] → O(1)2.2 分词器——搜索质量的根基
// 中文分词对比// Standard分词器(按字切分)"中华人民共和国"→["中","华","人","民","共","和","国"]// IK分词器(按词切分)"中华人民共和国"→["中华人民共和国","中华人民","中华","华人"]// 搜索"华人"时:// Standard: 匹配"华"+"人",结果包含"华为"、"中华"等大量噪音// IK: 精准匹配"华人",结果准确率大幅提升三、ES集群架构设计
3.1 节点角色分离
┌──────────────┐ │ Client │ ← 负载均衡 + 请求分发 │ Nginx/Kibana│ └──────┬───────┘ │ ┌────────────┼────────────┐ │ │ │ ┌───────▼──────┐ ┌──▼──────┐ ┌───▼────────┐ │ Master Node │ │Data Node│ │Data Node │ │ × 3(候选) │ │ Hot │ │ Cold │ │ 集群管理 │ │ SSD │ │ HDD │ │ 不存数据 │ │ 热数据 │ │ 冷数据归档 │ └──────────────┘ └─────────┘ └────────────┘角色分配原则:
- Master节点:3台,只做集群管理,不存数据,配置不用太高
- Data Hot节点:SSD,存储热数据(最近30天),配置要高
- Data Cold节点:HDD,存储冷数据,配置可降低
- Client节点:可选的协调节点,分散查询压力
3.2 索引设计——分片策略
// 创建索引时决定分片和副本PUT/orders{"settings":{"number_of_shards":10,"number_of_replicas":2,"refresh_interval":"30s","routing.allocation.require.box_type":"hot"},"mappings":{"properties":{"order_id":{"type":"keyword"},"user_id":{"type":"long"},"product_name":{"type":"text","analyzer":"ik_max_word","search_analyzer":"ik_smart"},"amount":{"type":"scaled_float","scaling_factor":100},"status":{"type":"keyword"},"create_time":{"type":"date","format":"yyyy-MM-dd HH:mm:ss"}}}}分片数计算公式:
单分片最大容量 ≈ 50GB(SSD)或 30GB(HDD) 总分片数 = ceil(总数据量 / 50GB) 例:1TB数据 → 20个主分片 每个节点不超过 20分片/GB堆内存(经验值)四、深入查询优化
4.1 避免深度分页
// 问题:Page 1000, Size 20GET/orders/_search{"from":20000,"size":20}// ES需要从每个分片取20020条,汇总后排序取20条// 内存开销:20020 * 分片数// 解决方案1:Search After(推荐)GET/orders/_search{"size":20,"query":{"match":{"status":"PAID"}},"sort":[{"create_time":"desc"},{"order_id":"desc"}],"search_after":[1625097600000,"123456"]// 上一页最后一条的排序值}// 解决方案2:业务限制深分页// 前端只允许查前100页,超过提示用户缩小查询范围4.2 Filter vs Query
// Query(计算评分,有缓存)GET/orders/_search{"query":{"bool":{"must":[{"match":{"product_name":"手机"}}// 需要评分],"filter":[{"term":{"status":"PAID"}},// 精确过滤,有缓存{"range":{"create_time":{"gte":"2025-06-01"}}}]}}}// filter结果会被ES缓存,重复查询直接走缓存4.3 聚合优化
// 大数据量聚合优化GET/orders/_search{"size":0,// 不需要原始文档,只要聚合结果"aggs":{"by_category":{"terms":{"field":"category_id","size":100,"execution_hint":"map"// 直接使用字段值,不计算全局序数},"aggs":{"total_amount":{"sum":{"field":"amount"}}}}}}五、集群监控与调优
5.1 关键监控指标
| 指标 | 正常值 | 告警阈值 | 说明 |
|---|---|---|---|
| 集群状态 | Green | Yellow/Red | 有未分配分片 |
| 节点CPU | < 60% | > 80% | 查询压力大 |
| 堆内存使用 | < 75% | > 85% | 接近GC临界点 |
| 磁盘使用 | < 70% | > 85% | 水位线保护 |
| Search Queue | < 100 | > 500 | 查询积压 |
| GC频率 | < 5/min | > 20/min | 频繁Full GC |
5.2 堆内存配置
# jvm.options# 堆内存不超过32GB(压缩指针阈值)-Xms16g-Xmx16g# 堆内存不超过物理内存的50%# 剩下50%留给Lucene的OS Cache(文件缓存)# GC选型-XX:+UseG1GC-XX:MaxGCPauseMillis=2005.3 索引生命周期管理(ILM)
// 自动管理索引的生命周期PUT_ilm/policy/orders_policy{"phases":{"hot":{"min_age":"0ms","actions":{"rollover":{"max_size":"50gb","max_age":"1d"}}},"warm":{"min_age":"30d","actions":{"forcemerge":{"max_num_segments":1},"shrink":{"number_of_shards":1}}},"cold":{"min_age":"60d","actions":{"allocate":{"require":{"box_type":"cold"}}}},"delete":{"min_age":"90d","actions":{"delete":{}}}}}六、双11扛压实战
压测发现的问题和优化: 1. 热点分片 → 添加routing,按user_id路由 2. 查询慢 → 用filter替代query,利用缓存 3. 聚合慢 → eager_global_ordinals预加载 4. GC频繁 → 堆内存从32G降到16G(Lucene用OS Cache) 5. 写入慢 → refresh_interval从1s改为30s 6. 凌晨数据倾斜 → 用rollover按时间切分索引 最终结果: - 峰值写入:50万 docs/s - 查询P99:< 200ms - 集群规模:15个Data Hot + 5个Data Cold + 3个Master七、总结
ES的调优之路就是不断做减法的过程:减少扫描、减少排序、减少聚合、减少不必要的字段。
核心实践:
- 倒排索引是灵魂——理解分词和倒排原理才能写出高效查询
- 能用filter不用query——filter有缓存,query要计算评分
- 深分页是集群杀手——用search_after代替from/size
- 堆内存不超过32GB——剩下内存留给Lucene的OS Cache
- ILM自动化索引生命周期——Hot/Warm/Cold/Delete四阶段托管
- 监控先行——堆内存、队列长度、GC频率是三大关键指标
血的教训:ES集群挂了不要直接重启所有节点,先停写入流量,恢复Master节点,等集群状态Green后再逐个恢复Data节点。
个人观点,仅供参考
编程学习
技术分享
实战经验