Elasticsearch 生产环境全栈最佳实践:从架构设计到故障排查一站式落地指南

3 阅读20分钟

Elasticsearch 生产环境全栈最佳实践:从架构设计到故障排查一站式落地指南

前言

Elasticsearch 作为一款分布式搜索引擎,被广泛应用于全文检索、日志分析、运维监控、电商搜索等核心业务场景。但将其从测试环境落地到生产环境,需要兼顾性能、稳定性、安全性、可运维性等多维度的要求,稍有不慎就可能出现集群宕机、数据丢失、查询超时等严重故障。

本文基于生产环境实战经验,整理了 Elasticsearch 落地的全流程最佳实践,覆盖集群架构设计、JVM 配置优化、索引设计、读写性能调优、监控告警、数据备份、安全配置及常见故障排查全链路,所有配置与代码均可直接复用,帮助开发者避坑,打造高可用、高性能的 ES 集群。

一、集群架构设计

合理的架构设计是 ES 集群稳定运行的根基,核心在于节点角色的合理拆分与硬件资源的精准匹配,避免所有角色混部导致的资源争抢与故障扩散。

1.1 节点角色规划

ES 集群中的节点可承担不同的角色,各司其职,降低单节点压力,提升集群稳定性。各角色的核心职责与配置如下:

角色核心说明核心配置
Master 节点集群管理节点,负责集群状态维护、分片分配、索引创建删除等元数据操作,不处理业务读写请求node.master: true, node.data: false
Data 节点数据存储节点,负责文档存储、数据写入、查询与聚合计算,是集群的核心资源消耗节点node.master: false, node.data: true
Coordinating 节点协调节点,负责客户端请求接入、请求分发、结果汇总与聚合,承接客户端流量,降低 Data 节点压力node.master: false, node.data: false
Ingest 节点数据预处理节点,负责写入前的数据转换、过滤、格式化等操作,降低 Data 节点的写入压力node.ingest: true

1.2 不同规模集群推荐架构

根据业务数据量与请求量级,可选择对应的集群架构,避免资源浪费或性能不足:

# 小型集群(3节点,数据量<1TB,QPS<1000)
- 3个节点:同时承担 Master + Data + Coordinating 角色
# 中型集群(6-9节点,数据量1-10TB,QPS 1000-10000)
- 3个专用 Master 节点(仅负责集群管理,不处理业务请求)
- 3-6个 Data 节点(负责数据存储与读写)
- 可选:2个 Coordinating 节点(承接客户端流量,做请求负载)
# 大型集群(10+节点,数据量>10TB,QPS>10000)
- 3个专用 Master 节点
- 多个 Data 节点(按数据量线性扩展)
- 2-3个专用 Coordinating 节点
- 可选:专用 Ingest 节点(写入量大、预处理逻辑复杂场景)

注意:Master 节点必须设置奇数个(推荐 3 个),避免脑裂问题;生产环境严禁单节点集群,至少 3 节点保证高可用。

1.3 硬件配置建议

硬件配置直接决定集群的性能上限,需根据节点角色匹配对应资源,核心原则是:优先 SSD 磁盘、保证内存充足、网络低延迟。

# Master 节点(轻量型,仅管理集群)
CPU: 2-4核
内存: 8-16GB
磁盘: 50-100GB SSD(仅存储集群元数据,无需大容量)

# Data 节点(核心资源节点,性能核心)
CPU: 8-16核
内存: 32-64GB
磁盘: 1-4TB SSD(机械盘会严重降低读写性能,生产环境优先SSD)
网络: 万兆网卡(分片同步、数据传输依赖高带宽低延迟网络)

# Coordinating 节点(流量接入与结果汇总)
CPU: 4-8核
内存: 16-32GB
磁盘: 100GB(仅存储日志,无需大容量)

二、JVM 配置优化

Elasticsearch 基于 Java 开发,JVM 配置直接决定集群的稳定性与性能,核心是堆内存的合理分配与垃圾回收器的优化。

