🛡️ 引言:跑起来容易,跑稳了是门学问
前九篇我们把 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 版本演进持续更新。