钩子与扩展开发与 PG 高可用 Patroni

共 19 题
📑 题目列表 19 题
#
★★★

1. PostgreSQL 的钩子(Hook)机制,ProcessUtility_hook、ExecutorStart_hook?

请说明 PostgreSQL 的钩子(Hook)机制,包括 ProcessUtility_hook 和 ExecutorStart_hook?

  • 钩子机制的概念
  • 常用钩子(ProcessUtility_hook、ExecutorStart_hook)
  • 钩子开发的应用

PostgreSQL 的钩子(Hook)机制是扩展通过共享库注册的回调函数,在服务器执行特定阶段被调用,从而在不改动核心代码的情况下拦截或扩展行为。常见钩子:ProcessUtility_hook 在实用命令(DDL、VACUUM、COPY 等)执行时调用,可用于拦截/审计/改写 DDL;ExecutorStart_hook 在查询执行开始时调用,用于查询审计、改写、监控。钩子按链式调用,扩展通过 _PG_init 注册并保存原钩子以便链式传递。典型应用:pgAudit、query 改写、权限增强、SQL 监控。

钩子机制是 PG 扩展开发的核心能力,让第三方在不动源码下扩展服务器行为。理解钩子链与注册时机(shared_preload_libraries 中加载)是开发扩展的基础。

static ProcessUtility_hook_type prev_ProcessUtility = NULL;
void _PG_init(void) {
  prev_ProcessUtility = ProcessUtility_hook;
  ProcessUtility_hook = my_ProcessUtility;
}
#
★★★

2. Patroni + etcd 的高可用架构?

请说明 Patroni 配合 etcd 实现 PostgreSQL 高可用的架构?

  • Patroni 的角色
  • etcd 作为 DCS
  • 高可用流程

Patroni 是 PostgreSQL 高可用编排工具,使用分布式一致性存储(DCS,如 etcd、ZooKeeper、Consul)作为决策中心。架构中,Patroni 管理多个 PG 节点(一个 primary、多个 standby),通过 DCS 进行 leader(primary)选举与状态存储。etcd 存储 leader key、节点列表、配置等,用 TTL 租约实现故障检测。当 primary 故障,Patroni 通过 DCS 选举新 leader,把某个 standby promote 为 primary,并更新其他节点的连接信息。Patroni 通过 REST API 提供运维接口(/primary、/switchover、/restart 等)。

Patroni 的核心是用 DCS 实现 leader 选举与一致性,避免"双主"(脑裂)。etcd 提供强一致性与 TTL 租约,是常见 DCS 选择。理解架构对部署与故障处理至关重要。

#
★★★

3. Patroni 的配置,bootstrap、standby cluster、复制槽?

请说明 Patroni 的配置,包括 bootstrap、standby cluster 和复制槽?

  • bootstrap 配置
  • standby cluster 配置
  • 复制槽管理

Patroni 配置主要通过 bootstrap 段定义集群初始化:bootstrap.dcs 定义集群级配置(如 TTL、retry_timeout、synchronous_mode、replication 槽),bootstrap.method 定义初始化方式(默认 pg_basebackup 或 initdb),bootstrap.users 定义初始用户和密码。standby cluster 指把该 Patroni 集群作为另一个集群的备库(逻辑/物理),通过 bootstrap.standby_cluster 配置源集群信息。复制槽:Patroni 可配置 slots 参数(permanent logical slots)在升主后保留复制槽,避免 failover 后 logical replication 丢失;通过 synchronous_mode 与 synchronous_standby_names 管理同步复制。

Patroni 的 bootstrap 决定集群首次如何建立,standby cluster 用于级联复制/迁移,复制槽管理保证逻辑复制在故障切换后不中断。理解配置项是部署 Patroni 的核心。

#
★★★

4. C 扩展与 PL/pgSQL 扩展的取舍?

请说明 C 扩展与 PL/pgSQL 扩展的取舍?

  • C 扩展的特点
  • PL/pgSQL 存储过程的特点
  • 选型考量

