系统学习 演练学习路径:35 个 lab 的难度排序、4 周基础计划与复盘方法

版本信息最后核对:2026-10-25

Lab 状态:实跑通过2026-10-25 · Docker Compose / Elasticsearch 8.15.3

快照、删除、恢复、mapping/文档数与别名切换均实测通过

在 Compose Elasticsearch 中注册快照仓库,删除索引后恢复到临时索引并核对文档数与 mapping

演练目标

安全边界:本 Lab 只使用本地 Compose 项目、127.0.0.1:19200 和 /tmp/lab-es-restore 目录,不连接任何生产集群。不要在生产删除索引、清空快照仓库或直接恢复同名索引;快照仓库必须独立于数据盘,仓库对象不能手工删除。真实恢复还要停止采集端、暂停 ILM/SLM,并确认版本、插件、安全和容量兼容。

环境准备

准备 Docker、Compose v2 和 jq,预留约 1.5 GB 内存。快照仓库使用宿主机目录并挂载到容器;为了让容器内 Elasticsearch 用户可写,实验目录临时放宽权限。生产必须用正确的属主和最小权限,不能照搬 chmod 0777。镜像标签不写死,请用你已验证、且快照格式兼容的 Elasticsearch 镜像。

timeout 10 mkdir -p /tmp/lab-es-restore/snapshot /tmp/lab-es-restore/evidence
export ES_IMAGE="${ES_IMAGE:?请设置已验证的 Elasticsearch 镜像}"
timeout 30 chmod 0777 /tmp/lab-es-restore/snapshot
timeout 30 docker version --format '{{.Server.Version}}'
timeout 30 docker compose version
timeout 30 jq --version
timeout 30 tee /tmp/lab-es-restore/docker-compose.yml >/dev/null <<'EOF'
name: lab-es-restore
services:
  es:
    image: ${ES_IMAGE:?set ES_IMAGE}
    container_name: lab-es-restore
    environment:
      discovery.type: single-node
      xpack.security.enabled: "false"
      ES_JAVA_OPTS: "-Xms512m -Xmx512m"
    command: ["elasticsearch", "-Epath.repo=/snapshot"]
    ports:
    - "127.0.0.1:19200:9200"
    volumes:
    - /tmp/lab-es-restore/snapshot:/snapshot
    - esdata:/usr/share/elasticsearch/data
volumes:
  esdata:
    name: lab-es-restore-data
EOF
timeout 180 docker compose -f /tmp/lab-es-restore/docker-compose.yml up -d
timeout 180 bash -c 'until curl -fsS http://127.0.0.1:19200/_cluster/health >/dev/null 2>&1; do sleep 2; done'
timeout 30 curl -fsS 'http://127.0.0.1:19200/_cluster/health?pretty'

注册文件系统快照仓库并执行 _verify。_verify 只证明仓库可访问、基本结构可用,不证明每个分片都可读;后续仍要结合快照状态和实际恢复验证:

timeout 30 curl -fsS -X PUT 'http://127.0.0.1:19200/_snapshot/lab_repo?pretty' \
  -H 'Content-Type: application/json' \
  -d '{"type":"fs","settings":{"location":"/snapshot","compress":true}}'
timeout 30 curl -fsS -X POST 'http://127.0.0.1:19200/_snapshot/lab_repo/_verify?pretty'

创建 1 主 0 副的索引,避免单节点上副本长期未分配。写入 5 条文档并刷新,记录文档数、金额聚合、mapping 和别名基线:

timeout 30 curl -fsS -X PUT 'http://127.0.0.1:19200/orders-000001?pretty' \
  -H 'Content-Type: application/json' \
  -d '{
    "settings":{"number_of_shards":1,"number_of_replicas":0},
    "mappings":{"properties":{
      "order_id":{"type":"keyword"},
      "amount":{"type":"integer"},
      "created_at":{"type":"date"}
    }},
    "aliases":{"orders":{"is_write_index":true}}
  }'
