文章目录
-
- [一、 ES 是什么?](#一、 ES 是什么?)
- [二、 ES 的架构与原理图解(Mermaid 版)](#二、 ES 的架构与原理图解(Mermaid 版))
-
- [1. 核心原理:倒排索引(快的原因)](#1. 核心原理:倒排索引(快的原因))
- [2. 物理架构:分布式集群与分片(能存海量的原因)](#2. 物理架构:分布式集群与分片(能存海量的原因))
- [3. 写入原理:近实时 NRT(为什么有 1 秒延迟)](#3. 写入原理:近实时 NRT(为什么有 1 秒延迟))
- [三、 为什么要用 ES?](#三、 为什么要用 ES?)
- [四、 什么场景下用 ES?](#四、 什么场景下用 ES?)
- [五、 ES 常见问题有哪些?(生产环境的坑)](#五、 ES 常见问题有哪些?(生产环境的坑))
-
- [1. 数据一致性问题(最常见)](#1. 数据一致性问题(最常见))
- [2. 深分页问题](#2. 深分页问题)
- [3. "删除"数据后磁盘空间不减少](#3. “删除”数据后磁盘空间不减少)
- [4. OOM(内存溢出)与集群只读](#4. OOM(内存溢出)与集群只读)
- [六、 ES 搜索性能如何优化?(实战指南)](#六、 ES 搜索性能如何优化?(实战指南))
-
- [1. 硬件与 OS 层面优化](#1. 硬件与 OS 层面优化)
- [2. 索引设计层面优化(建库时决定生死)](#2. 索引设计层面优化(建库时决定生死))
- [3. 查询语句层面优化(代码端优化)](#3. 查询语句层面优化(代码端优化))
一、 ES 是什么?
Elasticsearch 是一个基于 Apache Lucene 构建的开源、分布式、RESTful 风格的搜索和分析引擎 。它是一个文档型 NoSQL 数据库,专门为海量数据的全文检索、复杂查询和实时分析 而生。常与 Logstash、Kibana 组成著名的 ELK 技术栈。
二、 ES 的架构与原理图解(Mermaid 版)
1. 核心原理:倒排索引(快的原因)
传统数据库是"通过 ID 找内容"(正向索引),ES 是"通过内容找 ID"(倒排索引)。
Elasticsearch: 倒排索引
查询: 搜索引擎
查倒排词典
搜索: Doc 1, 2
引擎: Doc 1, 2
求交集
结果: 毫秒级
传统数据库: 正向索引
查询: 搜索引擎
逐行扫描全表
Doc 1: 包含
Doc 2: 包含
Doc 3: 不包含
结果: 慢
2. 物理架构:分布式集群与分片(能存海量的原因)
ES 天生分布式,数据被切成多片分布在多台机器上,主分片负责写,副本分片负责读和容灾。
ES Cluster 集群
Node 2 - 数据节点
Node 1 - 主节点+数据节点
数据同步
数据同步
Node 3 - 数据节点
数据同步
Index A: 主分片 P1
Index B: 副本分片 R0
Index A: 主分片 P0
Index A: 副本分片 R0
Index B: 主分片 P1
Index B: 副本分片 R1
Client 客户端
3. 写入原理:近实时 NRT(为什么有 1 秒延迟)
数据写入后并非直接落盘,而是先在内存中缓冲,每隔 1 秒刷新到系统缓存中开放搜索。
每1秒 Refresh
开放查询
异步记录
异步 Flush
宕机恢复
写入请求
内存 Buffer
OS Cache 系统缓存
Search 搜索请求
Translog 事务日志
磁盘 Segment 永久文件
三、 为什么要用 ES?
-
模糊查询降维打击 :MySQL 用
LIKE '%词%'会导致全表扫描锁表,百万级数据就崩溃;ES 基于倒排索引,十亿级数据也能毫秒级响应。 【代码对比】MySQL 灾难级写法 :
SELECT * FROM goods WHERE title LIKE '%苹果手机%';(无法走索引)
ES 毫秒级写法:jsonGET /goods/_search { "query": { "match": { "title": "苹果手机" } } } -
支持复杂打分与相关性:搜"苹果",ES 会通过 BM25 算法算出"卖苹果手机的店"排在"卖苹果水果的店"前面,MySQL 很难做到。
-
天生分布式,横向扩展极简:MySQL 分库分表极其痛苦,ES 加机器只需改配置,自动完成数据迁移和负载均衡。
-
强大的聚合分析 :类似 SQL 的
Group By,但可以处理海量数据并实时出结果。
四、 什么场景下用 ES?
黄金法则:ES 绝不能当核心业务主库(无事务支持),必须是 MySQL 的"异构索引库"或"附属分析库"。
- 搜索类:电商商品搜索、App 内内容搜索(微信搜聊天记录、知乎搜文章)、企业内部文档检索。
- 日志与监控类:IT 运维日志分析(ELK)、微服务链路追踪(APM)、安全日志审计。
- 数据分析类:双十一实时成交额大屏、用户行为漏斗分析、BI 报表。
- 地理空间类:滴滴找附近的车、美团找附近的店(内置 Geo 数据类型)。
五、 ES 常见问题有哪些?(生产环境的坑)
1. 数据一致性问题(最常见)
- 现象:MySQL 修改了数据,但 ES 搜索出来的还是旧数据。
- 原因:同步延迟。无论是通过 Canal 监听 Binlog 还是通过 MQ 异步同步,都会有毫秒到秒级的延迟。
- 对策:对于强一致性要求的业务(如库存),以 MySQL 为准;对于搜索容忍最终一致性。
2. 深分页问题
- 现象 :查询
from=9990, size=10时,报错或极其缓慢,甚至把集群拖垮。 - 原因 :ES 的查询逻辑是集中式 的。假设有 5 个分片,查第 10000 条数据,每个分片都要查出前 10010 条数据返回给协调节点,协调节点合并 50050 条数据后,丢弃前 10000 条,返回最后 10 条。内存和网络的消耗随页码呈指数级上升 。(ES 默认
max_result_window限制为 10000 条)。 【报错复现】jsonGET /goods/_search { "from": 10000, "size": 10, "query": { "match_all": {} } } // 返回报错:Result window is too large, from + size must be less than or equal to: [10000]
3. "删除"数据后磁盘空间不减少
- 现象:删除了 ES 里几千万条数据,磁盘空间丝毫没变小。
- 原因 :Lucene 的 Segment 文件是不可变的。删除操作实际上只是在 Segment 里标记了一个"删除位",并没有真正从物理磁盘抹掉数据。
- 对策 :需要手动触发
Force Merge(强制合并段)操作,或者等 ES 后台自动合并。 【解决 API】 (注:建议在业务低峰期执行,非常消耗 CPU)
bash
# 强制将索引合并为 1 个段文件,物理删除带删除标记的数据
POST /my_index/_forcemerge?max_num_segments=1
>
4. OOM(内存溢出)与集群只读
- 现象:节点掉线,或集群状态变红,无法写入新数据。
- 原因 :
- 堆内存设置过大(超过 31GB,导致 JVM 压缩指针失效)。
- 复杂的聚合查询吃光了内存。
- 磁盘空间超过 95% (ES 会触发自我保护,将索引变为只读模式)。 【解除只读 API】
json
PUT /_all/_settings { "index.blocks.read_only_allow_delete": null }
六、 ES 搜索性能如何优化?(实战指南)
1. 硬件与 OS 层面优化
- 内存分配黄金法则 :ES 的 JVM 堆内存最多分配 31GB (利用零基压缩指针,节省内存)。且堆内存不要超过物理内存的 50% ,剩下的 50% 必须留给操作系统做 Lucene 的文件系统缓存,这是快的关键。
- 磁盘 :绝对不要用 NFS 等网络存储,必须用本地 SSD。
2. 索引设计层面优化(建库时决定生死)
-
拒绝动态映射 :生产环境必须关闭
dynamic: true,手动定义 Mapping。避免 ES 自动推断字段类型导致性能浪费。 -
精准控制字段属性 :
- 不需要搜索、排序、聚合的字段,设置
"index": false。 - 对于需要精确匹配(如状态码、手机号、ID)的字段,类型设为
keyword,绝不要用text(text会走分词器,浪费 CPU 且无法精确匹配)。 【Mapping 设计黄金模板】jsonPUT /goods { "mappings": { "dynamic": "false", // 1. 拒绝动态映射 "properties": { "title": { "type": "text", "analyzer": "ik_max_word", // 中文分词器 "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } // 支持精确匹配的混合字段 } }, "status": { "type": "keyword" // 2. 绝对精确匹配坚决用 keyword }, "price": { "type": "double" }, "description": { "type": "text", "index": false // 3. 仅展示不搜索的字段,关闭索引节省内存 } } } }
- 不需要搜索、排序、聚合的字段,设置
-
路由优化 :如果查询总是带着某个特定条件(如
tenant_id),设置自定义路由。查询时直接去对应分片查,避免扫全部分片。 -
分片数量控制 :分片不是越多越好。单个分片大小建议保持在 10GB - 50GB 之间。分片过多会导致集群元数据庞大、恢复极慢。
3. 查询语句层面优化(代码端优化)
-
用 Filter 替代 Query :
query(如match)会计算相关性打分,耗费 CPU。filter(如term、range)只判断 Yes/No,不参与打分,且结果会被 ES 自动缓存 。对于"状态=1 且 价格>100"这种绝对条件,必须放在filter里。 【Query 与 Filter 结合的正确姿势】jsonGET /goods/_search { "query": { "bool": { "must": [ { "match": { "title": "手机" } } // 参与打分,决定排序相关性 ], "filter": [ // 不打分,结果直接进缓存 { "term": { "status": "1" } }, // 精确匹配 { "range": { "price": { "gte": 1000, "lte": 5000 } } } // 范围匹配 ] } } }
-
避免通配符开头的模糊查询 :
*abc会导致全词典扫描,性能极差。尽量使用 ES 的ngram分词器在建索引时处理好前缀匹配。 -
解决深分页的三大法宝 :
-
search_after(强烈推荐) :类似 MySQL 的游标翻页。每次查询带上上一页最后一条数据的排序值,性能极高且无深度限制。 【search_after 实战代码】json// 第一页查询 GET /goods/_search { "size": 10, "query": { "match": { "title": "手机" } }, "sort": [ { "price": "asc" }, // 排序字段1 { "_id": "asc" } // 排序字段2(必须加唯一字段防并发相同值) ] } // 假设第一页返回的最后一条数据 price 是 2999, _id 是 "abc123" // 第二页查询(将上条数据的 sort 值原封不动放入 search_after) GET /goods/_search { "size": 10, "query": { "match": { "title": "手机" } }, "sort": [ { "price": "asc" }, { "_id": "asc" } ], "search_after": [2999.00, "abc123"] } -
scroll:适用于海量数据的全量导出/批处理(维护上下文快照),绝对不要用于前端翻页。 -
业务折中:类似百度/谷歌,前端只允许翻到第 100 页,拒绝提供无限下拉翻页功能。
-
-
避免返回大字段 :使用
_source_includes只返回列表需要的字段,拒绝SELECT *,大幅减少网络传输开销。 【拒绝 SELECT * 的写法】jsonGET /goods/_search { "_source": ["id", "title", "price", "main_image"], // 仅返回这4个字段 "query": { "match_all": {} } }