Elasticsearch 的 Bulk 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 掩盖部分失败。