数据库页面共用操作系统已经维护的那层缓存。

PostgreSQL 使用两层数据缓存

普通缓冲式关系文件读写中,PostgreSQL 的共享缓冲位于操作系统文件缓存之上。命中共享缓冲时,不需要再次通过下面的文件读取路径;没有命中时,也可能由操作系统直接从内存提供数据。同一文件内容可以在两层各有副本,所以这里并不承诺数据库与内核共用同一份物理内存,更不能把两层缓存理解为完全没有重复占用。 坏掉的样子 执行计划里的共享读取计数,表示块被读入 PostgreSQL 缓冲池,不证明存储设备实际执行了一次物理读取。如果把每次共享读取都算成磁盘未命中,已经温热的操作系统缓存也会被误认为冰冷,从而引出错误的内存配置判断。同样,第二次查询更快,也不能独自说明究竟是哪一层缓存发生了变化,或是否只是其他执行条件改变。 缓存驻留是临时状态。其他查询可能挤走页面,工作集超过容量时,也不能指望所有页永久留在内存里;关系扫描采用的缓冲策略还可能影响复用方式。因此应使用已知大小的工作集、检查实际访问路径,并把耗时结论与真正观察到的缓存层事实分开。没有设备层测量,就不能仅凭一个计划字段宣布磁盘速度发生了变化。 快速验证 使用 PostgreSQL 18 可丢弃实例和超级用户,确保附带的缓冲观察与预热扩展可安装、且尚未安装。本次实例使用一百二十八兆字节共享缓冲,下面的数据可以轻松放下。通过一个全新的 psql -X -v ON_ERROR_STOP=1 会话按顺序执行。关系驱逐函数用于开发测试,这里只针对隔离实验创建的对象操作: CREATE SCHEMA cache_layer_demo; CREATE EXTENSION pg_buffercache WITH SCHEMA cache_layer_demo; CREATE EXTENSION pg_prewarm WITH SCHEMA cache_layer_demo; CREATE TABLE cache_layer_demo.items (id integer PRIMARY KEY,payload text) WITH (autovacuum_enabled=false); INSERT INTO cache_layer_demo.items SELECT g,repeat(md5(g::text),10) FROM generate_series(1,12000) AS g; VACUUM (ANALYZE) cache_layer_demo.items; CREATE VIEW cache_layer_demo.residency AS SELECT count(*) AS heap_buffers FROM cache_layer_demo.pg_buffercache WHERE reldatabase=(SELECT oid FROM pg_database WHERE datname=current_database()) AND relfilenode=pg_relation_filenode('cache_layer_demo.items') AND relforknumber=0; SET max_parallel_workers_per_gather=0; SELECT pg_relation_size('cache_layer_demo.items')/current_setting('block_size')::int AS heap_blocks; SELECT heap_buffers>0 AS initially_buffered FROM cache_layer_demo.residency; 本次构建的主堆有五百四十六个块,初始缓存比较返回真。观察视图同时限制数据库、当前文件标识与主分支,避免把其他关系或辅助分支的缓冲算进来。接着,仅驱逐这个关系的共享缓冲: ...

一月 27, 2028 · 1 分钟 · XB Field Notes
只服务精确等值键查找的索引。

哈希索引复核碰撞,也要维护变化的键