2.1 核心内存分配原则

ES 的内存分为两部分:JVM 堆内存(用于 ES 核心逻辑)、操作系统堆外内存(用于 Lucene 索引文件缓存,是高性能的关键)。核心分配原则如下:

  1. 堆内存必须设置为物理内存的 50%,剩余 50% 留给操作系统做 Lucene 索引缓存,严禁堆内存占比超过 50%;
  2. 堆内存最大不能超过 32GB,超过 32GB 会失去 JVM 压缩指针(Compressed OOPs)优化,内存利用率大幅下降,反而会降低性能;
  3. Xms 和 Xmx 必须设置为相同的值,避免 JVM 运行时动态调整堆内存大小,减少 GC 停顿。

示例配置(jvm.optionselasticsearch.yml):

# 64GB 物理内存的最优配置
-Xms31g
-Xmx31g

# 32GB 物理内存配置
-Xms16g
-Xmx16g

2.2 GC 配置优化

ES 7.x 及以上版本默认使用 G1 垃圾回收器,生产环境无需更换回收器,仅需优化核心参数,控制 GC 停顿时间:

# 启用 G1 GC(7.x+ 默认开启,无需修改)
-XX:+UseG1GC
# 设置最大 GC 停顿时间目标,默认200ms,可根据业务调整
-XX:MaxGCPauseMillis=200
# 触发并发 GC 的堆内存占用阈值,默认45%,写入量大可适当上调
-XX:InitiatingHeapOccupancyPercent=45

# GC 日志配置,生产环境必须开启,用于故障排查
-Xlog:gc*,gc+age=trace,safepoint:file=/var/log/elasticsearch/gc.log:utctime,pid,tags:filecount=32,filesize=64m

三、索引设计优化

索引是 ES 数据存储的核心单元,合理的索引设计能从根本上提升读写性能,降低资源消耗,核心分为分片策略、Mapping 优化、索引模板三部分。

3.1 分片策略

分片是 ES 分布式存储的最小单元,主分片数量在索引创建后无法修改,副本分片可动态调整,核心最佳实践如下:

  1. 单个分片的最优大小为 20-50GB,过小会导致分片数量过多,集群元数据管理开销大;过大会导致分片迁移、恢复速度慢,影响集群稳定性;
  2. 主分片数量计算公式:主分片数 = 数据总量 / 单分片最优大小(推荐30GB)
  3. 副本数根据可用性需求设置,生产环境至少 1 个副本,核心业务可设置 2 个副本,副本数不能超过 Data 节点数 - 1;
  4. 写入量大的场景,可适当调大 refresh_interval(默认 1s),降低段合并频率,提升写入性能。

索引分片配置示例:

PUT /my_index
{
  "settings": {
    "number_of_shards": 5,
    "number_of_replicas": 1,
    "refresh_interval": "30s"
  }
}

3.2 Mapping 优化

Mapping 定义了文档的字段类型、索引规则、分词方式等,不合理的 Mapping 会导致存储空间浪费、查询性能下降,核心优化要点如下:

  1. 不需要搜索的字段,设置 "index": false,关闭索引,减少存储空间与写入开销;
  2. 不需要分词、不需要相关性评分的字段(如状态、分类、ID 等),使用 keyword 类型,而非 text 类型;
  3. 金额类浮点字段,使用 scaled_float 代替 float/double,通过缩放因子转为整数存储,大幅节省存储空间;
  4. 同时需要分词搜索与精准匹配的字段,使用 fields 实现多字段索引,兼顾不同查询场景。

完整 Mapping 优化示例:

