瞬间抓取文件系统状态、既能快速恢复又近乎零成本的机制

ZFS 快照给文件系统一个回退键

ZFS 快照就是给某个数据集在某个 transaction group 上的块树起的一个名字。它不复制任何东西、不冻结任何东西,代价只是几个元数据块:zfs snapshot tank/data@before-upgrade 会在 shell 提示符重画完之前就返回。由于 ZFS 从不原地覆盖仍在使用的块,旧块树在磁盘上一直有效,直到最后一个引用消失——而快照就是这个引用。 正是这一点让“一墙的每日历史”变得可行,也正是本文标题最危险的地方。快照给你数据的许多个版本,却不给你“池不会丢”这件事的保障。tank/data 的每一份快照都住在 tank 里,所以带走池的那次故障,会在同一瞬间带走数据集和它的全部历史。多版本与多版本安全是两种不同的性质,后者只能由复制来提供。 为一个一致的树命名 快照建立在 transaction group 边界上,所以它捕获的树内部自洽——数据库的文件与元数据冻结在同一瞬间,而不是相差几微秒。-r 会连同所有后代数据集一起打快照,凡是有子数据集的地方都该这么用;否则一个递归布局会在不同层级留下时间不同的快照,层与层之间的增量复制就变得别扭。 名字由你决定,而它是保留策略唯一的抓手:tank/data@daily-2026-09-16 可以按模式清理,自动生成的名字则需要额外记账。zfs list -t snapshot -o name,used,refer,creation -s creation 列出整个快照集以及每一份钉住的空间。 空间在哪里:zfs list -o space ZFS 用一组列来回答唯一重要的问题:这是谁的数据?zfs list -o space tank/data 会给出 USED、USEDSNAP、USEDDS、USEDREFRESERV、USEDCHILD、REFER 与 AVAIL。REFER 是数据集自身需要的量,USEDSNAP 是快照钉住的量,USED 是二者之和。USEDSNAP 持续上涨而 REFER 保持不变,说明吃掉池子的是快照,不是数据。具体到某一份快照,used 是它单独钉住的块,refer 是它能引用到的全部块,两者之差正是与其他版本共用的那部分。 ...

三月 21, 2028 · 1 分钟 · XB Field Notes
一个备份架,用一次恢复演练证明副本能回来。

没验证的备份只是空想

