bulk.write() 反常慢主因是单次请求过大导致内存拷贝、序列化阻塞或es拒绝,应控制单批5–15mb(非固定条数),复用buffer、动态分批、并发限流、错误隔离,并注意es侧超时、mapping冲突与_id类型一致。

为什么 bulk.Write() 会慢得反常?
Go 客户端(如 olivere/elastic 或官方 elastic/go-elasticsearch)调用 BulkService 时,如果单次请求塞入过多文档(比如 >1000 条),反而可能因内存拷贝、序列化阻塞或 ES 端拒绝而变慢。这不是“写得不够多”,而是没控制好节奏。
- ES 默认
http.max_content_length是 100MB,但实际建议单次 bulk 请求控制在 5–15MB,对应约 500–2000 条文档(取决于单条大小) -
bulk.Index().Id().Doc()链式调用本身不发请求,真正耗时在Do(ctx)序列化 + HTTP 发送 - 未复用
*bytes.Buffer或提前预估 JSON 大小,会导致高频内存分配(runtime.mallocgc上升明显)
如何让 bulk 请求真正“批起来”?
关键不是堆数量,而是稳住吞吐节奏:固定批次大小 + 并发控制 + 错误隔离。官方客户端不自带缓冲队列,得自己搭一层轻量封装。
- 用
sync.Pool复用*bytes.Buffer,避免每次bulk.Do()都 new 一个 - 按文档字节数动态分批(而非固定条数):累计
len(jsonBytes)达到 8MB 就提交,比硬设 1000 条更抗抖动 - 并发提交多个 bulk 请求时,用
semaphore控制最大并行数(如 4),防止打爆 ES 连接池或触发EsRejectedExecutionException - 对
bulk.Responses中的每条错误单独处理(比如 status=400 的 document_id 写入失败),不要整批丢弃
用 go-elasticsearch 实现带流控的批量写入
官方 elastic/go-elasticsearch 更轻量,也更适合自定义。重点在构造 bytes.Reader 前把所有操作序列化进 buffer,跳过客户端内部拼接逻辑。
// 示例:构建紧凑 bulk body
var buf bytes.Buffer
for _, doc := range docs {
// 写 action line: {"index":{"_index":"logs","_id":"123"}}
action := map[string]map[string]string{
"index": {"_index": "logs", "_id": doc.ID},
}
json.NewEncoder(&buf).Encode(action) // 不换行,但 es 接受
json.NewEncoder(&buf).Encode(doc.Data)
}
// 直接 POST /_bulk,body = buf.Bytes()
resp, err := es.Bulk(bytes.NewReader(buf.Bytes())).Do(ctx)
- 不用
esutil.BulkIndexer(它适合超大数据流,但默认重试+缓冲会掩盖背压问题) - 手动构造 body 可省掉客户端对每条 doc 的反射序列化,实测小文档场景快 20%+
- 务必检查
resp.Errors == true,再遍历resp.Body解析具体哪条失败(ES 返回的是混合 JSON stream)
容易被忽略的坑:timeouts 和 mapping conflict
批量写入出问题,八成不在代码逻辑,而在 ES 侧配置或数据结构漂移。
-
context.WithTimeout(ctx, 30*time.Second)必须设——bulk 超时不是由 ES 的timeout参数控制,而是 HTTP client 层 - 若文档含新字段,且 index mapping 是
dynamic: true,首次写入会触发 mapping update,导致 bulk 整体卡住甚至失败(报illegal_argument_exception) - 不同 goroutine 往同一 index 写时,避免混用
_id类型(string vs int),ES 对 _id 做哈希时类型不一致会隐式转成 string,但可能引发 routing 不均 - 别依赖
refresh=true参数来“立刻查到”,它会让 bulk 性能跌 50%,真要查用refresh=wait_for或查后手动POST /logs/_refresh
_nodes/stats/bulk 和 GC p99,再决定是调 batch size 还是加节点。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