C 扩展(如 postgis、pg_stat_statements)用 C 语言编写,编译为共享库,性能高、可访问底层 API、可注册新类型/索引/钩子,但开发维护成本高、需编译、有内存与安全风险。PL/pgSQL(PL/pgSQL 函数/存储过程)是内置过程语言,开发快、易维护、安全、与 SQL 集成好,但性能相对受限、适合业务逻辑。取舍:需要底层能力、性能敏感、新类型/索引/钩子用 C 扩展;业务逻辑、数据处理、快速迭代用 PL/pgSQL。很多扩展也提供 PL/pgSQL 辅助函数。

选型取决于"性能/能力需求"与"开发维护成本"。多数业务逻辑用 PL/pgSQL,只有需要底层能力或极致性能才用 C 扩展。理解两者边界可避免过度工程。

#
★★★

5. pg_stat_statements 的源码阅读路径?

请说明 pg_stat_statements 的源码阅读路径,即如何阅读其实现源码?

  • 源码结构
  • 关键函数
  • 扩展机制对应

pg_stat_statements 源码位于 contrib/pg_stat_statements/。阅读路径:pg_stat_statements.c 是核心实现,包含 _PG_init(注册钩子与 GUC)、pgss_ProcessUtility(通过 ProcessUtility_hook 拦截 DDL/工具语句)、pgss_ExecutorEnd(通过 ExecutorEnd hook 收集查询统计)、pgss_hash_create(初始化统计 hash)。它使用 queryId(由 pg_query 生成的一致性指纹)为 key 聚合统计。关键结构 pgssEntry 保存统计项,pgss_store 更新统计。源码体现了钩子(ProcessUtility_hook、ExecutorEnd_hook)与共享内存(HashTable)的典型用法。

阅读 pg_stat_statements 源码是学习 PG 扩展开发(钩子、共享内存、GUC)的最佳范例。理解其"钩子收集 + 哈希聚合 + 查询视图"的链路即可掌握原理。

#
★★★

6. C 扩展开发的内存管理,palloc 与内存上下文(MemoryContext)

请说明 C 扩展开发的内存管理,包括 palloc 与内存上下文(MemoryContext)?

  • palloc 内存分配
  • MemoryContext 机制
  • 内存泄漏防护

PG 扩展用 palloc(而非 malloc)分配内存,内存由内存上下文(MemoryContext)管理。每个上下文是一个树,包括事务上下文、查询上下文、portal 上下文等。palloc 分配的内存属于当前上下文,上下文被删除/重置时其内所有内存自动释放,避免手动管理泄漏。扩展应使用 CurrentMemoryContext 或创建自己的 MemoryContext(AllocSetContextCreate)并在其生命周期内使用,避免把短生命周期数据存到长生命周期上下文导致膨胀。常用 palloc/palloc0/repalloc,配合 pfree(在合适的上下文)释放。

内存上下文是 PG 内存管理的核心,让扩展无需手动跟踪每个分配。理解当前上下文与上下文切换(MemoryContextSwitchTo)是避免泄漏和过量占用的关键。

MemoryContext ctx = AllocSetContextCreate(CurrentMemoryContext,
    "myctx", ALLOCSET_DEFAULT_SIZES);
MemoryContext old = MemoryContextSwitchTo(ctx);
char *buf = palloc(1024);
MemoryContextSwitchTo(old);
MemoryContextDelete(ctx);  // 释放 ctx 内所有内存
#
★★★

7. 扩展如何通过 _PG_init 注册 GUC 参数并安装钩子?shared_preload_libraries 与 session_preload_libraries 的加载时机差异是什么?

请说明扩展如何通过 _PG_init 注册 GUC 参数并安装钩子,以及 shared_preload_libraries 与 session_preload_libraries 的加载时机差异?

  • _PG_init 的作用
  • GUC 注册与钩子安装
  • 两种 preload 的时机差异