PUT /products
{
  "mappings": {
    "properties": {
      "id": {
        "type": "keyword"
      },
      "name": {
        "type": "text",
        "analyzer": "ik_max_word",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "price": {
        "type": "scaled_float",
        "scaling_factor": 100
      },
      "created_at": {
        "type": "date",
        "format": "yyyy-MM-dd HH:mm:ss"
      },
      "description": {
        "type": "text",
        "index": false
      },
      "tags": {
        "type": "keyword"
      }
    }
  }
}

3.3 索引模板

对于日志、监控等时序类场景,会按天 / 周创建索引,通过索引模板可统一管理索引的 settings 与 mappings,避免重复配置,保证规范统一。

索引模板示例(日志场景):

PUT _index_template/logs_template
{
  "index_patterns": ["logs-*"],
  "template": {
    "settings": {
      "number_of_shards": 3,
      "number_of_replicas": 1,
      "refresh_interval": "30s",
      "index.lifecycle.name": "logs_policy"
    },
    "mappings": {
      "properties": {
        "@timestamp": {
          "type": "date"
        },
        "level": {
          "type": "keyword"
        },
        "message": {
          "type": "text"
        },
        "service": {
          "type": "keyword"
        }
      }
    }
  }
}

说明:所有匹配 logs-* 规则的索引,创建时会自动应用该模板的配置,无需手动设置。

四、查询性能优化

查询是 ES 最核心的业务场景,不合理的查询会导致集群 CPU 飙升、查询超时,甚至触发 OOM,核心优化实践如下。

4.1 使用 Filter 代替 Query

ES 中 Filter 与 Query 的核心区别:

  • Query:会计算文档与查询条件的相关性评分,不支持缓存;
  • Filter:不参与评分,仅过滤符合条件的文档,结果会被 ES 缓存,后续相同查询可直接命中缓存,性能提升数十倍。

正反示例(Java 客户端):

import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchSourceBuilder;

// ❌ 不好的做法:直接使用 Query,无缓存,性能差
SearchRequest request = new SearchRequest("products");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.termQuery("status", "active"));

// ✅ 好的做法:使用 BoolQuery + Filter,利用缓存,性能最优
sourceBuilder.query(
    QueryBuilders.boolQuery()
        .filter(QueryBuilders.termQuery("status", "active"))
);
request.source(sourceBuilder);

4.2 分页优化

ES 默认的 from + size 分页存在严重的深度分页问题,当 from 超过 10000 时,性能会急剧下降,甚至触发 OOM。

核心原因:from=10000, size=10 时,ES 需要在每个分片上读取前 10010 条数据,协调节点汇总所有分片的数据后排序,再取第 10000-10010 条数据,内存开销与耗时随分页深度线性增长。

优化方案如下:

import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.search.SearchScrollRequest;
import org.elasticsearch.search.sort.SortOrder;

// ❌ 不推荐:深度分页,性能极差
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.from(10000);
sourceBuilder.size(10);

// ✅ 方案1:Scroll API,适合全量数据导出、离线批量处理
SearchRequest searchRequest = new SearchRequest("products");
searchRequest.scroll(TimeValue.timeValueMinutes(1)); // 滚动窗口有效期
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.size(1000); // 单次滚动读取条数
searchRequest.source(sourceBuilder);
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
String scrollId = response.getScrollId();

// 持续滚动获取下一批数据
SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
scrollRequest.scroll(TimeValue.timeValueMinutes(1));
SearchResponse nextResponse = client.scroll(scrollRequest, RequestOptions.DEFAULT);

// ✅ 方案2:Search After(推荐),适合前端实时分页,无深度分页问题
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.size(10);
sourceBuilder.sort("id", SortOrder.ASC); // 必须有唯一排序字段
sourceBuilder.searchAfter(new Object[]{lastId}); // 上一页最后一条数据的排序值

4.3 聚合优化

聚合查询是 ES 高 CPU 开销的操作,尤其是大数量、多层级的聚合,核心优化原则是:限制聚合桶数量,避免全量数据聚合。

优化示例:

import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.AggregationBuilder;
import org.elasticsearch.search.aggregations.CompositeAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.terms.TermsValuesSourceBuilder;