PostgreSQL 哈希索引先利用哈希码缩小等值查询的候选范围,再回到表中核对原始值。哈希匹配只说明“可能相等”,不能直接证明相等。两个不同值可能具有相同哈希码,不同哈希码也可能映射到同一个桶。正常复核存在时,碰撞和共享桶都不意味着查询会返回错误结果;它们是实现需要处理的普通情况,而不是可以忽略的例外。 桶里究竟保存什么 索引条目包含四字节哈希值和指向表元组的引用,不保存完整的被索引列值。桶映射把查询导向相关索引页面。桶是共享存储空间,并不是给每个不同键单独分配一个专属位置。一个桶的主页装满后,可以继续连接溢出页,查询这个桶时也需要访问这些页面。因此,“一次算出桶”不等于“永远只读取一个页面”。 PostgreSQL 可以在单个索引页内部按哈希码排序,以便加速局部搜索,但这不会提供原始列值的顺序。哈希索引不能满足键范围或有序输出,只支持单列等值访问,不能强制唯一性,也不支持 IS NULL。B-tree 仍然可以服务等值查询,同时支持更广的一组操作,不能因为键看起来适合哈希就自动排除其他候选。 由于条目中没有原始值,哈希扫描是有损候选扫描,需要用表中实际值复核。因此,碰撞可以在执行计划中表现为 Rows Removed by Index Recheck。这说明数据库正确剔除了只在哈希上匹配的候选,而不是把两个碰撞值都当成相等返回。解读这项指标时,应把候选数量与最终答案区分开。 坏掉的样子 把索引键更新描述为“只碰一个桶”,掩盖了实际工作。修改被索引列会改变元组对应的索引值,新条目可能进入另一个桶。PostgreSQL 使用行版本与延后索引清理,旧引用不一定立刻从结构中消失。数据增长还可能触发桶分裂,溢出页也会增加读写。因此不能用一个理想化的哈希表赋值过程来解释数据库的全部更新成本。 高度重复的值、快速增长的表,与近乎唯一而稳定的等值负载,表现可能很不一样。哈希查询不是无条件的恒定延迟承诺。现代 PostgreSQL 的哈希索引支持崩溃恢复,但持久性既不会消除维护成本,也不会让它突然具备唯一性约束能力。读性能、写维护和数据契约必须分别确认。 快速验证 在可丢弃的 PostgreSQL 18 数据库中,用 psql 执行下面代码。它建立二十万个采用 C 排序规则的临时文本键,使用当前服务器自己的 hashtext 找到一组碰撞,再验证等值查询和键更新。实验没有把哈希码当成跨版本稳定的业务标识,最后回滚会移除所有夹具。 \set ON_ERROR_STOP on BEGIN; CREATE TEMP TABLE hash_items (key text COLLATE "C"); INSERT INTO hash_items SELECT 'key-'||g FROM generate_series(1,200000) AS g; CREATE TEMP TABLE collision_pair AS SELECT min(key) AS a,max(key) AS b FROM hash_items GROUP BY hashtext(key) HAVING count(*)>1 ORDER BY hashtext(key) LIMIT 1; SELECT a AS collision_a,b AS collision_b FROM collision_pair \gset SELECT a,b,hashtext(a),hashtext(b) FROM collision_pair; CREATE INDEX hash_items_key ON hash_items USING hash(key); ANALYZE hash_items; EXPLAIN (ANALYZE, BUFFERS, TIMING OFF) SELECT key FROM hash_items WHERE key=:'collision_a'; UPDATE hash_items SET key='changed-'||key WHERE key=:'collision_a'; SELECT count(*) AS old_key_rows FROM hash_items WHERE key=:'collision_a'; SELECT count(*) AS new_key_rows FROM hash_items WHERE key='changed-'||:'collision_a'; DO $$ BEGIN IF (SELECT hashtext(a)=hashtext(b) AND a<>b FROM collision_pair) IS DISTINCT FROM true THEN RAISE EXCEPTION 'fixture did not establish a collision'; END IF; IF (SELECT count(*) FROM hash_items h JOIN collision_pair p ON h.key=p.a) <> 0 OR (SELECT count(*) FROM hash_items h JOIN collision_pair p ON h.key='changed-'||p.a) <> 1 OR (SELECT count(*) FROM hash_items h JOIN collision_pair p ON h.key=p.b) <> 1 THEN RAISE EXCEPTION 'hash lookup or indexed update returned wrong keys'; END IF; END $$; ROLLBACK; PostgreSQL 18.6 中找到的两个键是 key-129539 与 key-7295,哈希码都是 -1960436116。等值计划使用 hash_items_key,最终只返回一行,同时报告一行被索引复核移除。两段字符串虽然哈希相同,却仍然是不同的值。这是反对“哈希码没有碰撞”这一错误假设的实际负向验证,不能用普通的无碰撞点查替代。 ...

一月 24, 2028 · 2 分钟 · XB Field Notes
为外层每一行单独运行的那样表达式。

LATERAL 让每一行都有自己的子查询窗口