备份任务完成,证明备份过程生成了一份材料;恢复演练则要证明这份材料、现有工具以及必要环境,能共同恢复出可用系统。这是不同的结论。恢复证据应明确使用了哪份备份、恢复到什么状态、补齐了哪些依赖,以及花了多久才让结果对应用真正有用。不能只保留一个任务成功时间,就省略后面的验证。 坏掉的样子 合法的数据库归档仍然可能依赖范围之外的对象。PostgreSQL 的单库导出工具只导出一个数据库,角色却属于整个集群。在独立集群里恢复带授权信息的备份时,可能因为被引用的角色不存在而失败。如果演练始终在原集群进行,这种缺少恢复材料的问题就会被环境中已有的角色掩盖。 恢复错误也需要明确处理。恢复工具默认可以在 SQL 出错后继续,并在末尾报告错误数量。部分表能够查询,不代表半恢复状态就可以接受。这个小型夹具选择把恢复放进一个事务,使成功具有原子性;发生预期失败时,也不会留下已经恢复了一半的表,便于确认依赖修正之后重新执行的结果。 恢复位置正确是另一项要求。完全恢复成功的备份,也可能按设计不包含它之后发生的变更。校验和证明材料与记录一致,不证明其中包含最新业务事件。同样,总行数相同,并不能排除金额、关联关系或权限错误。验证项需要表达应用语义,不能因为容易查询就只检查几个不能区分正确与错误状态的数字。 快速验证 在 Linux 环境中使用非超级操作系统账户运行脚本,准备 PostgreSQL 18 服务端、客户端和摘要校验工具。本次实际执行版本是 PostgreSQL 18.6。脚本建立互相独立的临时源集群与目标集群,使用私有 Unix 套接字,不打开网络监听。实验角色没有登录能力,通过受控角色切换验证授权,不涉及真实账户凭据。 #!/bin/sh set -eu umask 077 lab=$(mktemp -d /tmp/xbcatm-restore.XXXXXX) mkdir "$lab/socket" source=$lab/source target=$lab/target cleanup() { for cluster in "$target" "$source"; do if test -f "$cluster/postmaster.pid"; then pg_ctl -D "$cluster" -m fast -w stop >/dev/null fi done } trap cleanup EXIT for cluster in "$source" "$target"; do initdb -D "$cluster" --no-locale --encoding=UTF8 --auth-local=trust --auth-host=reject >/dev/null cat >> "$cluster/postgresql.conf" <<CONF listen_addresses = '' unix_socket_directories = '$lab/socket' CONF done printf 'port = 56432\n' >> "$source/postgresql.conf" printf 'port = 56433\n' >> "$target/postgresql.conf" pg_ctl -D "$source" -l "$lab/source.log" -w -t 15 start >/dev/null pg_ctl -D "$target" -l "$lab/target.log" -w -t 15 start >/dev/null export PGHOST=$lab/socket PGPORT=56432 PGDATABASE=postgres PGUSER=$(id -un) export PGCONNECT_TIMEOUT=2 psql -X -v ON_ERROR_STOP=1 -qAt <<'SQL' CREATE ROLE backup_reader NOLOGIN; CREATE TABLE customers (id integer PRIMARY KEY,name text NOT NULL); CREATE TABLE orders (id integer PRIMARY KEY,customer_id integer REFERENCES customers,amount_cents integer CHECK (amount_cents>0)); INSERT INTO customers VALUES (1,'Ada'); INSERT INTO orders VALUES (10,1,4210),(11,1,1865); CREATE VIEW order_totals AS SELECT c.name,sum(o.amount_cents) AS total_cents FROM customers c JOIN orders o ON o.customer_id=c.id GROUP BY c.id,c.name; GRANT SELECT ON customers,orders,order_totals TO backup_reader; SQL pg_dump -Fc -w -f "$lab/snapshot.dump" postgres sha256sum "$lab/snapshot.dump" > "$lab/snapshot.sha256" psql -X -v ON_ERROR_STOP=1 -qAt -c 'INSERT INTO orders VALUES (12,1,1000)' printf 'source_after_backup_orders ' psql -X -v ON_ERROR_STOP=1 -qAt -c 'SELECT count(*) FROM orders' sha256sum -c "$lab/snapshot.sha256" if PGPORT=56433 pg_restore --single-transaction --exit-on-error -w -d postgres "$lab/snapshot.dump" >"$lab/missing-role.log" 2>&1; then echo 'restore unexpectedly ignored a missing role' >&2 exit 1 fi grep -F 'role "backup_reader" does not exist' "$lab/missing-role.log" test "$(PGPORT=56433 psql -X -v ON_ERROR_STOP=1 -qAt -c "SELECT to_regclass('public.orders') IS NULL")" = t echo failed_restore_rolled_back PGPORT=56433 psql -X -v ON_ERROR_STOP=1 -qAt -c 'CREATE ROLE backup_reader NOLOGIN' PGPORT=56433 pg_restore --single-transaction --exit-on-error -w -d postgres "$lab/snapshot.dump" PGPORT=56433 psql -X -v ON_ERROR_STOP=1 -qAt <<'SQL' SELECT count(*) FROM orders; SELECT name,total_cents FROM order_totals; SELECT has_table_privilege('backup_reader','orders','SELECT'),has_table_privilege('backup_reader','orders','INSERT'); DO $$ BEGIN IF (SELECT count(*) FROM orders)<>2 OR (SELECT string_agg(id||':'||amount_cents,',' ORDER BY id) FROM orders) IS DISTINCT FROM '10:4210,11:1865' OR (SELECT total_cents FROM order_totals WHERE name='Ada') IS DISTINCT FROM 6075 OR (SELECT count(*) FROM orders o LEFT JOIN customers c ON c.id=o.customer_id WHERE c.id IS NULL)<>0 THEN RAISE EXCEPTION 'restored business data mismatch'; END IF; END $$; DO $$ BEGIN BEGIN INSERT INTO orders VALUES (98,999,100); RAISE EXCEPTION 'restored foreign key unexpectedly accepted an orphan'; EXCEPTION WHEN foreign_key_violation THEN RAISE NOTICE 'restored foreign key rejected orphan'; END; END $$; SET ROLE backup_reader; SELECT id,amount_cents FROM orders ORDER BY id; DO $$ BEGIN BEGIN INSERT INTO orders VALUES (99,1,100); RAISE EXCEPTION 'read-only role unexpectedly wrote data'; EXCEPTION WHEN insufficient_privilege THEN RAISE NOTICE 'read-only role rejected write'; END; END $$; SQL printf 'lab_directory %s\n' "$lab" 源库包含一个客户、两笔订单、汇总视图、外键以及只读角色授权。自定义格式归档捕获这个状态,之后源库又增加第三笔订单。输出中源库订单数为三,证明选定备份点之外确实存在后续活动,避免测试把当前源库和已经保存的快照混为一谈。恢复目标是否应该包含第三笔,需要由选择的备份点决定。 ...

六月 2, 2027 · 3 分钟 · XB Field Notes