深入 Elasticsearch(14):搜索延迟、写入吞吐与调优思路
上一篇解决了 segment 和索引的生命周期管理——merge 控制 segment 数量,ILM 管理索引的存储分层。这一篇进入 ES 的性能瓶颈在哪里、调优的思维框架是什么。
性能调优不是背参数表。参数是工具,瓶颈定位才是方法。ES 的性能受搜索延迟和写入吞吐两个维度的独立因素影响,先确定瓶颈在哪个维度的哪个因素上,再针对性调整。
本文只抓一个问题:ES 的性能模型——搜索延迟和写入吞吐分别受什么因素影响,用什么工具定位瓶颈。
搜索延迟的关键因素
1 | |
| 因素 | 影响机制 | 观察方式 |
|---|---|---|
| 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 | |
| 因素 | 影响机制 | 调整方式 |
|---|---|---|
| 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 | |
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 | |
重点关注 rejected 列——非零值表示该线程池曾因过载拒绝请求。
查看节点资源使用:
1 | |
查看 filesystem cache 效果——比较 total_in_bytes 和 OS 可用内存:
1 | |
如果所有 index 的总大小远大于 OS 可用内存(总内存减去 JVM heap),filesystem cache 命中率会很低,搜索延迟升高。
用 _search?profile=true 定位慢查询中的热点:每个 Lucene segment 上各查询组件的耗时一目了然。
模式提炼:定位-度量-调整
1 | |
| 症状 | 可能的瓶颈 | 度量工具 | 调整方向 |
|---|---|---|---|
| 搜索延迟高 | 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。
练习
-
用
_cat/thread_pool?v&h=name,active,queue,rejected查看当前线程池状态。在高并发写入时再次查看,观察 queue 和 rejected 的变化。 -
用
_nodes/stats?filter_path=nodes.*.jvm.mem.heap_used_percent查看 JVM heap 使用率。如果持续超过 75%,需要关注 GC 压力。 -
对一个搜索请求加
profile: true,阅读返回的 profile 信息,找出最耗时的查询组件。
系列导航
| 上一篇 | 下一篇 |
|---|---|
| Segment Merge 与 Index Lifecycle Management | 安全体系:认证、授权与加密 |