timeout 30 tee /tmp/lab-es-restore/orders.ndjson >/dev/null <<'EOF'
{"index":{"_id":"1"}}
{"order_id":"A-1","amount":100,"created_at":"2026-10-20T10:00:00Z"}
{"index":{"_id":"2"}}
{"order_id":"A-2","amount":200,"created_at":"2026-10-20T10:01:00Z"}
{"index":{"_id":"3"}}
{"order_id":"A-3","amount":300,"created_at":"2026-10-20T10:02:00Z"}
{"index":{"_id":"4"}}
{"order_id":"A-4","amount":400,"created_at":"2026-10-20T10:03:00Z"}
{"index":{"_id":"5"}}
{"order_id":"A-5","amount":500,"created_at":"2026-10-20T10:04:00Z"}
EOF
timeout 30 curl -fsS -X POST 'http://127.0.0.1:19200/orders-000001/_bulk?refresh=true&pretty' \
  -H 'Content-Type: application/x-ndjson' \
  --data-binary @/tmp/lab-es-restore/orders.ndjson | timeout 30 tee /tmp/lab-es-restore/evidence/bulk-before.json
timeout 30 curl -fsS 'http://127.0.0.1:19200/orders-000001/_count' \
  | timeout 30 jq -S . | timeout 30 tee /tmp/lab-es-restore/evidence/count-before.json
timeout 30 curl -fsS 'http://127.0.0.1:19200/orders-000001/_mapping' \
  | timeout 30 jq -S '.[].mappings' > /tmp/lab-es-restore/evidence/mapping-before.json
timeout 30 curl -fsS 'http://127.0.0.1:19200/orders-000001/_settings' \
  | timeout 30 jq -S '.[].settings.index | {number_of_shards,number_of_replicas}' \
  > /tmp/lab-es-restore/evidence/settings-before.json
timeout 30 curl -fsS 'http://127.0.0.1:19200/_cat/aliases/orders?v' \
  | timeout 30 tee /tmp/lab-es-restore/evidence/aliases-before.txt
timeout 30 curl -fsS 'http://127.0.0.1:19200/orders-000001/_search?pretty' \
  -H 'Content-Type: application/json' \
  -d '{"size":0,"aggs":{"sum_amount":{"sum":{"field":"amount"}}}}' \
  | timeout 30 jq -S '.aggregations' > /tmp/lab-es-restore/evidence/agg-before.json

演练步骤

1. 拍摄快照

快照只包含 orders-000001,不包含全局状态,避免恢复时把旧模板、旧 ILM 策略或旧集群设置覆盖到目标。创建时使用 wait_for_completion=true,再读取快照详情和状态:

SNAP="lab-snap-$(timeout 5 date +%Y%m%d%H%M%S)"
timeout 10 tee /tmp/lab-es-restore/evidence/snapshot-name.txt >/dev/null <<< "$SNAP"
timeout 120 curl -fsS -X PUT "http://127.0.0.1:19200/_snapshot/lab_repo/$SNAP?wait_for_completion=true&pretty" \
  -H 'Content-Type: application/json' \
  -d '{"indices":"orders-000001","include_global_state":false}' \
  | timeout 30 tee /tmp/lab-es-restore/evidence/snapshot-create.json
timeout 30 curl -fsS "http://127.0.0.1:19200/_snapshot/lab_repo/$SNAP?pretty" \
  | timeout 30 jq -S '.snapshots[0] | {snapshot,state,indices,shards}' \
  > /tmp/lab-es-restore/evidence/snapshot-info.json
timeout 30 curl -fsS "http://127.0.0.1:19200/_snapshot/lab_repo/$SNAP/_status?pretty" \
  | timeout 30 tee /tmp/lab-es-restore/evidence/snapshot-status.json

快照状态应为 SUCCESS,失败分片为 0。若为 PARTIAL 或存在失败分片,先判断缺失的是否是关键索引,不要默认可用,也不要用 partial=true 把不完整恢复当成成功。

