目录
正在加载目录…

ES 写入最佳实践

发布于 2026-05-26 10:25 · 最后编辑于 2026-07-31 15:52 · 字数 2,531 👁 84 次阅读

从 Mapping 设计、索引模板、Bulk 批写、Refresh/Translog 调优四个层面系统梳理 ES 写入性能优化手段,覆盖从"数据建模"到"参数配置"的全链路实践。

目录

章节说明
Mapping 设计字段类型选择、动态映射控制
索引模板与滚动策略时序索引、冷热分离
Bulk 批量写入批大小选择、自动 ID
Refresh 与 Translog 调优降低刷盘频率提升吞吐
段合并策略写入与合并的 Trade-off
副本与写入副本数对写入的影响
性能基准参考不同机型的写入 QPS 参考值

Mapping 设计

字段类型选择

场景推荐类型原因
精确匹配(ID、状态码)keywordterm 查询性能比数值类型快 3-5 倍
全文检索text分词建倒排索引
数值仅做精确 term 查询keyword数字类型 term 查询性能差,CPU 占用高
数值需要范围查询或聚合long / doubleBKD 树结构更适合范围查询
不需要排序/聚合的字段关闭 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.limit1000单索引最大字段数
depth.limit20嵌套对象最大深度
nested_fields.limit50nested 字段最大数量

_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 个,否则查询需要遍历过多分片,影响性能。要定期解绑或清理历史索引。

写入优化

es write optimization

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.durabilityrequestasync异步刷盘,提升吞吐
translog.sync_interval5s60s刷盘间隔,可适当调大

⚠️ 异步模式下节点崩溃可能丢失最近 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/物理机~50010-13~10018-20
高性能 SSD(16C 32G)-~10009~20019

结论

  • Bulk 写入效率远高于单条写入
  • 写入性能对 CPU 较为敏感,高 QPS 业务需要更好的 CPU
  • 使用高性能 SSD 写入 QPS 可达普通机型的 2 倍

参考资料

← 返回列表

评论 (0)

暂无评论,来留下第一条吧。
登录注册 后才能发表评论