扩展通过 _PG_init 函数(服务器启动/进程初始化时调用)注册 GUC 参数(DefineCustomIntVariable 等)和安装钩子(把 ProcessUtility_hook 等指向自己的函数)。shared_preload_libraries 中的库在 postmaster 启动时加载,可访问共享内存、注册钩子、声明并行工作进程,适合需要全局能力(如 pg_stat_statements、pg_cron);session_preload_libraries 中的库在每次新会话(backend 进程)启动时加载,仅会话级可用,无法注册全局钩子或共享内存。加载时机差异决定扩展能做什么:钩子若需影响所有会话须用 shared_preload_libraries。

shared_preload_libraries 在 postmaster 加载,能注册受 postmaster 管理的钩子与共享内存;session_preload_libraries 在会话加载,能力受限。选择正确的 preload 方式决定扩展能力边界。

void _PG_init(void) {
  DefineCustomIntVariable("my_ext.limit", NULL, NULL, &limit, 10, 0, 1000, PGC_USERSET, 0, NULL, NULL, NULL);
  prev_ProcessUtility = ProcessUtility_hook;
  ProcessUtility_hook = my_ProcessUtility;
}
shared_preload_libraries = 'pg_stat_statements,pg_cron'
#
★★★

8. Patroni 在 DCS 中存储哪些内容(leader key、成员列表、配置)?TTL 租约如何用于故障检测与避免双主?

请说明 Patroni 在 DCS 中存储的内容,以及 TTL 租约如何用于故障检测与避免双主?

  • DCS 中存储的数据
  • TTL 租约机制
  • 故障检测与防脑裂

Patroni 在 DCS 中存储:leader key(标识当前 primary 的节点,带 TTL 租约)、成员列表(每个节点的 key/value,记录节点状态、版本、LSN 等)、集群配置(/config key,含 DCS 级配置)、以及一些状态/锁文件。TTL 租约机制:Patroni 定期(ttl/2 间隔)续租 leader key(往 DCS 写入并延长 TTL);若 primary 故障未能续租,leader key 过期,其他节点通过 DCS 竞争选举(用 CAS/抢锁)成为新 leader,从而检测故障。避免双主:只有成功获取 leader key 的节点才能作为 primary 服务写操作,其他节点检测到已有 leader 后不会自行 promote,从而避免双主(脑裂)。

DCS 的强一致性与 TTL 租约是实现"唯一 leader + 故障检测"的基础。续租失败即判故障,选举用分布式锁保证唯一 leader,这是防双主的关键。

#
★★

9. 扩展开发的基本步骤,Makefile、控制文件、SQL 文件?

请说明 PostgreSQL 扩展开发的基本步骤,包括 Makefile、控制文件、SQL 文件?

  • 扩展文件结构
  • 控制文件与 SQL 脚本
  • 构建安装

一个 PostgreSQL 扩展通常包含:控制文件(.control,定义扩展名、版本、需要加载的库、schema 等)、SQL 脚本(xx--xx.sql,创建扩展对象如函数/类型,如 myext--1.0.sql 和升级脚本 myext--1.0--1.1.sql)、C 源码(可选,实现底层函数)、Makefile(使用 PGXS 模板,定义 MODULE_big、EXTENSION、DATA 等)。构建:make && make install;安装:CREATE EXTENSION myext。控制文件的 default_version 决定默认版本,SQL 脚本按版本号管理升级路径。

PGXS 让扩展构建统一。控制文件+SQL 脚本+版本升脚本构成扩展的"安装与升级"体系,理解结构才能写出可维护、可升级的扩展。

# Makefile
EXTENSION = myext
DATA = myext--1.0.sql
MODULES = myext
PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)
# myext.control
comment = 'my extension'
default_version = '1.0'
module_pathname = '$libdir/myext'
relocatable = true
#
★★

10. Patroni 的工作原理,基于 etcd/ZooKeeper/Consul 的 leader 选举?

请说明 Patroni 的工作原理,特别是基于 etcd/ZooKeeper/Consul 的 leader 选举?

  • Patroni 的 leader 选举
  • DCS 的作用
  • 选举流程

