都说 Elasticsearch 吃内存,凭什么几百 G 日志存得下还查得动?
📝 摘要:排查 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。 两条理由:
- 留一半内存给 page cache:堆吃光了内存,page cache 就没空间了,热数据缓存不住,每次查询都得啃磁盘——你以为给 ES 加了油,其实抽走了它真正的加速器。
- 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,落在这个窗口里最舒服。
七、几条能带走的经验
把上面的原理拧成几条可执行的结论:
- ES 是磁盘数据库,不是内存数据库:几百 G 存得下靠磁盘,查得动靠 page cache 按需缓存热数据。
- 堆别超 32 G,也别超物理内存一半:把内存留给 page cache 才是真加速;越过 32 G 还会丢掉压缩指针,纯亏。
- 分清 refresh 与 flush:refresh 让数据「可搜索」(1s,进缓存),flush 让数据「已落盘」(fsync)。写入量大时调大 refresh 间隔减小 segment 碎片。
- 段合并是磁盘 IO 大户:写多删多的场景,合并压力大;**控制单分片大小(10~50 G)**就是给合并减负,避免一次巨型合并打满磁盘。
- 聚合 / 排序用 doc values(默认就是),别退回老的 fielddata,否则真会吃堆 OOM。
- 时序数据按时间滚索引 + 配冷热分离 / ILM(Index Lifecycle Management,索引生命周期管理):老数据迁到冷节点或直接删整索引,比逐条删高效得多。
下次再有人说「ES 太吃内存」,你可以反问一句:你说的是 JVM 堆,还是 page cache? 把这两本账分开,ES 的内存模型、为什么几百 G 也扛得住,就全通了。
觉得有用就点赞收藏吧——下次给 ES 配机器、分堆内存、定分片大小时,翻出来对一遍,能少踩不少坑。👍
延伸阅读
- 《SkyWalking 消费积压 6 亿条:一次 Kafka 分区倾斜的深度复盘》——本系列第一篇,Kafka 侧的分区倾斜。
- 《SkyWalking 日志又积压 1900 万:Kafka 分区明明均匀了,凶手却藏在 ES 段合并里》——本系列第二篇,ES 段合并热点,本文很多概念的实战出处。
🏷️ 标签:Elasticsearch Lucene page cache JVM 堆 段合并 性能优化
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)