上一篇解决了 segment 和索引的生命周期管理——merge 控制 segment 数量,ILM 管理索引的存储分层。这一篇进入 ES 的性能瓶颈在哪里、调优的思维框架是什么。

性能调优不是背参数表。参数是工具,瓶颈定位才是方法。ES 的性能受搜索延迟和写入吞吐两个维度的独立因素影响,先确定瓶颈在哪个维度的哪个因素上,再针对性调整。

本文只抓一个问题:ES 的性能模型——搜索延迟和写入吞吐分别受什么因素影响,用什么工具定位瓶颈。

搜索延迟的关键因素

1
搜索延迟 = f(segment数, query复杂度, shard数, cache命中率, 数据结构选择)
因素 影响机制 观察方式
Segment 数量 每个 segment 独立搜索,结果合并 _segments API
Query 复杂度 深层嵌套 bool、wildcard、regexp 代价高 _search?profile=true
Shard 数量 scatter-gather 开销随 shard 数线性增长 _cat/shards
Filesystem cache Lucene 文件在 OS page cache 中命中 vs 磁盘读取 _nodes/stats → fs
Doc values vs fielddata fielddata 占 JVM heap,doc values 用 mmap _nodes/stats → indices.fielddata

五个因素之间存在因果链——上游因素恶化会放大下游的开销:

flowchart LR
    Seg[Segment 数量过多] -->|每个 segment<br/>独立搜索| Scatter[Scatter-Gather<br/>开销增大]
    Shard[Shard 数量过多] --> Scatter
    Scatter --> CPU[CPU / 线程池<br/>压力上升]
    Query[Query 复杂度高] --> CPU
    CPU --> Mem{Filesystem Cache<br/>是否命中?}
    Mem -->|命中| Fast[毫秒级返回]
    Mem -->|未命中| Disk[磁盘 I/O<br/>延迟飙升]
    DV[Fielddata 占 Heap<br/>挤压 Cache 空间] --> Mem

Filesystem cache 是搜索性能最关键的因素。Lucene 的 segment 文件(倒排索引、doc values、stored fields)通过 mmap 或 NIOFSDirectory 读取,OS 的 page cache 决定了这些文件是从内存还是磁盘读取。

写入吞吐的关键因素

1
写入吞吐 = f(refresh频率, bulk大小, replica数, translog持久化, merge压力)
因素 影响机制 调整方式
refresh_interval 每次 refresh 生成新 segment,频率高则 segment 多 增大间隔(批量导入时设 -1)
Bulk size 单条写入开销大,批量写入摊薄网络和事务开销 每批 5-15MB
Replica 数 每条写入同步到所有 in-sync replica 批量导入时可临时设 replica=0
Translog durability request 模式每次 fsync,async 模式定期 fsync 可接受丢失时用 async
Merge 压力 后台 merge 和写入竞争 I/O 控制 merge 线程数和 throttle

内存模型:JVM Heap vs Off-Heap

ES 的内存使用分两块:

1
2
3
4
5
6
7
8
9
10
11
Total Server RAM
├── JVM Heap(ES 进程的 Java 堆)
│ ├── Query cache(过滤器结果缓存)
│ ├── Request cache(shard 级搜索结果缓存)
│ ├── Fielddata(text 字段聚合/排序,尽量避免)
│ ├── Indexing buffer(写入缓冲)
│ └── Shard 元数据、Circuit breaker 预留等

└── Off-Heap(OS 管理)
├── Filesystem cache(Lucene segment 文件缓存)
└── 网络缓冲、OS 内核等

JVM Heap 和 Filesystem Cache 的关系是零和博弈——给 Heap 多了,留给 Lucene segment 文件缓存的空间就少了:

flowchart TB
    RAM["服务器总内存<br/>例: 64 GB"]
    RAM --> Heap["JVM Heap ≤ 31 GB<br/>(compressed oops 上限)"]
    RAM --> OS["OS 管理 ≥ 33 GB"]

    Heap --> QCache[Query Cache]
    Heap --> RCache[Request Cache]
    Heap --> FD[Fielddata]
    Heap --> IBuf[Indexing Buffer]
    Heap --> CB[Circuit Breaker 预留]

    OS --> FSCache["Filesystem Cache<br/>(Lucene segment 文件)"]
    OS --> Net[网络缓冲 / 内核]

    style FSCache stroke:#e74c3c,stroke-width:2px

