版本信息最后核对:2026-10-25
Lab 状态:实跑通过
快照、删除、恢复、mapping/文档数与别名切换均实测通过
_verify 不等于分片可恢复。安全边界:本 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
快照只包含 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 条文档,模拟正常业务继续发生;再停止 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。此时唯一可用副本是快照仓库;仓库位于独立挂载目录,不随索引删除或数据卷丢失。接下来的恢复只创建临时索引,不覆盖任何原有名称。
使用重命名把快照中的索引恢复到 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
先核对文档数、金额聚合、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 在恢复窗口删除或滚动索引。
_verify。_verify 只检查仓库访问,_status 和 _cat/recovery 才能说明分片恢复情况;失败分片是否关键要按索引逐个判断。accepted 只表示任务提交,不代表完成。要跟踪 _cat/recovery、_cluster/health 和 _cat/shards,确认没有长时间 INITIALIZING 或 UNASSIGNED。jq -S 归一化后比较;已有字段类型不能原地修改,必要时恢复到新索引后用 _reindex 重建。include_global_state=true 可能覆盖目标集群的模板、ILM、pipeline 和设置。需要恢复这些元数据时,先在隔离环境导出并逐项对比,不要直接覆盖生产配置。_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
SUCCESS,失败分片为 0,快照包含 orders-000001;snapshot-status.json 已保存。_cat/recovery 没有 active 分片,索引 health 达到预期;单节点 0 副本时 green 可解释。restored-orders-000001 文档数为 5,金额聚合为 1500;6、7 号文档不存在,说明快照后的 2 条写入是 RPO。_id=1 内容正确,_id=6 返回 found:false;查询、聚合和排序结果符合基线。orders 指向恢复索引,canary 文档写入后计数为 6;旧索引没有被重新创建或覆盖。/tmp/lab-es-restore 已清理。_verify 成功、恢复请求返回 accepted,为什么仍不能判定恢复完成?include_global_state=true 会带来哪些覆盖风险?ILM 为什么要在恢复前暂停?include_global_state=true 有什么风险?恢复前应怎样处理 ILM 和 SLM?
小程序版
微信扫码,随时查命令