版本信息最后核对:2026-10-25
Lab 状态:部分通过
DNS、入口切换、数据库切换/回滚、证书轮换与清理通过;kind 上游与 200 业务响应未验证
本 Lab 用修改本地 DNS 记录、停止入口容器代替 BGP 收敛和区域网络隔离。真实区域故障还涉及 Anycast、运营商缓存、跨区链路和云厂商控制面,不能直接外推结论。
安全边界:只允许在一次性虚拟机、kind 和本地 Docker Compose 中进行。不要修改公司 DNS、生产 hosts、真实负载均衡或线上证书,也不要把本页命令直接套到生产入口。所有命令都必须带
timeout,按阶段执行;任一阶段失败先停止并回滚当前阶段,不要带着未知状态进入下一阶段。实验结束删除全部容器、集群、卷和临时证书。
OpenSSL 兼容性:
s_client -verify_hostname和x509 -ext需要 OpenSSL 1.1.1+;macOS 自带 LibreSSL 不支持这些选项,演练时使用 Linux 虚拟机或 Homebrew OpenSSL 3,并把openssl指向对应二进制。
准备 Docker/Compose v2、kind、kubectl、dig、curl 和 OpenSSL。所有文件放在 /tmp/lab-region。Compose 使用 172.28.0.0/24:resolver 为 .10,edge-a 为 .11,edge-b 为 .12,HTTP 探针为 .13,数据库探针为 .14,主库为 .21,从库为 .22。kind 中运行两副本 web,通过 NodePort 30080 暴露给入口。本页命令按 Linux/bash 与 GNU coreutils 编写;macOS 默认没有 timeout,可安装 coreutils 后将命令中的 timeout 替换为 gtimeout,或在 Linux 虚拟机中执行。
resolver 在宿主机发布为 127.0.0.1:15353,不要用 5353:macOS 的 mDNSResponder(以及 Linux 上的 avahi)会占用该 UDP 端口,导致 Compose 启动失败。若宿主机设置了 HTTP(S)_PROXY,curl --resolve 仍可能走代理并报 SSL 错误,本页 HTTPS 检查命令已加 --noproxy '*'。
先创建旧 CA、入口叶子证书和目录,SAN 必须是 app.lab.internal:
timeout 20 mkdir -p /tmp/lab-region/{certs/old,certs/new,certs/trust,certs/active-a,certs/active-b,dns,backup}
timeout 30 openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out /tmp/lab-region/certs/old/ca-old.key
timeout 30 openssl req -x509 -new -key /tmp/lab-region/certs/old/ca-old.key -sha256 -days 30 -subj "/CN=Lab Region Old Root CA" -out /tmp/lab-region/certs/old/ca-old.crt -addext "basicConstraints=critical,CA:TRUE" -addext "keyUsage=critical,keyCertSign,cRLSign"
timeout 30 openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out /tmp/lab-region/certs/old/leaf-old.key
timeout 30 openssl req -new -key /tmp/lab-region/certs/old/leaf-old.key -out /tmp/lab-region/certs/old/leaf-old.csr -subj "/CN=app.lab.internal"
timeout 10 tee /tmp/lab-region/certs/old/leaf.ext >/dev/null <<'EOF'
subjectAltName=DNS:app.lab.internal
extendedKeyUsage=serverAuth
keyUsage=critical,digitalSignature,keyEncipherment
EOF
timeout 30 openssl x509 -req -in /tmp/lab-region/certs/old/leaf-old.csr -CA /tmp/lab-region/certs/old/ca-old.crt -CAkey /tmp/lab-region/certs/old/ca-old.key -CAcreateserial -CAserial /tmp/lab-region/certs/old/ca-old.srl -days 7 -sha256 -extfile /tmp/lab-region/certs/old/leaf.ext -out /tmp/lab-region/certs/old/leaf-old.crt
timeout 30 openssl verify -CAfile /tmp/lab-region/certs/old/ca-old.crt /tmp/lab-region/certs/old/leaf-old.crt
timeout 10 install -m 0644 /tmp/lab-region/certs/old/leaf-old.crt /tmp/lab-region/certs/active-a/tls.crt
timeout 10 install -m 0600 /tmp/lab-region/certs/old/leaf-old.key /tmp/lab-region/certs/active-a/tls.key
timeout 10 install -m 0644 /tmp/lab-region/certs/old/leaf-old.crt /tmp/lab-region/certs/active-b/tls.crt
timeout 10 install -m 0600 /tmp/lab-region/certs/old/leaf-old.key /tmp/lab-region/certs/active-b/tls.key
timeout 10 openssl x509 -in /tmp/lab-region/certs/old/leaf-old.crt -noout -fingerprint -sha256 | tee /tmp/lab-region/backup/fingerprint-old.txt
写 CoreDNS 配置和两条内部记录。初始 TTL 为 30 秒,便于观察预降 TTL;改完 hosts 后重启 resolver,不依赖 reload 插件:
timeout 10 tee /tmp/lab-region/dns/Corefile >/dev/null <<'EOF'
.:53 {
hosts /etc/coredns/hosts {
ttl 30
fallthrough
}
forward . 127.0.0.11
log
errors
}
EOF
timeout 10 tee /tmp/lab-region/dns/hosts >/dev/null <<'EOF'
172.28.0.11 app.lab.internal
172.28.0.21 db.lab.internal
EOF
timeout 10 tee /tmp/lab-region/edge.conf >/dev/null <<'EOF'
server {
listen 80;
server_name app.lab.internal;
location / {
proxy_pass http://host.docker.internal:30080;
proxy_set_header Host $host;
proxy_connect_timeout 2s;
proxy_read_timeout 5s;
}
}
server {
listen 443 ssl;
server_name app.lab.internal;
ssl_certificate /etc/nginx/tls/tls.crt;
ssl_certificate_key /etc/nginx/tls/tls.key;
location / {
proxy_pass http://host.docker.internal:30080;
proxy_set_header Host $host;
proxy_connect_timeout 2s;
proxy_read_timeout 5s;
}
}
EOF
创建 PostgreSQL 认证与复制账号。trust 只用于隔离的 Docker 网络,生产必须使用强密码、TLS 和最小权限:
timeout 10 tee /tmp/lab-region/pg_hba.conf >/dev/null <<'EOF'
local all all trust
host all all 172.28.0.0/24 scram-sha-256
host replication replicator 172.28.0.0/24 trust
EOF
timeout 10 tee /tmp/lab-region/init-replication.sql >/dev/null <<'EOF'
CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD 'labpass';
EOF
Compose 中 resolver 提供内部域名,两台 nginx 是入口,两个探针模拟持续访问的应用,并直接使用 resolver:
timeout 10 tee /tmp/lab-region/compose.yaml >/dev/null <<'EOF'
name: lab-region
services:
resolver:
image: coredns/coredns:1.11.3
container_name: lab-dns
command: -conf /etc/coredns/Corefile
ports:
- "127.0.0.1:15353:53/udp"
- "127.0.0.1:15353:53/tcp"
volumes:
- ./dns:/etc/coredns:ro
networks:
lab:
ipv4_address: 172.28.0.10
edge-a:
image: nginx:stable
container_name: lab-edge-a
ports:
- "127.0.0.1:8080:80"
- "127.0.0.1:8443:443"
volumes:
- ./edge.conf:/etc/nginx/conf.d/default.conf:ro
- ./certs/active-a:/etc/nginx/tls:ro
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
lab:
ipv4_address: 172.28.0.11
edge-b:
image: nginx:stable
container_name: lab-edge-b
ports:
- "127.0.0.1:8081:80"
- "127.0.0.1:9443:443"
volumes:
- ./edge.conf:/etc/nginx/conf.d/default.conf:ro
- ./certs/active-b:/etc/nginx/tls:ro
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
lab:
ipv4_address: 172.28.0.12
probe:
image: curlimages/curl:8.10.1
container_name: lab-probe
dns:
- 172.28.0.10
entrypoint: ["/bin/sh", "-c"]
command:
- |
while :; do
timeout 5 curl -sS --max-time 4 -o /dev/null -w "http_code=%{http_code} remote_ip=%{remote_ip}\n" http://app.lab.internal/ || echo http_probe_failed
timeout 5 sleep 2
done
depends_on: [resolver, edge-a, edge-b]
networks:
lab:
ipv4_address: 172.28.0.13
db-probe:
image: postgres:16
container_name: lab-db-probe
environment:
PGPASSWORD: labpass
dns:
- 172.28.0.10
entrypoint: ["/bin/sh", "-c"]
command:
- |
until timeout 5 pg_isready -h db.lab.internal -U lab; do timeout 5 sleep 1; done
while :; do
timeout 5 psql -h db.lab.internal -U lab -d app -Atc "INSERT INTO probe_events DEFAULT VALUES RETURNING id;" || echo db_probe_failed
timeout 5 sleep 3
done
depends_on: [resolver, pg-primary, pg-replica]
networks:
lab:
ipv4_address: 172.28.0.14
pg-primary:
image: postgres:16
container_name: lab-pg-primary
environment:
POSTGRES_USER: lab
POSTGRES_PASSWORD: labpass
POSTGRES_DB: app
command: postgres -c wal_level=replica -c max_wal_senders=5 -c hot_standby=on -c hba_file=/etc/postgresql/pg_hba.conf
volumes:
- pgprimary:/var/lib/postgresql/data
- ./pg_hba.conf:/etc/postgresql/pg_hba.conf:ro
- ./init-replication.sql:/docker-entrypoint-initdb.d/10-replication.sql:ro
networks:
lab:
ipv4_address: 172.28.0.21
pg-replica:
image: postgres:16
container_name: lab-pg-replica
user: postgres # 覆盖 entrypoint 时避免以 root 运行 postgres
environment:
PGDATA: /var/lib/postgresql/replica
depends_on: [pg-primary]
command: >
bash -c "until timeout 5 pg_isready -h pg-primary -U lab; do timeout 5 sleep 1; done;
timeout 180 pg_basebackup -h pg-primary -U replicator -D /var/lib/postgresql/replica -Fp -Xs -R -P;
exec postgres -c hba_file=/etc/postgresql/pg_hba.conf -c hot_standby=on"
volumes:
- ./pg_hba.conf:/etc/postgresql/pg_hba.conf:ro
networks:
lab:
ipv4_address: 172.28.0.22
networks:
lab:
name: lab-region-net
ipam:
config:
- subnet: 172.28.0.0/24
volumes:
pgprimary:
EOF
先建 kind 和 web,再启动 Compose。入口通过 host.docker.internal:30080 访问 NodePort;Linux 上由 host-gateway 映射,不通时先核对地址:
timeout 10 tee /tmp/lab-region/kind.yaml >/dev/null <<'EOF'
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 30080
hostPort: 30080
protocol: TCP
- role: worker
EOF
timeout 300 kind create cluster --name lab-region --config /tmp/lab-region/kind.yaml
timeout 120 kubectl wait --for=condition=Ready nodes --all --timeout=90s
timeout 10 tee /tmp/lab-region/web.yaml >/dev/null <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 2
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: nginx:stable
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: web
spec:
type: NodePort
selector:
app: web
ports:
- port: 80
targetPort: 80
nodePort: 30080
EOF
timeout 120 kubectl apply -f /tmp/lab-region/web.yaml
timeout 150 kubectl rollout status deployment/web --timeout=120s
timeout 20 curl -sS -o /dev/null -w 'nodeport=%{http_code}\n' --max-time 5 http://127.0.0.1:30080/
timeout 300 docker compose -f /tmp/lab-region/compose.yaml up -d
timeout 180 bash -c 'until docker exec lab-pg-replica pg_isready -U lab -d app; do sleep 2; done'
创建业务表,等待复制追平并记录基线。probe 应持续返回 200,db-probe 应插入递增 ID:
timeout 30 docker exec lab-pg-primary psql -U lab -d app -c "CREATE TABLE probe_events (id bigserial PRIMARY KEY, note text NOT NULL DEFAULT 'probe', created_at timestamptz NOT NULL DEFAULT now());"
timeout 30 sleep 5
timeout 30 docker exec lab-pg-replica psql -U lab -d app -Atc "SELECT count(*) FROM probe_events;"
timeout 30 docker exec lab-pg-primary psql -U lab -d app -c "SELECT application_name, state, sync_state, replay_lag FROM pg_stat_replication;"
timeout 30 docker exec lab-pg-primary psql -U lab -d app -Atc "SELECT count(*), coalesce(max(id),0) FROM probe_events;" | tee /tmp/lab-region/backup/db-baseline.txt
timeout 20 dig @127.0.0.1 -p 15353 +noall +answer app.lab.internal db.lab.internal | tee /tmp/lab-region/backup/dns-baseline.txt
timeout 30 docker compose -f /tmp/lab-region/compose.yaml logs --tail=10 probe db-probe
timeout 20 curl --noproxy '*' --cacert /tmp/lab-region/certs/old/ca-old.crt --resolve app.lab.internal:8443:127.0.0.1 -sS -o /dev/null -w 'https_baseline=%{http_code}\n' --max-time 5 https://app.lab.internal:8443/
基线不通过就不要注入故障。成功基线应同时满足:DNS 返回 .11 和 .21;probe 最近 5 行都是 200;db-probe 有递增 ID;从库追平;旧证书验证成功。失败时先查日志和端口映射,回滚用 down -v 重建。
下面分五个阶段。每个阶段结束都把实际时间写进时间线,后面的 RTO 用这些时间戳计算,而不是凭印象估算。
把 resolver 的 TTL 从 30 秒降到 5 秒,等待 31 秒让旧缓存过期。真实环境要覆盖所有递归 DNS、客户端缓存和连接池,等待时间取旧 TTL 的上限;只改权威记录而不等缓存,切入口时仍会打到旧地址。
timeout 10 tee /tmp/lab-region/dns/Corefile >/dev/null <<'EOF'
.:53 {
hosts /etc/coredns/hosts {
ttl 5
fallthrough
}
forward . 127.0.0.11
log
errors
}
EOF
timeout 90 docker compose -f /tmp/lab-region/compose.yaml restart resolver
timeout 90 sleep 31
timeout 20 dig @127.0.0.1 -p 15353 +noall +answer app.lab.internal
timeout 5 sh -c 'date -u +"%FT%TZ stage=ttl_ok" >> /tmp/lab-region/timeline.log'
成功判据:Answer 中的 TTL 小于等于 5,probe 没有持续失败。失败判据:TTL 仍为 30、DNS 超过 10 秒不可用、probe 连续失败。回滚:恢复 ttl 30 的 Corefile 并重启 resolver,确认解析恢复后再继续;此时还没有切流量,回滚成本最低。
把 app.lab.internal 从 edge-a 改到 edge-b,等 resolver 生效后观察 probe 的 remote_ip。确认新入口稳定,再停掉 edge-a,模拟区域入口失联。不要先停 edge-a 再改 DNS,否则在 TTL 窗口内会出现一段本可避免的中断。
timeout 10 tee /tmp/lab-region/dns/hosts >/dev/null <<'EOF'
172.28.0.12 app.lab.internal
172.28.0.21 db.lab.internal
EOF
timeout 90 docker compose -f /tmp/lab-region/compose.yaml restart resolver
timeout 30 sleep 8
timeout 20 dig @127.0.0.1 -p 15353 +noall +answer app.lab.internal
timeout 20 curl -sS -o /dev/null -w 'edge_b=%{http_code}\n' --max-time 5 http://127.0.0.1:8081/
timeout 30 docker stop lab-edge-a
timeout 20 curl -sS -o /dev/null -w 'edge_a_after_stop=%{http_code}\n' --max-time 5 http://127.0.0.1:8080/ || echo edge_a_down
timeout 30 docker compose -f /tmp/lab-region/compose.yaml logs --tail=20 probe
timeout 5 sh -c 'date -u +"%FT%TZ stage=edge_switch_ok" >> /tmp/lab-region/timeline.log'
成功判据:dig 返回 172.28.0.12 且 TTL 小于等于 5;probe 的 remote_ip 切到 .12 并持续 200;edge-a 停止后 probe 没有持续失败。失败判据:DNS 仍返回 .11、edge-b 返回 5xx、probe 连续失败超过 30 秒、停掉 edge-a 后流量没有切走。回滚:把 hosts 改回 .11,启动 edge-a,确认 probe 恢复 200;edge-b 保留为备用入口。
按“记录基线、停旧主、确认从库只读、提升、切换 db.lab.internal、验证新写入”的顺序执行。db-probe 每次插入都会新建连接,因此 DNS 切换后它会自然重连;如果应用使用长连接或连接池,必须额外刷新连接池或滚动重启。
timeout 30 docker exec lab-pg-primary psql -U lab -d app -Atc "SELECT count(*), coalesce(max(id),0) FROM probe_events;" | tee /tmp/lab-region/backup/db-pre-failover.txt
timeout 30 docker exec lab-pg-primary psql -U lab -d app -Atc "SELECT pg_current_wal_lsn();" | tee /tmp/lab-region/backup/lsn-pre-failover.txt
timeout 60 docker stop lab-pg-primary
timeout 30 docker exec lab-pg-replica psql -U lab -d app -c "INSERT INTO probe_events DEFAULT VALUES;"; echo "replica_write_exit=$?"
timeout 60 docker exec -u postgres lab-pg-replica pg_ctl promote -D /var/lib/postgresql/replica
timeout 30 docker exec lab-pg-replica psql -U lab -d app -c "SELECT pg_is_in_recovery(), pg_current_wal_lsn();"
timeout 10 tee /tmp/lab-region/dns/hosts >/dev/null <<'EOF'
172.28.0.12 app.lab.internal
172.28.0.22 db.lab.internal
EOF
timeout 90 docker compose -f /tmp/lab-region/compose.yaml restart resolver
timeout 30 sleep 8
timeout 20 dig @127.0.0.1 -p 15353 +noall +answer db.lab.internal
timeout 30 docker compose -f /tmp/lab-region/compose.yaml logs --tail=20 db-probe
timeout 30 docker exec lab-pg-replica psql -U lab -d app -Atc "SELECT count(*), coalesce(max(id),0) FROM probe_events;"
timeout 5 sh -c 'date -u +"%FT%TZ stage=db_failover_ok" >> /tmp/lab-region/timeline.log'
成功判据:新主 pg_is_in_recovery() 为 false 且能提交写入;db.lab.internal 解析到 .22;db-probe 恢复插入;旧主保持停止;行数和最大 ID 能由“切换前基线 + 切换后写入 - 丢失行”解释。失败判据:提升失败、两个节点同时可写、db-probe 持续失败、行数或最大 ID 无法解释。回滚:若新主尚未接受写入,可重启旧主并把 DNS 改回 .21;若新主已经接受写入,只能保留新主,把旧主重建为从库,绝不能把旧主直接启动为可写主库。
只替换当前入口 edge-b 的叶子证书,但先把新旧根放进合并信任包。顺序必须是:生成新链、分发合并信任包、替换叶子、验证,观察期结束后再移除旧 CA。不要先删旧 CA,也不要把合并包当作签发证书使用。
timeout 30 openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out /tmp/lab-region/certs/new/ca-new.key
timeout 30 openssl req -x509 -new -key /tmp/lab-region/certs/new/ca-new.key -sha256 -days 30 -subj "/CN=Lab Region New Root CA" -out /tmp/lab-region/certs/new/ca-new.crt -addext "basicConstraints=critical,CA:TRUE" -addext "keyUsage=critical,keyCertSign,cRLSign"
timeout 30 openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out /tmp/lab-region/certs/new/leaf-new.key
timeout 30 openssl req -new -key /tmp/lab-region/certs/new/leaf-new.key -out /tmp/lab-region/certs/new/leaf-new.csr -subj "/CN=app.lab.internal"
timeout 10 tee /tmp/lab-region/certs/new/leaf.ext >/dev/null <<'EOF'
subjectAltName=DNS:app.lab.internal
extendedKeyUsage=serverAuth
keyUsage=critical,digitalSignature,keyEncipherment
EOF
timeout 30 openssl x509 -req -in /tmp/lab-region/certs/new/leaf-new.csr -CA /tmp/lab-region/certs/new/ca-new.crt -CAkey /tmp/lab-region/certs/new/ca-new.key -CAcreateserial -CAserial /tmp/lab-region/certs/new/ca-new.srl -days 7 -sha256 -extfile /tmp/lab-region/certs/new/leaf.ext -out /tmp/lab-region/certs/new/leaf-new.crt
timeout 30 openssl verify -CAfile /tmp/lab-region/certs/new/ca-new.crt /tmp/lab-region/certs/new/leaf-new.crt
timeout 10 sh -c 'cat /tmp/lab-region/certs/old/ca-old.crt /tmp/lab-region/certs/new/ca-new.crt > /tmp/lab-region/certs/trust/ca-bundle.crt'
timeout 30 openssl verify -CAfile /tmp/lab-region/certs/trust/ca-bundle.crt /tmp/lab-region/certs/old/leaf-old.crt
timeout 30 openssl verify -CAfile /tmp/lab-region/certs/trust/ca-bundle.crt /tmp/lab-region/certs/new/leaf-new.crt
timeout 10 install -m 0644 /tmp/lab-region/certs/new/leaf-new.crt /tmp/lab-region/certs/active-b/tls.crt
timeout 10 install -m 0600 /tmp/lab-region/certs/new/leaf-new.key /tmp/lab-region/certs/active-b/tls.key
timeout 30 docker compose -f /tmp/lab-region/compose.yaml exec -T edge-b nginx -t
timeout 30 docker compose -f /tmp/lab-region/compose.yaml exec -T edge-b nginx -s reload
timeout 10 sleep 3
timeout 20 openssl s_client -connect 127.0.0.1:9443 -servername app.lab.internal -verify_hostname app.lab.internal -verify_return_error -CAfile /tmp/lab-region/certs/trust/ca-bundle.crt </dev/null | grep -E 'Verify return code|issuer='
timeout 20 curl --noproxy '*' --cacert /tmp/lab-region/certs/trust/ca-bundle.crt --resolve app.lab.internal:9443:127.0.0.1 -sS -o /dev/null -w 'https_after_rotation=%{http_code}\n' --max-time 5 https://app.lab.internal:9443/
timeout 5 sh -c 'date -u +"%FT%TZ stage=cert_rotation_ok" >> /tmp/lab-region/timeline.log'
成功判据:openssl verify 对新旧叶子都成功;s_client 返回 Verify return code: 0 (ok),issuer 为新 CA,SAN 为 app.lab.internal;HTTPS 返回 200;probe 和 db-probe 没有持续失败。nginx -s reload 是优雅切换,旧 worker 可能仍在处理新连接;验证前等待 2–3 秒,否则可能读到上一版证书。失败判据:unknown authority、hostname mismatch、证书过期、入口 5xx、旧叶子在合并包下验证失败。回滚:把 active-b 的 tls.crt 和 tls.key 换回旧证书,nginx -t 后 reload,再用旧 CA 和合并包分别验证;不要删除新 CA,它可能已被其他入口或客户端使用。
最后重启两个探针,强制丢弃旧连接和旧解析结果,观察它们是否重新解析到 edge-b 和 pg-replica。重启不是生产首选,但在这个一次性实验里能清楚区分“DNS 已切换”和“应用仍握着旧连接”。
timeout 120 docker compose -f /tmp/lab-region/compose.yaml restart probe db-probe
timeout 30 sleep 10
timeout 30 docker compose -f /tmp/lab-region/compose.yaml logs --tail=20 probe db-probe
timeout 20 curl --noproxy '*' --cacert /tmp/lab-region/certs/trust/ca-bundle.crt --resolve app.lab.internal:9443:127.0.0.1 -sS -o /dev/null -w 'app_https=%{http_code}\n' --max-time 5 https://app.lab.internal:9443/
timeout 30 docker exec lab-pg-replica psql -U lab -d app -Atc "SELECT count(*), coalesce(max(id),0) FROM probe_events;"
timeout 5 sh -c 'date -u +"%FT%TZ stage=app_reconnect_ok" >> /tmp/lab-region/timeline.log'
成功判据:probe 的 remote_ip 为 .12 且持续 200;db-probe 插入成功;HTTPS 证书验证通过;新主行数持续增长。失败判据:仍解析到旧地址、连接池复用旧连接、DB 写入失败、证书不受信任。回滚:按证书、数据库、入口、DNS 的逆序逐层恢复;数据库只要有了新写入,就只做向前修复。
阶段 1 只看到 TTL 从 30 降到 5,probe 基本不受影响。阶段 2 的 dig 答案从 .11 变为 .12,probe 日志中的 remote_ip 在一个 TTL 周期内切换;停掉 edge-a 后,直连 8080 会 connection refused,但经 DNS 的 probe 仍返回 200。阶段 3 停主库后,从库写入报 cannot execute INSERT in a read-only transaction;提升后 pg_is_in_recovery() 变为 false,db-probe 从失败恢复为持续插入。阶段 4 的 s_client 签发者从旧 CA 变为新 CA,合并包下旧叶子仍然通过。阶段 5 重启探针后,DNS、入口、数据库和证书四条链路同时恢复。
建议把实际时间线整理成下表,表里的“目标耗时”只是演练预算,不是生产 SLO:
| 阶段 | 动作 | 关键观察 | 目标耗时 |
|---|---|---|---|
| 0 | 建立基线 | probe 200、复制 lag、旧证书通过 | — |
| 1 | TTL 30→5 | dig 的 TTL ≤ 5 | ≤ 1 分钟 |
| 2 | app→edge-b,停 edge-a | probe 的 remote_ip 变为 .12 | ≤ 2 分钟 |
| 3 | 停主库、提升、db→.22 | 新主可写、db-probe 恢复 | ≤ 10 分钟 |
| 4 | 替换 edge-b 叶子 | s_client 返回 0、issuer 为新 CA | ≤ 5 分钟 |
| 5 | 重启两个探针 | HTTP/HTTPS 和 DB 写入同时恢复 | ≤ 3 分钟 |
RPO 定义为旧主最后已确认的写入与新主可见数据之间的差距,用 max(id) 和缺失行数衡量;RTO 定义为从停旧主到 db-probe 恢复成功写入的时长。本实验不预设 RPO 为 0:db-probe 在停旧主前仍在写入,异步复制下最后几行可能未到达从库,必须按实际差值记录。若要把目标设为 0,应先暂停 db-probe、等待从库回放追平并记录 LSN,再停旧主。出现缺失必须按实际差值记录,不能用“复制正常”掩盖。入口切换的 RTO 单独计算:从停 edge-a 到 probe 恢复 200,目标不超过 2 分钟。
数据一致性至少检查:切换前后 count(*)、max(id)、重复 ID 和缺失 ID 区间;在新主写入一条唯一标记,等待重建从库读到;从库 pg_is_in_recovery() 与新主写入同时成立;DNS 答案、证书指纹、入口进程和探针日志互相印证。序列可能因失败插入产生空洞,空洞要结合时间线和日志解释,不能直接判定为丢数据。
dig 的 Answer、TTL 和 authority,再用 getent hosts 或应用日志确认实际解析。TTL 未过期时旧地址仍可能被缓存;所有名字都失败查 resolver 容器和 53 端口,只有某个名字失败则查 hosts 文件、Corefile 和重启是否成功。remote_ip 判断。pg_is_in_recovery()、pg_stat_replication、pg_last_wal_replay_lsn() 和 pg_stat_wal_receiver 区分主从、复制中断和回放延迟。提升后仍只读,先看提升命令是否成功、数据目录是否正确、是否还残留 recovery 配置。openssl verify 看链,用 s_client -showcerts 看服务端实际发送的链,用 x509 -dates -ext subjectAltName 看有效期和主机名。curl -k 成功不代表证书受信任,HTTP 200 也不代表客户端校验通过。回滚顺序与变更顺序相反:证书、数据库、入口、DNS。每一层先确认上一层的成功判据,再动下一层。任何回滚都要保留日志、时间线、证书指纹和数据库基线。
证书回滚只影响入口:把 active-b 的两个文件换回旧证书,执行 nginx -t 和 reload,然后用旧 CA 与合并包各验证一次。旧 CA 和旧叶子在观察期内不得删除。数据库回滚要区分是否已有新写入:没有新写入时,启动旧主、把 db.lab.internal 指回 .21、确认从库重新指向旧主;已有新写入时,旧主只能通过基础备份重建为从库:
timeout 30 docker rm -f lab-pg-old-rebuilt 2>/dev/null || true
timeout 240 docker run -d --name lab-pg-old-rebuilt --network lab-region-net --user postgres -e PGDATA=/var/lib/postgresql/replica -v /tmp/lab-region/pg_hba.conf:/etc/postgresql/pg_hba.conf:ro postgres:16 bash -c "until timeout 5 pg_isready -h lab-pg-replica -U lab; do timeout 5 sleep 1; done; timeout 180 pg_basebackup -h lab-pg-replica -U replicator -D /var/lib/postgresql/replica -Fp -Xs -R -P; exec postgres -c hba_file=/etc/postgresql/pg_hba.conf -c hot_standby=on"
timeout 180 bash -c 'until docker exec lab-pg-old-rebuilt pg_isready -U lab -d app; do sleep 2; done'
timeout 30 docker exec lab-pg-old-rebuilt psql -U lab -d app -Atc "SELECT pg_is_in_recovery();"
timeout 30 docker exec lab-pg-old-rebuilt psql -U lab -d app -Atc "SELECT status, sender_host, sender_port FROM pg_stat_wal_receiver;"
入口回滚是把 hosts 改回 .11 并启动 edge-a;如果 edge-a 仍不可用,保留 edge-b,不要为了“回滚”停掉唯一可用入口。DNS 回滚是恢复 TTL 30 和原记录,确认所有探针稳定后再进入观察期。全局回滚触发条件:RPO 超限、出现双写、证书连续 5 分钟不受信任、probe 连续失败超过 2 分钟、数据库提升失败。触发后停止后续阶段,由负责人决定继续向前修复还是回滚。
实验结束只清理本实验的资源,不要对任何其他 Compose 项目执行 down -v:
timeout 60 docker rm -f lab-pg-old-rebuilt 2>/dev/null || true
timeout 120 docker compose -f /tmp/lab-region/compose.yaml down -v --remove-orphans
timeout 180 kind delete cluster --name lab-region
timeout 30 rm -rf /tmp/lab-region
最终验证要覆盖 DNS、入口、数据库、证书和应用五个层面,所有命令都带超时:
timeout 20 dig @127.0.0.1 -p 15353 +noall +answer app.lab.internal db.lab.internal
timeout 30 docker ps --format '{{.Names}} {{.Status}}' | grep '^lab-'
timeout 20 curl --noproxy '*' --cacert /tmp/lab-region/certs/trust/ca-bundle.crt --resolve app.lab.internal:9443:127.0.0.1 -sS -o /dev/null -w 'https_verify=%{http_code}\n' --max-time 5 https://app.lab.internal:9443/
timeout 20 openssl s_client -connect 127.0.0.1:9443 -servername app.lab.internal -verify_hostname app.lab.internal -verify_return_error -CAfile /tmp/lab-region/certs/trust/ca-bundle.crt </dev/null | grep -E 'Verify return code|issuer='
timeout 30 docker exec lab-pg-replica psql -U lab -d app -Atc "SELECT pg_is_in_recovery(), count(*), coalesce(max(id),0), count(DISTINCT id) FROM probe_events;"
timeout 30 docker exec lab-pg-replica psql -U lab -d app -Atc "SELECT count(*) FROM generate_series(1, (SELECT max(id) FROM probe_events)) AS g(id) LEFT JOIN probe_events p ON p.id = g.id WHERE p.id IS NULL;"
timeout 30 docker compose -f /tmp/lab-region/compose.yaml logs --tail=20 probe db-probe
timeout 5 sh -c 'date -u +"%FT%TZ stage=final_verify_ok" >> /tmp/lab-region/timeline.log'
pg_stat_wal_receiver 显示 streaming;行数、最大 ID、重复 ID 和缺失区间能够解释。pg_rewind 重建为从库,属于向前修复。-k、缓存连接或没有校验主机名;还要看 openssl verify、s_client 的验证码、issuer、SAN 和有效期。
小程序版
微信扫码,随时查命令