首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Elasticsearch 原理全拆解:从一次慢查询事故说起,我把底层扒了个底朝天

Elasticsearch 原理全拆解:从一次慢查询事故说起,我把底层扒了个底朝天

作者头像
悠悠12138
发布2026-07-27 12:31:29
发布2026-07-27 12:31:29
260
举报

凌晨 1 点多,告警群炸了。

业务那边说搜索接口超时,TP99 从 50ms 飙到 3 秒,用户开始疯狂刷退款单。

我上去一看,ES 集群 CPU 飘红,节点负载打满,第一反应就是加机器、扩分片。结果倒好,分片一加,查询更慢了,差点把整个集群拖崩。

后来硬着头皮啃了三天源码,翻了十几篇官方文档,把 ES 的核心原理从倒排索引到段合并、从写流程到查询流程全撸了一遍,才慢慢摸清楚这东西到底是怎么工作的。

今天这篇文章,我把 ES 底层那些绕来绕去的东西掰开揉碎讲清楚,尽量少画饼,多上干货。建议先收藏,因为你大概率不会看第二遍。


先说清楚:ES 到底是个啥玩意儿

很多人觉得 ES 就是个搜索引擎,其实不太准确。

ES 底层用的是 Lucene,这点大家都知道。但它跟 Lucene 不一样的地方在于,Lucene 是个 Java 库,你得自己写代码去调;ES 把它包装成分布式的服务,HTTP 协议一开,RESTful 接口一暴露,用起来就跟调 API 一样简单。

但简单归简单,坑是真的多。

比如你往里写一条数据,默认情况下 1 秒之后才能搜到。很多人不知道这个点,写完数据立刻查,查不到就以为写失败了,结果疯狂重试,把集群打爆。

这事儿我干过,别笑。

所以理解 ES 原理,不只是为了面试,更是为了别在生产环境翻车。下面我挑几个最核心的模块讲,尽量讲人话。


倒排索引:ES 为什么能秒级检索

传统的数据库,比如 MySQL,做模糊查询的时候用的是 LIKE '%xxx%',这种查询是全表扫描,数据量一上百万就开始拉垮。

ES 之所以快,核心就在于倒排索引

啥叫倒排索引?我举个真实的例子你就懂了。

假设我们有三条文档:

代码语言:javascript
复制
doc1: 运维躬行录 是一个 干货 公众号
doc2: 分享 运维 实战 经验
doc3: 公众号 分享 干货 内容

传统的正排索引是:文档 -> 词项。

代码语言:javascript
复制
doc1 -> [运维躬行录, 是, 一个, 干货, 公众号]
doc2 -> [分享, 运维, 实战, 经验]
doc3 -> [公众号, 分享, 干货, 内容]

倒排索引反过来,是:词项 -> 文档。

代码语言:javascript
复制
运维躬行录 -> [doc1]
干货 -> [doc1, doc3]
公众号 -> [doc1, doc3]
分享 -> [doc2, doc3]
运维 -> [doc2]
实战 -> [doc2]
经验 -> [doc2]
内容 -> [doc3]

这样一来,你搜 "干货" 的时候,ES 不用扫描所有文档,直接从倒排表里拿到 [doc1, doc3] 就完事了。这就是为啥 ES 搜得快。

但这里面有个细节,很多人不知道。

ES 在建倒排索引的时候,不是简单地把词项存起来,它还记录了词频(TF)和位置信息(Position)。这就是为什么 ES 支持短语查询、高亮、相关性打分这些高级功能。

如果你想做精确匹配,可以用 keyword 类型;如果你想分词搜索,用 text 类型。这两种类型在建索引的时候走的是完全不同的路径,性能差距非常大。生产环境里,keyword 用于精确过滤,text 用于全文搜索,两者结合用,查询效率能提升好几倍。


段(Segment):理解 ES 写入流程的钥匙

这是 ES 最难理解的部分之一,也是很多线上事故的根源。

ES 的数据不是直接写入磁盘的,而是先写到一个内存缓冲区(In-Memory Buffer),同时记录到 Translog(事务日志)。

这个内存缓冲区会被分成一个个段(Segment)。你可以把段想象成一个个小文件,每个段都是一份完整的、不可变的数据集。

那什么时候段会落盘呢?有两个关键操作:

第一个是 refresh。

默认情况下,ES 每 1 秒执行一次 refresh。refresh 干了啥呢?它把内存缓冲区里的数据生成一个新的段,写到文件系统缓存(不是磁盘!),然后这个段就可以被搜索了。

这就是为啥 ES 是**近实时(NRT, Near Real-Time)**的,而不是实时。1 秒的延迟,是默认配置,你可以改小,但不建议,太小会影响写入性能。

第二个是 flush。

flush 才是真正把数据写磁盘的动作。它干了三件事:

  1. 把内存里的段刷到磁盘
  2. 写一个 commit point(提交点)
  3. 清空 Translog

