← 返回题目列表

Elasticsearch 的 Bulk API 如何优化写入性能?

高频 中等 第 4 / 30 题 更新于 2026/07/29
ElasticsearchBulk API写入优化性能优化

简化版

Bulk API 把多条 index、create、update、delete 操作合并成一次请求,减少网络往返和协调开销,是 ES 高吞吐写入的基础。优化时要控制批次大小、并发数、refresh 策略、副本数、mapping、索引数量和失败重试,不能盲目把单批做得越大越好。

详细版

Bulk 请求使用 NDJSON 格式,一行动作元数据,一行文档内容:

{ "index": { "_index": "logs", "_id": "1" } }
{ "message": "ok", "level": "INFO" }

面试回答要讲清楚:Bulk 提升的是吞吐,不保证每条都成功;响应里每个 item 都要检查错误。常见优化包括批次按 5MB、10MB、20MB 量级压测,控制客户端并发,导入期间适当调大 refresh_interval,必要时临时降低 replicas,避免动态 mapping 爆炸和过多小索引。

完整版教学

一、Bulk 解决的是写入往返成本

如果每条文档都单独发 HTTP 请求,网络 RTT、序列化、协调节点分发都会成为瓶颈。

10000 docs
single request per doc -> 10000 round trips
bulk 1000 docs/batch   -> 10 round trips

Bulk API 把多条写操作打包,让 ES 一次解析、一次分发、批量执行,吞吐通常会明显提升。

记忆钩子:Bulk 提升吞吐,不等于每条自动成功。

二、Bulk 请求格式是 NDJSON

Bulk 不是普通 JSON 数组,而是 newline delimited JSON。每个操作由 action 行和可选 source 行组成。

{ "index": { "_index": "logs", "_id": "1" } }
{ "level": "INFO", "message": "start" }
{ "delete": { "_index": "logs", "_id": "2" } }
{ "update": { "_index": "logs", "_id": "3" } }
{ "doc": { "level": "WARN" } }

最后通常也需要换行。格式错误会导致整个请求解析失败,这是新手常见坑。

操作类型的区别:

操作含义是否需要 source 行
index有则覆盖,无则新增需要
create只新增,已存在报错需要
update局部更新或脚本更新需要
delete删除文档不需要

三、批次大小要靠压测,不是越大越好

单批太小,网络和协调开销占比高;单批太大,会造成内存压力、队列堆积、GC、请求超时和失败重试变重。

常见经验是从 5MB、10MB、20MB 或 1000 到 5000 条这样的量级开始压测,而不是一上来塞 10 万条。

batch = 500 docs  -> QPS 低,RTT 占比高
batch = 5000 docs -> 吞吐上升
batch = 50000 docs -> 内存和超时风险上升

不同文档大小差异很大,所以“条数”不如“请求体大小 + 延迟 + 失败率”可靠。

四、并发数要和集群吞吐匹配

Bulk 客户端通常会并发发送多个批次。并发太低,集群吃不满;并发太高,写线程池、队列、磁盘和 merge 都会被打爆。

观察指标包括:

bulk rejection
indexing latency
CPU / disk I/O
merge time
refresh time
JVM heap / GC

如果出现 429 或 bulk thread pool rejection,要降低并发或削峰,而不是无限重试。重试也要做退避,否则失败流量会把集群压得更重。

五、refresh 和 replica 会影响导入速度

ES 默认近实时搜索,写入后经过 refresh 才能被搜索到。批量历史导入时,如果不需要马上搜索,可以临时调大 refresh_interval

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

如果是离线重建索引,也可能临时把副本数设为 0,导入完成后再恢复副本。

调整项好处风险
调大 refresh_interval减少 refresh 开销数据可搜索延迟变长
临时 replicas=0减少副本写入节点故障时冗余不足
完成后 force merge减少段数量消耗 I/O,需低峰执行

线上实时写入不要随便降低副本,要结合可用性要求。

六、失败处理必须逐 item 检查

Bulk HTTP 返回 200,不代表每条都成功。响应里可能部分成功、部分失败。

{
  "errors": true,
  "items": [
    { "index": { "status": 201 } },
    { "index": { "status": 409, "error": { "type": "version_conflict_engine_exception" } } }
  ]
}

应用必须遍历 items,区分可重试错误和不可重试错误。比如 429、503 可以退避重试,mapping 类型错误通常要进死信队列或修数据。

这点比“怎么调快”更重要,因为批量导入最怕静默丢数据。

七、常见误区与追问

  • 误区:Bulk 单批越大越快。 单批过大会导致内存、超时和重试成本上升,最佳值要压测。
  • 误区:HTTP 200 就代表全部写入成功。 Bulk 可能部分失败,必须检查每个 item。
  • 误区:写入慢只要加客户端并发。 并发过高会造成队列拒绝、merge 压力和 GC,可能更慢。
  • 追问:批量导入怎么调 refresh? 不要求马上搜索时可调大 refresh_interval,导入后恢复。
  • 追问:为什么临时设置 replicas=0? 离线导入时减少副本写放大,但期间高可用降低。
  • 追问:Bulk 失败怎么处理? 分类错误,退避重试可恢复错误,不可恢复错误记录死信并告警。

八、加强记忆

Bulk 写入记成“批量降往返,大小靠压测,失败逐条看”。它能显著提升吞吐,但不是把批次无限放大;要一起调批次大小、客户端并发、refresh、副本、mapping 和重试策略。生产里最重要的是检查 item 级错误,避免 HTTP 200 掩盖部分失败。