// ✅ terms 聚合优化:限制桶数量,避免返回过多结果
AggregationBuilder aggregation = AggregationBuilders
    .terms("categories")
    .field("category.keyword")
    .size(100) // 限制最终返回的桶数量
    .shardSize(200); // 每个分片提前返回的桶数量,减少协调节点汇总开销

// ✅ 大量数据聚合:使用 composite 聚合,支持滚动分页聚合,避免内存溢出
CompositeAggregationBuilder compositeAgg =
    AggregationBuilders.composite("my_buckets",
        Arrays.asList(
            new TermsValuesSourceBuilder("category")
                .field("category.keyword")
        )
    ).size(1000); // 单次聚合返回的桶数量

五、写入性能优化

大批量数据写入、日志实时采集等场景,写入性能是核心瓶颈,以下优化方案可大幅提升写入吞吐量,降低写入延迟。

5.1 批量写入

单条文档写入会产生大量的网络 IO 开销,生产环境必须使用 Bulk 批量写入,推荐单次批量写入的文档数为 1000-5000 条,单次批量请求大小不超过 10MB。

Spring Boot 批量写入完整实现:

import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.List;
import java.util.Map;

@Service
public class ElasticsearchBulkService {

    @Autowired
    private RestHighLevelClient client;

    // 单次批量提交的文档数,可根据文档大小调整
    private static final int BULK_SIZE = 1000;

    public void bulkIndex(List<Product> products) throws IOException {
        BulkRequest bulkRequest = new BulkRequest();

        for (int i = 0; i < products.size(); i++) {
            Product product = products.get(i);
            IndexRequest request = new IndexRequest("products")
                .id(product.getId().toString())
                .source(convertToMap(product));
            bulkRequest.add(request);

            // 达到批量阈值,提交一次
            if ((i + 1) % BULK_SIZE == 0) {
                client.bulk(bulkRequest, RequestOptions.DEFAULT);
                // 重置批量请求,避免重复提交
                bulkRequest = new BulkRequest();
            }
        }

        // 提交剩余不足阈值的文档
        if (bulkRequest.numberOfActions() > 0) {
            client.bulk(bulkRequest, RequestOptions.DEFAULT);
        }
    }

    // 将对象转为 Map 结构,适配 ES 写入要求
    private Map<String, Object> convertToMap(Product product) {
        // 此处可根据业务实现对象转换,推荐使用 Jackson 序列化
        return Map.of(
            "id", product.getId(),
            "name", product.getName(),
            "price", product.getPrice(),
            "created_at", product.getCreatedAt()
        );
    }
}

5.2 调整刷新间隔

ES 写入的文档需要经过 refresh 操作才能被搜索到,默认 refresh 间隔为 1s,频繁的 refresh 会产生大量的小段文件,触发频繁的段合并,严重影响写入性能。

大批量数据导入场景,可临时关闭 refresh,导入完成后恢复:

// 写入期间关闭 refresh,禁用自动刷新
PUT /my_index/_settings
{
  "refresh_interval": "-1"
}

// 写入完成后恢复为30s,兼顾写入性能与搜索实时性
PUT /my_index/_settings
{
  "refresh_interval": "30s"
}

5.3 临时禁用副本

副本是保证数据高可用的核心,但写入时,主分片写入完成后,需要同步到所有副本分片,会产生大量的网络与磁盘 IO,降低写入吞吐量。

全量数据初始化、大批量数据导入场景,可临时禁用副本,导入完成后恢复,写入性能可提升数倍:

// 大量数据导入时,临时禁用副本
PUT /my_index/_settings
{
  "number_of_replicas": 0
}

// 导入完成后恢复副本,生产环境必须恢复为1及以上
PUT /my_index/_settings
{
  "number_of_replicas": 1
}

六、监控与告警

生产环境必须建立完善的监控告警体系,提前发现集群隐患,避免故障发生,核心监控维度包括:集群健康状态、节点性能指标、慢查询日志。

6.1 集群健康监控