Translog 是干嘛的?你可以理解为 MySQL 的 binlog。ES 写入数据的时候,先写 Translog,再写内存。这样万一机器宕机,内存里的数据丢了,还能从 Translog 恢复。

但这里有个坑,我之前踩过。

我们有个业务场景,写入量特别大,默认的 Translog 配置扛不住,导致节点频繁重启。后来查了文档才知道,Translog 有个参数 index.translog.flush_threshold_size,默认是 512mb,这个值在大写入场景下明显偏小。还有 index.translog.durability,默认是 request,意思是每次写请求都要 fsync 一次,性能很差。改成 async 之后性能能提升不少,但代价是宕机时可能丢几秒的数据。

这种 trade-off,你得根据业务场景来选。


段合并:ES 后台最忙的家伙

段这玩意儿有个问题:它不可变

这意味着数据一旦写入段,就不能修改了。如果你更新了一条文档,ES 不是去改原来的段,而是生成一个新的段,把更新后的文档放进去,同时在原来的段里标记那条文档为"已删除"。

这就引出了一个问题:随着时间推移,段会越来越多,越来越大,查询性能会越来越差。

所以 ES 有一个后台线程,专门干一件事:段合并(Segment Merging)

合并的逻辑很简单:把多个小的段合并成一个大的段,同时把标记为"已删除"的文档真正清理掉。

这个过程是自动的,但你可以通过 index.merge.policy 来调优。比如:

  • index.merge.policy.floor_segment:小于这个值的段会被优先合并,默认 2mb
  • index.merge.policy.max_merge_at_once:一次最多合并多少个段,默认 10
  • index.merge.policy.segments_per_tier:每层多少个段,默认 10

段合并对查询性能影响很大,但合并过程中会消耗大量的 IO 和 CPU。生产环境里经常出现这种情况:业务低峰期没啥事,一到高峰期查询就慢,监控一看,节点 IO 打满,全在合并段。

遇到这种情况怎么办?

第一,调大 refresh_interval,从默认的 1 秒改成 5 秒或者 10 秒,减少段生成的数量。

第二,把 merge 操作限流。ES 有个参数 indices.store.throttle.type,可以设置成 none 或者 merge,但这玩意儿我一般不敢乱动,除非你很清楚后果。

第三,也是最重要的,业务低峰期做 force merge。调用 _forcemerge API,把段合并成一个,对只读索引特别有效,比如日志场景。


分片与副本:分布式 ES 的基石

ES 集群是怎么分布数据的?靠的是分片(Shard)

一个索引在创建的时候可以指定分片数,比如 5 个主分片。数据写入的时候,会根据文档 ID 的 hash 值路由到对应的分片上。

每个主分片可以有多个副本分片,副本分片是主分片的完整拷贝。

这里有个很多人搞混的概念:主分片数量创建后不能改,副本分片数量可以随时改

如果你想改主分片数量,只能用 reindex,也就是把数据从老索引搬到新索引。这个操作在大集群上执行起来非常麻烦,资源消耗也大。所以生产环境创建索引的时候,一定要提前规划好分片数

我之前就吃过这个亏。一个索引创建的时候分片数设了 3,后来数据量涨到几百 GB,每个分片都有 100 多 G,查询性能开始下降。这时候想加分片?不好意思,加不了,只能 reindex 到一个新索引。reindex 跑了 8 个小时,期间业务搜索就特别慢。

经验之谈:单个分片控制在 10-50GB 之间是最佳实践。如果你的索引最终规模在 200GB 左右,分片数可以设 5 个或者 8 个。分片数太少,单个分片太大,查询慢;分片数太多,集群元数据管理开销大,查询协调也慢。

副本的作用主要是两个:高可用和读扩展

主分片挂了,副本分片会自动升级为主分片,服务不中断。读扩展的意思是,搜索请求可以同时打到主分片和副本分片上,提升查询吞吐。

但这里有个细节你可能不知道:副本分片数的设置会影响写入性能。每写一条文档,都要同步到所有副本,等所有副本返回成功才算写入完成。副本越多,写入越慢。所以对于写入密集型的场景,副本数保持默认的 1 就行;查询密集型的场景,可以适当增加副本。


查询流程:两阶段查询的奥秘

ES 的查询流程是分两阶段的:query 阶段和 fetch 阶段

Query 阶段干的事是:

  1. 协调节点(Coordinate Node)接收到查询请求
  2. 把请求转发到所有相关的分片(主分片或者副本分片都行,通过 preference 参数控制)
  3. 每个分片在本地执行查询,返回文档 ID 和排序值(不返回完整文档)
  4. 协调节点对所有分片返回的结果进行合并、排序、分页,选出最终的文档 ID 列表

Fetch 阶段干的事是:

  1. 协调节点拿着文档 ID 列表,去对应的分片拉取完整的文档内容
  2. 把结果返回给客户端

