从 Mapping 设计、索引模板、Bulk 批写、Refresh/Translog 调优四个层面系统梳理 ES 写入性能优化手段,覆盖从"数据建模"到"参数配置"的全链路实践。
目录
| 章节 | 说明 |
|---|---|
| Mapping 设计 | 字段类型选择、动态映射控制 |
| 索引模板与滚动策略 | 时序索引、冷热分离 |
| Bulk 批量写入 | 批大小选择、自动 ID |
| Refresh 与 Translog 调优 | 降低刷盘频率提升吞吐 |
| 段合并策略 | 写入与合并的 Trade-off |
| 副本与写入 | 副本数对写入的影响 |
| 性能基准参考 | 不同机型的写入 QPS 参考值 |
Mapping 设计
字段类型选择
| 场景 | 推荐类型 | 原因 |
|---|---|---|
| 精确匹配(ID、状态码) | keyword |
term 查询性能比数值类型快 3-5 倍 |
| 全文检索 | text |
分词建倒排索引 |
| 数值仅做精确 term 查询 | keyword |
数字类型 term 查询性能差,CPU 占用高 |
| 数值需要范围查询或聚合 | long / double |
BKD 树结构更适合范围查询 |
| 不需要排序/聚合的字段 | 关闭 doc_values |
节省磁盘和内存 |
| 不需要全文检索的字符串 | 关闭 index |
不建倒排,节省存储 |
核心原则:不需要范围查询时优先用
keyword;需要范围查询时才用数值类型。ES 5.x 起数字类型 term 查询性能相比 2.x 下降约 80%,CPU 利用率飙升 30%+。
Schema 扁平化
- ES 不支持 JOIN,Schema 应尽量扁平,避免 nested 和 parent-child
- nested 查询比普通查询慢 10 倍以上
- parent-child 查询比普通查询慢 100 倍以上
- 将 join 逻辑放到应用层处理
关键 Mapping 参数
{
"mappings": {
"dynamic": "strict",
"_source": {
"enabled": true
},
"properties": {
"id": { "type": "keyword" },
"price": { "type": "double" },
"name": {
"type": "text",
"fields": {
"keyword": { "type": "keyword", "ignore_above": 256 }
}
},
"tag": {
"type": "keyword",
"doc_values": false
}
}
},
"settings": {
"index.mapping.total_fields.limit": 1000,
"index.mapping.depth.limit": 20,
"index.mapping.nested_fields.limit": 50
}
}
| 参数 | 默认值 | 说明 |
|---|---|---|
dynamic: strict |
- | 禁止写入未定义字段,防止字段爆炸 |
total_fields.limit |
1000 | 单索引最大字段数 |
depth.limit |
20 | 嵌套对象最大深度 |
nested_fields.limit |
50 | nested 字段最大数量 |
_source 的作用与取舍
_source 是 ES 写入时单独保存的原始 JSON 副本,与倒排索引、Doc Values 并存,互相独立:
写入一条文档时,ES 同时构建:
├─ 倒排索引 → 用于搜索
├─ Doc Values → 用于排序/聚合
└─ _source → 存储原始 JSON,用于返回文档内容 / update / reindex / highlight
_source 在磁盘上以**压缩 JSON(LZ4)**存储,占索引总体积的 30%~50%。
_source: false 的影响
| 功能 | 是否依赖 _source |
|---|---|
| 搜索命中(返回文档内容) | ✅ 依赖,关闭后返回空 |
_update 部分字段更新 |
✅ 依赖,关闭后报错 |
reindex 跨索引迁移 |
✅ 依赖,关闭后无法迁移 |
highlight 高亮 |
✅ 依赖 |
| 搜索匹配本身(召回) | ❌ 不依赖(走倒排索引) |
| 排序/聚合 | ❌ 不依赖(走 Doc Values) |
_source: false 几乎没有合适的使用场景——节省的存储空间换来的是一堆功能残废,且无法恢复(不存就是没了)。
真正有用的是 includes/excludes 裁剪:只存需要的字段,兼顾存储节省与功能完整:
"_source": {
"includes": ["id", "name", "price", "status"],
"excludes": ["raw_content", "embedding_vector"]
}
⚠️ 如需部分字段更新,必须保留
_source: true(或至少 includes 包含相关字段),否则_update请求会报错。
索引模板与滚动策略
什么时候用模板
- 数据量持续增长 → 必须使用模板,按时间周期(天/月/年)滚动创建索引
- 数据量固定 → 可以不用模板
按时间滚动的好处
- 单索引大小可控,便于后续数据清理和归档
- 历史冷数据可整索引迁移到冷集群(冷热分离)
- 小索引迁移成本低
分片大小规范
- 单个分片物理大小 不超过 40 GB(推荐 10-30 GB)
- 单个索引建议 不超过 200 GB
- 根据每日/每月数据增量决定滚动周期
索引别名
# 创建索引时绑定别名
PUT /orders-2024-01
{
"aliases": {
"orders": {}
}
}
# 原子切换别名(零停机)
POST /_aliases
{
"actions": [
{ "remove": { "index": "orders-2024-01", "alias": "orders" }},
{ "add": { "index": "orders-2024-02", "alias": "orders" }}
]
}
⚠️ 别名绑定的索引不宜超过 20 个,否则查询需要遍历过多分片,影响性能。要定期解绑或清理历史索引。
写入优化
Bulk 批量写入
核心原则
- 永远用 Bulk 而非单条写入,Bulk 效率远高于单条
- 单次 Bulk 数据量控制在 5 MB ~ 15 MB,不超过 100 MB
- 逐步从 5 MB 增加,直到性能不再提升为止
自动 ID vs 自定义 ID
// 推荐:自动生成 ID,避免 Get-before-Write 查重
POST /my_index/_doc
{
"field": "value"
}
// 不推荐(如无必要):指定 ID 时 ES 需先查重
PUT /my_index/_doc/custom_id_123
{
"field": "value"
}
使用自定义 ID 会触发写入前查重(Get-before-Write),增加额外开销,尽量使用 ES 自动生成的文档 ID。
Index Buffer 调优
# elasticsearch.yml
indices.memory.index_buffer_size: 15%
写入期间数据先缓存在 index buffer,适当增大此值可提高写入效率(默认 10%)。
Refresh 与 Translog 调优
Refresh 策略
ES 默认每 1 秒 refresh 一次(将内存数据刷到文件系统缓存),每次 refresh 产生一个新 segment。
# 对实时性要求不高的索引,延长 refresh 间隔
PUT /my_index/_settings
{
"index": {
"refresh_interval": "30s"
}
}
# 大批量导入时,临时关闭 refresh
PUT /my_index/_settings
{
"index.refresh_interval": "-1"
}
# 导入完成后恢复
PUT /my_index/_settings
{
"index.refresh_interval": "1s"
}
Translog 异步策略
Translog 默认每次写入都同步刷盘(request 模式),改为异步可大幅提升写入吞吐:
PUT /my_index/_settings
{
"index.translog.durability": "async",
"index.translog.sync_interval": "60s"
}
| 参数 | 默认值 | 推荐值 | 说明 |
|---|---|---|---|
translog.durability |
request |
async |
异步刷盘,提升吞吐 |
translog.sync_interval |
5s |
60s |
刷盘间隔,可适当调大 |
⚠️ 异步模式下节点崩溃可能丢失最近 sync_interval 内的数据,非强一致性场景下才适用。
段合并策略
为什么要合并段
每次 refresh 产生一个新 segment,segment 过多会:
- 消耗大量文件句柄和内存
- 每次查询需遍历所有 segment,查询变慢
- delete 操作只是标记删除,合并时才真正释放空间
手动强制合并
# 对不再写入的历史只读索引执行强制合并,极大提升查询速度
POST /my_index/_forcemerge?max_num_segments=1
⚠️ 不要对正在写入的索引执行 forcemerge,会消耗大量 I/O,影响正常写入和查询。避免在高峰期执行。
提升段合并速度(SSD 场景)
PUT /_cluster/settings
{
"persistent": {
"indices.store.throttle.max_bytes_per_sec": "100mb"
}
}
默认合并速度限制为 20 MB/s,SSD 可调高到 100 MB/s。
副本与写入
副本越多,写入越慢(每条数据需同步到所有副本节点)。
# 大批量导入前临时关闭副本
PUT /my_index/_settings
{
"index.number_of_replicas": 0
}
# 导入完成后恢复
PUT /my_index/_settings
{
"index.number_of_replicas": 1
}
关闭副本期间集群无容灾能力,仅适用于全量数据导入场景,增量写入不建议关闭副本。
性能基准参考
以下为简单写入场景、单节点压测参考值,复杂场景需自行压测。索引大小 50 GB,文档数 2 亿+,字段数 8 个。
| 机器类型 | 配置 | 单条写入 QPS | 单条写入 avg(ms) | Bulk QPS(bulk=100) | Bulk avg(ms) |
|---|---|---|---|---|---|
| SSD(8C 16G) | VM/物理机 | ~500 | 10-13 | ~100 | 18-20 |
| 高性能 SSD(16C 32G) | - | ~1000 | 9 | ~200 | 19 |
结论:
- Bulk 写入效率远高于单条写入
- 写入性能对 CPU 较为敏感,高 QPS 业务需要更好的 CPU
- 使用高性能 SSD 写入 QPS 可达普通机型的 2 倍
参考资料
评论 (0)
发表评论