Elasticsearch 单机部署实战:JVM 堆内存、mmap 与 bootstrap checks 三处 OOM 排查

为什么个人站长会用上 Elasticsearch

大多数人第一次接触 Elasticsearch(以下简称 ES)都不是因为它「好玩」。真实的原因通常是三种:站内搜索命中率太差,MySQL 的 LIKE '%关键词%' 一旦数据量过几万条就慢得不能看;日志量上来了,用 grep 翻 fifty MB 的 nginx 日志要等半分钟;或者装了一个带搜索功能的 CMS / 论坛插件,它默认就要连 ES。

问题是,ES 是一个用 Java 写的、默认假设你有一台 8G 以上内存机器的分布式系统。放到一台 2G 或 4G 内存的个人 VPS 上,新手几乎必然遇到两个结果:容器一直在重启,或者一启动就把整台机器连同 MySQL、Nginx 一起拖死。这篇文章不讲集群、不讲分片副本这些分布式话题,只讲单机小内存场景下,从安装到排掉 OOM 的完整路径。如果你只是想做站内中文搜索,且数据量在百万条以内,其实 Meilisearch 或 PostgreSQL 全文索引更省事;ES 的价值在于日志检索和多字段聚合。选型之前先想清楚这一点,能省掉很多折腾。

第一坑:JVM 堆内存的默认值是个陷阱

ES 基于 Lucene,装在 JVM 里跑。JVM 启动时会根据物理内存自动挑选堆大小:在没有显式配置的情况下,ES 7.x 默认给堆设 -Xms1g -Xmx1g——注意,是写死在 jvm.options 里的,不是「按内存比例算」。很多人把 ES 装在 2G 内存的机器上,1G 堆 + 几百 MB 的 Lucene 堆外(off-heap)开销 + 文件系统缓存需求,加起来早就超过物理内存,于是 OOM Killer 出手,第一个被杀的往往不是 ES,而是内存占用同样很大的 MySQL。

ES 官方给的规则非常明确,只有两条:

  • 堆的 Xms 和 Xmx 必须设成同一个值。否则 JVM 会在运行期不断扩堆、触发 GC 抖动,搜索延迟肉眼可见地不稳。
  • 堆不要超过物理内存的 50%,也永远不要超过 31G。第二条是因为 JVM 在堆小于 32G 时能使用「压缩普通对象指针」(Compressed OOPs),一旦超过 31G 这个优化失效,指针翻倍变长,多出来的内存全被指针吃掉,反而更慢。个人站根本碰不到这个上限,记住前半句就够了。

在 4G 内存的机器上,合理的配置是 1.5G 堆;2G 内存的机器上,最多给 768M,并且要接受「这台机器基本只能跑 ES + 一个小站」这个现实。修改的位置在 config/jvm.options.d/ 目录下新建一个文件(7.7 之后的版本支持这种片段式覆盖,比直接改主文件安全,升级时不会被覆盖):

# /usr/share/elasticsearch/config/jvm.options.d/heap.options
-Xms1536m
-Xmx1536m

改完之后必须重启进程,然后用这条命令确认生效:

curl -s 'localhost:9200/_nodes/stats/jvm?pretty' | grep -A3 heap_used_in_bytes
# 或者更直接地看命令行参数:
ps -ef | grep elasticsearch | grep -o '\-Xm[sx][0-9a-z]*'

如果输出还是 -Xms1g -Xmx1g,说明你的配置文件放错了目录,或者被主 jvm.options 里更靠后的同名参数覆盖了(后来者生效,片段文件按文件名字母序加载)。

第二坑:bootstrap checks 拒绝启动

ES 在绑定到非 localhost 地址(也就是真正提供服务)时会做一轮「启动自检」,官方叫 bootstrap checks。自检不通过,进程直接退出,日志里是这么一句:

ERROR: [1] bootstrap checks failed
[1]: max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]

