📝 摘要:排查 SkyWalking 日志积压时冒出一个反直觉的问题:ES 常被说成「吃内存」,凭什么几百 G 日志存得下还查得动?答案是 ES/Lucene 数据根本不全装内存——它落在磁盘的不可变 segment 文件里,靠操作系统 page cache 把热数据按需缓存。本文从这个疑问出发,串起写入链路(buffer/translog/refresh/flush)、段合并、倒排索引与 doc values、分片副本与索引滚动,最后给出堆别超 32G、留内存给 page cache、控住单分片大小等实用经验。

起因是上回排查 SkyWalking 日志积压:一个 ES 节点装着几百 G 的日志,单台机器内存才 16 G。我盯着监控突然卡壳——ES 不是出了名的「吃内存」吗?16 G 内存凭什么扛得住几百 G 数据,还能让人在页面上秒查日志?

如果你也下意识觉得「ES = 把数据全塞内存里的搜索引擎」,那这篇就是来拆这个误解的。把它拆明白,顺带就把 segment、段合并、page cache、分片副本这些概念一次串清楚了。

📖 背景串联:这篇是「SkyWalking 日志积压」系列的知识延伸。想看那次积压怎么从 Kafka 一路追到 ES 段合并的,见 《SkyWalking 消费积压 6 亿条》《SkyWalking 日志又积压 1900 万:Kafka 分区明明均匀了,凶手却藏在 ES 段合并里》


一、先破误解:ES 到底把数据存哪了?

最大的误会就在这儿——Elasticsearch 不是内存数据库,它是个不折不扣的「磁盘数据库」。

ES 的存储内核是 Lucene,数据落地的基本单位叫 segment(段)。你写进去的每一条文档,最终都变成磁盘上一个个 segment 文件。几百 G 日志,就是磁盘上几百 G 的 segment,老老实实躺在硬盘上,不在内存里

那 JVM 堆(heap)里放的是什么?只是「为了查得快」而留的一小撮东西:

放在哪 放什么 占比
磁盘(segment 文件) 全量数据:倒排索引、列存、原始文档 几百 G,大头
JVM 堆 词典索引(FST)、各类 cache、segment 元信息 几个 G,零头
操作系统 page cache(堆外内存) 最近读过的 segment 数据块 按需,弹性

打个比方:segment 文件像图书馆里一排排上了架的书,几十万本都在书架上(磁盘)。JVM 堆只是前台那本索书目录——告诉你「某本书在 3 区 5 排」,它自己很薄。你不会把整个图书馆的书都搬到前台桌上,对吧?

所以「几百 G 存得下」根本不是问题:磁盘有多大就能存多少,跟那 16 G 内存没关系。真正反直觉的是第二个问题——数据在磁盘上,查询凭什么还快?


二、几百 G 怎么查得动:真正的主角是 page cache

答案是操作系统的 page cache(页缓存)

Linux 有个特性:任何进程读磁盘文件,读过的数据块会被内核自动缓存在内存里(这块内存叫 page cache,属于堆外,不占 JVM 堆)。下次再读同一块,直接命中内存,不碰磁盘。

ES 把 segment 文件通过 mmap 映射进来读取,等于把「热数据」的缓存这件事,完全甩给了操作系统

  • 你查的日志大多是最近几天的 → 这些 segment 块被 page cache 缓存住 → 查询基本走内存,飞快。
  • 偶尔翻很老的日志 → page cache 没命中 → 读一次磁盘,顺带又缓存上。
  • 几百 G 全量数据从不需要一次性进内存,只有「被查到的那部分」按需进 page cache。

这就解释了那个反差:16 G 内存扛几百 G 数据,靠的不是把数据装进去,而是「用得到的才进来,用完还能换出去」。

这也顺手解释了一条 ES 最重要、却最常被违背的调优铁律——

为什么 ES 的堆「别配太大」

很多人觉得内存大就该多分给 ES,于是给 64 G 的机器配 60 G 堆。这恰恰是反优化。

ES 官方建议:JVM 堆不超过物理内存的一半,且绝对别超过 ~32 G。 两条理由:

  1. 留一半内存给 page cache:堆吃光了内存,page cache 就没空间了,热数据缓存不住,每次查询都得啃磁盘——你以为给 ES 加了油,其实抽走了它真正的加速器。
  2. 32 G 是压缩指针的红线:JVM 堆 ≤ ~32 G 时能用「压缩对象指针(compressed oops)」,省内存、寻址快;一旦越过,指针变长,可能 40 G 堆的实际可用对象空间还不如 31 G,纯亏。

