第10篇(终篇):生产级 ES 运维——监控、备份、安全与故障排查完全手册

🛡️ 引言:跑起来容易,跑稳了是门学问

前九篇我们把 ES 从安装到 AI 向量检索、从数据同步到 SaaS 多租户,功能全部打通了。但"能跑"和"跑稳"之间,还有很长的距离。

生产环境里,ES 集群随时面对各种挑战:流量突增导致查询堆积、某个节点 OOM 退出、磁盘写满触发只读保护、Segment 碎片化拖慢查询......这些问题不可避免,但可以通过完善的监控提前预警、通过规范的备份在灾难时快速恢复、通过故障排查手册快速定位和解决。

本篇是整个系列的压轴,也是最接近"生产实战"的一篇。


一、监控体系:你不能优化你看不见的东西

1.1 必须监控的核心指标

bash 复制代码
ES 核心指标体系
├── 集群健康
│   ├── cluster.status(green/yellow/red)
│   ├── 未分配分片数(unassigned_shards)
│   └── 节点数量变化
├── JVM
│   ├── heap.used_percent(> 85% 告警)
│   ├── gc.old.collection_time_ms(Full GC 停顿)
│   └── gc.old.collection_count(Full GC 次数)
├── 写入性能
│   ├── indexing.index_total(写入 TPS)
│   ├── indexing.index_time_in_millis(写入延迟)
│   └── thread_pool.write.rejected(写入被拒绝数,> 0 即告警)
├── 查询性能
│   ├── search.query_total(查询 QPS)
│   ├── search.query_time_in_millis(查询延迟)
│   └── thread_pool.search.rejected(查询被拒绝数)
├── 存储
│   ├── fs.total.available_in_bytes(可用磁盘空间)
│   └── store.size_in_bytes(索引数据总大小)
└── 网络
    └── transport.rx_size_in_bytes / tx_size_in_bytes

1.2 Prometheus + Grafana 搭建

yaml 复制代码
# docker-compose-monitoring.yml
services:
  # ES 指标导出器(把 ES 指标转成 Prometheus 格式)
  elasticsearch-exporter:
    image: quay.io/prometheuscommunity/elasticsearch-exporter:v1.7.0
    container_name: es-exporter
    command:
      - '--es.uri=https://elastic:${ELASTIC_PASSWORD}@es-node1:9200'
      - '--es.ssl-skip-verify'          # 自签名证书跳过验证(生产用 --es.ca 替代)
      - '--es.all'                       # 采集所有索引的指标
      - '--es.cluster_settings'          # 采集集群配置指标
      - '--es.indices_settings'          # 采集索引配置指标
      - '--collector.clustersettings'
    ports:
      - "9114:9114"
    networks:
      - monitoring

  prometheus:
    image: prom/prometheus:v2.50.0
    container_name: prometheus
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - ./alert_rules.yml:/etc/prometheus/alert_rules.yml
      - prometheus-data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.retention.time=30d'    # 保留 30 天监控数据
    ports:
      - "9090:9090"
    networks:
      - monitoring

  grafana:
    image: grafana/grafana:10.3.0
    container_name: grafana
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
    volumes:
      - grafana-data:/var/lib/grafana
    ports:
      - "3000:3000"
    networks:
      - monitoring

volumes:
  prometheus-data:
  grafana-data:
yaml 复制代码
# prometheus.yml
global:
  scrape_interval: 15s      # 每 15 秒采集一次
  evaluation_interval: 15s  # 每 15 秒评估告警规则

rule_files:
  - "alert_rules.yml"

scrape_configs:
  - job_name: 'elasticsearch'
    static_configs:
      - targets: ['elasticsearch-exporter:9114']
    metrics_path: '/metrics'

1.3 告警规则配置

这是最关键的部分------哪些指标超阈值必须告警,不能靠人盯着 Dashboard:

yaml 复制代码
# alert_rules.yml
groups:
  - name: elasticsearch_alerts
    rules:
      # ── 集群健康告警 ──────────────────────────────────────────────────
      - alert: ESClusterRed
        expr: elasticsearch_cluster_health_status{color="red"} == 1
        for: 1m          # 持续 1 分钟才告警(避免短暂波动误报)
        labels:
          severity: critical
        annotations:
          summary: "ES 集群进入 RED 状态!"
          description: "集群 {{ $labels.cluster }} 有主分片未分配,部分数据不可用"

      - alert: ESClusterYellow
        expr: elasticsearch_cluster_health_status{color="yellow"} == 1
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "ES 集群进入 YELLOW 状态"
          description: "集群有副本分片未分配,数据可用但容灾能力下降"

      - alert: ESUnassignedShards
        expr: elasticsearch_cluster_health_active_shards_percent_as_number < 100
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "ES 存在未分配分片"

      # ── JVM 告警 ──────────────────────────────────────────────────────
      - alert: ESHeapUsageHigh
        # 堆内存使用率 > 85%,持续 5 分钟
        expr: |
          (elasticsearch_jvm_memory_used_bytes{area="heap"}
           / elasticsearch_jvm_memory_max_bytes{area="heap"}) * 100 > 85
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "ES 节点 {{ $labels.node }} 堆内存使用率过高: {{ $value | printf \"%.1f\" }}%"

      - alert: ESHeapUsageCritical
        expr: |
          (elasticsearch_jvm_memory_used_bytes{area="heap"}
           / elasticsearch_jvm_memory_max_bytes{area="heap"}) * 100 > 95
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "ES 节点 {{ $labels.node }} 堆内存即将耗尽!"

      # ── 磁盘告警 ─────────────────────────────────────────────────────
      - alert: ESDiskSpaceLow
        expr: |
          (elasticsearch_filesystem_data_available_bytes
           / elasticsearch_filesystem_data_size_bytes) * 100 < 15
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "ES 节点 {{ $labels.node }} 磁盘空间不足 15%"

      - alert: ESDiskSpaceCritical
        expr: |
          (elasticsearch_filesystem_data_available_bytes
           / elasticsearch_filesystem_data_size_bytes) * 100 < 5
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "ES 节点 {{ $labels.node }} 磁盘空间严重不足!"

      # ── 写入/查询拒绝告警(立即告警,无延迟)─────────────────────────
      - alert: ESWriteRejected
        expr: rate(elasticsearch_thread_pool_rejected_count{type="write"}[5m]) > 0
        for: 0m
        labels:
          severity: critical
        annotations:
          summary: "ES 写入请求被拒绝!写入队列已满"

      - alert: ESSearchRejected
        expr: rate(elasticsearch_thread_pool_rejected_count{type="search"}[5m]) > 0
        for: 0m
        labels:
          severity: warning
        annotations:
          summary: "ES 查询请求被拒绝!搜索队列已满"

      # ── GC 告警 ───────────────────────────────────────────────────────
      - alert: ESHighGCTime
        # 过去 1 分钟内,Old GC 时间超过 30 秒(说明 GC 压力极大)
        expr: rate(elasticsearch_jvm_gc_collection_seconds_sum{gc="old"}[1m]) > 0.5
        for: 3m
        labels:
          severity: critical
        annotations:
          summary: "ES 节点 {{ $labels.node }} Old GC 频繁,可能即将 OOM"

1.4 Grafana Dashboard

导入官方 ES Dashboard(ID: 6483),在 Grafana 的 Import 里填入这个 ID 即可获得开箱即用的监控看板,包含:集群健康、节点状态、JVM 堆、GC 趋势、写入/查询吞吐、磁盘使用等所有核心指标的可视化图表。


二、索引生命周期管理(ILM)

对于日志、时序数据等随时间增长的索引,ILM 可以自动管理索引从热数据到冷归档再到删除的完整生命周期,大幅降低运维成本。

java 复制代码
// IlmPolicyManager.java
@Component
@RequiredArgsConstructor
public class IlmPolicyManager {

    private final ElasticsearchClient esClient;