Patroni 通过 DCS(etcd、ZooKeeper、Consul 等)实现 leader 选举。每个节点启动时尝试在 DCS 中创建 leader key(带 TTL 租约),成功创建者成为 primary(leader);若 leader key 已存在,则成为 standby。leader 定期续租,standby 监控 leader key 的有效性。当 leader 故障(租约过期),standby 竞争创建 leader key(CAS/锁),先到者成为新 primary 并 promote。DCS 提供强一致性与原子操作,保证同一时刻只有一个 leader,避免双主。ZooKeeper 用临时节点/序列号,etcd 用租约+CAS,Consul 用 session 实现类似功能。

选举本质是"谁能在 DCS 中持有 leader key"。Patroni 抽象了 DCS 接口,支持多种后端,核心是分布式锁/租约的争用。理解选举机制是理解高可用的核心。

#
★★

11. Patroni 的限制与替代方案(Stolon、repmgr)?

请说明 Patroni 的限制及其替代方案(Stolon、repmgr)?

  • Patroni 的限制
  • Stolon 的特点
  • repmgr 的特点

Patroni 的功能限制:依赖 DCS(需额外维护 etcd 等)、配置复杂、对网络分区场景需谨慎(防脑裂)、standby 不承载写、逻辑复制槽在切换后需管理。替代方案:repmgr 是更轻量的复制管理工具,基于 PostgreSQL 的复制,提供故障切换、级联、监控,但领袖选举与一致性较弱、协调性不如 Patroni;Stolon 是另一个基于 etcd 的 HA 方案,引入 proxy/keeper 抽象,架构更复杂但独立。选择上:追求强一致与自动切换用 Patroni,想要轻量简单可用 repmgr。

各方案权衡"一致性/自动化"与"复杂度/轻量"。Patroni 功能最强但依赖 DCS;repmgr 简单但协调弱;Stolon 架构独立。选型需结合团队与运维能力。

#
★★

12. Patroni 的故障检测与切换流程?

请说明 Patroni 的故障检测与切换流程?

  • 故障检测机制
  • 切换流程
  • promote 与降级

Patroni 故障检测通过 DCS 租约与 PostgreSQL 健康检查。primary 若无法续租(TTL 过期)或健康检查失败(如无法连接 PG),则被判定故障。切换流程:standby 检测到 leader key 过期,竞争获取 leader key 成为新 leader,对新 leader 执行 promote(触发 pg_ctl promote 或调 promote 接口),原 primary 若恢复则降级为 standby(重新加入集群同步)。切换期间写流量需重定向到新 primary(通过负载均衡器或客户端发现)。Patroni 支持手动 switchover(优雅切换)与自动 failover(故障切换)。

切换的核心是"租约过期 -> 抢锁 -> promote -> 旧主降级"。理解流程可设计正确的应用侧重连与连接管理。同步复制模式下需配置 synchronous_standby_names 保证切换不丢已提交数据。

#
★★

13. PG 扩展开发,C 扩展的 PG_MODULE_MAGIC 与调用约定?

请说明 PG 扩展开发中 C 扩展的 PG_MODULE_MAGIC 与调用约定?

  • PG_MODULE_MAGIC 的作用
  • 调用约定
  • 版本兼容

PG_MODULE_MAGIC 是一个宏,必须在 C 扩展模块中定义,用于记录模块编译时的 PG 版本信息(magic block)。服务器加载模块时校验 magic block 与当前服务器版本是否兼容,不兼容则拒绝加载(报错),防止因 ABI 不匹配导致崩溃。扩展函数需声明为 PG_FUNCTION_INFO_V1 并遵循 V1 调用约定(返回 Datum、通过 PG_FUNCTION_ARGS 访问参数、用 PG_RETURN_* 返回值)。PG_MODULE_MAGIC 与 PG_FUNCTION_INFO_V1 是 C 扩展的基本要求。

