ElasticSearch(四):集群高可用、运维与生态
ElasticSearch(四):集群高可用、运维与生态
导语:最后一篇解决「线上真的会出问题」的那些场景。讲清 Master 选举与脑裂的版本演进(以及网上照抄旧版的错误答案)、green/yellow/red 的排查链路、磁盘水位与只读恢复、分片重平衡与恢复调优、零停机重建索引,以及快照备份与 ELK 生态,共 15 题。
一、集群与高可用
1. Master 选举与脑裂(split-brain)如何避免?
答: 先说清 Master 的职责边界:Master 只管集群元数据(索引创建/删除、Mapping、分片分配与再平衡、节点上下线),不处理文档级读写。
选举规则:候选 master 节点(node.roles 含 master)参与选举,必须获得多数派(quorum)投票才能成为 master。
版本演进(这是本题最大的考点,也是网上错误答案的重灾区):
| 版本 | 选主与防脑裂机制 |
|---|---|
| 7.0 之前 | ZenDiscovery,需要手动配置 discovery.zen.minimum_master_nodes,取值应为 N/2 + 1。配置漏了或写错就直接脑裂,这是当年 ES 最常见的生产事故 |
| 7.0 起 | 引入基于 Raft 算法的新集群协调子系统,discovery.zen.* 与 minimum_master_nodes 被彻底移除,ES 自动维护投票配置(voting configuration),从机制上消除了"配置错就脑裂" |
| 7.x / 8.x | 引导期需要 cluster.initial_master_nodes(仅首次集群引导用,集群成型后应移除,否则有风险);8.x 用 node.roles: [master] 取代 node.master: true |
必须纠正的说法:原稿写「
discovery.zen.minimum_master_nodes在旧版;7.x 后由集群自动多数决」方向是对的,但指向的版本不够精确——是 7.0 就被移除了,不是「7.x 之后」。回答时明确说出 7.0 + Raft + 自动投票配置 才显得准确。
脑裂会导致什么:网络分区后出现两个 master,各自接受元数据变更,恢复后一侧的变更被丢弃(典型的"静默丢元数据",比如新建的索引消失)。
现代防脑裂的手段(7.0+ 依然要做的运维层面的事):
- 候选 master 节点数为奇数(3 或 5)——保证多数派唯一,避免平票;
- 多数派机制天然防护:少数派分区无法凑够票数,选不出 master,会持续重试直到网络恢复后自动收敛。所以「10 个节点 5 个选 A、5 个选 B」在 7.0+ 基本不会发生;
- 专用 master 节点:不要把 master 和重负载 data 混部。master 因 GC 停顿/重查询被误判失联,会反复触发选主,把集群搞成"选举风暴";
- 网络质量要可靠:不要跨机房/跨高延迟链路部署同一个集群的 master 通信(多机房要用 CCR,见第 13 题);
- 不要随意使用
cluster.initial_master_nodes:集群成型后配置残留会带来风险。
一句话:7.0 之前靠"人工配对参数",7.0 之后靠"算法保证正确性"——但「奇数台专用 master + 网络可靠」这条运维铁律永远成立。
2. Master 节点宕机,数据读写会中断吗?
答: 不会中断读写,但会暂停元数据变更。
| 功能 | Master 宕机时是否受影响 |
|---|---|
| 文档读写(CRUD) | ✅ 不受影响:请求由 data 节点上的分片直接处理,不需要 master 参与 |
| 搜索与聚合 | ✅ 不受影响 |
| 副本复制 | ✅ 不受影响(主分片主导,见《ElasticSearch(二)》第 10 题) |
| 创建/删除索引、改 Mapping、改副本数 | ❌ 暂停,直到新 master 选出 |
| 分片分配与再平衡、故障恢复 | ❌ 暂停 |
新 master 的选出速度:通常秒级(取决于 cluster.fault_detection.* 的超时配置与候选节点数)。
真正的风险不在于 master 本身,而在于"故障叠加":
master 节点宕机 + 同时有 data 节点故障
-> 需要重新分配分片,但没有 master 来做这个决定
-> 分片恢复被卡住,直到新 master 上任所以生产建议:
- 专用 master(3 台奇数),与 data 节点物理分离,避免"一台机器挂了既丢 master 又丢数据分片";
- master 节点配置要好(低 GC 停顿),只做元数据;
- 监控
cluster.pending_tasks(待处理的集群任务数)——它持续增长说明 master 处理不过来。
3. 集群健康状态 green / yellow / red 分别代表什么?怎么排查?
答:
| 状态 | 含义 | 业务影响 |
|---|---|---|
| green | 所有主分片与副本都已成功分配 | 正常 |
| yellow | 所有主分片已分配,但有副本未分配 | 读写正常,但失去冗余——再挂一个节点就可能变 red,属于"必须处理"的告警 |
| red | 存在主分片未分配 | 那部分数据无法读写,是最高级别故障 |
排查链路(按顺序执行):
# ① 定位是哪个索引的问题
GET _cluster/health?level=indices
# ② 看未分配的分片及其原因
GET _cat/shards?v&h=index,shard,prirep,state,unassigned.reason
# ③ 【最关键】让 ES 直接解释"为什么这个分片分配不了"
GET _cluster/allocation/explain_cluster/allocation/explain 会给出明确的 unassigned_info.reason,常见的有:NODE_LEFT、ALLOCATION_FAILED、CLUSTER_RECOVERED、INDEX_CREATED、REPLICA_ADDED、NO_VALID_SHARD_COPY、DANGLING_INDEX_IMPORTED——照它说的原因处理,比自己猜快得多。
常见成因与处理:
| 现象 | 常见原因 | 处理 |
|---|---|---|
| yellow | ① 单节点集群(副本无处可放)② 副本数 > 节点数 ③ 有节点刚下线 | 加节点;或临时 number_of_replicas: 0(牺牲冗余,只建议在临时环境);等节点回来 |
| red | ① 主分片所在节点宕机且没有可用副本 ② 磁盘水位导致无法分配 ③ 分片数据损坏 | 恢复节点上线 → 自动分配;清磁盘;数据损坏需从 snapshot 恢复 |
| 长期 yellow | 副本数设置不合理(如 3 节点却设 3 副本) | 按节点数设置副本数(一般 1~2) |
极端手段(高风险,先备份):
POST _cluster/reroute手动分配分片;allocate_stale_primary(接受数据可能丢失)或allocate_empty_primary(直接丢弃该分片数据)——只在数据无法恢复且业务允许丢数据时使用。
两个容易被忽略的注意点:
- yellow 不要习以为常:它是"冗余已失效"的信号,此时单节点故障就会升级为 red 且可能丢数据;
- 不要用
number_of_replicas: 0长期抹平 yellow:副本是恢复能力的来源,长期关副本等于放弃了容灾。
4. 如何监控 ES 集群?该盯哪些指标?
答: 分层看,优先级从高到低:
1)集群层
GET _cluster/health:status(green/yellow/red)、unassigned_shards、initializing_shards、relocating_shards、active_shards_percent;GET _cluster/pending_tasks:待处理集群任务数持续增长 = master 处理不过来(危险信号);GET _cluster/stats:集群总量视角。
2)节点资源层(GET _nodes/stats)
| 分类 | 关键指标 | 告警建议 |
|---|---|---|
| JVM | jvm.mem.heap_used_percent、jvm.gc.collectors.old.collection_time_in_millis、jvm.gc.collectors.young.* | 堆 > 75% 持续、Old GC 耗时突增 |
| OS | os.cpu.percent、os.cpu.load_average、fs.total.available_in_bytes | CPU 持续 > 70%、磁盘 > 85% |
| 磁盘 IO | fs.io_stats.*(读写延迟/吞吐) | 延迟飙高(往往是 merge 或恢复引起) |
| 线程池 | thread_pool.write.queue、thread_pool.search.queue、thread_pool.*.rejected | rejected > 0 就要告警(最灵敏的一级指标之一) |
| 熔断器 | breakers.*.tripped | 触发次数增长说明有请求在吃爆内存(见《ElasticSearch(三)》第 9 题) |
3)索引与操作层
indexing.index_total/indexing.index_time_in_millis(写入量与时延);search.query_total/search.query_time_in_millis(查询量与时延);refresh.total/refresh.time_in_millis(refresh 过频会推高 merge);merges.total/merges.total_time_in_millis/merges.current(merge 风暴的观测点);segments.count/segments.memory_in_bytes(段数量膨胀);indices.fielddata.memory_size_in_bytes(fielddata 在吃堆,重点盯)。
4)工具链
| 工具 | 用途 |
|---|---|
| Kibana → Stack Monitoring | 官方监控,开箱即用(需采集) |
| Cerebro | 集群可视化,看分片分布/未分配分片最直观 |
| Prometheus + elasticsearch_exporter + Grafana | 自建监控与告警的主流方案 |
| Metricbeat / Elastic Agent | 采集 ES 自身指标 |
_cat/*(nodes、indices、allocation、thread_pool、shards、recovery) | 命令行快速排查 |
| 慢查询日志 | 定位慢查询(见《ElasticSearch(三)》第 11 题) |
一句话:先盯「rejected + 堆使用率 + 磁盘水位 + merge 耗时」这四个,能覆盖八成的线上故障。
二、分片分配、恢复与容量规划
5. 分片分配与重平衡怎么控制?
答: 分片分配/重平衡会消耗大量 IO(数据搬迁),可控性是运维的关键。
触发分配与重平衡的场景:节点上下线、副本数变更、索引创建、手工 reroute、磁盘水位变化。
常用参数(按使用频率排序):
| 参数 | 默认 | 作用 |
|---|---|---|
index.unassigned.node_left.delayed_timeout | 1m | 节点离线后延迟多久才把它的分片重新分配。调大到 5m 可以让短暂重启完全不触发重平衡,是滚动重启的关键参数 |
cluster.routing.allocation.enable | all | primaries(只分配主分片,滚动重启时用)、none(禁止分配,节点下线前用)、new_primaries |
cluster.routing.allocation.node_concurrent_recoveries | 2 | 单节点并发恢复的副本分片数 |
cluster.routing.allocation.node_initial_primaries_recoveries | 4 | 单节点并发恢复的初始主分片数(重启时最先恢复主分片,所以默认更大) |
cluster.routing.allocation.cluster_concurrent_rebalance | 2 | 集群级并发再平衡分片数 |
cluster.routing.rebalance.enable | all | none 可彻底关闭再平衡(维护窗口用);也可只允许 primaries / replicas |
cluster.routing.allocation.awareness.attributes + .force.* | — | 机架/可用区感知:同一分片的主副本强制分散到不同故障域(force 版本在节点数不足时也强约束,用容量换安全) |
index.routing.allocation.include/exclude/require | — | 按节点属性过滤(如 node.attr.temperature: hot),这是 ILM 冷热分离的实现基础 |
两个实战动作:
① 节点安全下线(把分片搬空)
# 让 ES 把该节点上的分片全部迁走,再下线(比直接 kill 更平滑)
PUT _cluster/settings
{ "transient": { "cluster.routing.allocation.exclude._name": "node-to-remove" } }
# 等 _cat/allocation 显示该节点 shards 为 0 后再停进程② 滚动重启的正确姿势
① 确认副本状态良好(不要在有 red 分片时滚动重启)
② (可选)cluster.routing.allocation.enable: primaries —— 只恢复主分片,加速启动
③ 停一个节点 -> 重启 -> 等它加入集群
④ 确认分片恢复完成(_cat/recovery、_cat/health)后再处理下一个
⑤ 全部完成后把 allocation.enable 改回 all- 或者干脆依赖
delayed_timeout: 5m:短暂重启期间分片不会被判定为"需要重新分配",成本最低。
分配被"卡住"时的排查:GET _cluster/allocation/explain(见第 3 题),它会明确告知是磁盘、awareness、filter、还是 enable: none 导致的。
6. 磁盘水位线是什么?索引变只读怎么恢复?
答: 这是 ES 生产上最容易踩、也最容易误判的一组配置。
三档水位(默认按节点数据路径的磁盘使用率计算,可写百分比或绝对容量,取更严格者):
| 水位 | 默认值 | 触发的行为 |
|---|---|---|
低水位 disk.watermark.low | 85% | 不再向该节点分配新的分片(该节点只出不进) |
高水位 disk.watermark.high | 90% | ES 尝试把该节点上的分片迁走(能迁多少迁多少) |
洪泛水位 disk.watermark.flood_stage | 95% | 该节点上所有索引被置为只读(index.blocks.read_only_allow_delete: true),写入直接报错 |
重要纠正:原稿说「默认 85% 只读、95% 禁止分配」——完全错了。85% 只是"不再接收新分片",只有 95%(洪泛)才会把索引置为只读,中间还有 90% 的"开始往外搬"。这三档语义必须记准,否则线上排查时会判断错方向。
索引变只读(洪泛触发)后的恢复步骤:
# ① 先腾空间(删旧索引 / 删数据 / 扩容磁盘)——这一步必须做,否则解除只读后立刻又被打满
DELETE /old-logs-2026.05.*
# ② 空间降回阈值以下后解除只读
PUT /_all/_settings
{ "index.blocks.read_only_allow_delete": null }- 注意:ES 只在磁盘使用率降到阈值以下时才会自动解封,实践中经常需要手动解封(尤其是在"刚扩完盘但水位判断还没刷新"的时候);
cluster.info.update.interval(默认 30s)控制水位检查频率;cluster.routing.allocation.disk.threshold_enabled: false可关闭水位检查——不推荐(等于放弃了磁盘保护)。
"磁盘明明还有空间,为什么报满?"(高频追问)
- ES 判断的是节点数据路径所在磁盘的可用空间,不是整个机器的;
- 段合并需要额外临时空间(新旧段共存),磁盘过紧会阻断 merge → 反过来卡住写入与查询;
- 分片分配还有"能否放得下"的判断(分片大小 vs 可用空间);
- 水位是分档动作:85% 已经不接收新分片了,此时"还有 15% 空间"在运维上其实已经不可用。
根治手段(比事后救火重要):
- 索引必须滚动(Rollover / data stream)——索引不滚动是磁盘打满的第一大原因;
- 用 ILM 的 delete 阶段自动清理过期数据;
- 冷数据
force_merge+_shrink(空间与段数双降); - 容量规划留冗余(单节点使用率常态控制在 60%~70% 以下)。
7. 分片恢复慢、集群重启恢复慢怎么优化?
答: 恢复(recovery)是分片级的数据搬迁,是 ES 最重的后台操作之一。
关键参数:
| 参数 | 默认 | 作用 |
|---|---|---|
indices.recovery.max_bytes_per_sec | 40mb | 单节点恢复带宽上限。内网高带宽环境可调大(如 200mb),但要评估磁盘与网络 |
indices.recovery.max_concurrent_file_chunks | 2 | 恢复时并发传输的文件块数 |
indices.recovery.max_concurrent_operations | 1 | 恢复时的并发操作数 |
cluster.routing.allocation.node_concurrent_recoveries | 2 | 单节点并发恢复的副本数 |
cluster.routing.allocation.node_initial_primaries_recoveries | 4 | 单节点并发恢复的初始主分片数 |
index.unassigned.node_left.delayed_timeout | 1m | 延迟重新分配,避免短暂重启引发重平衡 |
优化思路(按收益排序):
- 优先"避免恢复"而不是"加速恢复":
- 滚动重启,绝不全集群同时重启;
- 调大
delayed_timeout(如5m),让短暂重启根本不触发重分配; - 重启前做一次
POST /_flush:把 translog 清空并落盘,这样节点起来后只需加载段文件,不必重放大量 translog,启动更快(注意:_flush/synced这个旧 API 已在 7.x 移除,现在用_flush);
- 提高恢复并行度与带宽:调大
indices.recovery.max_bytes_per_sec与node_concurrent_recoveries(注意别把磁盘/网络打满,反而拖慢线上查询); - 减少需要恢复的分片数量:分片数越多,恢复越慢(分片是最小恢复单位)→
_shrink合并冷索引的分片; - 提升磁盘性能:SSD/NVMe;避免网络存储;
- 灾备用 CCR / 快照:跨集群复制(低 RPO 的持续复制)或定期快照恢复,比"从零重建数据"快得多。
必须纠正的过时答案:
gateway.recover_after_nodes、gateway.expected_nodes、gateway.recover_after_time是 ES 1.x / 2.x 时代的参数,早已被移除(原稿给的正是这套)。现代 ES 用上表的cluster.routing.allocation.*与index.unassigned.node_left.delayed_timeout来控制恢复节奏。在面试中背gateway.*参数,会直接被判定为知识陈旧。
8. 大数据量(数十亿)如何规划索引与生命周期?
答: 核心原则:不要让单个索引无限增长,用「滚动 + 分层 + 生命周期」来管理。
为什么必须滚动:
- Lucene 单个分片的文档数上限约 2^31-1(约 21 亿);
- 超大索引的 段合并、查询、恢复、删除 全都极其痛苦(删一个几百 GB 的索引会造成集群状态变更风暴);
- 按时间滚动后,查询带上时间范围就能只扫少量索引,性能与成本同时改善。
落地方式:索引模板 + Rollover / Data Stream + ILM
① 索引模板(index template / component template)
· 定义 Mapping、分片数、副本数、生命周期策略
② 滚动触发条件(rollover)
· max_primary_shard_size(推荐,如 30~50GB)
· max_age(如 1d)、max_docs
③ ILM 生命周期四阶段
hot -> 写入,可 rollover(SSD 节点)
warm -> 不再写入:force_merge 到 1 段、shrink 减少分片、降低副本(HDD 节点)
cold -> 极少查询:迁到廉价节点;可用 searchable snapshot 只存对象存储
delete-> 到期自动删除
④ 日志场景用 data stream(7.9+)
· 写入指向 data stream 名,自动 rollover 成 .ds-<name>-<date>-<gen> 索引
· 业务只面对读写别名,不用感知具体索引名冷热分离的实现:给节点打属性标签(node.attr.temperature: hot/warm/cold),ILM 的 allocate 动作按标签搬迁分片;查询侧用读别名覆盖全部 backing index,业务无感。
四个高频踩坑:
| 坑 | 后果 | 对策 |
|---|---|---|
| 索引不滚动 | 磁盘打满 → 洪泛水位 → 索引只读(见第 6 题) | ILM 的 rollover 必须配 max_primary_shard_size |
直接 DELETE /logs-* 批量删 | 集群状态变更风暴,master 卡住 | 用 ILM 逐个删;分批 + 限速 |
对写入中的索引 force_merge | 白做工 + 大量 IO(见《ElasticSearch(二)》第 7 题) | 只在 warm/只读阶段合并 |
| 分片数设太多 | 恢复慢、集群状态膨胀(见《ElasticSearch(一)》第 3 题) | 按 30~50GB/分片倒推 |
一句话:「按时间滚动 + ILM 分层 + 别名读写分离」是十亿级 ES 的标准答案,比"把机器堆更多"有效得多。
三、部署与资源调优
9. Linux / JVM 层面有哪些部署优化?
答:
1)系统层(这几条是"没配就会出问题"的硬要求)
| 配置 | 说明 |
|---|---|
禁用 swap(swapoff -a,并注释 /etc/fstab 中的 swap) | 换页会把毫秒级查询拖成秒级,还会让 GC 停顿失控。可用 bootstrap.memory_lock: true 配合 ulimit -l unlimited 把堆锁在物理内存 |
vm.max_map_count ≥ 262144 | 必配。Lucene 用 mmap 映射大量段文件,默认值(65530)会导致启动失败或运行崩溃 |
文件描述符 nofile ≥ 65536 | Lucene 大量占用文件句柄,分片多时尤其明显 |
ulimit -u(进程/线程数) | 线程池与 Lucene merge 线程需要 |
| NTP 时钟同步 | 分片分配、日志与监控都依赖时间一致 |
| SSD/NVMe 本地盘 | 避免网络存储(IO 延迟会放大到分片恢复与查询上) |
| 不要跨数据中心部署 | 集群状态同步与分片恢复会被延迟拖死;多机房用 CCR |
2)JVM 堆(最容易答错的一题)
- 经验法则:堆 ≤ 物理内存的 50%,且不超过约 31GB;
- 为什么不能超过 32GB:超过后 JVM 压缩指针(compressed oops)失效,对象引用从 4 字节变 8 字节,实际可用内存反而可能变少(与《Java并发》/JVM 篇讲的是同一个知识点);
- 为什么只给一半:Lucene 主要靠 OS filesystem cache(page cache) 缓存段文件,堆给多了会饿死 page cache,查询反而变慢——这是 ES 与"普通 Java 应用"最大的不同;
-Xms与-Xmx设为相等,避免运行期扩堆抖动;- 7.11+ 支持自动推导堆大小(基于节点总内存与角色计算),但生产建议显式设置,避免升级后行为变化;
- 容器化注意:
-Xmx要与容器的 memory limit 对齐(否则被 OOMKilled),并留出堆外内存(page cache、mmap、线程栈)的空间。
3)其他
- CPU:多核优于高频单核(并行分片查询、merge、恢复都靠并行度);
- 磁盘:单节点多盘可用 RAID0 提升吞吐(冗余交给 ES 副本,不靠 RAID);
- 数据路径:不要用
overlayfs(Docker 默认)存数据,挂载独立数据卷; - 用
GET _nodes/stats/process检查mlockall是否生效(mlockall为 false 说明锁内存没成功)。
10. GC 方面要注意什么?
答:
先说结论:ES 8.x 跑在 JDK 17+,默认使用 G1GC,现代场景基本不需要手调 GC 参数——重点是控制堆压力来源。
ES 里 GC 的特殊之处:
- Lucene 的核心数据结构大多不占堆:FST 词典、
doc_values、倒排表都通过 mmap 访问,占用的是操作系统 page cache; - 所以「堆没满但机器内存满了」是正常现象,不应该通过调 GC 去解决;
- 真正占堆的是:请求/响应对象、聚合的中间结果、
fielddata、scroll 上下文、批量写入的缓冲。
GC 的真正风险(连锁反应):
堆压力大 -> Old GC 停顿变长(秒级)
-> master 节点被误判失联 / 分片被误判失败
-> 触发选主与分片重新分配
-> 恢复本身又是重 IO/CPU 操作
-> 形成 "GC 风暴 + 恢复风暴" 的恶性循环堆压力大的常见根因(按频率):
fielddata滥用:对text字段做聚合/排序会把词项加载到堆(这是最典型的 OOM 主因)→ 改用keyword子字段;- 高基数聚合:
terms聚合 + 高基数字段 → 桶数量爆炸(见《ElasticSearch(三)》第 8 题); - 一次返回过多文档 / 过大
size(见《ElasticSearch(三)》第 12 题); - scroll 上下文过多(长时间不释放,占堆);
- 分片数过多:每个分片都有常驻的段级结构;
- 熔断限额过松,把保护机制调没了。
监控与处置:
- 监控
jvm.mem.heap_used_percent、jvm.gc.collectors.old.collection_time_in_millis、jvm.gc.collectors.young.collection_time_in_millis; - 堆使用率常态应 < 75%,长期高位就必须查上面的根因;
- 用 熔断器做请求级保护(见《ElasticSearch(三)》第 9 题);
- 用
GET _nodes/hot_threads+_nodes/stats/jvm联合定位; - 不要常规性地手调 GC 参数(除非有明确的、被 profiling 证实的停顿问题)。
四、索引治理与数据同步
11. 如何零停机重建索引(reindex)?
答: 这是最有价值的实战题之一——只要字段类型改错、要换分词器、要改分片数、要升级大版本,就必须重建索引,而线上不能停服。
什么时候必须重建:Mapping 已有字段的类型不能修改(text 改 keyword、long 改 keyword、改分词器/分析链都会影响倒排索引);以及分片数变更、大版本升级。
标准流程(别名 + reindex + 追增量 + 原子切换):
# ① 旧索引已有一个别名(业务读写的入口),假设 alias = orders,真实索引 orders_v1
# ② 按新 Mapping 建新索引
PUT /orders_v2 { "settings": {...}, "mappings": {...} }
# ③ 迁移历史全量数据(大索引要限速 + 并行 + 异步)
POST /_reindex?wait_for_completion=false
{
"source": { "index": "orders_v1", "size": 2000 },
"dest": { "index": "orders_v2", "op_type": "create" } # op_type=create 防止覆盖重建期间的新数据
}
# ④ 【最关键】同步重建期间的增量写入(不能漏,步骤见下)
# ⑤ 校验:对比两个索引的文档数与关键字段抽样
GET /_cat/count/orders_v1?format=json
GET /_cat/count/orders_v2?format=json
# ⑥ 原子切换别名(一个请求里 remove + add,业务无感)
POST /_aliases
{
"actions": [
{ "remove": { "index": "orders_v1", "alias": "orders" } },
{ "add": { "index": "orders_v2", "alias": "orders" } }
]
}
# ⑦ 观察一段时间(保留 v1 便于回滚),确认稳定后再删除 v1第 ④ 步「追增量」有三种做法(这步最容易被漏,也是本题的核心):
| 方案 | 做法 | 评价 |
|---|---|---|
| 双写 | 写入侧同时写 v1 与 v2 | 简单直接,但侵入业务代码,容易漏写或写失败 |
| 二次 reindex 追加 | 按 updated_at 时间范围,把重建期间变更的数据再 reindex 一次 | 适合有可靠更新时间字段的场景;需要多轮追平直到差值为 0 |
| CDC 链路(推荐) | 如果同步链路本来就是 binlog → MQ → ES,让同一条变更消息同时写 v1 和 v2(或在切换点让新索引开始消费) | 无需侵入业务,可靠、可重放 |
其他关键细节:
- 别名切换是原子的:
_aliases一次请求内完成"摘旧挂新",不存在"两个索引都没挂"的中间态; _reindex的常用参数:source.query(只迁部分数据)、source._source(只迁部分字段)、script(字段转换/改名)、conflicts(冲突策略)、requests_per_second(限速,避免压垮集群);- 大索引务必限速:不加限制的 reindex 会打满磁盘 IO 与 CPU,直接影响线上查询;
- 7.10+ 可用 PIT 保证一致性视图:
source里带 PIT,避免重建过程中数据变动导致遗漏/重复; - 也可以用
_reindex的remote做跨集群迁移(需在elasticsearch.yml配置reindex.remote.whitelist)。
12. 如何保证 ES 与上游数据库的数据一致性?
答: 先摆正定位:ES 不是源数据库,是检索副本。所以目标不是"强一致",而是最终一致 + 可观测 + 可修复。
同步方案对比:
| 方案 | 说明 | 评价 |
|---|---|---|
| 双写 | 业务同时写 DB 与 ES | 最简单,但失败补偿难、与业务耦合、并发下容易不一致 |
| CDC(binlog 订阅) | Canal / Debezium 解析 MySQL binlog → MQ → 写 ES | 主流方案:解耦、准实时、可重放、不侵入业务 |
| MQ 异步消息 | 业务发消息,消费者写 ES | 类似 CDC,但依赖业务正确发消息(漏发就丢) |
| 定时全量/增量重建 | 定时任务扫 DB 回写 | 兜底补偿,延迟大,适合作为对账后的修复手段 |
| Logstash JDBC / DataX 等 | 工具直连 DB 拉取 | 适合初始化与离线批量 |
CDC 方案的六个难点(面试的深度在这里):
- 顺序性:同一文档的多次变更必须按顺序写 ES,否则旧值会覆盖新值 → 按主键 hash 到同一 MQ 分区,保证单 key 有序;
- 幂等:消息可能重复消费 → 用业务主键作为文档
_id做覆盖写(天然幂等),必要时用if_seq_no拒绝乱序更新; - 删除语义:binlog 的
DELETE要映射为 ES 的删除;物理删除 vs 逻辑删除要设计清楚(逻辑删除用字段过滤更安全); - 更新类型的取舍:
UPDATE在 binlog 里可能只带变更列 → 要么用_update(read-modify-write,更重),要么让源头补全整行(binlog_row_image=FULL); - 批量与限速:大事务/批量更新会产生海量变更,必须拆分 + 限速 + 背压,避免把 ES 打挂;
- DDL 与字段演进:DB 加字段要同步 Mapping(
dynamic: strict会直接报错,需要先把 Mapping 更新到位再放流量)。
一致性保障的兜底三件套:
- MQ 重试 + 死信队列 + 本地补偿表:写入 ES 失败不能"打个日志就算";
- 定时对账:抽样比对 DB 与 ES 的文档数/关键字段/更新时间,发现漂移;
- 可重建:保留
_source、保留 binlog 位点,任何漂移都能通过重放修复。
一句话:「CDC 保实时 + 对账保正确 + 重放保修复」,这就是 ES 数据一致性的完整答案。
13. 如何做 ES 的备份与恢复(snapshot)?
答: 先纠正一个常见误解:副本不是备份。副本是同集群内的实时拷贝,误删索引、脏数据、Mapping 写错都会同步到副本上。所以必须做离线快照。
支持三种仓库类型(通过 path.repo 注册):
PUT _snapshot/my_repo
{ "type": "fs", "settings": { "location": "/mnt/backups", "compress": true } }
# 生产更推荐对象存储:type = s3 / gcs / azure核心特性:
- 增量 + 去重:第一次全量,之后只备份新增的段(segment 不可变,天然适合增量备份),所以快照之间共享数据段,存储成本远低于全量重复;
- 粒度灵活:可只备份指定索引/data stream;
- 快照不阻塞写入(后台执行)。
常用操作:
# 创建快照(异步)
PUT /_snapshot/my_repo/snap_20260626?wait_for_completion=false
# 查看快照与其中的索引
GET /_snapshot/my_repo/_all
GET /_snapshot/my_repo/snap_20260626
# 恢复(务必恢复到新索引名,别覆盖线上)
POST /_snapshot/my_repo/snap_20260626/_restore
{
"indices": "orders",
"rename_pattern": "orders_(.+)",
"rename_replacement": "restored_orders_$1"
}最佳实践:
- 用 SLM(Snapshot Lifecycle Management)自动定时快照:
PUT _slm/policy/nightly配置调度与保留策略(如每日一次、保留 30 天); - 版本兼容性:快照只能恢复到「创建它的同版本」或「更高版本」,不能恢复到低版本;跨大版本升级的推荐路径是「快照 → 升级集群 → 恢复」或
_reindexfrom remote; - 恢复演练必须做:定期在隔离环境演练恢复(没演练过的备份不算备份);
- 恢复用
rename_pattern隔离,避免误覆盖线上索引; - 只恢复需要的索引,全量恢复会引发大规模分片分配(配合第 5、7 题的参数控制节奏)。
CCR(跨集群复制)——与快照的定位差异:
| Snapshot | CCR | |
|---|---|---|
| 机制 | 周期性快照(增量) | 持续异步复制 |
| RPO | 取决于快照频率(小时级) | 秒级(低 RPO) |
| 用途 | 备份与归档、误删恢复、跨版本迁移 | 异地容灾、就近读、集群迁移 |
| 注意 | 需额外存储 | 7.0 起是付费(企业版)特性,开源版不可用;follower 索引只读 |
一句话:快照保"能恢复",CCR 保"不用等恢复"。 两者互补,不互相替代。
五、生态与版本
14. 什么是 ELK / Elastic Stack?各自职责?
答: 经典 ELK = Elasticsearch + Logstash + Kibana:
| 组件 | 职责 | 特点 |
|---|---|---|
| Elasticsearch | 存储、索引、检索、分析(核心) | 分布式、近实时、REST API |
| Logstash | 服务端数据管道:采集、解析(grok/JSON)、转换、富化、输出 | JVM 应用,资源消耗大,但解析能力强、支持多源汇聚与多路输出 |
| Kibana | 可视化与交互:Discover(检索)、Dashboard、Lens、Dev Tools、Stack Monitoring | 纯前端 + 后端代理,不吃数据 |
现代 Elastic Stack 已经扩展为(面试加分项):
- Beats 家族(Go 编写,部署在数据源侧的轻量采集器):
- Filebeat(日志,最常用)、Metricbeat(指标)、Winlogbeat(Windows 事件)、Packetbeat(网络)、Auditbeat、Heartbeat;
- 优势:资源占用极低、支持背压、用注册表(registry)保证不丢
- Elastic Agent + Fleet(7.8+ 起的统一采集方案,逐步取代各 Beats 独立部署);
- Elastic APM(应用链路追踪)、Elastic Security(SIEM)、Elastic Observability。
Filebeat 与 Logstash 怎么选(高频追问):
- 源端采集 → Beats / Elastic Agent(轻、稳、不丢数据);
- 需要复杂解析/富化/多路分发(grok 不规则日志、geoip 富化、从多个源汇聚、同时输出到 ES 和对象存储)→ Logstash;
- 两者经常组合使用:
应用日志 -> Filebeat(采集)
-> Kafka(削峰、解耦、可重放;大规模日志必备)
-> Logstash(grok/JSON 解析、字段富化、脱敏)
-> Elasticsearch(data stream + ILM 滚动与冷热分层)
-> Kibana(检索与看板)日志场景的选型对比(会被问到):
| 方案 | 特点 | 适用 |
|---|---|---|
| ES + Kibana | 全文检索 + 聚合分析能力强 | 需要全文搜索、复杂分析、可观测性一体化 |
| OpenSearch | AWS 主导的 ES 7.10 分叉,Apache 2.0 | 云上托管、AWS 生态 |
| Loki + Grafana | 只索引标签、不索引日志内容,成本极低 | 按标签定位日志、成本敏感 |
| ClickHouse | 列存,结构化日志的聚合分析极快 | 强聚合、弱全文检索 |
| 对象存储 + 查询引擎 | 存储成本最低 | 归档、合规留存 |
常见组合:热数据 ES 检索 + 冷数据对象存储归档;或 Loki 兜底全量日志 + ES 承载核心业务检索。
15. ES 7.x / 8.x 有哪些重要版本变化?
答: 面试里问版本变化,本质是筛「你知不知道自己在用什么版本」。
7.x 的关键变化:
| 变化 | 说明 |
|---|---|
| 移除 Type | 一个索引只剩 _doc 一种类型,索引正式对应"表"(见《ElasticSearch(一)》第 1 题) |
| 新集群协调子系统(7.0) | 基于 Raft,移除 discovery.zen.* 与 minimum_master_nodes,自动维护投票配置,从根本上缓解脑裂 |
| 默认主分片数 5 → 1 | 官方倾向"少分片 + 靠滚动扩展" |
hits.total 默认变成 relation | 默认最多精确统计到 10000,返回 { "value": 10000, "relation": "gte" };要精确计数需 track_total_hits: true(这个变化坑过很多上报统计) |
| PIT(7.10)与 data stream / data tiers(7.9~7.12) | 深分页与日志数据治理的新基建 |
_flush/synced 移除、TransportClient 废弃 | 7.0 废弃 TransportClient,8.0 移除 |
| Lucene 升级 | 打分细节与排序 tie-breaker 要求更严格 |
8.x 的关键变化:
| 变化 | 说明 |
|---|---|
| 安全默认开启 | xpack.security.enabled 默认 true(自动生成证书与密码),HTTPS + 认证成为默认配置——升级/连接报错大多源于此 |
node.roles 取代布尔开关 | node.master / node.data 等被移除,必须用 node.roles: [...];node.roles: [] 即纯协调节点 |
| 内置 JDK 17 | 默认 G1GC;不再支持 JDK 8 |
| kNN 向量检索 | 8.0 引入 dense_vector 的 knn 查询,后续版本支持 filter 预过滤;配合 RRF(8.8+)与 semantic_text(8.15+)形成完整的语义检索能力 |
| ES|QL | 8.11 起引入的管道式查询语言,后续版本逐步 GA,聚合分析场景比 DSL 更直观 |
| 清理大量旧 API/参数 | 破坏性变更较多 |
升级建议(面试常延伸问):
- 逐大版本滚动升级:
7.x → 7.x 最新版 → 8.x,不要跳版本; - 升级前必读官方 breaking changes,重点排查:
_type用法、hits.total语义、安全配置、废弃参数(如gateway.*); - 升级前做快照(见第 13 题),并先在预发环境演练;
- 8.x 首次启动必须处理安全配置(证书、密码、
discovery.type),否则连不上会误以为是集群故障。
系列完结:四篇覆盖了 ES 的概念与数据结构 → 写入与段管理 → 查询与聚合 → 集群运维与生态完整链路。复习时抓住这条主线:倒排索引(检索)+ 正排 doc_values(排序聚合)→ 段不可变与 refresh/translog/merge(NRT)→ 分片副本与路由(分布式)→ Query Then Fetch(查询)→ ILM/别名/快照(治理),把 Lucene 底层与集群机制串起来讲,深度自然就出来了。
