ElasticsearchNotes

第 28 章:性能调优

zjc 于 2026-01-28 发布

这是《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"}

建议:

28.2.2 Refresh

默认 1 秒刷新一次。高吞吐日志可放宽:

PUT logs-app/_settings
{
  "index.refresh_interval": "30s"
}

收益:

代价:

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 都会放大写入。

治理:

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 和深分页

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_scorernext_docscoreaggregationfetch。如果时间主要在 build_scorer,可能是查询树或 high frequency term 问题;如果在 next_doc,可能是候选集过大。

28.4 聚合调优

核心是控制三个规模:

候选文档数 x 桶数量 x 每桶工作

建议:

查看聚合缓存:

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
      }
    }
  }
}

说明:

28.5.2 控制 dynamic mapping

生产模板建议:

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:

GET /_nodes/stats/jvm?human
GET /_nodes/hot_threads

常见问题:

现象 可能原因
Old GC 频繁 堆不足、查询结果过大、fielddata、segment 内存
Heap 100% 且熔断 聚合桶过多、深分页、大请求
GC 后堆不下降 缓存或长期对象占用
JVM 正常但查询慢 页缓存不足、磁盘 IO、查询扇出

不要把堆调到机器内存的 80%。Lucene 依赖操作系统页缓存,堆过大反而降低整体性能。

28.7 磁盘与文件系统

生产建议:

查看磁盘:

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

压测必须覆盖:

  1. 混合读写;
  2. 目标数据规模;
  3. 目标字段 Mapping;
  4. 查询语句集合;
  5. 峰值持续时长;
  6. merge 和 refresh 周期;
  7. 节点故障场景;
  8. 磁盘接近水位后的行为。

只做短时间纯写入压测,无法代表生产负载。

28.11 调优清单

本章小结

性能调优的第一步是定位瓶颈:写入端看 bulk、refresh、merge、translog 和磁盘;查询端看扇出、分页、候选集、聚合桶和 fetch;资源端看 JVM、页缓存、IO 和线程池。大多数优化来自更小的数据范围、更合理的 Mapping 和更受控的请求模式,而不是神秘参数。

思考题

  1. 为什么 JVM 堆不是越大越好?
  2. write rejected 和 search rejected 的处理思路有什么不同?
  3. 放宽 refresh interval 会带来什么代价?
  4. 哪些字段可以关闭 doc_values,哪些不能?
  5. 如果压测吞吐很高但线上延迟很差,可能遗漏了什么测试维度?