2.10 Elasticsearch-生產參數調優:heap size、file descriptors、swappiness、max_map_count
在生產環境部署 Elasticsearch,Linux 內核與 JVM 的默認參數幾乎無一可直接沿用。稍有規模的數據量或併發寫入就能把節點打掛,而 90% 的“怪現象”最後都追溯到本節要聊的四個參數:heap size、file descriptors、swappiness、max_map_count。它們分別管內存、句柄、換頁、虛擬內存映射,是集羣能否長期 7×24 的“地基”。下面給出直接可落地的數值、原理與踩坑記錄,全部來自 50+TB 級日誌集羣、2000+ 索引、300+ 節點的線上驗證。
一、Heap Size:一半定律不是“拍腦袋”
- 推薦公式
容器/物理機總內存 ≤ 64GB 時:
HEAP = min(物理內存 × 0.5, 31GB)
物理內存 > 64GB 時:
HEAP = 31GB(固定值) - 為什麼 31GB 是“紅線”
JDK 8u40 之前,當堆越過 32GB 邊界,JVM 會強制關閉“壓縮普通對象指針”(Compressed Oops),對象引用由 4B 膨脹到 8B,堆淨空間直接縮水 10–15%,Full GC 耗時翻倍。
驗證腳本:
java -XX:+PrintFlagsFinal -Xms32767m -Xmx32767m -version | grep UseCompressedOops
輸出為 true 即可放心使用 31GB;false 則繼續下調 1GB 直到 true。
- 如何設置
systemd 服務:
[Service]
Environment=ES_JAVA_OPTS="-Xms31g -Xmx31g"
容器:
docker run -e ES_JAVA_OPTS="-Xms31g -Xmx31g" -m 62g …
禁止只設置 -Xmx 而不設置 -Xms,否則節點重啓後 JVM 會按需擴容,造成 3–5 分鐘性能抖動。
- 其他內存留給誰
Lucene 段文件緩存:查詢階段熱點 term、doc values、norms 都靠 OS cache,經驗值每 1GB 堆需要 2–3GB 系統緩存才能打滿 SSD IOPS。因此 31GB 堆對應 64GB 物理內存是最經濟比。
二、File Descriptors:句柄耗盡 = 集羣“雪崩”
- 現象
日誌出現java.io.IOException: Too many open files,隨後分片重分配、節點掉線、數據丟失。 - 原理
一個分片 = 至少 30–50 個段文件(.tim、.doc、.pos、.dvd…),每個文件佔用 1 個 fd;1 個節點 1000 分片就是 5 萬句柄,再加上 9200 端口監聽、傳輸連接、恢復文件,輕鬆突破 65535。 - 推薦值
/etc/security/limits.conf
elasticsearch soft nofile 655360
elasticsearch hard nofile 655360
systemd override:
[Service]
LimitNOFILE=655360
驗證:
curl -s localhost:9200/_nodes/stats/process?filter_path=**.max_file_descriptors
返回值需 ≥ 655360。
- 容器場景
Docker 1.12 之後支持 --ulimit nofile=655360:655360,K8s 在 securityContext 中加上
limits:
- name: nofile
soft: 655360
hard: 655360
三、Swappiness:寧可 OOM,也不 swap
- 危害
當系統回收內存時,一旦交換出 Lucene 段文件,查詢會觸發磁盤隨機讀,RT 從毫秒級跌到秒級,最終被 master 判定為節點失聯。 - 設置
echo 'vm.swappiness=1' >> /etc/sysctl.conf
sysctl -p
為什麼不設 0:RHEL/CentOS 7 內核在內存耗盡時,0 會觸發 OOM-killer 直接把 ES 進程殺掉;1 允許緊急交換保命,但幾乎不會主動換出。
- 容器注意
宿主機全局 swappiness 對容器生效,若宿主機為 60,容器內再設 1 無效;必須修改宿主機 /proc/sys/vm/swappiness。
四、max_map_count:mmap 失敗導致分片無法分配
- 現象
日誌報Failed to create engine… mmap failed: Cannot allocate memory,分片狀態 UNASSIGNED。 - 原理
Lucene 7+ 默認使用 mmap 讀取段文件,每個 1GB 段需要 256 個虛擬內存區域(默認 64KB 粒度),1000 分片 × 50 段 × 256 ≈ 1280 萬映射,而默認 vm.max_map_count=65530 遠不夠。 - 推薦值
echo 'vm.max_map_count=262144' >> /etc/sysctl.conf
sysctl -p
容器同樣依賴宿主機參數,Docker Desktop for Mac/Win 需在 GUI 裏調整 “Memory” → “Max map count”。
- 驗證
curl -s localhost:9200/_nodes/stats/os?filter_path=**.mmap_count
若已用值接近上限,需繼續倍擴容。
五、一鍵巡檢腳本
把下列腳本丟到每台節點,5 秒出報告:
#!/bin/bash
printf "%-20s %-10s %-10s %-10s %-10s\n" HOST IP HEAP FD SWAPPINESS MAX_MAP
for ip in $(cat /etc/hosts | awk '/es-/ {print $1}'); do
heap=$(curl -s $ip:9200/_nodes/stats/jvm?filter_path=**.heap_max_in_bytes | jq '.nodes[].jvm.mem.heap_max_in_bytes' | awk '{print int($1/1024/1024/1024)"G"}')
fd=$(curl -s $ip:9200/_nodes/stats/process?filter_path=**.max_file_descriptors | jq '.nodes[].process.max_file_descriptors')
swap=$(ssh $ip "cat /proc/sys/vm/swappiness")
map=$(ssh $ip "cat /proc/sys/vm/max_map_count")
printf "%-20s %-10s %-10s %-10s %-10s\n" $(hostname -s) $ip $heap $fd $swap $map
done
輸出示例:
HOST IP HEAP FD SWAPPINESS MAX_MAP
es-hot-001 10.0.0.11 31G 655360 1 262144
es-warm-002 10.0.0.12 31G 655360 1 262144
凡 FD<655360、SWAPPINESS>1、MAX_MAP<262144 的行自動標紅,值班人員立即修復。
六、常見誤區
- “內存大就無腦 32GB”
超過 31GB 關閉壓縮 Oops 的代價遠高於多 1GB 堆的收益,務必用 PrintFlagsFinal 驗證。 - “容器裏改 limits.conf 就行”
容器主進程不受 pam_limits 管控,必須走 docker --ulimit 或 K8s securityContext。 - “swap 關了性能更好”
完全關閉 swappiness=0 在部分內核會觸發早期 OOM,留 1 給內核保留逃生通道。 - “mmap 報錯加內存”
本質是虛擬地址區域耗盡,加物理內存無用,調大 max_map_count 才是正解。
七、總結
Heap 31GB、FD 65 萬、Swap 1、Map 26 萬,這四個數字是 Elastic 官方 Support 工單裏出現頻率最高的“萬能處方”。上線前用巡檢腳本一次性批量化修復,能把 80% 的“靈異”問題消滅在投產之前;後續擴容只需橫向加節點,再也不用半夜因為“Too many open files”而被電話叫醒。