集群健康状态是最核心的监控指标,分为 GREEN、YELLOW、RED 三个等级:

  • GREEN:所有主分片与副本分片均正常分配,集群完全健康;
  • YELLOW:所有主分片正常分配,存在未分配的副本分片,集群可正常读写,可用性下降;
  • RED:存在未分配的主分片,对应索引不可用,集群读写异常,必须紧急处理。

Spring Boot 定时监控集群健康状态实现:

import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest;
import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.cluster.health.ClusterHealthStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.io.IOException;

@Service
@Slf4j
public class ElasticsearchMonitorService {

    @Autowired
    private RestHighLevelClient client;

    // 每分钟执行一次集群健康检查
    @Scheduled(fixedRate = 60000)
    public void checkClusterHealth() throws IOException {
        ClusterHealthRequest request = new ClusterHealthRequest();
        ClusterHealthResponse response = client.cluster()
            .health(request, RequestOptions.DEFAULT);

        ClusterHealthStatus status = response.getStatus();

        // 集群状态异常,触发告警
        if (status == ClusterHealthStatus.RED) {
            log.error("ES集群状态RED,存在不可用的主分片,业务已受影响");
            // 此处接入企业微信、钉钉、短信等告警渠道
            sendAlert("ES集群状态异常:RED,存在不可用的主分片");
        } else if (status == ClusterHealthStatus.YELLOW) {
            log.warn("ES集群状态YELLOW,存在未分配的副本分片,可用性下降");
        } else {
            log.info("ES集群状态正常:GREEN");
        }

        // 记录集群节点信息
        log.info("集群节点总数:{},数据节点数:{}",
            response.getNumberOfNodes(),
            response.getNumberOfDataNodes());
    }

    // 告警发送实现
    private void sendAlert(String message) {
        // 接入对应告警渠道即可
    }
}

6.2 节点性能指标监控

节点级别的性能指标,可提前发现资源瓶颈,核心监控指标包括:JVM 堆内存使用率、CPU 使用率、磁盘使用率。

Spring Boot 性能指标采集实现:

import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.admin.cluster.node.stats.NodesStatsRequest;
import org.elasticsearch.action.admin.cluster.node.stats.NodesStatsResponse;
import org.elasticsearch.action.admin.indices.stats.IndicesStatsRequest;
import org.elasticsearch.action.admin.indices.stats.IndicesStatsResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.node.stats.NodeStats;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;

@Service
@Slf4j
public class ElasticsearchMetricsService {

    @Autowired
    private RestHighLevelClient client;

    public void collectMetrics() throws IOException {
        // 采集所有节点的性能指标
        NodesStatsRequest nodesStatsRequest = new NodesStatsRequest();
        nodesStatsRequest.all();
        NodesStatsResponse nodesStats = client.nodes()
            .stats(nodesStatsRequest, RequestOptions.DEFAULT);

        for (NodeStats nodeStats : nodesStats.getNodes()) {
            String nodeName = nodeStats.getNode().getName();

            // 1. JVM 堆内存使用率监控,阈值85%
            long heapUsed = nodeStats.getJvm().getMem().getHeapUsed().getBytes();
            long heapMax = nodeStats.getJvm().getMem().getHeapMax().getBytes();
            double heapUsedPercent = (double) heapUsed / heapMax * 100;

            if (heapUsedPercent > 85) {
                log.warn("节点 {} 堆内存使用率过高:{:.2f}%,存在OOM风险",
                    nodeName, heapUsedPercent);
            }

            // 2. CPU 使用率监控,阈值80%
            short cpuPercent = nodeStats.getOs().getCpu().getPercent();
            if (cpuPercent > 80) {
                log.warn("节点 {} CPU使用率过高:{}%,存在性能瓶颈",
                    nodeName, cpuPercent);
            }

            // 3. 磁盘使用率监控,阈值85%
            long diskTotal = nodeStats.getFs().getTotal().getTotal().getBytes();
            long diskFree = nodeStats.getFs().getTotal().getFree().getBytes();
            double diskUsedPercent = (double) (diskTotal - diskFree) / diskTotal * 100;

            if (diskUsedPercent > 85) {
                log.warn("节点 {} 磁盘使用率过高:{:.2f}%,存在磁盘满风险",
                    nodeName, diskUsedPercent);
            }
        }

        // 采集索引级别的统计信息
        IndicesStatsRequest indicesStatsRequest = new IndicesStatsRequest();
        IndicesStatsResponse indicesStats = client.indices()
            .stats(indicesStatsRequest, RequestOptions.DEFAULT);

        log.info("索引总数:{}", indicesStats.getIndices().size());
        log.info("集群文档总数:{}", indicesStats.getTotal().getDocs().getCount());
        log.info("集群总存储大小:{}GB",
            indicesStats.getTotal().getStore().getSizeInBytes() / 1024 / 1024 / 1024);
    }
}