为啥要分两阶段?这是个性能优化。

你想啊,如果 query 阶段就返回完整文档,数据量得多大,网络传输得多慢。通过两阶段,query 阶段只传 ID 和排序值,数据量小得多;fetch 阶段只拉需要的文档(默认 10 条),效率高得多。

但这里有个坑:深度分页

ES 默认的 from + size 分页,from + size 不能超过 index.max_result_window,默认是 10000。超过这个值,ES 直接报错。

为啥要限制?因为 ES 每次分页都要把 from 之前的数据全取出来,然后丢掉。如果你 from = 10000,size = 10,那 ES 得从每个分片取 10010 条数据,然后排序丢弃前 10000 条。这性能,崩得很快。

解决深度分页有几个方案:

  1. Search After:用上一页的最后一条数据作为下一页的起点,官方推荐的方式
  2. Scroll API:一次性生成一个快照,适合全量导出,但有上下文保留的开销
  3. Point In Time (PIT):7.10 版本之后的新特性,结合 search after 用,比 scroll 轻量

我们业务上主要用 Search After,配合 PIT 效果很好,几十万页的数据翻起来也不卡。


写入一致性:你以为的写入成功不是真的成功

最后讲一个让我印象深刻的事。

之前有个业务反馈,说数据写入 ES 之后,查不到。但监控显示写入成功了。查了半天才发现,是一致性级别的问题。

ES 写入的时候可以指定 consistency 参数,有三个值:

  • one:只要主分片写入成功就返回
  • quorum:大多数分片写入成功才返回(默认)
  • all:所有分片都写入成功才返回

我们集群当时刚好有一个节点网络抖动,主分片写入成功,但副本同步失败,quorum 级别等不到大多数分片的确认,写入请求一直挂着。后来用户重试,写入了新的文档,但因为 ID 一样(业务自己生成 ID),旧版本把新版本覆盖了,导致数据错乱。

这个 case 教会我一件事:不要用业务自增 ID,用 ES 自动生成的 UUID。如果一定要用业务 ID,要么用 op_type=create 防止覆盖,要么在业务层做幂等。


避坑清单:这些坑我替你踩过了

整理几个生产环境最常见的坑,建议保存:

  • 不要无脑使用动态映射:动态映射会猜字段类型,猜错的概率不低,尤其是日期和数字类型。建议显式定义 mapping,或者用 dynamic templates 控制。
  • 不要把所有字段都设成 text:需要精确匹配的字段(比如 ID、状态、标签)用 keyword,能极大提升查询性能。
  • 不要忽略 _source 字段:默认开启是好事,能拿原始文档,但如果你索引特别大,可以考虑关闭 _source 节省存储,代价是没法 reindex、没法 update。
  • 不要用 wildcard 字段做精确查询:wildcard 是扫描匹配,性能很差。精确匹配用 keyword,模糊匹配用 match + 分词。
  • 不要在生产环境跑 _delete_by_query:这玩意儿一旦执行没法回滚,会锁段,影响查询。要删数据,先用 _search 确认范围,再小批量删除。
  • 不要忽视 JVM 堆内存:ES 堆内存不要超过物理内存的 50%,不要超过 32GB(因为压缩指针问题)。剩下的内存留给文件系统缓存,这是 ES 的第二级缓存,性能影响极大。
  • 不要忘了慢查询日志:设置 index.search.slowlog.threshold.query.warn: 10s,超过 10 秒的慢查询会自动打到日志里,定期分析能发现很多潜在问题。

说点掏心窝子的话

ES 这东西,学起来不难,用起来全是坑。网上很多教程只讲怎么用,不讲为啥这么用,结果一上生产就各种问题。

我自己也是踩了无数坑之后,才慢慢搞明白这玩意儿的设计思路。ES 的作者 Shay Banon 当年写这个东西的时候,目标是做一个分布式、可扩展、近实时的搜索引擎,所以它的很多设计都是为了这个目标服务的。

理解这些设计原则,比死记硬背参数有用得多。

比如你知道 ES 是近实时的,就不会在写入后立刻查;你知道段是不可变的,就知道为啥要合并;你知道分片数不可改,就会在建索引的时候多花点时间规划。

这些东西,文档里都有,但只有你真的踩过坑,才能体会其中的深意。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-07-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 运维躬行录 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 先说清楚:ES 到底是个啥玩意儿
  • 倒排索引:ES 为什么能秒级检索
  • 段(Segment):理解 ES 写入流程的钥匙
  • 段合并:ES 后台最忙的家伙
  • 分片与副本:分布式 ES 的基石
  • 查询流程:两阶段查询的奥秘
  • 写入一致性:你以为的写入成功不是真的成功
  • 避坑清单:这些坑我替你踩过了
  • 说点掏心窝子的话
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档