    /**
     * 为日志类索引创建 ILM 策略
     *
     * 典型的 Hot-Warm-Cold-Delete 流程:
     * Hot(活跃写入)→ Warm(只读,压缩)→ Cold(归档,低频访问)→ Delete
     *
     * 适合场景:操作日志、搜索日志、审计日志等时序数据
     */
    public void createLogIndexPolicy() throws IOException {
        esClient.ilm().putLifecycle(p -> p
            .name("log-index-policy")
            .policy(policy -> policy
                .phases(phases -> phases
                    // ── Hot 阶段:活跃写入 ─────────────────────────────
                    .hot(hot -> hot
                        .minAge(Time.of(t -> t.time("0ms")))   // 立即进入 hot
                        .actions(actions -> actions
                            // rollover:满足任意条件就滚动到新索引
                            // 旧索引进入下一个阶段,新索引继续接收写入
                            .rollover(r -> r
                                .maxAge(Time.of(t -> t.time("7d")))    // 超过 7 天
                                .maxSize(ByteSize.of(b -> b.mb(50000L))) // 超过 50GB
                                .maxDocs(10000000L)                      // 超过 1000 万条
                            )
                            // 优先级:hot 阶段的索引优先分片恢复
                            .setPriority(SetPriorityAction.of(sp -> sp.priority(100)))
                        )
                    )
                    // ── Warm 阶段:只读,压缩优化 ──────────────────────
                    .warm(warm -> warm
                        .minAge(Time.of(t -> t.time("7d")))    // 7 天后进入 warm
                        .actions(actions -> actions
                            // 去掉副本(节省存储,warm 阶段数据访问频率低)
                            .allocate(a -> a.numberOfReplicas(0))
                            // 强制合并:把碎片化的 Segment 合并(提升查询性能,压缩存储)
                            .forcemerge(fm -> fm.maxNumSegments(1L))
                            // 缩小分片数(可选,数据量小时节省资源)
                            .shrink(s -> s.numberOfShards(1))
                            .setPriority(SetPriorityAction.of(sp -> sp.priority(50)))
                        )
                    )
                    // ── Cold 阶段:归档,极低频访问 ────────────────────
                    .cold(cold -> cold
                        .minAge(Time.of(t -> t.time("30d")))   // 30 天后进入 cold
                        .actions(actions -> actions
                            // freeze:降低内存占用(冻结索引,查询时再加载)
                            // ES 8.x 已废弃 freeze,改用 searchable snapshots
                            .allocate(a -> a.numberOfReplicas(0))
                            .setPriority(SetPriorityAction.of(sp -> sp.priority(0)))
                        )
                    )
                    // ── Delete 阶段:永久删除 ───────────────────────────
                    .delete(delete -> delete
                        .minAge(Time.of(t -> t.time("90d")))   // 90 天后删除
                        .actions(actions -> actions.delete(DeleteAction.of(d -> d)))
                    )
                )
            )
        );
    }
}

三、备份与恢复:Snapshot

3.1 配置 Snapshot Repository(OSS/S3)

java 复制代码
// SnapshotManager.java
@Component
@RequiredArgsConstructor
@Slf4j
public class SnapshotManager {

    private final ElasticsearchClient esClient;

    /**
     * 注册阿里云 OSS Snapshot 仓库
     *
     * 为什么用云存储而不是本地磁盘?
     * 本地磁盘备份在节点宕机时可能一起丢失,失去了备份的意义。
     * 云存储(OSS/S3)与 ES 集群物理隔离,任何节点故障都不影响备份数据。
     */
    public void registerOssRepository() throws IOException {
        esClient.snapshot().createRepository(r -> r
            .name("oss-backup")
            .settings(s -> s
                .put("endpoint", "oss-cn-hangzhou-internal.aliyuncs.com")
                .put("bucket", "my-es-backup-bucket")
                .put("base_path", "es-snapshots/")       // 存储路径前缀
                .put("access_key_id", System.getenv("OSS_ACCESS_KEY"))
                .put("secret_access_key", System.getenv("OSS_SECRET_KEY"))
                .put("compress", "true")                  // 压缩存储(节省空间)
                .put("chunk_size", "1gb")                 // 分块大小(大文件拆分上传)
            )
            .type("oss")    // 需要安装 repository-oss 插件
        );
        log.info("OSS Snapshot 仓库注册成功");
    }