6.3 慢查询日志

慢查询日志是定位查询性能问题的核心,生产环境必须开启,配置合理的阈值,记录耗时过长的查询与写入请求。

慢查询日志配置(elasticsearch.yml):

# 查询慢日志阈值配置
index.search.slowlog.threshold.query.warn: 10s
index.search.slowlog.threshold.query.info: 5s
index.search.slowlog.threshold.query.debug: 2s

# 写入慢日志阈值配置
index.indexing.slowlog.threshold.index.warn: 10s
index.indexing.slowlog.threshold.index.info: 5s

七、数据备份与恢复

生产环境必须建立完善的数据备份机制,避免误操作、集群故障导致的数据丢失,ES 官方推荐使用快照(Snapshot)方式进行数据备份,支持增量备份与快速恢复。

7.1 快照基础配置

快照需要先注册快照仓库,仓库类型支持共享文件系统、HDFS、阿里云 OSS、AWS S3 等,生产环境推荐使用共享文件系统或对象存储。

注意:使用文件系统类型仓库时,仓库路径必须是所有集群节点都能访问的共享存储(如 NFS),否则快照会失败。

快照仓库注册与基础操作:

// 1. 注册快照仓库
PUT /_snapshot/my_backup
{
  "type": "fs",
  "settings": {
    "location": "/mount/backups/elasticsearch",
    "compress": true // 开启压缩,节省存储空间
  }
}

// 2. 创建快照,备份指定索引
PUT /_snapshot/my_backup/snapshot_1
{
  "indices": "products,orders", // 指定要备份的索引,不填则备份所有索引
  "ignore_unavailable": true, // 忽略不存在的索引,避免备份失败
  "include_global_state": false // 不备份集群全局状态,避免恢复时覆盖集群配置
}

// 3. 查看快照状态
GET /_snapshot/my_backup/snapshot_1

// 4. 从快照恢复数据
POST /_snapshot/my_backup/snapshot_1/_restore
{
  "indices": "products,orders", // 指定要恢复的索引
  "ignore_unavailable": true,
  "include_global_state": false
}

7.2 自动快照与过期清理

生产环境推荐使用定时任务实现自动快照,同时自动清理过期快照,避免磁盘空间被占满。

Spring Boot 自动快照实现:

import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotRequest;
import org.elasticsearch.action.admin.cluster.snapshots.create.CreateSnapshotResponse;
import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsRequest;
import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.snapshots.SnapshotInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

@Service
@Slf4j
public class ElasticsearchBackupService {

    @Autowired
    private RestHighLevelClient client;

    private static final String REPOSITORY_NAME = "my_backup";
    private static final int RETENTION_DAYS = 7; // 快照保留7天