一句话:ES 的内存是「堆 + page cache」两本账,堆只是小头,把内存留给操作系统才是正解。

📖 这套堆大小 + 压缩指针的取舍,Elastic 官方有篇经典博客讲得很透:《A Heap of Trouble: Managing Elasticsearch’s Managed Heap》


三、数据是怎么写进去的:buffer、translog、refresh、flush

理解了「数据在磁盘、缓存靠 OS」,再看写入链路就顺了。一条文档从写入到能被搜到、再到落盘,要过几道关:

写入请求
  ├─ 1) 进 内存 buffer(还搜不到)
  └─ 2) 同时写 translog(事务日志,先保证不丢)
        │
        │  ── refresh(默认每 1s)──> 把 buffer 里的数据生成一个新 segment,
        │                            放进 filesystem cache(page cache),
        │                            此刻起「可被搜索」(但还没 fsync 落盘)
        │
        └─ ── flush(攒够量 / 定时)──> Lucene commit + fsync 真正刷盘,
                                       然后清空 translog

三个容易混的概念,一次说清:

动作 干了啥 触发时机 关键点
translog 写一份顺序日志做兜底 每次写入 ES 不是写完立刻刷盘,靠它保证宕机不丢
refresh 生成新 segment、变得可搜索 默认 1s 这就是 ES「近实时(NRT)」的来历——写完不是立刻能搜,而是 1s 后
flush fsync 落盘 + 清 translog 攒够 translog / 定时 真正把数据焊死在磁盘上

划重点:「可搜索」和「已落盘」是两件事。 refresh 让数据可搜(进 page cache 即可),flush 才让数据持久(fsync 到磁盘)。这俩拆开,正是 ES 又快又不丢的关键设计。官方对 refresh / 近实时的解释见 Near real-time search、translog 见 Translog

顺带,这也解释了为什么日志类场景常把 refresh 间隔调大(比如 30s):写入量巨大时,1s 一个 segment 会生成海量小 segment,反而拖累——而这就引出了下一个主角:段合并


四、segment 为什么必须合并:一切「轮动热点」的根源

Lucene 的 segment 有个铁律:一旦生成,就不可变(immutable)。

不可变带来一堆好处(无锁读、好缓存、好压缩),但也带来一个副作用——

  • 每次 refresh 都生成一个新的小 segment,写得越猛,小 segment 越多。
  • 删除文档?不能就地删,只能在一个 .del 标记里记一笔「这条作废了」,原数据还占着地方。
  • 更新文档?= 删旧的 + 写新的,更费。

小 segment 多了,查询要同时翻几百个段,越来越慢;废弃文档也越积越多。于是 Lucene 在后台默默干一件事——段合并(segment merge):把多个小 segment 读出来、归并成一个大 segment,再把小的删掉(顺带丢掉被标记删除的文档)。

注意:合并的首要目的是"合小段为大段",跟删不删没关系。 像日志这种只增不删的场景照样得合——写得越猛、refresh 越频繁,生出的小段就越多,不合并查询就会被几千个小段拖垮。append-only 不仅免不了合并,高写入正是合并压力的来源。

官方对段合并的说明(含"小段合大段、顺带清理删除、合并做 IO 限流")见 Elastic 官方文档 · Merge

segment 合并像图书馆理书:零散还回来的书先堆在临时架(小 segment),管理员定期把它们按类目合并、剔掉报废的,重新上架成整齐的大架(大 segment)。

关键来了:合并要把多个老 segment 整个读出来再写回去,是个读 + 写 + CPU 都重的操作。段越大,一次合并读写的数据越多。

这正是上次 SkyWalking 积压的根因——日志索引单个分片被养到 20 G,合并一个这么大的段就是一次巨型磁盘读,哪台机器的大段在合并,哪台的磁盘 IO 就被打满,看着像「热点在机器间轮动」,其实是合并的突发性在轮流引爆。详见 《SkyWalking 日志又积压 1900 万:Kafka 分区明明均匀了,凶手却藏在 ES 段合并里》

记住这条因果:段太大 → 合并太重 → 磁盘 IO 被打满 → 写入卡住。而合并永远出不了「单个分片」这个边界——分片多大,一次合并的体量就多大,没法分摊给别的分片或索引。 所以「控制单分片大小」不是玄学,是直接给合并减负。


五、查询为什么快:倒排索引 + doc values,各司其职