2. 制造快照后的写入与误删

快照完成后写入 2 条文档,模拟正常业务继续发生;再停止 ILM/SLM 并删除索引。恢复后应只有快照中的 5 条,6、7 号文档属于可解释的 RPO 缺口:

timeout 30 tee /tmp/lab-es-restore/post-snapshot.ndjson >/dev/null <<'EOF'
{"index":{"_id":"6"}}
{"order_id":"A-6","amount":600,"created_at":"2026-10-20T10:05:00Z"}
{"index":{"_id":"7"}}
{"order_id":"A-7","amount":700,"created_at":"2026-10-20T10:06:00Z"}
EOF
timeout 30 curl -fsS -X POST 'http://127.0.0.1:19200/orders-000001/_bulk?refresh=true&pretty' \
  -H 'Content-Type: application/x-ndjson' \
  --data-binary @/tmp/lab-es-restore/post-snapshot.ndjson \
  | timeout 30 tee /tmp/lab-es-restore/evidence/bulk-post-snapshot.json
timeout 30 curl -fsS 'http://127.0.0.1:19200/orders-000001/_count' \
  | timeout 30 jq -S . | timeout 30 tee /tmp/lab-es-restore/evidence/count-before-delete.json
timeout 30 curl -sS -X POST 'http://127.0.0.1:19200/_ilm/stop?pretty' \
  | timeout 30 tee /tmp/lab-es-restore/evidence/ilm-stop.json
timeout 30 curl -sS -X POST 'http://127.0.0.1:19200/_slm/stop?pretty' \
  | timeout 30 tee /tmp/lab-es-restore/evidence/slm-stop.json
timeout 30 curl -fsS -X DELETE 'http://127.0.0.1:19200/orders-000001?pretty'
timeout 30 curl -s 'http://127.0.0.1:19200/_cat/indices/orders-*?v'
timeout 30 curl -s 'http://127.0.0.1:19200/_cat/aliases/orders?v'

删除后原索引和别名都不存在,集群可能回到 green。此时唯一可用副本是快照仓库;仓库位于独立挂载目录,不随索引删除或数据卷丢失。接下来的恢复只创建临时索引,不覆盖任何原有名称。

3. 恢复到临时索引

使用重命名把快照中的索引恢复到 restored-orders-000001,关闭别名和全局状态恢复。恢复请求默认异步,这里等待完成,但仍要检查集群健康和恢复分片:

timeout 120 curl -fsS -X POST "http://127.0.0.1:19200/_snapshot/lab_repo/$SNAP/_restore?wait_for_completion=true&pretty" \
  -H 'Content-Type: application/json' \
  -d '{
    "indices":"orders-000001",
    "ignore_unavailable":true,
    "include_global_state":false,
    "include_aliases":false,
    "rename_pattern":"(.+)",
    "rename_replacement":"restored-$1"
  }' | timeout 30 tee /tmp/lab-es-restore/evidence/restore.json
timeout 120 curl -fsS 'http://127.0.0.1:19200/_cluster/health/restored-orders-000001?wait_for_status=green&timeout=60s&pretty'
timeout 30 curl -fsS 'http://127.0.0.1:19200/_cat/recovery/restored-orders-000001?active_only=true&v' \
  | timeout 30 tee /tmp/lab-es-restore/evidence/recovery-active.txt

4. 验证并切换别名

先核对文档数、金额聚合、mapping 和抽样文档,再创建写别名并写入 canary。确认恢复索引可写后,才恢复 ILM/SLM 并补拍快照;不要在验证前把生产别名切到未验证的索引:

timeout 30 curl -fsS 'http://127.0.0.1:19200/restored-orders-000001/_count' \
  | timeout 30 jq -S . | timeout 30 tee /tmp/lab-es-restore/evidence/count-after.json