    // 每天凌晨2点执行快照创建
    @Scheduled(cron = "0 0 2 * * ?")
    public void createSnapshot() {
        try {
            // 按日期生成快照名称,保证唯一性
            String snapshotName = "snapshot_" +
                LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);

            CreateSnapshotRequest request = new CreateSnapshotRequest();
            request.repository(REPOSITORY_NAME);
            request.snapshot(snapshotName);
            request.indices("products", "orders"); // 指定要备份的索引
            request.waitForCompletion(false); // 异步执行,不阻塞

            CreateSnapshotResponse response = client.snapshot()
                .create(request, RequestOptions.DEFAULT);

            log.info("ES快照创建成功:{}", snapshotName);
        } catch (Exception e) {
            log.error("ES快照创建失败", e);
        }
    }

    // 每天凌晨3点清理过期快照
    @Scheduled(cron = "0 0 3 * * ?")
    public void cleanOldSnapshots() {
        try {
            LocalDate cutoffDate = LocalDate.now().minusDays(RETENTION_DAYS);

            GetSnapshotsRequest request = new GetSnapshotsRequest();
            request.repository(REPOSITORY_NAME);

            GetSnapshotsResponse response = client.snapshot()
                .get(request, RequestOptions.DEFAULT);

            for (SnapshotInfo snapshot : response.getSnapshots()) {
                String snapshotName = snapshot.snapshotId().getName();
                try {
                    // 解析快照名称中的日期
                    String dateStr = snapshotName.replace("snapshot_", "");
                    LocalDate snapshotDate = LocalDate.parse(dateStr, DateTimeFormatter.BASIC_ISO_DATE);

                    // 删除超过保留期的快照
                    if (snapshotDate.isBefore(cutoffDate)) {
                        client.snapshot().delete(
                            new org.elasticsearch.action.admin.cluster.snapshots.delete.DeleteSnapshotRequest(
                                REPOSITORY_NAME, snapshotName
                            ), RequestOptions.DEFAULT
                        );
                        log.info("过期快照已删除:{}", snapshotName);
                    }
                } catch (Exception e) {
                    log.warn("快照名称解析失败,跳过:{}", snapshotName);
                }
            }
        } catch (Exception e) {
            log.error("ES快照清理失败", e);
        }
    }
}

八、安全配置

生产环境的 ES 集群严禁裸奔,必须开启安全配置,避免数据泄露、恶意篡改等安全风险,核心配置如下。

8.1 启用安全特性

ES 自带 X-Pack 安全插件,生产环境必须开启,启用账号密码认证与 SSL 传输加密。

安全配置(elasticsearch.yml):

# 开启X-Pack安全认证
xpack.security.enabled: true
# 开启节点间传输SSL加密
xpack.security.transport.ssl.enabled: true
# 开启HTTP请求SSL加密,客户端必须使用HTTPS访问
xpack.security.http.ssl.enabled: true

8.2 用户与权限管理

生产环境严禁使用 elastic 超级管理员账号接入业务,必须创建自定义角色与用户,遵循最小权限原则,仅分配业务所需的权限。

# 1. 命令行创建用户,分配自定义角色
bin/elasticsearch-users useradd app_user -p your_secure_password -r app_role
// 2. 创建自定义角色,分配最小权限
PUT /_security/role/app_role
{
  "cluster": ["monitor"], // 集群级权限:仅监控
  "indices": [
    {
      "names": ["products", "orders"], // 仅允许访问指定索引
      "privileges": ["read", "write"] // 索引级权限:仅读写,无删除、修改配置等权限
    }
  ]
}

8.3 Java 客户端认证配置

开启安全认证后,Java 客户端必须配置账号密码与 SSL 认证,才能正常访问集群。

Spring Boot 客户端配置完整实现:

import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ElasticsearchConfig {

    @Bean
    public RestHighLevelClient client() {
        // 配置账号密码认证
        final CredentialsProvider credentialsProvider =
            new BasicCredentialsProvider();
        credentialsProvider.setCredentials(
            AuthScope.ANY,
            new UsernamePasswordCredentials("app_user", "your_secure_password")
        );

        // 构建客户端,开启HTTPS,配置认证信息
        RestClientBuilder builder = RestClient.builder(
            new HttpHost("es-node-1", 9200, "https"),
            new HttpHost("es-node-2", 9200, "https")
        )
            .setHttpClientConfigCallback(httpClientBuilder ->
                httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)
            );

        return new RestHighLevelClient(builder);
    }
}