这不是 ES 刁难你,而是 Lucene 的工作方式决定的:它用 mmap 把索引文件映射进内存,每个映射段算一个「虚拟内存区域」,默认的内核上限 65530 在索引分片多、段多的时候会被用尽,然后 ES 抛 OutOfMemoryError: Map failed,一种看起来像内存不足、实则是内核参数不足的假 OOM。

修复方式是调内核参数,并写进 sysctl 持久化:

sysctl -w vm.max_map_count=262144
echo 'vm.max_map_count=262144' >> /etc/sysctl.d/99-elasticsearch.conf
sysctl -p /etc/sysctl.d/99-elasticsearch.conf
# 验证
sysctl vm.max_map_count

# 文件句柄数也要提,ES 要求至少 65536
ulimit -n 65536
# systemd 环境下要写进 unit,ulimit 命令对已有服务无效:
# /etc/systemd/system/elasticsearch.service.d/override.conf
# [Service]
# LimitNOFILE=65536
# LimitMEMLOCK=infinity

这里有个常被忽略的坑:在 Docker 里跑 ES 时,vm.max_map_count 是宿主机的内核参数,容器内改不了。必须在宿主机上执行 sysctl -w vm.max_map_count=262144。很多人照抄容器里的命令,改了没效果,还以为是自己参数写错了。

第三坑:swap 与文件系统缓存的两难

ES 官方文档建议生产环境关闭 swap,理由是:一旦 ES 的堆被换出到磁盘,GC 停顿会从几十毫秒飙到几秒,集群心跳超时,节点被判死。关闭 swap 有三种做法——swapoff -a 并注释掉 /etc/fstab 里的 swap 行是最彻底的;或者设置 bootstrap.memory_lock: true 让 ES 用 mlockall 把堆锁在物理内存里,但这要求 LimitMEMLOCK=infinity,否则又会触发一条 bootstrap check 失败:

[1]: memory locking requested for elasticsearch process but memory is not locked

但在小内存单机上,我的建议恰好相反:保留 swap,只是把 swappiness 调到很低。原因是这台机器上不全都是 ES,还有 MySQL 和 Nginx。把 swap 彻底关掉之后,内存紧张时内核没有退路,直接调 OOM Killer 杀进程,OOM Killer 的选择是随机的、破坏性的;而保留一点 swap,配合低 swappiness,内核会优先回收文件缓存、把最冷的内存页慢慢换出去,给你留出「发现异常并处理」的时间窗口。

# 保留 swap,但只在真的紧张时才用
echo 'vm.swappiness=10' >> /etc/sysctl.d/99-elasticsearch.conf
sysctl -p /etc/sysctl.d/99-elasticsearch.conf

# 确认 swap 是否被真的使用(第三列 si/so 长期非 0 就要警惕)
vmstat 5 10

另外,Lucene 和大多数数据库一样,把索引文件交给操作系统的文件系统缓存,而不是自己读进堆里。所以你看到的 ES「内存占用」是这样分布的:堆里是 Lucene 的段元数据、查询执行状态;真正的文档数据在堆外,靠 page cache 缓存。这解释了一个反直觉的现象:给 ES 加堆不一定让它更快,反而因为挤占了 page cache,让磁盘随机读变多。这也是 50% 规则的由来——一半给堆,一半留给系统和 page cache。

用 OOM 现场的日志倒推根因

当 OOM Killer 真的动手了,你要做的第一件事不是重启,而是先把现场读下来。核心证据在三处:

# 1. 内核日志里的「凶手判决书」,包含被杀进程和被杀的候选评分
dmesg -T | grep -i -A10 'killed process'

# 2. systemd 环境下的 OOM 记录
journalctl -k --since "1 hour ago" | grep -i oom

# 3. ES 自己的日志,看是堆 OOM 还是 map OOM
tail -100 /var/log/elasticsearch/elasticsearch.log | grep -i -E 'OutOfMemory|circuit_breaking'

关键在第二步要对得上号:如果是 java.lang.OutOfMemoryError: Java heap space,说明是堆不够,要么加 -Xmx(前提是物理内存允许),要么找出吃内存的查询(比如 terms 聚合的高基数字段、size 设成 10000 的无分页搜索)。如果是 OutOfMemoryError: Map failed,那是 vm.max_map_count 不够或者进程虚拟内存地址被 ulimit 限制,跟物理内存没关系,加内存也没用。