LATERAL 允许 FROM 中的一个项目引用它前面项目提供的列。每个父记录需要一个带自身参数的小结果集时,它很实用,例如每位客户最近的两笔订单。整个查询仍是一条 SQL、一次数据库往返,横向子查询明确表达哪些外层值决定当前结果。但这种逻辑关系不承诺唯一的物理执行策略,规划器仍可进行保持语义的优化。 把关联条件写清楚 普通 FROM 子查询不能随意引用旁边表的列,LATERAL 允许在合适的连接位置使用这些引用。o.customer_id = c.id 才是当前客户与订单之间的关联;只写关键词不会自动产生这个条件。如果删除关联,每位客户可能都拿到同一份全局结果。语法通过只能证明语句合法,不能证明取到的是这个客户自己的订单。 实现每组前几名时,把父记录过滤条件、ORDER BY 和 LIMIT 都放进横向子查询。放在整个连接外面的 LIMIT 限制的是最终组合结果,不是每位客户最多几笔订单。排序还需要唯一的决胜字段:两笔订单完全可能具有相同时间戳,只按时间排序时,边界位置选择哪一行没有保证,换一个计划就可能换一个答案。 子查询里的 ORDER BY 决定入选哪些行,并不保证最终结果的展示顺序。客户端要求稳定的客户与订单顺序时,还需要外层 ORDER BY。导出报表和自动化检查尤其依赖这一点;不能因为当前执行计划碰巧按某个顺序返回,就把这种偶然表现当成接口契约,否则升级或统计信息刷新后会出现难以解释的行顺序变化。 决定没有匹配时保留什么 CROSS JOIN LATERAL 在子查询没有返回行时会丢掉对应父记录。LEFT JOIN LATERAL 配合 ON true 则保留父记录,并把右侧列补成 null。对尚未下单的客户,后者往往才符合页面或报表需求。如果把右侧条件移到外层 WHERE,补出来的 null 行可能被过滤掉,原本想保留空客户的行为就被破坏了。 聚合还有一层区别。COUNT(r.id) 只统计非空订单标识,COUNT(*) 连左连接补出的那一行也会计算。没有 GROUP BY 的相关聚合,即使输入为空也可能返回一行,COUNT 是零,SUM 则是 null。因此不能把所有 LATERAL 表达式都理解成同一种列表查询,必须看它实际返回零行、一行还是多行,以及每一列的空值含义。 ...

一月 19, 2028 · 2 分钟 · XB Field Notes
从三次单独的一致快照里导出的逻辑转储。

pg_dump 拍一张一致的快照

pg_dump 使用事务数据的一致视图导出一个 PostgreSQL 数据库,同时允许其他会话继续提交变化。它避免了普通逐表复制的问题:前一张表和后一张表不会因为读取时间不同,就混入彼此不一致的已提交状态。但快照只是备份契约的一部分;归档还必须覆盖需要的数据范围,能被兼容工具读取,并且实际恢复成可使用的数据库。 理解快照覆盖什么 导出期间,快照可见的行构成稳定视图。后续提交不会因为某张表读得晚,就逐渐混进同一次导出。并行导出让各 worker 使用同步快照,而不是各自随时读取。并行 dump 要求 directory 格式;custom 格式支持并行恢复,但不支持并行导出。格式选择应根据导出与恢复两边的需求分别判断,不能把两个方向混为一谈。 数据库快照不会自动让整个业务流程原子化。如果应用分两个事务提交批次头和明细,一致快照完全可能合法地落在这两次提交之间。序列状态也不是普通的 MVCC 行数据,外部文件和其他服务更不在数据库快照范围内。恢复需要满足什么业务不变量,必须结合真实事务边界和所需数据源定义,不能只凭“同一快照”推断所有业务状态都完整。 计算负载和依赖 普通读取和写入通常可以在导出期间继续,但 pg_dump 仍获取 ACCESS SHARE 锁,也消耗 CPU、I/O、连接和快照存活时间。冲突的 DDL 可能等待,也可能使导出失败;长快照会阻止某些旧行版本被清理。增加并行 worker 只是用更多并发资源换时间,不会让备份开销消失。应记录事务年龄和线上负载,避免把备份的影响误认为无关故障。 使用备库可以分担读取,但要考虑恢复冲突与复制延迟,而且从备库导出会排除非日志表内容。一次 pg_dump 也不包含集群级角色和表空间。扩展、所有者、权限、排序规则行为及目标配置,都需要通过合适机制纳入恢复计划。把整个库拆成互不协调的单表 dump,可能丢失共同快照,也可能遗漏这些表依赖的对象,不能当成免费的优化。 坏掉的样子 归档目录能够列出,不代表恢复一定成功。目标缺少角色或扩展时,表数据可能只恢复了一部分。应把归档加载到隔离空库,明确处理错误,再验证业务不变量和代表性查询。pg_restore 默认遇到 SQL 错误仍会继续,最后汇总错误数量;只看到最后一条查询有输出是不够的。小型演练可使用单事务恢复获得原子性,但这个模式不能与并行 job 同用。 工具版本是另一条边界。pg_dump 客户端会拒绝比自身主版本更新的服务器,输出主要面向相同或更新的目标版本,不能保证可以加载到更旧的主版本。应核对客户端、源库、目标库、扩展和恢复选项的实际组合,而不是只说“都是 PostgreSQL 所以可以恢复”。即使语法兼容,排序规则或权限差异也可能改变应用行为。 ...