PG_MODULE_MAGIC 保证加载的模块与服务器版本兼容,是安全机制。调用约定规定函数签名与参数/返回值传递方式,是 C 扩展函数编写的基础。

#include "postgres.h"
#include "fmgr.h"
#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif
PG_FUNCTION_INFO_V1(my_func);
Datum my_func(PG_FUNCTION_ARGS) {
  int32 v = PG_GETARG_INT32(0);
  PG_RETURN_INT32(v * 2);
}
#
★★

14. Patroni 的 watchdog/fencing 机制如何防止脑裂(双主写)

请说明 Patroni 的 watchdog/fencing 机制如何防止脑裂(双主写)?

  • 脑裂的概念
  • watchdog 机制
  • fencing 防双主

脑裂(双主)指集群中两个节点同时认为自己是 primary 并接受写,导致数据分裂。Patroni 通过多种 fencing 机制防止:DCS leader key 的强一致保证只有一个 leader;watchdog(Watchdog Device)——Patroni 支持连接系统 watchdog(如 /dev/watchdog),若节点失去领导权(如 DCS 连接断开)且 watchdog 无法喂狗,则 watchdog 会强制重启/宕机该节点(fencing),防止它继续作为 primary 写。这种"故障节点被物理隔离"的机制确保不会出现两个可写主。此外可用 STONITH(fence 工具)配合。

防脑裂需要"逻辑上唯一 leader + 物理上隔离故障节点"。watchdog 提供物理 fencing:失去 DCS 续租的节点被 watchdog 强制中断,杜绝其继续写。这是 HA 的关键安全机制。

#
★★

15. 自定义 background worker 如何注册与启动?与 cron 定时任务相比在数据库内执行周期任务的优缺点是什么?

请说明自定义 background worker 如何注册与启动,并比较它与 cron 定时任务在数据库内执行周期任务的优缺点?

  • background worker 注册
  • background worker 启动
  • 与 cron 的对比

自定义 background worker 通过 RegisterBackgroundWorker 注册(在 _PG_init 中调用,需放在 shared_preload_libraries 中,postmaster 启动时注册),启动时 postmaster 会 fork 该 worker 进程,执行 BackgroundWorkerMain 入口函数。worker 可访问数据库(通过 bgw_extra、参数)周期执行任务。与 cron 定时任务相比:background worker 在数据库进程内运行,可访问共享内存、数据库连接、注册钩子,延迟低、与数据库集成紧密、可精确控制;但开发复杂、需编译、不易管理。cron 定时任务开发简单、易管理,但需外部连接、无法访问进程内共享状态。适合需要数据库内高性能周期任务的场景用 background worker。

background worker 是数据库内常驻进程机制,适合需共享内存/数据库紧耦合的周期任务(如 pg_stat_statements 的采样、pg_cron 的 worker)。cron 简单但无法访问进程内共享状态。选型依赖任务对数据库内部资源的访问需求。

void _PG_init(void) {
  BackgroundWorker worker;
  worker.bgw_function_name = "my_main";
  worker.bgw_library_name = "myext";
  worker.bgw_start_time = BgWorkerStart_RecoveryFinished;
  worker.bgw_restart_time = 5;
  RegisterBackgroundWorker(&worker);
}
#
★★

16. Patroni 的 synchronous_mode 如何动态维护 synchronous_standby_names?切换时如何避免异步降级丢数据?

请说明 Patroni 的 synchronous_mode 如何动态维护 synchronous_standby_names,以及切换时如何避免异步降级丢数据?

  • synchronous_mode 的作用
  • 动态维护 synchronous_standby_names
  • 切换时避免丢数据

Patroni 的 synchronous_mode 启用同步复制,动态维护 synchronous_standby_names:根据当前同步备库(sync standby)列表,自动把 synchronous_standby_names 设置为可同步的备库(如 "*" 或指定节点),并监控同步备库状态。切换时避免异步降级丢数据:Patroni 在切换前会确保新 primary 追平旧 primary 的 WAL(等待同步到新节点),或通过配置保证切换时同步模式不降级;当同步备库故障时,Patroni 可配置 allow_promote 与前缀策略,避免降级为异步导致已提交数据丢失。理想情况下同步复制保证 RPO=0,切换不丢已提交事务。