timeout 30 curl -fsS 'http://127.0.0.1:19200/restored-orders-000001/_mapping' \
  | timeout 30 jq -S '.[].mappings' > /tmp/lab-es-restore/evidence/mapping-after.json
timeout 30 curl -fsS 'http://127.0.0.1:19200/restored-orders-000001/_search?pretty' \
  -H 'Content-Type: application/json' \
  -d '{"size":0,"aggs":{"sum_amount":{"sum":{"field":"amount"}}}}' \
  | timeout 30 jq -S '.aggregations' > /tmp/lab-es-restore/evidence/agg-after.json
timeout 30 diff -u /tmp/lab-es-restore/evidence/mapping-before.json /tmp/lab-es-restore/evidence/mapping-after.json
timeout 30 diff -u /tmp/lab-es-restore/evidence/agg-before.json /tmp/lab-es-restore/evidence/agg-after.json
timeout 30 curl -fsS 'http://127.0.0.1:19200/restored-orders-000001/_doc/1' \
  | timeout 30 jq -S '{found,source:._source}'
timeout 30 curl -sS 'http://127.0.0.1:19200/restored-orders-000001/_doc/6' \
  | timeout 30 jq -S '.found'
timeout 30 curl -fsS -X POST 'http://127.0.0.1:19200/_aliases?pretty' \
  -H 'Content-Type: application/json' \
  -d '{"actions":[{"add":{"index":"restored-orders-000001","alias":"orders","is_write_index":true}}]}'
timeout 30 curl -fsS 'http://127.0.0.1:19200/_cat/aliases/orders?v' \
  | timeout 30 tee /tmp/lab-es-restore/evidence/aliases-after.txt
timeout 30 curl -fsS -X POST 'http://127.0.0.1:19200/orders/_doc/8?refresh=true&pretty' \
  -H 'Content-Type: application/json' \
  -d '{"order_id":"A-8","amount":800,"created_at":"2026-10-21T10:00:00Z"}'
timeout 30 curl -fsS 'http://127.0.0.1:19200/orders/_count' \
  | timeout 30 jq -S . | timeout 30 tee /tmp/lab-es-restore/evidence/count-after-canary.json
timeout 30 curl -sS -X POST 'http://127.0.0.1:19200/_ilm/start?pretty' \
  | timeout 30 tee /tmp/lab-es-restore/evidence/ilm-start.json
timeout 30 curl -sS -X POST 'http://127.0.0.1:19200/_slm/start?pretty' \
  | timeout 30 tee /tmp/lab-es-restore/evidence/slm-start.json
timeout 120 curl -fsS -X PUT "http://127.0.0.1:19200/_snapshot/lab_repo/post-restore-$(timeout 5 date +%Y%m%d%H%M%S)?wait_for_completion=true&pretty" \
  -H 'Content-Type: application/json' \
  -d '{"indices":"restored-orders-000001","include_global_state":false}' \
  | timeout 30 tee /tmp/lab-es-restore/evidence/post-restore-snapshot.json

观察现象

删除前索引有 7 条文档,快照中只有 5 条。恢复请求应把 orders-000001 重命名为 restored-orders-000001,不恢复别名和全局状态。恢复完成后,临时索引文档数为 5、金额聚合为 1500,mapping 与快照前一致,6、7 号文档不存在;原索引名仍不存在,验证通过后才把别名 orders 指向恢复索引。

成功的判据是:快照状态 SUCCESS、失败分片为 0;恢复任务完成,_cat/recovery 没有持续 active 的分片;临时索引文档数、聚合和 mapping 与快照时点一致;抽样文档内容正确;别名切换后 canary 写入成功;ILM/SLM 已恢复;RPO 的 2 条文档有业务解释。失败判据是:快照为 PARTIAL 仍直接恢复;恢复后文档数不对却无法用快照时点解释;mapping 字段类型变化;为了恢复同名索引先删除生产索引;ILM 在恢复窗口删除或滚动索引。

