数据库运维20 min read

Elasticsearch 运维实战:集群部署、索引设计与性能调优

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.314.5 TB

每个 Hot 节点存储 ≈ 14.5TB / 3 / 3(天) × 1.52.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 执行流程

  1. Hot:索引正在写入,达到阈值(1天或30GB)后 rollover 创建新索引
  2. Warm:3 天后,shrink 到 1 个分片 + forcemerge 减少段数量,迁移到 warm 节点
  3. Cold:15 天后,迁移到 cold 节点(HDD),不再参与频繁查询
  4. 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 运维的核心要点:

  1. 架构设计先行:Master/Data/Coordinating 角色分离,冷热数据分层
  2. 分片策略是关键:创建前算好分片数,目标 10-50GB/分片,不可更改
  3. 映射必须显式定义:禁止动态映射,text 和 keyword 分清楚
  4. 写入调优三板斧:批量 bulk + 降低 refresh + 临时去副本
  5. 查询优化记住四条:filter 替代 query、search_after 替代深分页、keyword 聚合、索引别名
  6. JVM 堆不超过 31GB:G1GC,Xms=Xmx
  7. 监控磁盘水位线:85%/90%/95% 三道防线
  8. 快照备份不能少:定时快照 + 定期清理

ES 的坑很多,但大部分问题都可以通过合理的架构设计和索引规划来避免。遇到问题先看 _cat API 和慢查询日志,大部分原因都能找到。

分享:

相关文章

评论区