    /**
     * 手动创建快照(上线前、重大变更前必须执行)
     */
    public String createSnapshot(String snapshotName) throws IOException {
        CreateSnapshotResponse response = esClient.snapshot().create(c -> c
            .repository("oss-backup")
            .snapshot(snapshotName)
            .indices("*")               // 备份所有索引(可以指定特定索引)
            .includeGlobalState(true)   // 包含集群全局状态(索引模板、ILM 策略等)
            .waitForCompletion(true)    // 等待备份完成(异步场景改为 false)
        );

        SnapshotInfo info = response.snapshot();
        log.info("快照创建完成: name={}, state={}, shards={}/{}",
            info.snapshot(), info.state(),
            info.successfulShards(), info.totalShards());

        return snapshotName;
    }

    /**
     * 从快照恢复(灾难恢复)
     *
     * 恢复前必须确认:
     * 1. 目标索引不能存在(或先删除),否则恢复会失败
     * 2. 跨版本恢复只支持升级(ES 7 的快照可以恢复到 ES 8),不支持降级
     * 3. 恢复期间集群资源占用大,建议在低峰期进行
     */
    public void restoreSnapshot(String snapshotName, List<String> indices) throws IOException {
        log.info("开始从快照 [{}] 恢复索引: {}", snapshotName, indices);

        esClient.snapshot().restore(r -> r
            .repository("oss-backup")
            .snapshot(snapshotName)
            .indices(indices)
            .renamePattern("(.+)")              // 恢复时重命名(可选)
            .renameReplacement("restored_$1")   // 前缀 "restored_",避免与原索引冲突
            .includeGlobalState(false)          // 通常不需要恢复全局状态
            .waitForCompletion(true)
        );

        log.info("快照恢复完成");
    }
}

3.2 自动快照策略(SLM)

java 复制代码
// 配置 Snapshot Lifecycle Management(SLM)------ 自动定时备份
public void createSnapshotPolicy() throws IOException {
    esClient.slm().putLifecycle(p -> p
        .policyId("daily-backup")
        .schedule("0 30 1 * * ?")    // 每天凌晨 1:30 自动备份(cron 格式)
        .name("<daily-snapshot-{now/d}>")  // 快照名称包含日期(自动生成)
        .repository("oss-backup")
        .config(c -> c
            .indices("*")
            .includeGlobalState(true)
        )
        .retention(r -> r
            .expireAfter(Time.of(t -> t.time("30d")))  // 保留 30 天
            .minCount(5L)     // 至少保留 5 个快照(即使都超过 30 天)
            .maxCount(30L)    // 最多保留 30 个快照
        )
    );
    log.info("自动快照策略创建成功");
}

四、ES 安全配置

4.1 用户认证与角色(RBAC)

java 复制代码
// SecurityManager.java
@Component
@RequiredArgsConstructor
public class SecurityManager {

    private final ElasticsearchClient esClient;

    /**
     * 为 SaaS 应用创建最小权限角色
     *
     * 安全原则:最小权限,各组件使用独立账号
     * 应用账号:只能读写业务索引,不能操作集群配置
     * Logstash 账号:只能写入特定索引
     * 只读账号:只能搜索,不能写入
     * 监控账号:只能查看集群状态,不能操作数据
     */
    public void createAppRole() throws IOException {
        esClient.security().putRole(r -> r
            .name("saas-app-role")
            .indices(i -> i
                .names("tenant_*_products", "products_shared_*")
                .privileges(
                    "read",           // 允许搜索和读取
                    "write",          // 允许写入和更新
                    "create_index",   // 允许创建索引(新租户注册时)
                    "delete_index",   // 允许删除索引(租户注销时)
                    "manage"          // 允许管理索引(修改 settings、mapping)
                )
            )
            .cluster(
                "monitor"            // 允许查看集群健康状态(健康检查接口需要)
            )
        );
    }

    public void createAppUser(String username, String password) throws IOException {
        esClient.security().putUser(u -> u
            .username(username)
            .password(password.toCharArray())
            .roles("saas-app-role")
            .enabled(true)
            .fullName("SaaS Application User")
        );
        log.info("用户 [{}] 创建成功", username);
    }

