4. 写入性能优化
Elasticsearch 的写入链路可以拆成一条清晰的信号链:客户端请求 → 协调节点路由 → 主分片写入内存缓冲与 translog → 副本分片同步 → refresh 生成可搜索段 → flush 落盘并清空 translog → merge 合并段。写入性能优化,本质上就是在这条链路的每个环节减少不必要的等待、I/O 与 CPU 消耗。本节围绕批量写入、refresh_interval、translog、flush、merge 和线程池六个抓手展开,并给出可落地的参数示例与判断依据。
一、批量写入:先解决“每次请求太小”的问题
单条写入的瓶颈往往不在磁盘,而在网络往返与协调开销。批量写入(Bulk API)把多条文档合并为一次请求,显著摊薄这些固定成本。但批量并非越大越好:过大的 bulk 会占用协调节点内存、拉长单次请求延迟,并在失败时造成更大的重试代价。
实践中建议以“单批 5~15 MB、每批 1000~5000 条文档”为起点,再根据文档大小和节点堆内存调整。判断依据是:观察 bulk 请求的 took 与节点写入队列,如果队列持续增长,说明批量过大或并发过高;如果 took 很小但吞吐上不去,说明批量过小。示例配置如下:
POST _bulk
{ "index": { "_index": "logs-2024", "_id": "1" } }
{ "msg": "a", "@timestamp": "2024-06-01T10:00:00Z" }
{ "index": { "_index": "logs-2024", "_id": "2" } }
{ "msg": "b", "@timestamp": "2024-06-01T10:00:01Z" }
同时应避免使用随机 _id,因为随机 ID 会让分片内文档分布更分散,增加后续 merge 的随机 I/O;若业务允许,使用时间序列或业务主键作为 _id 更利于段合并。
二、refresh_interval:在“可见性”与“段数量”之间取舍
refresh 决定内存缓冲中的数据何时变成可搜索的 Lucene 段。默认 1s 意味着每秒生成一个小段,写入压力大时会产生大量小段,加重 merge 负担。对于日志、指标等“先写后查”的场景,把 refresh_interval 调大是性价比最高的优化之一。
PUT logs-2024/_settings
{
"index.refresh_interval": "30s"
}
如果写入期间完全不需要搜索,可临时设为 -1 关闭自动 refresh,写入完成后再手动 POST logs-2024/_refresh 并恢复为 30s。判断依据:若 _segments 数量持续偏高、merge 线程繁忙,而查询延迟要求并不苛刻,就应优先调大 refresh_interval。
三、translog 配置:决定写入的持久化代价
translog 是写入的“安全绳”。默认 request 模式下,每次写请求都 fsync translog,持久性最强但吞吐最低;async 模式按 sync_interval 批量 fsync,吞吐更高,但节点崩溃时可能丢失最近一个同步周期内的数据。
PUT logs-2024/_settings
{
"index.translog.durability": "async",
"index.translog.sync_interval": "5s",
"index.translog.flush_threshold_size": "1gb"
}
选择原则:金融、订单等不能丢数据的场景保持 request;日志、监控等可容忍秒级丢失的场景用 async。注意 flush_threshold_size 控制 translog 达到多大时触发 flush,调大可减少 flush 次数,但会延长恢复时间。
四、flush 策略与 merge 调优:控制段的数量与大小
flush 会把内存缓冲写入新段、清空 translog,并触发一次 Lucene commit。频繁 flush 会产生大量小段,因此应让 flush 由 translog 阈值自然驱动,而不是人为频繁触发。merge 则是把多个小段合并为大段,减少查询时的段遍历开销,但 merge 本身消耗磁盘 I/O 和 CPU。
调优方向包括:使用 index.merge.scheduler.max_thread_count 控制 merge 并发(机械盘建议 1,SSD 可适当提高);通过 index.merge.policy.floor_segment 避免产生过多微小段;对只写不查的索引,可在写入完成后执行 _forcemerge?max_num_segments=1,但不要在写入过程中执行,否则会与写入争抢 I/O。
五、写入压力下的线程池管理
Elasticsearch 为写入、搜索、merge 等分别维护线程池。写入线程池(write)的队列满时,新请求会被拒绝并返回 EsRejectedExecutionException。这不是“故障”,而是背压信号:说明写入速率超过了节点处理能力。
处理思路分三层:第一,降低客户端并发或减小 bulk 大小,让队列不再持续打满;第二,检查是否因 refresh 过频、merge 过重导致磁盘 I/O 饱和,先调大 refresh_interval、限制 merge 并发;第三,若确认是 CPU 或堆内存不足,再考虑扩容节点或增加分片。可通过以下命令观察线程池状态:
GET _cat/thread_pool/write?v&h=node_name,active,queue,rejected,completed
判断依据:queue 长期大于 0 且 rejected 持续增长,说明写入压力已超过节点承载;若 active 很高但 queue 很小,说明线程都在忙,瓶颈可能在磁盘或 merge。此时应优先排查 merge 与 flush 是否过于频繁,而不是盲目加大 bulk 并发。
refresh_interval 设为 -1 或使用 async translog 会降低数据可见性与持久性,务必确认业务可接受;_forcemerge 是重 I/O 操作,禁止在写入高峰期执行。综合来看,写入性能优化不是单点调参,而是对“批量—可见性—持久化—段合并—线程背压”这条链路的整体权衡。建议在压测环境中固定文档大小与写入速率,逐项调整参数并记录吞吐、延迟、rejected 数量与段数量,形成属于自己业务的参数基线,再推广到生产集群。