一月 14, 2028 · 2 分钟 · XB Field Notes
两个冻结时点不同的时间函数。

NOW 与 clock_timestamp 的差别在冻结时机

PostgreSQL 提供多种“当前时间”。事务时间标记事务开始,语句时间标记客户端命令到达,墙钟读数则在表达式求值时读取当前时钟。选择不同函数,会改变审计字段、过期检查和长任务中间观测的含义。应先明确要记录哪个事件,再决定函数,不能因为名称都有时间含义就随意互换。 一个事务只有一个开始时刻 now()、transaction_timestamp() 与不带精度参数的 CURRENT_TIMESTAMP,返回同一个事务开始瞬间。在显式事务里,它们始终固定,即使后续语句已经晚了几秒或几分钟。CURRENT_TIMESTAMP 可以指定精度来舍入结果;小数位数的处理与采样时刻是两回事,不能因为显示精度不同就推断时钟来源改变。 这种稳定性适合让相关行共享同一个逻辑事务时间。它不记录每行真正插入的时刻,也不是最终提交时间。事务可能先等待锁或执行大量计算,随后才插入数据,最后还有可能回滚。把列命名为 created_at,并不能自动说明默认值到底代表哪个事件,需要在数据契约里写清楚。 普通自动提交模式下,连续语句通常各自处于不同事务,因此 now 看起来会随每条语句前进。但这种观察没有改变函数契约。后来为了原子性加上显式事务时,原本被误认为实时变化的时间,就可能暴露出早已固定的问题;验证时应覆盖真实事务边界,而不只在交互终端连续敲两条查询。 语句标记与实时读数 statement_timestamp() 返回服务器收到最近一条客户端命令消息的时间,在该命令执行期间保持稳定。事务中后续命令的语句时间可能与事务时间不同。同一协议消息里发送的多条 SQL 命令,也可能共享这个标记,所以它不一定代表等待结束后,每个内部操作真正开始的那一刻。 clock_timestamp() 在求值时读取当前墙钟,即使同一条语句内部也可能变化。由于时钟精度有限,两次调用并不保证得到不同值。它也不是单调计时器,时钟校正可能影响前后读数的差值。需要保证耗时测量不受墙钟调整影响时,应使用合适的单调时钟,而不是把数据库墙钟当成无条件递增的计数器。 这些值单独使用,都不能证明并发事务的提交顺序,也不能提供唯一事件身份。相关字段需要一致逻辑时间时选事务标记,观测命令到达时选语句标记,需要读取当前墙钟时才选实时观测。身份与顺序要求应另外明确,不能让时间戳自动承担它未承诺的语义。 坏掉的样子 长事务中的过期检查如果使用 now,就会持续与事务开始时刻比较。一个在事务开始时成立的条件,即使真实墙钟已经越过截止时间,也可能继续成立。反过来,把全部默认值换成 clock_timestamp,又可能让本该共享逻辑创建时间的相关行各不相同。修改函数前,应先确定列代表的是业务批次、命令到达还是实际观测。 列默认值中的特殊字面量 TIMESTAMPTZ 'now' 也是一个陷阱。PostgreSQL 在解析常量时就把它转换成时间,所以后续插入可能一直拿到建表时刻。DEFAULT now() 是函数调用,会在真正使用默认值时求值。两种写法看起来相似,并不代表求值生命周期相同;只检查默认表达式包含 now 这个词,会漏掉这个差别。 这些函数返回 timestamptz。切换会话时区会改变瞬间的显示形式,而不是改变已存储的瞬间本身。timestamptz 不保留最初传入的时区名称;应用需要记住原始时区时,应单独建模,不能从后来显示的偏移量反推原始地区。相同偏移量也不足以唯一确定某个时区名称。 快速验证 把下面内容保存为 timestamps.sql,在可丢弃的 PostgreSQL 18 数据库中执行 psql -X -v ON_ERROR_STOP=1 -f timestamps.sql。显式事务之外,应保持 psql 默认的自动提交行为;第二段需要让两次插入分别处在不同事务里。所有表都是临时表。 ...