synchronous_mode 的核心是让 synchronous_standby_names 随备库状态动态调整,保证至少一个同步备库。切换时通过确保新主追平 WAL 避免丢数据,这需要同步复制配合。

#

17. Patroni 的 REST API(/primary、/switchover)在运维编排与故障演练中的应用?

请说明 Patroni REST API(/primary、/switchover)在运维编排与故障演练中的应用?

  • REST API 端点
  • /switchover 手动切换
  • 运维演练

Patroni 提供 REST API(http://host:8008/)用于运维:/primary 返回当前 primary 节点信息;/switchover 触发手动切换(优雅故障转移,可指定目标节点,Patroni 会等待新 primary 追平);/restart 重启节点;/reinitialize 重新初始化;/failover 强制切换。这些接口用于运维编排(如批量切换、故障演练、缩容扩容)与自动化脚本集成。故障演练(chaos)可用 /switchover 模拟主备切换验证高可用与业务连续性。

REST API 使 Patroni 可被运维工具编排。/switchover 做优雅切换演练,验证应用故障转移能力而不真正触发故障。理解接口可设计自动化运维流程。

curl -X POST http://primary:8008/switchover -d '{"candidate":"node2"}'
curl http://primary:8008/primary
#

18. PG 的扩展生态,PostGIS/pgvector 的部署与性能?

请说明 PostgreSQL 扩展生态中的 PostGIS 与 pgvector 的部署与性能?

  • PostGIS 的部署与性能
  • pgvector 的部署与性能
  • 扩展生态

PostGIS 是地理空间扩展,提供空间类型(geometry/geography)、空间索引(GiST)、空间函数。部署:安装 postgis packages 后 CREATE EXTENSION postgis;性能优化重点:空间索引(GIST)、合理的 SRID 与空间范围、避免函数调用导致索引失效。pgvector 是向量检索扩展,提供 vector 类型与 HNSW/IVFFlat 索引,用于相似度检索;部署:CREATE EXTENSION vector,性能优化:HNSW 参数(m、ef_search/ef_construction)、维度与数据量、内存。两者都是 PG 生态的"杀手级"扩展,支撑空间与向量场景。

扩展生态是 PG 的竞争力。PostGIS 与 pgvector 分别覆盖空间与 AI 向量检索,性能关键在于正确使用索引与调优参数。理解部署与性能要点可支撑实际项目。

#

19. 扩展的发布与安装(CREATE EXTENSION 的版本控制与更新路径)

请说明 PostgreSQL 扩展的发布与安装,包括 CREATE EXTENSION 的版本控制与更新路径?

  • CREATE EXTENSION 安装
  • 版本控制机制
  • 更新路径(ALTER EXTENSION UPDATE)

扩展通过 CREATE EXTENSION name 安装,默认安装控制文件中的 default_version。版本控制:扩展由 .control 文件(default_version)和一系列 SQL 脚本(xx--1.0.sql、xx--1.0--1.1.sql 升级脚本)管理版本。升级路径:ALTER EXTENSION name UPDATE TO '1.1' 会按升级脚本链(1.0--1.1)逐步升级。文档中 extension_versions 表记录已安装版本。发布扩展可打包到 contrib 或通过 pgxn/pg_config 分发。理解版本链路可设计向后兼容的升级。

扩展的版本控制是"可安装、可升级"的前提。CREATE EXTENSION 装初始版本,ALTER EXTENSION UPDATE 沿升级脚本链升级。多版本升级脚本让大版本间平滑迁移。

CREATE EXTENSION myext;            -- 安装默认版本
SELECT extversion FROM pg_extension WHERE extname='myext';
ALTER EXTENSION myext UPDATE TO '1.1';