「性能调优与缓存策略」

讲解 ES 性能调优:JVM Heap 与 GC、Page Cache 利用、refresh 与 translog 写入调优、查询缓存与 Doc Values、Segment Merge 与慢日志。

ES 默认配置开箱可用,但生产流量下瓶颈各不相同:有人卡在 JVM GC,有人卡在磁盘 IO,有人卡在 segment 太多。本文沿写入与查询两条主线,拆解堆内存、系统缓存、段合并与查询缓存的调优要点,帮助你找准自己的瓶颈。

1. 性能模型与瓶颈定位

1.1 四类资源与典型瓶颈

资源典型瓶颈信号排查方向
CPU查询/写入线程利用率高分片热节点、合并风暴
内存GC 频繁、OOMHeap 过小、fielddata
IO磁盘等待高、刷新慢段合并、translog fsync
网络协调节点吞吐不足分片过多、跨机房延迟

1.2 先定位再调优

curl -s 'http://localhost:9200/_cat/nodes?v&h=name,cpu,load_1m,heap.percent,ram.percent,disk.used_percent,uptime'

生产惯例是保持 5-10 分钟基线监控,观察 GC 次数、慢查询比例、段数量与磁盘 IO 后,再动手调参。不要照抄别人的参数,同样的集群在不同流量下最优配置完全不同。

1.3 单分片压测

# 以只读压测隔离查询瓶颈
curl -s 'http://localhost:9200/_nodes/stat?pretty' | jq '.nodes | to_entries[0].value.indices.query_cache'

先跑单索引单分片压测,确认单分片吞吐,再按分片数外推集群容量。这能区分是分片路由开销还是单分片本身慢。

2. JVM Heap 调优

2.1 Heap 大小的黄金法则

# jvm.options
-Xms8g
-Xmx8g

Heap 建议设为物理内存的一半,且不超过 32GB。超过 32GB 会触发 JVM 启用压缩指针失效的 OOP 重定位,内存利用反而下降。

物理内存建议 Heap留给 Page Cache
16GB8GB8GB
32GB16GB16GB
64GB31GB33GB

2.2 Heap 用在哪

JVM Heap(默认一半内存)
├── 索引与查询工作缓冲
├── fielddata(聚合排序,已尽量移出)
├── segment 常驻结构(FST 词表等)
└── 请求队列与任务上下文

另一半留给 OS Page Cache
└── 缓存磁盘上的 term dictionary、doc values 等

ES 大量数据实际由 OS Page Cache 承载,Heap 不是越大越好。把 heap 撑满会导致 GC 长暂停,反而让查询卡顿。

2.3 GC 监控

curl -s 'http://localhost:9200/_nodes/stats/jvm?pretty' | jq '.nodes | to_entries[0].value.jvm.gc'

关注 young GC 频率与 old GC 耗时。生产调优手段包括:用 G1GC、控制并发合并线程、限制 fielddata、避免深分页大排序。若 old GC 频繁,通常不是 Heap 太小,而是请求把大量数据拉进堆内。

3. 操作系统与文件系统缓存

3.1 Page Cache 的作用

Lucene 索引文件通过 mmap 映射到 OS Page Cache,读请求优先命中缓存,二次查询远快于首次。因此保留足够空闲内存给 OS,是 ES 最划算的优化。

# 查看 page cache 命中情况
free -h
# 建议预留 50% 内存给 page cache,而非全部塞给 JVM

3.2 文件系统选择

文件系统优势劣势
ext4兼容性最好碎片化
xfs大文件友好、并发高缩容困难
本地 SSD低延迟容量小

生产经验:数据目录用独立磁盘,避免日志与数据争抢 IO;关闭 atime(noatime 挂载)减少写放大。

3.3 虚拟内存与交换

# 禁止 swap 或尽量降低 swappiness
sudo sysctl -w vm.swappiness=1
sudo sysctl -w vm.max_map_count=262144

swap 会让 ES 进程换页抖动,遇到 OOM killer。max_map_count 不足会导致 mmap 失败报 mlock 相关错误,ES 官方文档要求 262144。

4. 索引写入性能

4.1 写入链路

客户端批量请求
   │
   ▼
主分片写入内存 buffer + translog
   │  refresh(默认 1s)
   ▼
buffer 生成不可变段,可被查询
   │  flush(translog fsync + 段落盘)
   ▼
磁盘段进入合并流程

4.2 refresh 与 translog 调优

参数默认调整场景
refresh_interval1s日志写入拉长到 30s+
translog.durabilityrequest可降为 async
translog.sync_interval5s异步周期
index.translog.flush_threshold_size512MB大内存提高
curl -X PUT 'http://localhost:9200/log_index/_settings?pretty' \
  -H 'Content-Type: application/json' \
  -d '{
    "index": {
      "refresh_interval": "30s",
      "translog.durability": "async"
    }
  }'

注意 async translog 在节点宕机时最多丢 sync_interval 内的数据,日志类可接受,交易类必须保持 request。

4.3 批量写入与最佳大小

# 批量写 5000 条 × 每条 1KB
curl -X POST 'http://localhost:9200/log_index/_bulk?pretty' \
  -H 'Content-Type: application/json' \
  --data-binary @batch.ndjson

批量大小需要实测:单个 batch 控制在 5-15MB,过小浪费网络往返,过大会触发大段合并。经验公式是 1000-5000 条或 5-15MB,先小后大逐档测试。

4.4 写入并发与线程池