ES 还内置了一个保险丝叫 断路器(circuit breaker),它会在内存真正耗尽之前主动拒绝查询并返回 429 Too Many Requests,报错形如 [parent] Data too large, data for [<request>] would be larger than limit。看到这个报错,别急着调大 indices.breaker.total.limit——那是在拆保险丝。正确的做法是回头看是哪个查询这么吃内存。用下面的命令找出最重的查询:

# 慢查询与消耗统计
curl -s 'localhost:9200/_nodes/stats/breaker?pretty'
curl -s 'localhost:9200/_cat/thread_pool/search?v&s=queue:desc' | head
# 开启慢日志,把超过 1s 的查询记下来
curl -X PUT 'localhost:9200/myindex/_settings' -H 'Content-Type: application/json' -d '{
  "index.search.slowlog.threshold.query.warn": "1s",
  "index.search.slowlog.threshold.fetch.warn": "500ms"
}'

个人站的真实配置清单

把上面的结论收拢成一份可以直接抄的清单,前提是「4G 内存 VPS,同机跑 Nginx + MySQL + ES」:

# /etc/security/limits.d/elasticsearch.conf(需要 PAM,容器不生效)
elasticsearch soft nofile 65536
elasticsearch hard nofile 65536
elasticsearch soft memlock unlimited
elasticsearch hard memlock unlimited

# /usr/share/elasticsearch/config/jvm.options.d/heap.options
-Xms1536m
-Xmx1536m

# /usr/share/elasticsearch/config/elasticsearch.yml
cluster.name: my-single-node
node.name: node-1
path.data: /var/lib/elasticsearch
path.logs: /var/log/elasticsearch
network.host: 127.0.0.1        # 只监听本机,由 Nginx 反代,别直接暴露
http.port: 9200
discovery.type: single-node    # 单机必须显式声明,否则报集群引导失败
bootstrap.memory_lock: false   # 小内存机器不锁内存,保留 swap 弹性

# 索引分片数:单机单索引 1 个分片足够,分片多只会增加 mmap 压力和堆开销
# 创建索引时显式指定,别用默认的 1 主 1 副(副本在单机会永远 unassigned)
curl -X PUT 'localhost:9200/myindex' -H 'Content-Type: application/json' -d '{
  "settings": { "number_of_shards": 1, "number_of_replicas": 0 }
}'

最后那条特别值得说:ES 7 之后默认每个索引 1 个主分片 + 1 个副本。单机部署里副本分片无处可放,集群状态会一直停在 yellow。很多新手看到 yellow 就慌了,其实它并不影响读写,只是告诉你「副本没分配」。单机场景把 number_of_replicas 设成 0,集群状态才是 green,监控告警也就不会天天误报。

排查流程小结

  1. 先量体:free -h 看物理内存,ps -ef | grep -o '\-Xm[sx][0-9a-z]*' 看堆配置,确认堆 ≤ 物理内存 50%。
  2. 进程起不来:读日志的 bootstrap checks failed,按提示调 vm.max_map_count、LimitNOFILE、LimitMEMLOCK。
  3. 进程起了又死:dmesg -T | grep -i 'killed process' 区分是 OOM Killer 还是 ES 主动退出,再看 ES 日志区分 heap space 与 Map failed。
  4. 查询报 429:别拆断路器,先抓慢查询与高基数聚合。
  5. 集群 yellow:单机把副本数改成 0,别去折腾集群发现配置。

ES 本身并不适合「装在个人博客服务器上跑搜索」这个场景,它的强项是日志与结构化检索。如果你的目标只是让站内搜索好用,先评估 Meilisearch(中文分词开箱可用,内存占用是 ES 的十分之一)或直接上数据库全文索引,能少踩至少一半的坑。但如果你确实需要 ES,上面这份清单里 90% 的故障都能覆盖到。

Last modification:September 29th, 2026 at 12:27 pm

Leave a Comment