    /**
     * 为每个租户创建独立 API Key(细粒度权限控制)
     *
     * API Key 比用户名/密码更安全:
     * 1. 可以设置过期时间
     * 2. 可以随时吊销
     * 3. 权限可以细粒度到索引级别
     * 4. 不需要存储明文密码
     */
    public String createTenantApiKey(String tenantId) throws IOException {
        CreateApiKeyResponse response = esClient.security().createApiKey(k -> k
            .name("tenant-" + tenantId + "-key")
            .expiration(Time.of(t -> t.time("90d")))   // 90 天过期,需要定期轮换
            .roleDescriptors("tenant-restriction", rd -> rd
                .indices(i -> i
                    // 只允许访问该租户的索引(最小权限原则)
                    .names("tenant_" + tenantId + "_*")
                    .privileges("read", "write")
                )
            )
        );

        // 返回 base64 编码的 API Key,存入数据库(只返回一次!)
        String apiKeyId = response.id();
        String apiKeyEncoded = response.encoded();
        log.info("租户 [{}] API Key 创建成功: id={}", tenantId, apiKeyId);

        // 把 apiKeyEncoded 加密后存入数据库,应用层用这个 key 访问 ES
        return apiKeyEncoded;
    }
}
java 复制代码
// 使用 API Key 初始化 ES 客户端(在 EsConfig 里)
RestClient restClient = RestClient.builder(HttpHost.create(esUri))
    .setDefaultHeaders(new Header[]{
        // 使用 API Key 认证,而不是用户名/密码
        new BasicHeader("Authorization", "ApiKey " + apiKey)
    })
    .build();

五、故障排查完全手册

这是本篇最有实用价值的部分。把最常见的 ES 故障场景整理成排查步骤,遇到问题时按图索骥。

5.1 集群变 Yellow

bash 复制代码
现象:cluster.health status = yellow
原因:所有主分片正常,但有副本分片未分配

排查步骤:
1. 查看未分配分片
GET /_cluster/allocation/explain
{
  "index": "products",
  "shard": 0,
  "primary": false
}

常见原因及解决:
a) 节点数少于副本数+1 → 扩容节点,或临时减少副本数
   PUT /products/_settings { "number_of_replicas": 0 }

b) 磁盘使用率超过 watermark.low(85%) → 清理磁盘或扩容
   GET /_cat/allocation?v  # 查看各节点磁盘使用

c) 节点刚重启,分片正在恢复 → 等待,通常几分钟自动变绿

d) 分片过滤配置(allocation.include/exclude)→ 检查路由配置

5.2 集群变 Red

bash 复制代码
现象:cluster.health status = red
原因:有主分片未分配,该分片的数据暂时不可用

排查步骤:
1. 立刻查看哪些分片是 UNASSIGNED
GET /_cat/shards?h=index,shard,prirep,state,node&s=state&v
找到 state=UNASSIGNED 且 prirep=p(主分片)的条目

2. 查看分配失败原因
GET /_cluster/allocation/explain

3. 常见原因:
a) 节点宕机(数据尚未恢复)
   → 重启节点,ES 自动恢复分片

b) 索引数据损坏(罕见)
   → 重新分配分片(会丢失该分片数据!)
   POST /_cluster/reroute
   { "commands": [{ "allocate_empty_primary": {
     "index": "products", "shard": 0, "node": "es-node1",
     "accept_data_loss": true  // 警告:会丢失数据,只在别无选择时使用
   }}]}

c) 从快照恢复 → 恢复最近的快照(见备份章节)

5.3 写入被拒绝(429 Too Many Requests)

