Elasticsearch 运维实战:集群部署、索引设计与性能调优
前言
Elasticsearch 是当下最流行的分布式搜索和分析引擎。它既能做全文搜索,又能做日志分析(ELK),还能当文档数据库用。但"灵活"的代价是复杂——一个 ES 集群的性能表现,取决于架构设计、索引映射、分片策略、JVM 调优等多个环节,任何一个环节出问题都可能拖垮整个集群。
这篇文章从运维视角出发,覆盖 ES 从部署到调优的完整链路。
一、集群架构
1.1 节点角色
ES 7.9+ 引入了节点角色(Node Roles),取代了旧的节点类型(master-eligible/data/client):
| 角色 | 说明 | 生产建议 |
|---|---|---|
master |
主节点候选,参与选主 | 3个(奇数),专用,不承载数据 |
data |
数据节点,存储索引分片 | 按数据量规划,高配置 |
data_content |
内容数据节点 | 存储不随时间增长的索引 |
data_hot |
热数据节点 | 最新数据,高IO高CPU |
data_warm |
温数据节点 | 查询频率降低的数据 |
data_cold |
冷数据节点 | 很少查询的数据,可存本地磁盘 |
data_frozen |
冻结数据节点 | 极少查询,部分搜索 |
ingest |
摄取节点,运行 pipeline | 可与 data 节点合并 |
coordinating |
协调节点 | 路由请求、聚合结果,专用 |
ml |
机器学习节点 | 仅 Platinum 许可证 |
# elasticsearch.yml - 专用 master 节点
cluster.name: es-prod
node.name: master-01
node.roles: [master]
network.host: 0.0.0.0
http.port: 9200
transport.port: 9300
discovery.seed_hosts: ["10.0.0.1", "10.0.0.2", "10.0.0.3"]
cluster.initial_master_nodes: ["master-01", "master-02", "master-03"]
# 数据节点
node.name: data-01
node.roles: [data_hot, data_content]
# 冷热分离架构中,不同节点配不同角色
# 协调节点
node.name: coord-01
node.roles: [] # 空角色 = 纯协调节点
1.2 生产集群规划
以日志分析场景为例(日写入量 5 亿条,保留 30 天):
集群规模:10 个节点
├── 3 × Master 节点(专用)
│ 4C / 8G / 100G SSD
│ 不承载数据,只负责集群管理
│
├── 3 × Hot 数据节点
│ 16C / 64G / 2T NVMe SSD
│ 存储最近 3 天数据,高写入高查询
│
├── 2 × Warm 数据节点
│ 8C / 32G / 4T SATA SSD
│ 存储第 4-15 天数据,中等查询
│
├── 2 × Cold 数据节点
│ 4C / 16G / 8T HDD
│ 存储第 16-30 天数据,偶尔查询
│
└── 2 × 协调节点
8C / 16G
负责请求路由和结果聚合
容量估算公式:
原始数据量 × (1 + 副本数) × (1 + 预留空间)
= 日写入量 × 平均文档大小 × 保留天数 × 1.5(副本) × 1.3(预留)
= 5亿 × 500B × 30 × 1.5 × 1.3
≈ 14.5 TB
每个 Hot 节点存储 ≈ 14.5TB / 3 / 3(天) × 1.5 ≈ 2.4TB → 用 2T NVMe × 3 台
1.3 关键配置
# elasticsearch.yml
cluster.name: es-prod
node.name: data-01
node.roles: [data_hot, data_content]
path.data: /data/es,/data2/es # 多磁盘提升IO
path.logs: /var/log/elasticsearch
# 网络
network.host: 0.0.0.0
http.port: 9200
transport.port: 9300
transport.compress: true # 传输层压缩
# 发现
discovery.seed_hosts: ["10.0.0.1", "10.0.0.2", "10.0.0.3"]
cluster.initial_master_nodes: ["master-01", "master-02", "master-03"]
# 安全
xpack.security.enabled: true
xpack.security.transport.ssl.enabled: true
xpack.security.transport.ssl.verification_mode: certificate
# 线程池
# 写入队列
thread_pool.write.queue_size: 1000 # 默认 10000,适当降低防 OOM
# 搜索队列
thread_pool.search.queue_size: 1000 # 默认 1000
# jvm.options
-Xms32g # 堆内存最小值
-Xmx32g # 堆内存最大值(必须与最小值相同)
-XX:+UseG1GC # G1 垃圾回收器
-XX:MaxGCPauseMillis=200
-XX:G1ReservePercent=25
-Djava.io.tmpdir=/data/es/tmp
JVM 堆内存原则:
- 堆内存 = 物理内存的 50%,但不超过 31GB(压缩指针限制)
-Xms和-Xmx必须相同,避免动态调整带来的停顿- 超过 31GB 会失去指针压缩,反而降低性能
二、索引设计
2.1 分片策略
分片数量在索引创建时确定,不可更改(只能 reindex)。这是 ES 索引设计最重要的决策。
# 创建索引
PUT /logs-2026.07.07
{
"settings": {
"number_of_shards": 3, # 主分片数
"number_of_replicas": 1, # 副本数
"index.refresh_interval": "1s", # 刷新间隔
"index.translog.durability": "request", # 事务日志持久化
"index.translog.sync_interval": "30s"
}
}
分片数量计算公式:
分片大小 = 索引总大小 / 分片数
推荐分片大小:10GB - 50GB
- 太小(<1GB):开销大于收益,元数据浪费
- 太大(>50GB):查询慢、恢复慢、rebalance 慢
示例:日写入 10GB → 1 个分片
日写入 50GB → 2 个分片
日写入 200GB → 5 个分片
副本策略:
- 生产环境至少 1 个副本
- 副本可以提升查询吞吐量(读请求会分发到副本)
- 写入密集时可临时设为 0,写完再恢复
# 临时调整副本数
PUT /logs-2026.07.07/_settings
{
"number_of_replicas": 0
}
# 写入完成后恢复
PUT /logs-2026.07.07/_settings
{
"number_of_replicas": 1
}
2.2 映射设计
核心原则:永远显式定义映射,不要用动态映射!
PUT /articles
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
},
"mappings": {
"dynamic": "strict", # 禁止动态映射,未知字段直接报错
"properties": {
"title": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart",
"fields": {
"keyword": { // 子字段:用于聚合和排序
"type": "keyword",
"ignore_above": 256
}
}
},
"content": {
"type": "text",
"analyzer": "ik_max_word"
},
"author": {
"type": "keyword"
},
"tags": {
"type": "keyword"
},
"publish_date": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||epoch_millis"
},
"view_count": {
"type": "integer"
},
"price": {
"type": "scaled_float",
"scaling_factor": 100 // 精确到 0.01
},
"location": {
"type": "geo_point"
},
"metadata": {
"type": "object",
"enabled": false // 不索引,只存储
}
}
}
}
字段类型选择指南:
| 场景 | 推荐类型 | 原因 |
|---|---|---|
| 全文搜索 | text |
会被分词 |
| 精确匹配/聚合/排序 | keyword |
不分词,效率高 |
| 日期 | date |
支持范围查询 |
| 金额 | scaled_float |
比 double 精确,比 long 灵活 |
| 布尔 | boolean |
节省空间 |
| IP 地址 | ip |
支持 CIDR 查询 |
| 地理坐标 | geo_point |
支持距离查询 |
| 不需要搜索的 JSON | object + enabled: false |
只存储不索引 |
text vs keyword:
# text 类型会被分词
"title": "PostgreSQL性能调优" → ["postgresql", "性能", "调优"]
# 搜索 "性能" 能匹配
# keyword 类型不分词
"author": "张三" → ["张三"]
# 搜索 "张" 不能匹配,必须完全匹配 "张三"
2.3 索引模板
日志场景每天创建新索引,用模板统一配置:
PUT /_index_template/logs
{
"index_patterns": ["logs-*"],
"template": {
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"refresh_interval": "5s",
"index.lifecycle.name": "logs-policy"
},
"mappings": {
"dynamic": "strict",
"properties": {
"@timestamp": { "type": "date" },
"level": { "type": "keyword" },
"service": { "type": "keyword" },
"host": { "type": "keyword" },
"message": { "type": "text", "analyzer": "ik_max_word" },
"trace_id": { "type": "keyword" }
}
}
},
"priority": 100
}
2.4 ILM 索引生命周期管理
# 创建生命周期策略
PUT /_ilm/policy/logs-policy
{
"policy": {
"phases": {
"hot": {
"actions": {
"rollover": {
"max_age": "1d",
"max_primary_shard_size": "30gb"
},
"set_priority": { "priority": 100 }
}
},
"warm": {
"min_age": "3d",
"actions": {
"shrink": { "number_of_shards": 1 },
"forcemerge": { "max_num_segments": 1 },
"set_priority": { "priority": 50 }
}
},
"cold": {
"min_age": "15d",
"actions": {
"set_priority": { "priority": 0 }
}
},
"delete": {
"min_age": "30d",
"actions": {
"delete": {}
}
}
}
}
}
ILM 执行流程:
- Hot:索引正在写入,达到阈值(1天或30GB)后 rollover 创建新索引
- Warm:3 天后,shrink 到 1 个分片 + forcemerge 减少段数量,迁移到 warm 节点
- Cold:15 天后,迁移到 cold 节点(HDD),不再参与频繁查询
- Delete:30 天后自动删除
三、性能调优
3.1 写入优化
# 1. 批量写入(bulk)
POST /_bulk
{"index": {"_index": "logs-2026.07.07", "_id": "1"}}
{"@timestamp": "2026-07-07T10:00:00Z", "level": "info", "message": "用户登录"}
{"index": {"_index": "logs-2026.07.07", "_id": "2"}}
{"@timestamp": "2026-07-07T10:00:01Z", "level": "error", "message": "连接超时"}
# 批量大小建议:5-15MB,不超过 10000 条
# 2. 写入前临时调整设置
PUT /logs-2026.07.07/_settings
{
"index.refresh_interval": "30s", // 降低刷新频率(默认1s)
"index.number_of_replicas": 0, // 临时去掉副本
"index.translog.durability": "async", // 异步刷盘(有丢数据风险)
"index.translog.sync_interval": "30s"
}
# 3. 写入完成后恢复
PUT /logs-2026.07.07/_settings
{
"index.refresh_interval": "1s",
"index.number_of_replicas": 1,
"index.translog.durability": "request"
}
refresh_interval 原理:ES 默认每 1 秒刷新一次,将内存中的数据写入新的 Lucene 段,使其可被搜索。刷新会消耗 CPU 和 IO。写入密集时降低刷新频率能显著提升写入吞吐量。
# 4.forcemerge 减少段数量
# 写入完成后强制合并段,减少查询时需要扫描的段数
POST /logs-2026.07.06/_forcemerge?max_num_segments=1
3.2 查询优化
避免深分页
# 坏:from + size,越往后越慢
GET /logs-*/_search
{
"from": 10000,
"size": 20,
"query": { "match_all": {} }
}
# ES 需要从每个分片取 10020 条,协调节点合并后取 20 条
# 默认 max_result_window = 10000
# 好:search_after 游标分页
GET /logs-*/_search
{
"size": 20,
"sort": [
{ "@timestamp": "desc" },
{ "_id": "asc" } // 需要 tiebreaker
],
"search_after": ["2026-07-07T10:00:00Z", "12345"]
}
# 适合导出:scroll
GET /logs-*/_search
{
"size": 1000,
"query": { "match_all": {} }
}
# 响应中拿到 scroll_id
GET /_search/scroll
{
"scroll": "5m",
"scroll_id": "xxx"
}
使用 filter 代替 query
# filter 不计算相关性评分,可以利用缓存
GET /articles/_search
{
"query": {
"bool": {
"filter": [ // filter 上下文
{ "term": { "author": "张三" } },
{ "range": { "publish_date": { "gte": "2026-01-01" } } }
],
"must": [ // query 上下文,会算分
{ "match": { "content": "PostgreSQL" } }
]
}
}
}
聚合优化
# 坏:对 text 字段聚合(需要 fielddata,极慢)
GET /articles/_search
{
"aggs": {
"by_title": { "terms": { "field": "title" } } // text 字段!
}
}
# 好:对 keyword 子字段聚合
GET /articles/_search
{
"aggs": {
"by_title": { "terms": { "field": "title.keyword" } }
}
}
# 大基数聚合用 cardinality(近似计数)
GET /logs-*/_search
{
"aggs": {
"unique_ips": { "cardinality": { "field": "client_ip", "precision_threshold": 40000 } }
}
}
使用索引别名
# 创建别名指向当前索引
POST /_aliases
{
"actions": [
{ "add": { "index": "logs-2026.07.07", "alias": "logs-today" } }
]
}
# 查询时用别名
GET /logs-today/_search
{ "query": { "match_all": {} } }
# 无感切换索引
POST /_aliases
{
"actions": [
{ "remove": { "index": "logs-2026.07.07", "alias": "logs-today" } },
{ "add": { "index": "logs-2026.07.08", "alias": "logs-today" } }
]
}
3.3 JVM 调优
# jvm.options
# 堆内存 = 物理内存的 50%,不超过 31GB
-Xms31g
-Xmx31g
# G1 GC(ES 7+ 默认)
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1ReservePercent=25
-XX:InitiatingHeapOccupancyPercent=30 # 堆使用30%时开始并发标记
# GC 日志(排查问题用)
-Xlog:gc*,gc+heap=debug:file=/var/log/elasticsearch/gc.log:time,uptime:filecount=10,filesize=50m
GC 问题排查:
# 查看 GC 情况
GET /_nodes/stats/jvm
# 关注:
# gc.collectors.young.collection_time_in_millis → Young GC 总耗时
# gc.collectors.old.collection_time_in_millis → Old GC 总耗时
# 如果 Old GC 频繁或耗时长 → 堆内存不够或查询/聚合太重
# 常见 GC 问题:
# 1. Young GC 频繁 → 堆内存太小
# 2. Full GC 频繁 → 查询结果集太大或聚合基数太高
# 3. GC 停顿 > 1秒 → 集群可能触发脑裂
3.4 线程池与拒绝
# 查看线程池状态
GET /_cat/thread_pool?v&h=node_name,name,active,queue,rejected,completed
# 重点关注 rejected:
# write rejected → 写入太快,客户端应该重试
# search rejected → 查询太多/太重,需要优化查询或扩容
# 调整线程池大小(elasticsearch.yml)
# 写入线程池
thread_pool.write.size: 4 # 默认 = CPU 核数
thread_pool.write.queue_size: 500 # 默认 10000
# 搜索线程池
thread_pool.search.size: 12 # 默认 = int((cpu * 3) / 2) + 1
thread_pool.search.queue_size: 1000 # 默认 1000
拒绝策略:当队列满了,新请求会被拒绝(rejected++)。客户端应该实现退避重试。不要把队列设太大——那只是延迟问题暴露,不会提升吞吐量。
四、监控与运维
4.1 集群健康检查
# 集群健康状态
GET /_cluster/health
# status: green(主分片和副本都正常)
# status: yellow(主分片正常,部分副本缺失)
# status: red(部分主分片缺失,数据不完整)
# 详细健康
GET /_cluster/health?level=indices
GET /_cluster/health?level=shards
# 节点信息
GET /_cat/nodes?v
# node.role 列说明:
# m = master, d = data, i = ingest, r = coordinating only
# 分片分配
GET /_cat/shards?v
# 关注 UNASSIGNED 状态的分片
4.2 关键监控指标
# 1. 集群状态
GET /_cluster/health
# 2. 节点磁盘使用(关键!)
GET /_cat/allocation?v
# disk.percent 超过 85% 会触发 flood-stage 只读
# 超过 90% 会触发只读
# 超过 95% 会阻止分片分配
# 3. 索引大小
GET /_cat/indices?v&s=store.size:desc
# 关注 docs.count 和 store.size
# 4. 段数量
GET /_cat/segments?v
# 每个分片段数 > 50 需要 forcemerge
# 5. 待处理任务
GET /_cluster/pending_tasks
# 堆积说明 master 节点处理不过来
# 6. 分片重分配
GET /_cat/recovery?v
# 查看分片恢复进度
4.3 磁盘水位线
# elasticsearch.yml
cluster.routing.allocation.disk.watermark.low: 85% # 85% 停止新分片分配
cluster.routing.allocation.disk.watermark.high: 90% # 90% 开始迁移分片
cluster.routing.allocation.disk.watermark.flood_stage: 95% # 95% 索引设为只读
# 索引被设为只读后,清理空间后手动解除
PUT /logs-*/_settings
{
"index.blocks.read_only_allow_delete": null
}
4.4 快照备份
# 1. 注册快照仓库(共享文件系统)
PUT /_snapshot/backup_repo
{
"type": "fs",
"settings": {
"location": "/data/es-backup",
"compress": true,
"max_snapshot_bytes_per_sec": "100mb",
"max_restore_bytes_per_sec": "100mb"
}
}
# 2. 创建快照(全部索引)
PUT /_snapshot/backup_repo/snapshot_20260707?wait_for_completion=false
{
"indices": "logs-*,articles",
"ignore_unavailable": true,
"include_global_state": false
}
# 3. 查看快照状态
GET /_snapshot/backup_repo/snapshot_20260707
GET /_snapshot/backup_repo/_current
# 4. 恢复快照
POST /_snapshot/backup_repo/snapshot_20260707/_restore
{
"indices": "articles",
"rename_pattern": "articles",
"rename_replacement": "restored_articles"
}
# 5. 定时快照(用 cron)
# 每天凌晨 2 点创建快照
echo '0 2 * * * curl -X PUT "localhost:9200/_snapshot/backup_repo/snapshot_$(date +\%Y\%m\%d)" -H "Content-Type: application/json" -d "{\"indices\":\"logs-*\",\"ignore_unavailable\":true}"' | crontab -
# 6. 清理旧快照
# 每天删除 7 天前的快照
echo '0 3 * * * curl -X DELETE "localhost:9200/_snapshot/backup_repo/snapshot_$(date -d "7 days ago" +\%Y\%m\%d)"' | crontab -
五、常见问题排查
5.1 集群变黄/红
# 查看未分配的分片
GET /_cat/shards?v&h=index,shard,prirep,state,unassigned.reason | grep UNASSIGNED
# 查看未分配原因
GET /_cluster/allocation/explain
# 常见原因:
# - 节点磁盘超过水位线
# - 分片数超过 cluster.max_shards_per_node(默认 1000)
# - 节点离线
# - 版本不兼容
# 手动重分配
POST /_cluster/reroute
{
"commands": [
{
"allocate_stale_primary": {
"index": "articles",
"shard": 0,
"node": "data-01",
"accept_data_loss": true
}
}
]
}
5.2 写入被拒绝
# 查看拒绝情况
GET /_cat/thread_pool/write?v&h=node_name,active,queue,rejected
# 如果 rejected > 0:
# 1. 降低客户端写入速率
# 2. 增大 write.queue_size
# 3. 降低 refresh_interval
# 4. 去掉副本(临时)
# 5. 增加 data 节点
5.3 查询超时
# 查看慢查询日志
# elasticsearch.yml 配置:
index.search.slowlog.threshold.query.warn: 2s
index.search.slowlog.threshold.query.info: 1s
index.search.slowlog.threshold.fetch.warn: 1s
# 日志位置
tail -f /var/log/elasticsearch/es-prod_index_search_slowlog.log
# 常见原因:
# 1. 查询命中太多文档 → 加 filter 缩小范围
# 2. 聚合基数太大 → 用 cardinality 近似计数
# 3. 深分页 → 改用 search_after
# 4. 段太多 → forcemerge
# 5. 磁盘 IO 瓶颈 → 加 SSD 或增加节点
5.4 节点掉线
# 查看节点状态
GET /_cat/nodes?v
# 节点离线原因排查:
# 1. OOM → 检查 dmesg | grep -i kill
# 2. 磁盘满 → df -h
# 3. GC 停顿过长 → 检查 gc.log
# 4. 网络分区 → 检查节点间网络连通性
# 5. 防火墙 → 检查 9300 端口
# 节点恢复后自动重新加入集群
# 如果不自动加入,检查:
# 1. cluster UUID 是否一致
# 2. SSL 证书是否有效
# 3. discovery.seed_hosts 配置是否正确
5.5 分片过大
# 查看最大分片
GET /_cat/shards?v&s=store:desc | head -20
# 分片 > 50GB 的处理方案:
# 1. 如果是日志索引 → 用 ILM rollover 按大小切分
# 2. 如果是业务索引 → reindex 到新索引(更多分片)
# 3. 如果是历史数据 → 按时间拆分索引
# reindex 示例
POST /_reindex
{
"source": { "index": "old_index" },
"dest": { "index": "new_index" }
}
六、安全加固
6.1 基本安全配置
# elasticsearch.yml
xpack.security.enabled: true
# 传输层加密
xpack.security.transport.ssl.enabled: true
xpack.security.transport.ssl.key: /etc/elasticsearch/certs/node.key
xpack.security.transport.ssl.certificate: /etc/elasticsearch/certs/node.crt
xpack.security.transport.ssl.certificate_authorities: ["/etc/elasticsearch/certs/ca.crt"]
# HTTP 层加密
xpack.security.http.ssl.enabled: true
xpack.security.http.ssl.key: /etc/elasticsearch/certs/node.key
xpack.security.http.ssl.certificate: /etc/elasticsearch/certs/node.crt
6.2 用户与角色
# 创建角色
POST /_security/role/logs_reader
{
"indices": [
{
"names": ["logs-*"],
"privileges": ["read", "view_index_metadata"]
}
]
}
# 创建用户
POST /_security/user/log_agent
{
"password": "Str0ngP@ss!",
"roles": ["logs_reader"],
"full_name": "Log Agent"
}
# API Key(推荐用于程序访问)
POST /_security/api_key
{
"name": "log-writer",
"role_descriptors": {
"log_writer": {
"indices": [
{ "names": ["logs-*"], "privileges": ["write", "create_index"] }
]
}
}
}
总结
Elasticsearch 运维的核心要点:
- 架构设计先行:Master/Data/Coordinating 角色分离,冷热数据分层
- 分片策略是关键:创建前算好分片数,目标 10-50GB/分片,不可更改
- 映射必须显式定义:禁止动态映射,text 和 keyword 分清楚
- 写入调优三板斧:批量 bulk + 降低 refresh + 临时去副本
- 查询优化记住四条:filter 替代 query、search_after 替代深分页、keyword 聚合、索引别名
- JVM 堆不超过 31GB:G1GC,Xms=Xmx
- 监控磁盘水位线:85%/90%/95% 三道防线
- 快照备份不能少:定时快照 + 定期清理
ES 的坑很多,但大部分问题都可以通过合理的架构设计和索引规划来避免。遇到问题先看 _cat API 和慢查询日志,大部分原因都能找到。