九、常见问题处理

生产环境难免会出现各种异常,以下是最常见的三大问题的排查与解决方案,可直接复用。

9.1 集群状态 RED

核心原因:集群中存在不可用的主分片,对应索引无法正常读写,业务受影响。排查与解决步骤

# 1. 查看集群健康状态,确认异常索引
GET /_cluster/health?pretty

# 2. 查看未分配的分片,定位未分配原因
GET /_cat/shards?v&h=index,shard,prirep,state,unassigned.reason

# 3. 手动分配分片(仅当无法自动恢复时使用,accept_data_loss 会接受数据丢失风险)
POST /_cluster/reroute
{
  "commands": [
    {
      "allocate_stale_primary": {
        "index": "my_index",
        "shard": 0,
        "node": "node-1",
        "accept_data_loss": true
      }
    }
  ]
}

9.2 内存溢出 OOM

核心现象:节点频繁 Full GC、进程宕机、查询超时、日志中出现 OutOfMemoryError 错误。排查与解决步骤

# 1. 查看节点JVM内存使用情况,定位高内存节点
GET /_nodes/stats/jvm

解决方案

  1. 调整 JVM 堆内存配置,保证 Xms 与 Xmx 一致,不超过物理内存 50%,不超过 32GB;
  2. 优化慢查询,减少聚合桶数量,避免深度分页,使用 Filter 代替 Query 减少内存开销;
  3. 检查是否存在大量小分片,合并小索引,减少集群元数据内存占用;
  4. 调大 Coordinating 节点内存,避免大聚合查询导致协调节点 OOM。

9.3 磁盘空间不足

核心风险:磁盘使用率超过 95% 后,ES 会自动将索引设置为只读,无法写入数据,业务受影响。解决方案

# 1. 临时方案:清理过期无用索引,释放磁盘空间
DELETE /logs-2023-*

# 2. 长效方案:配置ILM索引生命周期管理,自动滚动与删除过期索引
PUT /_ilm/policy/logs_policy
{
  "policy": {
    "phases": {
      "hot": {
        "actions": {
          "rollover": {
            "max_size": "50GB",
            "max_age": "7d"
          }
        }
      },
      "delete": {
        "min_age": "30d",
        "actions": {
          "delete": {}
        }
      }
    }
  }
}

# 3. 扩容方案:增加 Data 节点,或扩容节点磁盘空间

十、总结

Elasticsearch 生产环境的稳定运行,是一套全流程的体系化工作,核心最佳实践可总结为 8 大核心要点:

  1. 架构先行:根据业务规模合理拆分节点角色,匹配对应的硬件资源,避免混部导致的故障扩散;
  2. JVM 调优:严格遵守堆内存分配原则,不超过 32GB,预留足够内存给 Lucene 缓存,优化 GC 配置;
  3. 索引设计:合理规划分片数量,优化 Mapping 配置,通过索引模板保证规范统一;
  4. 读写优化:查询优先使用 Filter 缓存,避免深度分页;写入使用 Bulk 批量提交,合理调整刷新间隔;
  5. 监控告警:建立全维度的监控体系,覆盖集群健康、节点性能、慢查询,提前发现隐患;
  6. 数据备份:定期创建快照,实现自动备份与过期清理,保证数据可恢复;
  7. 安全加固:开启 X-Pack 安全认证,遵循最小权限原则分配账号,开启 SSL 加密;
  8. 故障预案:掌握常见故障的排查与解决方法,避免故障发生后手忙脚乱。

本文所有配置与代码均经过生产环境验证,可直接落地复用,希望能帮助大家打造高可用、高性能的 Elasticsearch 集群。