java 复制代码
// 排查写入拒绝
@GetMapping("/diagnosis/write-rejected")
public Map<String, Object> diagnoseWriteRejected() throws IOException {
    NodesStatsResponse stats = esClient.nodes().stats(s -> s.metric("thread_pool"));

    Map<String, Object> result = new HashMap<>();
    stats.nodes().forEach((nodeId, nodeStats) -> {
        ThreadPoolStats writePool = nodeStats.threadPool().get("write");
        if (writePool != null && writePool.rejected() > 0) {
            result.put(nodeStats.name(), Map.of(
                "queue_size", writePool.queue(),      // 当前队列大小
                "active", writePool.active(),         // 正在处理的线程数
                "rejected_total", writePool.rejected() // 累计拒绝数
            ));
        }
    });
    return result;
}
bash 复制代码
写入拒绝解决方案(按优先级):
1. 降低写入速率(在应用层加限速,给 ES 喘息时间)
2. 增大写入队列(thread_pool.write.queue_size,默认 200 → 1000)
3. 检查是否有慢写入(如 refresh 太频繁,增大 refresh_interval)
4. 扩容数据节点(增加写入并行能力)
5. 临时关闭副本(只在确实撑不住时,批量导入完再开回来)

5.4 查询超时 / 慢查询

java 复制代码
// 排查慢查询的标准流程
public void diagnoseSlowQuery(String indexName, String slowQuery) throws IOException {
    // Step 1: 用 Profile API 分析查询各阶段耗时
    SearchResponse<Void> profileResult = esClient.search(s -> s
        .index(indexName)
        .profile(true)
        .size(0)           // 不需要数据,只要 profile 信息
        .source(SourceConfig.of(sc -> sc.filter(f -> f.excludes("*"))))
        // 粘贴你的慢查询 DSL...
        .query(/* 慢查询 */),
        Void.class
    );

    // Step 2: 检查 Profile 输出中耗时最长的查询类型
    profileResult.profile().shards().forEach(shard -> {
        shard.searches().forEach(search -> {
            search.query().forEach(q -> {
                if (q.timeInNanos() > 100_000_000L) { // > 100ms
                    log.warn("慢查询子句: type={}, time={}ms, breakdown={}",
                        q.type(),
                        q.timeInNanos() / 1_000_000,
                        q.breakdown()
                    );
                }
            });
        });
    });
}
bash 复制代码
常见慢查询原因速查:

查询类型          | 原因               | 解决方案
----------------|-------------------|------------------
WildcardQuery   | 前缀通配符全扫描    | 改用 match 全文检索
ScriptQuery     | 每文档执行脚本      | 改用 fieldValueFactor
TermsQuery(大量值) | terms 列表过长   | 改用 should + term 组合,或 termsSet
NestedQuery     | nested 文档过多    | 控制 nested 数组大小 < 100
AggsQuery       | fielddata 加载    | 改用 keyword 子字段聚合

5.5 节点 OOM 退出

bash 复制代码
现象:ES 进程消失,日志末尾是 java.lang.OutOfMemoryError

排查步骤:
1. 查看 GC 日志确认 OOM 类型
grep "OutOfMemoryError" /var/log/elasticsearch/gc.log

2. 常见 OOM 原因:
a) fielddata 无限增长
   → GET /_cat/fielddata?v 查看各节点 fielddata 占用
   → PUT /index/_settings { "index.fielddata.cache.size": "20%" } 限制大小
   → POST /index/_cache/clear?fielddata=true 清除 fielddata 缓存

b) 聚合结果集过大(如 terms size 设置了 10000+)
   → 减小 terms size,使用 composite aggregation 分页聚合

c) 堆内存设置不合理(设了 33GB,超过了压缩指针边界)
   → 改为 31GB

d) 并发深翻页(from=9000, size=10 的大量并发查询)
   → 禁止深翻页,改用 search_after

3. 临时恢复手段:
# 清除所有缓存(会降低性能,临时救急用)
POST /_cache/clear

5.6 磁盘写满,集群变只读

bash 复制代码
现象:写入报错 "cluster_block_exception: index [.x] blocked by [FORBIDDEN/12/index read-only / allow delete (api)]"
原因:磁盘使用率超过 flood_stage watermark(95%),ES 自动保护

应急处理:
1. 立刻清理磁盘(删旧索引、清快照、迁移数据)

2. 临时解除只读限制(在清理磁盘后执行)
PUT /_settings
{ "index.blocks.read_only_allow_delete": null }