一月 13, 2028 · 2 分钟 · XB Field Notes
数据库里脏数据与磁盘干净对接的时点。

checkpoint 把脏缓冲刷到磁盘

检查点为 PostgreSQL 崩溃恢复建立一个可用起点。它同步这个检查点所需的数据文件变更,并记录相应恢复信息。依赖某段日志的数据页写出之前,相关 WAL 必须先持久化。这样的顺序让事务提交不必每次都把全部修改过的表页立即刷到磁盘;发生崩溃时,再从恢复起点重放后续必要变更。 坏掉的样子 检查点不是事务提交。数据页可以包含尚未提交事务写入的版本,读者究竟能看到哪些行,仍然由可见性与事务状态决定。检查点完成,不会把这些写入变成已提交,也不会把随手复制一份正在变化的数据目录变成有效基础备份。并发活动还可能再次弄脏页面,所以不能把完成提示理解成缓冲池从此全部干净。 日志保留也有别的限制。最大 WAL 大小是与自动检查点调度有关的软目标,不是磁盘使用硬上限;归档失败或复制槽保留,都可能让日志超过这个值。最小 WAL 大小则涉及回收并预留文件供以后复用,不是另一条强制触发检查点的阈值。如果理解错这些参数,可能一边看到检查点间隔正常,一边忽略实际上还在持续增长的归档积压。 快速验证 准备一台安静的 PostgreSQL 18 可丢弃实例,开启甲、乙两个全新的 psql -X -v ON_ERROR_STOP=1 会话。显式检查点和控制文件信息使用具有相应权限的管理角色。保留正常开启的同步落盘与同步提交设置。乙先创建计数器,并保存当前检查点位置: SHOW fsync; SHOW synchronous_commit; CREATE SCHEMA checkpoint_demo; CREATE TABLE checkpoint_demo.items (id integer PRIMARY KEY,n integer); INSERT INTO checkpoint_demo.items VALUES (1,10); CREATE TABLE checkpoint_demo.baseline AS SELECT checkpoint_lsn FROM pg_control_checkpoint(); 显示的两个设置都应为开启,初始已经提交的值是十。甲在一个保持打开的事务中把它改成九十九: ...

一月 6, 2028 · 1 分钟 · XB Field Notes
一次持锁重写,化解严重的表膨胀。

VACUUM FULL 重建一张表而不是修剪

