这是《Elasticsearch 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 性能调优不是收集一堆“最佳参数”,而是先定位瓶颈,再在容量、延迟、成本和可靠性之间做取舍。Elasticsearch 的性能通常由五个资源共同决定:CPU、JVM 堆、操作系统页缓存、磁盘 IO 和网络。
本章按写入、查询、聚合、Mapping、JVM 和硬件几个层面梳理调优方法。
28.1 性能调优流程
一次完整的调优应遵循:
定义目标
-> 建立基线
-> 观察资源指标
-> 定位瓶颈层
-> 修改一个变量
-> 压测对比
-> 评估副作用
-> 记录参数和结论
目标必须具体:
| 模糊目标 | 可验证目标 |
|---|---|
| 写入快一点 | 10000 docs/s,P99 写入 < 200ms |
| 查询别慢 | P95 < 300ms,P99 < 1s |
| 聚合别卡 | 30 天报表 < 3s |
| 集群稳定 | 7 天无 429、无熔断、无节点离线 |
没有基线的调优,很容易只是把噪声当收益。
28.2 写入调优
28.2.1 批量写入
单文档请求固定成本高。应使用 Bulk:
POST _bulk
{"index": {"_index": "logs-app", "_id": "1"}}
{"message": "request ok", "log.level": "info"}
{"index": {"_index": "logs-app", "_id": "2"}}
{"message": "request failed", "log.level": "error"}
建议:
- 批次大小从 1MB 到 10MB 压测;
- 控制并发,而不是无限开线程;
- 部分失败时只重试失败项;
- 429 时指数退避;
- 保持请求顺序,方便定位 item 错误。
28.2.2 Refresh
默认 1 秒刷新一次。高吞吐日志可放宽:
PUT logs-app/_settings
{
"index.refresh_interval": "30s"
}
收益:
- 减少 Segment 数量;
- 降低 refresh CPU;
- 降低 merge 压力;
- 提高写入吞吐。
代价:
- 数据可搜索延迟增加;
- 监控告警数据变旧;
- 用户能看到的数据滞后。
28.2.3 副本与导入
大批量初始化导入时可以临时:
PUT import-v1/_settings
{
"number_of_replicas": 0,
"refresh_interval": "-1"
}
导入完成后恢复:
PUT import-v1/_settings
{
"number_of_replicas": 1,
"refresh_interval": "1s"
}
必须确保恢复动作自动化。否则一旦节点故障,索引可能只有一个主分片副本,风险极高。
28.2.4 Translog
可靠性优先:
PUT orders-v3/_settings
{
"index.translog.durability": "request"
}
吞吐优先且可接受少量丢失窗口:
PUT logs-app/_settings
{
"index.translog.durability": "async",
"index.translog.sync_interval": "5s"
}
订单、支付、审计不要为了性能直接改为 async,除非业务明确接受一致性代价。
28.2.5 写入放大
高频 update、频繁 refresh、大量分片和过度 force merge 都会放大写入。
治理:
- 能用全量替换就不用 update;
- 热点计数先在 Redis/Kafka 聚合,再低频写 ES;
- 控制嵌套对象数量;
- 降低写入端并发毛刺;
- 给 merge 留出 CPU 和 IO;
- 使用 rollover 控制单分片大小;
- 低峰执行 force merge。
28.3 查询调优
28.3.1 缩小数据范围
第一优先级永远是缩小范围:
GET logs-app/_search
{
"size": 20,
"query": {
"bool": {
"filter": [
{ "term": { "service.name": "order-service" } },
{ "range": { "@timestamp": { "gte": "now-15m", "lte": "now" } } }
]
}
},
"sort": [{ "@timestamp": "desc" }]
}
避免:
GET logs-*/_search
{
"size": 10000
}
索引名通配符越宽,查询扇出越大。
28.3.2 Filter 优先
不需要评分的条件放 filter:
"filter": [
{ "term": { "status": "on_sale" } },
{ "range": { "price": { "gte": 100, "lte": 500 } } }
]
filter 可以利用 node query cache,语义也更清晰。
28.3.3 控制 size 和深分页
- 默认
size用 10 或 20; - 列表页不超过 100;
- 深翻页用 search after;
- 导出用 PIT + search after;
- 不提高
max_result_window解决分页问题。
28.3.4 Source Filtering
只返回必要字段:
GET products-v3/_search
{
"_source": ["sku_id", "title", "price", "image"],
"query": { "match_all": {} }
}
大字段返回会放大 fetch、网络和 JSON 序列化成本。
28.3.5 避免昂贵查询
常见昂贵模式:
| 查询 | 问题 | 替代 |
|---|---|---|
wildcard: "*手机" |
无法高效用词典 | ngram、search_as_you_type、前缀查询 |
| 大范围 script | 每文档执行 | 预计算字段 |
| text 上高基数聚合 | fielddata / 错误建模 | keyword |
match_all + 大 size |
全量扫描 | 按时间/状态过滤 |
| 深分页 | 内存放大 | search after |
| 多层嵌套聚合 | 桶爆炸 | 预聚合 |
| 前缀通配符任意组合 | 查询树巨大 | 受控搜索词表 |
28.3.6 Profile
用 Profile 定位:
GET products-v3/_search
{
"profile": true,
"query": {
"bool": {
"must": [{ "match": { "title": "笔记本" } }],
"filter": [{ "term": { "status": "on_sale" } }]
}
}
}
重点看 build_scorer、next_doc、score、aggregation 和 fetch。如果时间主要在 build_scorer,可能是查询树或 high frequency term 问题;如果在 next_doc,可能是候选集过大。
28.4 聚合调优
核心是控制三个规模:
候选文档数 x 桶数量 x 每桶工作
建议:
size: 0;- 限制时间范围;
- 控制嵌套聚合;
- terms 使用低中基数字段;
- 控制 cardinality precision;
- 大报表预聚合;
- composite 用于导出;
- 慢查询日志和熔断指标监控。
查看聚合缓存:
GET orders-v3/_stats/request_cache,query_cache,fielddata?human
时间条件尽量取整:
"range": {
"@timestamp": {
"gte": "now-1d/d",
"lte": "now/d"
}
}
这样更容易命中 request cache。
28.5 Mapping 调优
28.5.1 字段只保留需要的索引结构
PUT events-v1
{
"mappings": {
"dynamic": false,
"properties": {
"event_type": {
"type": "keyword",
"doc_values": true,
"index": true
},
"trace_id": {
"type": "keyword",
"index": true,
"doc_values": false
},
"message": {
"type": "text",
"index": true
},
"raw_body": {
"type": "keyword",
"index": false,
"doc_values": false
}
}
}
}
说明:
trace_id只查不聚合,可关闭 doc_values;raw_body只返回不查不聚合,可关闭索引和 doc_values;message全文检索需要 text;- 不要默认让所有字段都拥有倒排、doc_values 和 multi-field。
28.5.2 控制 dynamic mapping
生产模板建议:
- 日志类:
dynamic: false或dynamic: strict; - 业务索引:
dynamic: strict; - 调试数据单独放测试索引;
- 新字段走评审;
- 使用
meta记录字段负责人和口径。
PUT orders-v4
{
"mappings": {
"dynamic": "strict",
"_meta": {
"owner": "order-team",
"version": "v4"
},
"properties": {}
}
}
28.5.3 嵌套与父子
nested 精确但昂贵;join 灵活但查询和内存成本更高。优先顺序通常是:
反范式冗余 > object(若语义允许) > nested > join
如果 nested 对象数量可能无限增长,必须设置上限并监控。
28.6 JVM 调优
Elasticsearch 对 JVM 堆和页缓存的依赖都很强。经验规则:
- JVM 堆不超过 50% 机器内存;
- 堆大小不超过 31GB;
- Xms 和 Xmx 相等;
- 保留足够页缓存给 Lucene;
- 优先使用 G1GC;
- 不要随意切换实验 GC;
- 观察老年代回收频率,而不是只看堆使用率。
查看 JVM:
GET /_nodes/stats/jvm?human
GET /_nodes/hot_threads
常见问题:
| 现象 | 可能原因 |
|---|---|
| Old GC 频繁 | 堆不足、查询结果过大、fielddata、segment 内存 |
| Heap 100% 且熔断 | 聚合桶过多、深分页、大请求 |
| GC 后堆不下降 | 缓存或长期对象占用 |
| JVM 正常但查询慢 | 页缓存不足、磁盘 IO、查询扇出 |
不要把堆调到机器内存的 80%。Lucene 依赖操作系统页缓存,堆过大反而降低整体性能。
28.7 磁盘与文件系统
生产建议:
- 数据盘使用 SSD,尤其是 hot 层;
- 避免网络存储或高抖动云盘;
- 磁盘吞吐与写入、merge、恢复匹配;
- 预留磁盘余量,避免 85% 水位;
- 使用
noatime; - 文件描述符上限充足;
- 关注 IO util、await、queue;
- merge 和 recovery 都会大量使用磁盘。
查看磁盘:
GET /_cat/allocation?v
GET /_cat/nodes?v&h=name,disk.used_percent,disk.total,disk.avail
GET /_nodes/stats/fs?human
28.8 线程池
查看线程池:
GET /_cat/thread_pool/search,write?v&h=node_name,name,active,queue,rejected,completed
拒绝处理:
| 池 | 常见原因 | 处理 |
|---|---|---|
| write | 写入并发过高、merge、磁盘慢 | 降并发、批量、扩容 |
| search | 查询扇出、深分页、聚合大 | 限流、优化 DSL、预聚合 |
| get | 高频点查过多 | 检查是否误用 ES 做主存储 |
| flush | 磁盘慢、分片多 | IO 评估、flush 频率 |
| refresh | 刷新过频 | 放宽 refresh_interval |
不建议首先加大队列。队列越长,故障时的内存压力和等待延迟越大。
28.9 冷热与隔离
常见隔离方式:
| 隔离 | 解决问题 |
|---|---|
| hot/warm/cold | 日志生命周期和成本 |
| 读写分离 | 重查询不影响写入 |
| 在线/离线集群 | 大分析不拖垮在线服务 |
| 多租户索引隔离 | 权限、流量和容量边界 |
| Kibana 与 API 集群分离 | 可视化流量隔离 |
如果团队经常在 30 天日志上做临时聚合,应把这些分析迁移到离线集群或数据仓库,而不是让在线日志集群承担分析负载。
28.10 压测方法
推荐使用 Rally 或自建压测:
esrally race --track=geonames --challenge=append-no-conflicts --car=4gheap
压测必须覆盖:
- 混合读写;
- 目标数据规模;
- 目标字段 Mapping;
- 查询语句集合;
- 峰值持续时长;
- merge 和 refresh 周期;
- 节点故障场景;
- 磁盘接近水位后的行为。
只做短时间纯写入压测,无法代表生产负载。
28.11 调优清单
- 有明确 SLO 和基线数据;
- 写入使用 Bulk 和合理并发;
- refresh interval 按场景设置;
- 导入后自动恢复副本;
- 权衡 translog 可靠性;
- 查询限制索引和时间范围;
- filter 优先;
- size 和分页受控;
- source filtering;
- 聚合限制桶数和基数;
- Mapping 关闭不用的 doc_values 和 index;
- JVM 堆和页缓存比例合理;
- 磁盘余量充足;
- 慢查询、rejected、GC、merge 有监控;
- 所有参数变更可回滚并记录原因。
本章小结
性能调优的第一步是定位瓶颈:写入端看 bulk、refresh、merge、translog 和磁盘;查询端看扇出、分页、候选集、聚合桶和 fetch;资源端看 JVM、页缓存、IO 和线程池。大多数优化来自更小的数据范围、更合理的 Mapping 和更受控的请求模式,而不是神秘参数。
思考题
- 为什么 JVM 堆不是越大越好?
- write rejected 和 search rejected 的处理思路有什么不同?
- 放宽 refresh interval 会带来什么代价?
- 哪些字段可以关闭 doc_values,哪些不能?
- 如果压测吞吐很高但线上延迟很差,可能遗漏了什么测试维度?