3. 或者手动调整 watermark(短暂放宽,给清理时间)
PUT /_cluster/settings
{
  "transient": {
    "cluster.routing.allocation.disk.watermark.flood_stage": "99%"
  }
}

4. 清理完成后恢复默认 watermark
PUT /_cluster/settings
{ "transient": { "cluster.routing.allocation.disk.watermark.flood_stage": null } }

六、ES 版本滚动升级

java 复制代码
// 滚动升级流程(以 8.12 → 8.13 为例)
// 注意:ES 支持跨一个大版本升级(7.x → 8.x),不支持跨两个大版本

升级前检查清单

bash 复制代码
# 1. 检查集群当前健康状态(必须是 green)
GET /_cluster/health

# 2. 检查 deprecation API(了解新版本不兼容的配置)
GET /_migration/deprecations

# 3. 备份快照(升级前必做)
POST /_slm/policy/pre-upgrade-backup/_execute

# 4. 关闭分片自动分配(升级过程中防止分片来回迁移)
PUT /_cluster/settings
{ "persistent": { "cluster.routing.allocation.enable": "primaries" } }

# 5. 执行同步刷新(减少未提交的 translog)
POST /_flush

滚动升级步骤(以 Docker 为例):

bash 复制代码
# 逐个节点升级(每次只停一个节点)
# 从非 Master 节点开始

# 1. 优雅停止节点
docker stop es-node3

# 2. 更新 docker-compose.yml 中 es-node3 的镜像版本到 8.13.0

# 3. 重启节点(使用新版本)
docker start es-node3

# 4. 等待节点加入集群并且集群重新变 GREEN
# 可以用这个命令监控:
watch -n 5 'curl -s -u elastic:pwd https://localhost:9200/_cat/nodes?v'

# 5. 确认 green 后,对下一个节点重复步骤 1-4
# 直到所有节点都升级完成

# 6. 升级完成后,恢复分片自动分配
PUT /_cluster/settings
{ "persistent": { "cluster.routing.allocation.enable": null } }

⚠️ 踩坑:滚动升级期间,集群会短暂进入 yellow 状态(因为有节点停了),这是正常的,不要惊慌。关键是一次只停一个节点,确保至少有过半数的 Master 候选节点在线(3 节点集群至少 2 个),否则集群会失去主节点选举能力,进入不可写状态。


七、生产运维检查清单

bash 复制代码
日常检查(建议每天):
□ 集群 health 状态是否 green
□ 未分配分片数是否为 0
□ 各节点 JVM 堆使用率 < 75%
□ 磁盘使用率 < 70%
□ 慢查询日志中有无新增的慢 SQL
□ 最新快照是否创建成功

每周检查:
□ 各索引 Segment 数量(过多则 force merge)
□ fielddata 缓存占用
□ 写入/查询拒绝计数(累计是否增长)
□ 节点 GC 统计(老年代 GC 次数和时间趋势)

每月检查:
□ 索引数据量增长趋势,评估是否需要扩容
□ ILM 策略执行情况(旧索引是否按时进入 warm/cold/delete)
□ 快照存储空间使用情况
□ API Key 是否临近过期(提前轮换)
□ ES 版本是否有安全补丁需要更新

八、系列总结与学习路线图

走过这十篇,我们从 ES 的第一行代码出发,走到了 AI 语义检索和生产运维的前沿。回顾一下这条路:

bash 复制代码
阶段一:理解基础
  第01篇:核心概念(集群、分片、倒排索引)
  第02篇:搭建集群 + Spring Boot 整合

阶段二:数据建模
  第03篇:Mapping 与字段类型设计
  第05篇:中文分词与搜索优化

阶段三:查询精通
  第04篇:Query DSL 全景(bool、聚合、分页)
  第07篇:性能调优

阶段四:工程落地
  第06篇:SaaS 多租户架构
  第09篇:数据同步方案

阶段五:AI 融合
  第08篇:向量检索 + RAG 智能问答

阶段六:生产运维
  第10篇:监控、备份、安全、故障排查

进阶学习方向(本系列之后):

