一、 ES 是什么?
Elasticsearch 是一个基于 Apache Lucene 构建的开源、分布式、RESTful 风格的搜索和分析引擎。它是一个文档型 NoSQL 数据库,专门为海量数据的全文检索、复杂查询和实时分析而生。常与 Logstash、Kibana 组成著名的 ELK 技术栈。
二、 ES 的架构与原理图解(Mermaid 版)
1. 核心原理:倒排索引(快的原因)
传统数据库是“通过 ID 找内容”(正向索引),ES 是“通过内容找 ID”(倒排索引)。
2. 物理架构:分布式集群与分片(能存海量的原因)
ES 天生分布式,数据被切成多片分布在多台机器上,主分片负责写,副本分片负责读和容灾。
3. 写入原理:近实时 NRT(为什么有 1 秒延迟)
数据写入后并非直接落盘,而是先在内存中缓冲,每隔 1 秒刷新到系统缓存中开放搜索。
三、 为什么要用 ES?
- 模糊查询降维打击:MySQL 用
LIKE '%词%'会导致全表扫描锁表,百万级数据就崩溃;ES 基于倒排索引,十亿级数据也能毫秒级响应。【代码对比】 MySQL 灾难级写法:
SELECT * FROM goods WHERE title LIKE '%苹果手机%';(无法走索引) ES 毫秒级写法:
> GET /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 条)。【报错复现】
> GET /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 后台自动合并。
# 强制将索引合并为 1 个段文件,物理删除带删除标记的数据
POST /my_index/_forcemerge?max_num_segments=1
4. OOM(内存溢出)与集群只读
- 现象:节点掉线,或集群状态变红,无法写入新数据。
- 原因:
- 堆内存设置过大(超过 31GB,导致 JVM 压缩指针失效)。
- 复杂的聚合查询吃光了内存。
- 磁盘空间超过 95%(ES 会触发自我保护,将索引变为只读模式)。
> 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 设计黄金模板】
- 不需要搜索、排序、聚合的字段,设置
> PUT /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 结合的正确姿势】
> GET /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 实战代码】
> // 第一页查询 > 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 * 的写法】
> GET /goods/_search > { > "_source": ["id", "title", "price", "main_image"], // 仅返回这4个字段 > "query": { "match_all": {} } > }