第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 全方案对比

相关推荐
Mr_愚人派13 小时前
当"Claude"不再是 Claude:一次第三方 API 代理引发的 AI 身份伪造排查实录
人工智能·安全
大树881 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠1 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
大志哥1231 天前
ES和Logstash日志链路系统上线后遭遇切片爆炸(解决)
大数据·elasticsearch
霸道流氓气质1 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
DaLi Yao1 天前
【无标题】
人工智能·安全
Inhand陈工1 天前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
Alsn861 天前
等待学习-学习目录:Docker 容器安全攻防
学习·安全·docker
网络研究院1 天前
2026年网络安全
网络·安全·法律·法规·趋势·发展
酣大智1 天前
ARP代理--工作原理
运维·网络·arp·arp代理