大量删除之后,PostgreSQL 表可能只剩少量记录,却仍保留很大的文件。普通清理让旧空间可以在表内复用,完全清理则把存活内容写入新文件,同时重建索引。更彻底的空间回收,需要另一套锁与容量预算,应作为有计划的维护操作,而不是一看到文件大小偏高就立刻执行的日常反应。 坏掉的样子 重写需要的是 ACCESS EXCLUSIVE 表锁,普通读取和写入都与它冲突。某个查询虽然已经执行完,但事务仍然打开、继续持有表锁,也会挡住重写。把它叫成独占行锁,或者以为只有写请求受影响,会低估实际中断范围。排队等待重写的强锁请求,还可能影响随后进入的其他取锁请求,不能只看当前命令有没有开始复制数据。 替代文件成功建立之前,原文件仍然存在,所以操作需要额外磁盘空间。等文件系统已经写满才着手完全清理,可能反而没有容纳新文件的余量。应提前评估替代堆表与索引、磁盘读写和日志影响,以及能够承受的持锁时长。常规自动回收不会自动替你执行这类完整重写。 快速验证 在同一个 PostgreSQL 18 可丢弃数据库中,开启甲、乙两个全新的 psql -X -v ON_ERROR_STOP=1 会话。清理命令必须在显式事务块之外执行。乙先创建五千行、删除前四千五百行,然后进行普通清理: CREATE SCHEMA compact_demo; CREATE TABLE compact_demo.items (id integer PRIMARY KEY,payload text) WITH (autovacuum_enabled=false); INSERT INTO compact_demo.items SELECT g,repeat(md5(g::text),10) FROM generate_series(1,5000) AS g; CREATE TABLE compact_demo.baseline AS SELECT pg_relation_size('compact_demo.items') AS heap_bytes, pg_relation_filenode('compact_demo.items') AS file_id; DELETE FROM compact_demo.items WHERE id<=4500; VACUUM (ANALYZE) compact_demo.items; SELECT pg_relation_size('compact_demo.items')=heap_bytes AS same_heap_size, pg_relation_filenode('compact_demo.items')=file_id AS same_file FROM compact_demo.baseline; 两个比较都返回真:堆文件大小与文件标识没有改变。存活行位于原文件尾部,末尾不存在可以截断的连续空页,所以普通清理不能通过截断来缩小这个特定文件。这不代表普通清理永远不会缩文件;如果末尾确实存在可释放的空页,就可能归还相应空间。这里要区分样本布局和命令的一般能力。 ...

一月 3, 2028 · 1 分钟 · XB Field Notes
由 WAL 流持续喂入、近乎实时的主库副本。

Postgres 流式复制让备用节点保持温热

