<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Pg_dump · XB 现场笔记</title><link>https://www.xbcatm.com/zh/tags/pg_dump/</link><description>基于 HTTP、DNS、TLS、Caddy、SSH、发布与恢复的可复现运维现场笔记。</description><generator>Hugo</generator><language>zh-CN</language><atom:link href="https://www.xbcatm.com/zh/tags/pg_dump/index.xml" rel="self" type="application/rss+xml"/><item><title>pg_dump 拍一张一致的快照</title><link>https://www.xbcatm.com/zh/posts/postgres-pg_dump-consistent-backup/</link><pubDate>Fri, 14 Jan 2028 00:00:00 +0000</pubDate><guid>https://www.xbcatm.com/zh/posts/postgres-pg_dump-consistent-backup/</guid><description>pg_dump 在一个事务里取得稳定快照，因此导出的永远不会是中途的变更串。</description><content:encoded>&lt;p&gt;&lt;code&gt;pg_dump&lt;/code&gt; 使用事务数据的一致视图导出一个 PostgreSQL 数据库，同时允许其他会话继续提交变化。它避免了普通逐表复制的问题：前一张表和后一张表不会因为读取时间不同，就混入彼此不一致的已提交状态。但快照只是备份契约的一部分；归档还必须覆盖需要的数据范围，能被兼容工具读取，并且实际恢复成可使用的数据库。&lt;/p&gt;
&lt;h2 id="理解快照覆盖什么"&gt;理解快照覆盖什么&lt;/h2&gt;
&lt;p&gt;导出期间，快照可见的行构成稳定视图。后续提交不会因为某张表读得晚，就逐渐混进同一次导出。并行导出让各 worker 使用同步快照，而不是各自随时读取。并行 dump 要求 directory 格式；custom 格式支持并行恢复，但不支持并行导出。格式选择应根据导出与恢复两边的需求分别判断，不能把两个方向混为一谈。&lt;/p&gt;
&lt;p&gt;数据库快照不会自动让整个业务流程原子化。如果应用分两个事务提交批次头和明细，一致快照完全可能合法地落在这两次提交之间。序列状态也不是普通的 MVCC 行数据，外部文件和其他服务更不在数据库快照范围内。恢复需要满足什么业务不变量，必须结合真实事务边界和所需数据源定义，不能只凭“同一快照”推断所有业务状态都完整。&lt;/p&gt;
&lt;h2 id="计算负载和依赖"&gt;计算负载和依赖&lt;/h2&gt;
&lt;p&gt;普通读取和写入通常可以在导出期间继续，但 pg_dump 仍获取 ACCESS SHARE 锁，也消耗 CPU、I/O、连接和快照存活时间。冲突的 DDL 可能等待，也可能使导出失败；长快照会阻止某些旧行版本被清理。增加并行 worker 只是用更多并发资源换时间，不会让备份开销消失。应记录事务年龄和线上负载，避免把备份的影响误认为无关故障。&lt;/p&gt;
&lt;p&gt;使用备库可以分担读取，但要考虑恢复冲突与复制延迟，而且从备库导出会排除非日志表内容。一次 pg_dump 也不包含集群级角色和表空间。扩展、所有者、权限、排序规则行为及目标配置，都需要通过合适机制纳入恢复计划。把整个库拆成互不协调的单表 dump，可能丢失共同快照，也可能遗漏这些表依赖的对象，不能当成免费的优化。&lt;/p&gt;
&lt;h2 id="坏掉的样子"&gt;坏掉的样子&lt;/h2&gt;
&lt;p&gt;归档目录能够列出，不代表恢复一定成功。目标缺少角色或扩展时，表数据可能只恢复了一部分。应把归档加载到隔离空库，明确处理错误，再验证业务不变量和代表性查询。&lt;code&gt;pg_restore&lt;/code&gt; 默认遇到 SQL 错误仍会继续，最后汇总错误数量；只看到最后一条查询有输出是不够的。小型演练可使用单事务恢复获得原子性，但这个模式不能与并行 job 同用。&lt;/p&gt;
&lt;p&gt;工具版本是另一条边界。pg_dump 客户端会拒绝比自身主版本更新的服务器，输出主要面向相同或更新的目标版本，不能保证可以加载到更旧的主版本。应核对客户端、源库、目标库、扩展和恢复选项的实际组合，而不是只说“都是 PostgreSQL 所以可以恢复”。即使语法兼容，排序规则或权限差异也可能改变应用行为。&lt;/p&gt;
&lt;p&gt;逻辑导出可以满足经测量确认的恢复目标，但不能任意重放到两次导出之间的时刻。物理基础备份加完整保留的 WAL 链，才提供对应的时间点恢复路径。导出间隔是否可接受，取决于允许的数据丢失窗口和恢复时间，不存在“短于一天就一定不行”的统一阈值。必须按实际要求设计留存与恢复链条。&lt;/p&gt;
&lt;h2 id="快速验证"&gt;快速验证&lt;/h2&gt;
&lt;p&gt;把 Bash 代码保存为 &lt;code&gt;dump.sh&lt;/code&gt;。它自行创建实验 PostgreSQL 集群，并在退出时删除。可以用非 root 用户运行 PostgreSQL 18 工具，也可以使用下面的隔离容器；命令不发布网络端口，不挂载宿主机数据。两个数据库、归档和成功标记都只存在于实验临时目录及集群中。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker run --rm --network none --user postgres --entrypoint bash -i &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; postgres:18-alpine &amp;lt; dump.sh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/usr/bin/env bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;dump_lab&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;mktemp -d /tmp/pgdump-lab.XXXXXX&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cleanup&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; -f &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$dump_lab&lt;/span&gt;&lt;span class="s2"&gt;/data/postmaster.pid&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; pg_ctl -D &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$dump_lab&lt;/span&gt;&lt;span class="s2"&gt;/data&amp;#34;&lt;/span&gt; -m fast -w stop &amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; rm -rf -- &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$dump_lab&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;trap&lt;/span&gt; cleanup EXIT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;initdb -D &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$dump_lab&lt;/span&gt;&lt;span class="s2"&gt;/data&amp;#34;&lt;/span&gt; --no-locale --encoding&lt;span class="o"&gt;=&lt;/span&gt;UTF8 --auth&lt;span class="o"&gt;=&lt;/span&gt;trust &amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pg_ctl -D &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$dump_lab&lt;/span&gt;&lt;span class="s2"&gt;/data&amp;#34;&lt;/span&gt; -l &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$dump_lab&lt;/span&gt;&lt;span class="s2"&gt;/server.log&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -o &lt;span class="s2"&gt;&amp;#34;-k &lt;/span&gt;&lt;span class="nv"&gt;$dump_lab&lt;/span&gt;&lt;span class="s2"&gt; -p 55432 -c listen_addresses=&amp;#39;&amp;#39;&amp;#34;&lt;/span&gt; -w start &amp;gt;/dev/null
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;PGHOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$dump_lab&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;PGPORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;55432&lt;/span&gt; &lt;span class="nv"&gt;PGDATABASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;source_db
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;DUMP_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$dump_lab&lt;/span&gt;&lt;span class="s2"&gt;/source.dump&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;DUMP_OK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$dump_lab&lt;/span&gt;&lt;span class="s2"&gt;/dump-ok&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;createdb source_db
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;createdb restored_db
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;psql -X -v &lt;span class="nv"&gt;ON_ERROR_STOP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;&amp;lt;&amp;#39;SQL&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;CREATE TABLE customers (id integer PRIMARY KEY, name text);
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;CREATE TABLE orders (id integer PRIMARY KEY, customer_id integer REFERENCES customers(id));
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;INSERT INTO customers VALUES (1,&amp;#39;before&amp;#39;);
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;INSERT INTO orders VALUES (10,1);
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;SELECT pg_export_snapshot() AS snapshot_id \gset
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;\setenv DUMP_SNAPSHOT :snapshot_id
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;\! psql -X -v ON_ERROR_STOP=1 -c &amp;#34;BEGIN; INSERT INTO customers VALUES (2,&amp;#39;after&amp;#39;); INSERT INTO orders VALUES (20,2); COMMIT;&amp;#34; &amp;amp;&amp;amp; pg_dump --format=custom --snapshot=&amp;#34;$DUMP_SNAPSHOT&amp;#34; --file=&amp;#34;$DUMP_FILE&amp;#34; &amp;amp;&amp;amp; touch &amp;#34;$DUMP_OK&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;COMMIT;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;SQL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -f &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$DUMP_OK&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pg_restore --list &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$DUMP_FILE&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &amp;gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$dump_lab&lt;/span&gt;&lt;span class="s2"&gt;/archive-list&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pg_restore --exit-on-error --single-transaction --dbname&lt;span class="o"&gt;=&lt;/span&gt;restored_db &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$DUMP_FILE&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;counts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;SELECT (SELECT count(*) FROM customers), (SELECT count(*) FROM orders)&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;psql -X -Atc &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$counts&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;2|2&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;psql -X -d restored_db -Atc &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$counts&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;1|1&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;psql -X -d restored_db -Atc &lt;span class="s1"&gt;&amp;#39;SELECT count(*) FROM orders o LEFT JOIN customers c ON c.id=o.customer_id WHERE c.id IS NULL&amp;#39;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;psql -X -d restored_db -v &lt;span class="nv"&gt;ON_ERROR_STOP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; -c &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;SELECT c.name,o.id FROM customers c JOIN orders o ON o.customer_id=c.id&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pg_dump --version
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;PASS: source=2|2 restored=1|1 orphan_orders=0\n&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;第一个会话导出快照时，库中只有一位客户和一笔订单。在该事务保持打开时，第二个会话提交另一组有关联的数据。pg_dump 明确导入较早快照，所以恢复后计数是 &lt;code&gt;1|1&lt;/code&gt;，源库当前计数则为 &lt;code&gt;2|2&lt;/code&gt;。关联查询返回名为 before 的客户和十号订单，悬空订单检查结果为零。这样验证的是确定的并发时序，不依赖导出恰好跑得够慢。&lt;/p&gt;
&lt;p&gt;PostgreSQL 18.6 上完整恢复与断言均通过。嵌套 shell 只有在写入和 dump 都成功后才创建标记，防止 psql 在外部命令失败后继续运行而掩盖失败。这个实验验证夹具中的快照选择和恢复，不代表已经测出真实应用的恢复时间。生产备份仍需保留实际恢复演练、归档校验值、留存策略与依赖记录，才能让一次成功导出成为可审阅的恢复能力。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/18/app-pgdump.html"&gt;PostgreSQL pg_dump&lt;/a&gt;，快照、格式、范围与版本兼容，已核验 2026-09-10。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/18/app-pgrestore.html"&gt;PostgreSQL pg_restore&lt;/a&gt;，恢复错误处理和事务选项，已核验 2026-09-10。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>