50% 规则:把服务器总内存的一半分给 JVM heap,另一半留给 OS filesystem cache。Lucene 大量使用 mmap 读取 segment 文件,filesystem cache 对搜索性能至关重要。

JVM heap 不要超过 32GB。超过 32GB 后 JVM 无法使用 compressed oops(压缩对象指针),指针从 4 字节变成 8 字节,实际可用内存反而减少。一台 64GB 内存的服务器,heap 设 31GB,剩余 33GB 给 filesystem cache。

Thread Pool 模型

ES 用线程池管理不同类型的操作:

线程池 用途 默认大小
search 搜索请求 CPU 核数 × 3/2 + 1
write 索引/更新/删除/bulk CPU 核数
get 按 ID 取文档 CPU 核数
management 集群管理操作 5
refresh refresh 操作 max(1, CPU/2)

线程池满时,新请求进入队列。队列也满时,请求被拒绝(rejected)。被拒绝的请求是性能问题的明确信号。

实验:用 API 定位瓶颈

查看线程池状态:

1
GET _cat/thread_pool?v&h=name,active,queue,rejected,completed

重点关注 rejected 列——非零值表示该线程池曾因过载拒绝请求。

查看节点资源使用:

1
GET _nodes/stats?filter_path=nodes.*.jvm.mem,nodes.*.os.mem,nodes.*.indices.fielddata

查看 filesystem cache 效果——比较 total_in_bytes 和 OS 可用内存:

1
GET _cat/indices?v&h=index,store.size

如果所有 index 的总大小远大于 OS 可用内存(总内存减去 JVM heap),filesystem cache 命中率会很低,搜索延迟升高。

_search?profile=true 定位慢查询中的热点:每个 Lucene segment 上各查询组件的耗时一目了然。

模式提炼:定位-度量-调整

1
2
3
1. 定位:搜索慢还是写入慢?
2. 度量:用 _nodes/stats、_cat/thread_pool、profile 找到具体因素
3. 调整:只调整瓶颈因素,不做无差别参数调优
症状 可能的瓶颈 度量工具 调整方向
搜索延迟高 segment 太多 _segments force merge(仅对静态 index)
搜索延迟高 filesystem cache 不足 _nodes/stats os.mem 增加内存或减少数据量
搜索延迟高 query 复杂 profile: true 简化查询、用 filter 替代 query
写入吞吐低 refresh 太频繁 refresh_interval 配置 增大 refresh_interval
写入吞吐低 单条写入 客户端代码 改用 bulk API
写入被拒绝 write 线程池满 _cat/thread_pool rejected 控制客户端并发
节点 OOM heap 不足或 fielddata 爆炸 JVM stats, fielddata stats 增大 heap 或禁用 fielddata

工程迁移表

调优维度 Elasticsearch MySQL Kafka
缓存层 OS filesystem cache InnoDB buffer pool OS page cache
内存规划 50% heap + 50% OS 80% buffer pool 极少 heap,主要靠 page cache
写入批量化 Bulk API batch insert batch produce (linger.ms)
延迟可见性 refresh_interval 事务提交 linger.ms + acks
后台维护 merge, ILM optimize, alter table compaction, retention
瓶颈定位 profile, _nodes/stats slow query log, EXPLAIN broker metrics

常见误解

误解一:性能调优就是调参数。 参数调优有用,但前提是先定位瓶颈。盲目调大 thread pool、调小 refresh interval 可能适得其反。定位瓶颈 → 度量影响 → 精确调整,这个顺序不能跳过。

误解二:JVM heap 越大越好。 超过 32GB 失去 compressed oops 优化。更重要的是,heap 占用过多会挤压 filesystem cache 的空间,Lucene 文件无法被缓存,搜索反而变慢。

误解三:shard 数越多搜索越快。 每个 shard 是一个独立的 Lucene index,有固定的内存和 CPU 开销。scatter-gather 的网络和合并开销也随 shard 数线性增长。单个 shard 建议 10-50GB。

练习

  1. _cat/thread_pool?v&h=name,active,queue,rejected 查看当前线程池状态。在高并发写入时再次查看,观察 queue 和 rejected 的变化。

  2. _nodes/stats?filter_path=nodes.*.jvm.mem.heap_used_percent 查看 JVM heap 使用率。如果持续超过 75%,需要关注 GC 压力。

  3. 对一个搜索请求加 profile: true,阅读返回的 profile 信息,找出最耗时的查询组件。

系列导航

上一篇 下一篇
Segment Merge 与 Index Lifecycle Management 安全体系:认证、授权与加密

参考资料