物理流复制把 PostgreSQL WAL 发送到备库,备库通过重放记录维持集群的另一份副本。发送不必等待整个日志段结束。这与选择部分表变更进行传送的逻辑复制不同。接收进程正在运行、备库可以连接,都是有价值的健康信号,但任何一个信号都不能单独证明某条已经提交的业务记录此刻在备库可见。 坏掉的样子 流复制默认是异步的。主库可以先向客户端确认提交,备库之后才收到或应用相关记录。若主库恰好在这个窗口丢失,切换到备库时可能丢掉最近已经确认的工作。备库也可能保持连接并持续接收日志,却由于暂停回放而一直返回旧行。因此网络交付在推进,并不能证明查询新鲜度也在推进。 应区分不同进度标记。备库的接收位置函数报告通过流复制已经接收并同步到磁盘的位置,回放位置函数则报告已经应用的位置,两者差值可以暴露回放积压。最后一次重放事务的时间戳,即使在完全空闲的系统上也会不断变旧;单纯用当前时间减去它,不能可靠判断究竟有多少尚未处理的工作。 快速验证 使用非超级操作系统账户,在 Linux 环境中准备 PostgreSQL 18 服务端与客户端程序。本脚本已经在 PostgreSQL 18.6 上执行。它创建私有临时集群及 Unix 套接字目录,不打开网络监听。本地信任认证仅用于这些可丢弃的实验实例。少量日志预留支持这个有界演练,脚本没有配置永久复制槽。 #!/bin/sh set -eu umask 077 lab=$(mktemp -d /tmp/xbcatm-streaming.XXXXXX) mkdir "$lab/socket" primary=$lab/primary standby=$lab/standby cleanup() { for cluster in "$standby" "$primary"; do if test -f "$cluster/postmaster.pid"; then pg_ctl -D "$cluster" -m fast -w stop >/dev/null fi done } trap cleanup EXIT initdb -D "$primary" --no-locale --encoding=UTF8 --auth-local=trust --auth-host=reject >/dev/null cat >> "$primary/postgresql.conf" <<CONF listen_addresses = '' unix_socket_directories = '$lab/socket' port = 56432 wal_level = replica max_wal_senders = 4 wal_keep_size = '64MB' CONF pg_ctl -D "$primary" -l "$lab/primary.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 TABLE marker (id integer PRIMARY KEY,value text); INSERT INTO marker VALUES (1,'base'); SQL pg_basebackup -D "$standby" -R -X stream -c fast -w cat >> "$standby/postgresql.conf" <<CONF port = 56433 hot_standby = on CONF pg_ctl -D "$standby" -l "$lab/standby.log" -w -t 15 start >/dev/null PGPORT=56433 psql -X -v ON_ERROR_STOP=1 -qAt -c 'SELECT pg_wal_replay_pause()' >/dev/null attempt=0 while test "$(PGPORT=56433 psql -X -v ON_ERROR_STOP=1 -qAt -c "SELECT pg_get_wal_replay_pause_state()='paused'")" != t; do attempt=$((attempt+1)) test "$attempt" -lt 100 || { echo 'replay did not pause' >&2; exit 1; } sleep 0.1 done psql -X -v ON_ERROR_STOP=1 -qAt -c "INSERT INTO marker VALUES (2,'later')" target=$(psql -X -v ON_ERROR_STOP=1 -qAt -c 'SELECT pg_current_wal_lsn()') attempt=0 while test "$(PGPORT=56433 psql -X -v ON_ERROR_STOP=1 -qAt -c "SELECT pg_last_wal_receive_lsn()>='$target'::pg_lsn")" != t; do attempt=$((attempt+1)) test "$attempt" -lt 150 || { echo 'receiver did not catch up' >&2; exit 1; } sleep 0.1 done psql -X -v ON_ERROR_STOP=1 -qAt -c 'SELECT state,sync_state FROM pg_stat_replication' PGPORT=56433 psql -X -v ON_ERROR_STOP=1 -qAt <<SQL SELECT pg_last_wal_receive_lsn()>='$target'::pg_lsn, pg_last_wal_replay_lsn()<'$target'::pg_lsn, (SELECT count(*) FROM marker); DO \$\$ BEGIN IF (pg_last_wal_receive_lsn()>='$target'::pg_lsn AND pg_last_wal_replay_lsn()<'$target'::pg_lsn AND (SELECT string_agg(id||':'||value,',' ORDER BY id) FROM marker)='1:base') IS NOT TRUE THEN RAISE EXCEPTION 'paused replay result mismatch'; END IF; END \$\$; SQL echo stale_read_while_receiving PGPORT=56433 psql -X -v ON_ERROR_STOP=1 -qAt -c 'SELECT pg_wal_replay_resume()' >/dev/null attempt=0 while test "$(PGPORT=56433 psql -X -v ON_ERROR_STOP=1 -qAt -c "SELECT pg_last_wal_replay_lsn()>='$target'::pg_lsn")" != t; do attempt=$((attempt+1)) test "$attempt" -lt 150 || { echo 'replay did not catch up' >&2; exit 1; } sleep 0.1 done PGPORT=56433 psql -X -v ON_ERROR_STOP=1 -qAt <<'SQL' SELECT id,value FROM marker ORDER BY id; DO $$ BEGIN IF (SELECT string_agg(id||':'||value,',' ORDER BY id) FROM marker) IS DISTINCT FROM '1:base,2:later' THEN RAISE EXCEPTION 'resumed replay result mismatch'; END IF; END $$; SQL pg_ctl -D "$primary" -m fast -w stop >/dev/null echo old_primary_stopped pg_ctl -D "$standby" -w -t 15 promote >/dev/null PGPORT=56433 psql -X -v ON_ERROR_STOP=1 -qAt <<'SQL' SELECT pg_is_in_recovery(); INSERT INTO marker VALUES (3,'promoted'); SELECT id,value FROM marker ORDER BY id; DO $$ BEGIN IF pg_is_in_recovery() OR (SELECT count(*) FROM marker)<>3 THEN RAISE EXCEPTION 'promotion result mismatch'; END IF; END $$; SQL printf 'lab_directory %s\n' "$lab" 脚本通过带恢复配置选项的物理基础备份建立副本,这个选项会写入备库连接信息并创建备库信号文件。副本在另一个套接字端口启动后,先请求暂停回放,再等待状态确实变成已经暂停。随后主库提交第二行,并捕获提交之后的日志位置,作为接收与回放比较的明确目标。 ...

八月 10, 2027 · 3 分钟 · XB Field Notes