bash 复制代码
搜索进阶
├── Learning to Rank(LTR):用机器学习优化搜索排序
├── Semantic Reranking:ES 8.x 内置的向量 + BM25 重排序
└── A/B 测试搜索策略:线上实验框架

大数据融合
├── ES + Flink:实时流计算 + 搜索
├── ES + Spark:离线大数据处理 + ES 写入
└── ES + ClickHouse:OLAP 分析场景对比

AI 深化
├── ES + LangChain:更复杂的 RAG 管道
├── Multimodal Search:图文混合搜索
└── ES + Knowledge Graph:知识图谱 + 搜索

❓ 高频面试 & AI 问答

Q: ES 集群变 Yellow 和 Red 分别是什么原因,怎么排查?

A: Yellow 表示主分片全部正常但有副本未分配,常见原因是节点数不足或磁盘使用率过高;Red 表示有主分片未分配,数据不可用,通常是节点宕机或数据损坏。排查首先用 GET /_cluster/allocation/explain 查看分片分配失败的具体原因。

Q: ES 如何配置自动备份?

A: 通过 SLM(Snapshot Lifecycle Management)配置自动快照策略:注册云存储(OSS/S3)作为 Snapshot Repository,创建 SLM 策略设置快照的时间表(cron)、保留规则(天数/数量),ES 会自动按计划创建和清理快照,无需人工干预。

Q: ES 监控应该关注哪些核心指标?

A: 最关键的几个:(1)JVM 堆使用率(> 85% 告警);(2)集群 health 状态(yellow/red 立即告警);(3)写入/查询拒绝数(> 0 立即告警);(4)未分配分片数(> 0 需关注);(5)磁盘使用率(> 85% 告警);(6)Old GC 停顿时间(持续 > 1s 需关注)。

Q: ES 如何做权限控制?

A: ES 通过 RBAC(角色 + 用户)实现权限控制。可以创建细粒度的角色(精确到索引 + 操作类型),分配给对应用户。在 SaaS 场景中更推荐使用 API Key(可以设置过期时间、随时吊销、权限限制到租户专属索引),比用户名密码更安全。


📌 系列完结语

十篇文章,从 ES 的第一个 curl 命令到 AI 语义搜索和生产运维,覆盖了 Java SaaS 项目中 ES 最核心的技术点。

工程经验的积累是长期的事,每个踩坑记录都来自真实的生产故障,每个最佳实践背后都有付出过的代价。希望这个系列能帮你少走一些弯路,把更多精力放在真正的业务价值上。

如果你在阅读过程中有任何疑问、发现了错误、或者有不同的实践经验,欢迎在评论区交流。这个系列会随着 ES 版本演进持续更新。

🔗 上一篇

第09篇ES 数据同步方案------Canal + Logstash + Flink 全方案对比

相关推荐
s_w.h1 小时前
【 linux 】文件管理与重定向
linux·运维·服务器
烟雨江南aabb1 小时前
Docker第一弹 Docker是什么?
运维·docker·容器
Cloud_Shy6181 小时前
Linux 系统定时任务 Cron(d) 服务应用实践(二:生产环境下的用户定时任务)
linux·运维·服务器·centos·云计算
Saniffer_SH1 小时前
【每日一题】不只是点亮画面:UniGraf 如何把 HDMI/DP 接口问题拆成可定位、可复现、可自动化验证的测试流程?
运维·人工智能·测试工具·fpga开发·性能优化·自动化·压力测试
HackTwoHub1 小时前
AI赋能Chrome MCP × JS逆向Skill自动化JS逆向挖洞
javascript·人工智能·chrome·安全·web安全·网络安全·自动化
STDD1 小时前
strace 和 perf:Linux 进程调试和性能分析深度指南
linux·运维·php
祁白_1 小时前
[0xV01D]_Release Echo_writeUp
大数据·安全·ctf·writeup
清水白石0081 小时前
构建企业级 Python 服务:配置、日志、指标与追踪的稳健之道
开发语言·python·elasticsearch
都在酒里1 小时前
Linux字符设备驱动开发(五):PWM调光——实现LED亮度控制与呼吸灯效果
linux·运维·驱动开发