判断与定位

  1. 先看快照状态,不要只看 _verify。_verify 只检查仓库访问,_status 和 _cat/recovery 才能说明分片恢复情况;失败分片是否关键要按索引逐个判断。
  2. 恢复请求返回 accepted 只表示任务提交,不代表完成。要跟踪 _cat/recovery、_cluster/health 和 _cat/shards,确认没有长时间 INITIALIZING 或 UNASSIGNED。
  3. 文档数少 2 条,先确认快照时点和快照后的写入,不要立刻判定恢复失败。只有差异超出快照窗口,才检查是否拿错快照、恢复错索引或刷新延迟。
  4. mapping 不一致时,先区分 JSON 字段顺序差异和真实类型差异。本 Lab 用 jq -S 归一化后比较;已有字段类型不能原地修改,必要时恢复到新索引后用 _reindex 重建。
  5. 同名索引处于 open 状态时,直接恢复通常会失败。优先恢复到临时名称并验证,再切换别名;不要用“删了再试”处理生产索引。
  6. include_global_state=true 可能覆盖目标集群的模板、ILM、pipeline 和设置。需要恢复这些元数据时,先在隔离环境导出并逐项对比,不要直接覆盖生产配置。
  7. 单节点、0 副本的 green 是实验配置,不代表生产高可用。恢复后副本未分配时,要查节点数、磁盘 watermark、分配过滤和 _cluster/allocation/explain,不能只看颜色。

恢复与回滚

如果快照不可用或状态为 PARTIAL,先保留仓库、快照状态、恢复日志和失败分片信息,再判断是否接受部分数据。能重拍就重拍;不能重拍时,把缺失范围写入事故记录,不要用 partial=true 掩盖问题。版本或插件不兼容时,先在隔离环境验证,不能通过修改配置强行读取不兼容的快照。

恢复失败时,只删除本次创建的临时索引,保留原索引、快照和证据;不要删除仓库对象,也不要在同一个临时索引上反复覆盖恢复。若临时索引验证通过但别名切换后发现问题,先把别名切回仍保留的旧索引;如果新索引已经接受写入,先停止写入、导出差异并评估补偿,不能直接丢弃新数据。

实验结束先归档证据,再注销仓库、删除索引和 Compose 资源。注销仓库不会自动删除宿主机上的快照文件,rm -rf 只针对本实验目录:

timeout 30 curl -sS -X DELETE 'http://127.0.0.1:19200/restored-orders-000001?pretty' || true
timeout 30 curl -sS -X DELETE 'http://127.0.0.1:19200/_snapshot/lab_repo?pretty' || true
timeout 60 docker compose -f /tmp/lab-es-restore/docker-compose.yml down -v --remove-orphans
timeout 30 rm -rf /tmp/lab-es-restore

验证

复盘

相关命令

参考链接

读完自测

  1. 快照是增量的,为什么仍不能手工删除仓库中的对象?
  2. 恢复同名生产索引时,为什么优先恢复到临时索引并切换别名?
  3. include_global_state=true 有什么风险?恢复前应怎样处理 ILM 和 SLM?
答案
  1. 多个快照共享 segment,手工删除仓库对象可能破坏其他快照的引用,导致后续恢复缺分片或失败。应通过快照 API 和生命周期策略管理,并保证仓库独立、可访问、有权限控制。
  2. 同名 open 索引会让恢复失败,直接关闭或删除旧索引又缺少验证和回滚空间。恢复到临时索引可以先核对文档数、mapping、聚合和抽样查询,再用别名原子切换,旧索引保留到确认完成。
  3. 它可能用旧快照覆盖目标集群的模板、ILM 策略、pipeline 和设置。应先对比目标配置,必要时在隔离集群导出后逐项恢复,并在恢复前暂停 ILM/SLM,避免策略删除或滚动恢复中的索引。
微信小程序码:扫码打开命令速查小程序

小程序版

微信扫码,随时查命令