数据在磁盘上还能秒查,除了 page cache 兜底,还靠 Lucene 把数据按「查询用途」存了两份不同结构:

结构 用途 存在哪 类比
倒排索引(inverted index) 全文检索:「含 error 的文档有哪些」 磁盘(词典 FST 部分常驻堆/堆外) 书末尾的关键词索引——按词找页码
doc values 排序 / 聚合:「按时间排序」「按状态分组计数」 磁盘(列式存储,堆外) 一张列存表——按列快速扫
  • 走倒排:给个关键词,FST 词典(小,常驻内存)快速定位,再去磁盘 postings 取文档列表。
  • 排序 / 聚合走 doc values:列式存储,扫某一列特别快,且在堆外、不撑爆 JVM。

两者都在磁盘,热的部分由 page cache 顶着。所以「查得快」= 结构设计得好(倒排 + 列存)+ 热数据缓存得住(page cache),跟「把数据全塞内存」没半毛钱关系。

早期 ES(1.x 及更早)用 fielddata 把字段加载进堆内存做聚合,才是真·吃堆内存,动不动 OOM。后来 2.0 起 doc values(堆外列存)成了默认5.0 又对 text 字段默认关掉 fielddata,才把这个坑填上——所以「ES 吃内存」的刻板印象,有一半是被老版本坑出来的。现在(7.x/8.x)你正常用 keyword 字段聚合走的都是堆外 doc values,除非手动给 text 字段开 fielddata: true 才会重回吃堆老路。官方对 doc values 的定义见 Elastic 官方文档 · doc_values


六、分片、副本、索引滚动:把大数据「切开、留备份、按时间滚」

最后三个概念,决定了 ES 怎么把「几百 G、上 T」的数据组织得既能扩展又抗造:

  • 分片(shard):一个分片就是一个独立的 Lucene 索引。一个 ES 索引可以切成 N 个分片摊到多台机器,水平扩展、并行查询的能力就来自这儿。
  • 副本(replica):每个主分片可以配若干副本,既做高可用(主挂了副本顶上),又分摊读(查询可落到副本)。代价是写入要多写几份、存储翻倍。
  • 按时间滚动索引:日志、链路这类时序数据,通常按天/按周滚一个新索引(log-2026.06.12)。好处是老数据整索引删除超便宜,也能让单个索引/分片别无限长大。

这三点又一次呼应了 SkyWalking 那次踩的坑:它的日志走「super dataset」,配了 36 分片、0 副本、5 天才滚一个索引——5 天的日志全挤进同一批分片,把单分片撑到 20 G,合并直接爆。后来把「5 天一滚」改成「1 天一滚」,单分片瞬间瘦到 4.6 G,积压几分钟就清空了。

分片不是越多越好,也不是越大越好。 太多分片 → 每个分片的元数据、FST 都占堆,分片数膨胀会撑爆堆内存;太大分片 → 合并和恢复都变重。经验区间是单分片 10~50 G,落在这个窗口里最舒服。


七、几条能带走的经验

把上面的原理拧成几条可执行的结论:

  1. ES 是磁盘数据库,不是内存数据库:几百 G 存得下靠磁盘,查得动靠 page cache 按需缓存热数据。
  2. 堆别超 32 G,也别超物理内存一半:把内存留给 page cache 才是真加速;越过 32 G 还会丢掉压缩指针,纯亏。
  3. 分清 refresh 与 flush:refresh 让数据「可搜索」(1s,进缓存),flush 让数据「已落盘」(fsync)。写入量大时调大 refresh 间隔减小 segment 碎片。
  4. 段合并是磁盘 IO 大户:写多删多的场景,合并压力大;**控制单分片大小(10~50 G)**就是给合并减负,避免一次巨型合并打满磁盘。
  5. 聚合 / 排序用 doc values(默认就是),别退回老的 fielddata,否则真会吃堆 OOM。
  6. 时序数据按时间滚索引 + 配冷热分离 / ILM(Index Lifecycle Management,索引生命周期管理):老数据迁到冷节点或直接删整索引,比逐条删高效得多。

下次再有人说「ES 太吃内存」,你可以反问一句:你说的是 JVM 堆,还是 page cache? 把这两本账分开,ES 的内存模型、为什么几百 G 也扛得住,就全通了。

觉得有用就点赞收藏吧——下次给 ES 配机器、分堆内存、定分片大小时,翻出来对一遍,能少踩不少坑。👍


延伸阅读


🏷️ 标签Elasticsearch Lucene page cache JVM 堆 段合并 性能优化

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