# 观察写入线程池队列
curl -s 'http://localhost:9200/_cat/thread_pool/write?v&h=name,active,queued,rejected'

write 线程池队列满会拒绝请求并返回 429。调优方向是控制客户端并发、增大批量、拉开 refresh,而不是盲目提高线程数。

5. 查询性能优化

5.1 查询缓存机制

缓存缓存内容失效时机
Query Cachefilter 结果集位图段变化
Request Cache聚合结果段变化
Shard Request Cache命中的 filter 聚合索引写入
curl -X PUT 'http://localhost:9200/blog/_settings?pretty' \
  -H 'Content-Type: application/json' \
  -d '{"index.requests.cache.enable": true}'

filter 子句天然走 Query Cache,把常用过滤条件从 must 挪到 filter 是立竿见影的优化,详见《Query DSL 与相关性打分》。

5.2 Doc Values 与 Fielddata

Doc Values 是面向列存的数值/排序数据,聚合、排序、脚本都依赖它,默认开启。text 字段无法用 doc values 做聚合,需要 keyword 子字段。

存储用途内存
倒排索引全文检索磁盘 + 少量内存
Doc Values排序/聚合/脚本磁盘(OS 缓存)
Fielddata旧版 text 聚合堆内存(避免)

生产规则:永远用 doc values 而非 fielddata;大字段聚合尽量拆分到独立索引。

5.3 慢查询诊断

curl -X PUT 'http://localhost:9200/blog/_settings?pretty' \
  -H 'Content-Type: application/json' \
  -d '{
    "index.search.slowlog.threshold.query.warn": "2s",
    "index.search.slowlog.threshold.fetch.warn": "1s",
    "index.indexing.slowlog.threshold.index.warn": "5s"
  }'

慢日志分级 warn/info/debug,记录查询与 fetch 两个阶段。定位慢分片后配合 _search profile 看是 filter 慢、collect 慢还是 fetch 大 _source 慢。

6. Segment 与 Merge 调优

6.1 段数量的代价

每个查询要遍历所有段,段越多查询越慢,同时 merge 占用 IO。refresh 频繁的索引段数量会飙升。

curl -s 'http://localhost:9200/_cat/segments/blog?v&h=shard,segment,docs.count,size.memory,committed,searchable'

6.2 Merge 策略调参

参数默认作用
index.merge.scheduler.max_thread_count核数/2合并并发
index.merge.policy.max_merged_segment5GB最大段大小
index.merge.policy.segments_per_tier10每层段数
index.merge.policy.deletes_pct_allowed33%删除占比触发合并
curl -X PUT 'http://localhost:9200/blog/_settings?pretty' \
  -H 'Content-Type: application/json' \
  -d '{"index.merge.scheduler.max_thread_count": 1}'

写入高峰时降低 merge 并发,避免合并风暴与写入抢 IO;低谷时恢复默认。

6.3 Force Merge 只读索引

# 对只读索引合并到单段
curl -X POST 'http://localhost:9200/blog_2026/_forcemerge?max_num_segments=1'

日志等不再写入的索引 force merge 到 1 段,查询提速明显。注意 force merge 会产生大段,后续少量更新会触发全段重写,因此只对真正只读的索引执行。

6.4 段合并与删除

频繁更新产生大量删除标记,deletes_pct_allowed 超过阈值会触发合并清理。索引设计上避免高频 update 同一文档,批量更新场景优先考虑 append 语义加时间版本。

7. 集群级调优与慢日志

7.1 分片大小与节点规格

部署规模节点规格分片策略
小业务2 核 8G × 3单分片起
中型8 核 32G × 5节点数 × 2
大型16 核 64G × 10+节点数 × 3-4

分片越小调度越灵活但聚合开销大,越大吞吐高但恢复慢,需要在基准测试中权衡。详情见《集群分片与高可用架构》的分片规划章节。

7.2 集群级动态设置

{
  "persistent": {
    "indices.breaker.total.limit": "60%",
    "indices.query.bool.max_clause_count": 2048,
    "cluster.routing.allocation.node_concurrent_recoveries": 4
  }
}

熔断器 total.limit 防止查询把堆打爆;max_clause_count 限制 bool 子句数,防止超大查询拖垮节点。

7.3 压测与基线

# 使用 rally 或自研压测脚本,记录基线
# 观察指标:QPS、P95 延迟、GC 次数、合并队列

每轮调整只改一个参数,记录前后基线对比,避免多参数耦合导致无法归因。调优是渐进过程,参数表要随版本与数据量更新。

8. 总结

环节要点
瓶颈定位先监控 CPU/内存/IO/网络,再动手调参
JVM Heap物理内存一半且不超 32GB,预留 Page Cache
OS 缓存保留空闲内存给 mmap,禁 swap,noatime 挂载
写入链路refresh 拉长 + translog async + 批量 5-15MB
查询优化filter 走缓存、Doc Values 聚合、慢日志定位
Segment控制段数量,force merge 只读索引
集群参数熔断器、并发恢复、bool 子句上限
压测基线单分片外推,一次只调一个参数

性能调优没有银弹,核心是让数据尽量待在 OS Page Cache,让查询走缓存与 Doc Values,让段数量保持在合理区间。先建立基线再逐项优化,比照搬参数更有价值。底层倒排结构见《倒排索引与分词原理》,集群参数见《集群分片与高可用架构》。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「elasticsearch」更多文章

  1. 「搜索服务架构:从索引到容错」
  2. 「安全加固与访问控制:从角色到审计」
  3. 「地理空间搜索:从坐标到地图」