在 PostgreSQL 的开发社区里,凡是跟“处理 GUC 的 extra 数据”打过照面的人,多半都经历过这样一个阶段:先是找到了pg_settings.extra_desc这一列,以为 extra 就是“额外描述”;再往下翻,在struct config_generic里看到一个void *extra,又搞不清楚它到底是不是同一回事。我最早写自定义扩展时也被这两个“extra”绕晕过,直到有一次参数 reload 后行为诡异,才下定决心把这个字段的前因后果彻底捋了一遍。这篇文章就是我动手排查、翻源码、做最小复现之后的完整记录,既写给打算在扩展里挂额外状态的开发者,也写给只想用 SQL 搞清楚“参数到底在哪被改”的 DBA。
1. 先搞清 “extra 数据” 在 GUC 里的三个真实落脚点
在动手处理之前,必须先认清:GUC 语境里的 “extra 数据” 并不是单一概念。它至少出现在三个完全不同的层次,用途、生命周期、处理方式都不同。把它们混为一谈是很多问题排查不出来的根源。
1.1 源码层面:config_generic.extra 指针
所有 GUC 参数,无论是 PostgreSQL 内置的shared_buffers、work_mem,还是扩展里通过DefineCustomStringVariable注册的自定义参数,在内存里都会表现为一个struct config_generic。这个结构体保存了一个参数的“主数据”:名字、类型、当前值、来源、上下文、重置值,以及一堆标志位。
其中有一个字段相当特殊:
void *extra;它没有任何默认行为,系统既不会在参数注册时自动分配它,也不会在参数生命周期结束时自动释放它。官方注释说得很简短:这是保留给自定义 GUC 使用的额外指针。说白了,这是一个“挂载点”,你想往上面挂什么就挂什么,后果自负。
主结构里经常被忽略的还有sourcefile和sourceline——它们记录当前值来自哪个配置文件的哪一行。这两个字段配合extra指针,在诊断“参数为什么是现在这个值”时往往能派上大用场。config_generic里最常见的字段可以整理成一张表:
| 字段 | 类型 | 作用 |
|---|---|---|
| name | const char * | 参数名 |
| vartype | enum config_type | 参数类型:bool/int/real/string/enum |
| context | GucContext | 设置该参数所需的最小上下文权限 |
| source | GucSource | 当前值的最终来源 |
| reset_source | GucSource | 执行 RESET 时恢复的来源 |
| extra | void * | 额外数据指针,自定义 GUC 的私有挂载点 |
| sourcefile | char * | 当前值来源的配置文件 |
| sourceline | int | 当前值来源的具体行号 |
| flags | int | 行为标志,比如是否允许修改、是否影响计划缓存 |
1.2 视图层面:pg_settings 里的可见元数据
pg_settings是查看 GUC 状态的官方窗口。它的列比大多数人印象中多得多,除了name和setting外,还有source、boot_val、reset_val、min_val、max_val、unit、extra_desc、context、sourcefile、sourceline、pending_restart等等。
这一大堆列就是参数在运行时的“额外数据”——它们不参与参数值本身的计算,却决定了你要不要去改这个参数、改完之后什么时候才生效,以及这个值到底是哪一层配置给出来的。extra_desc尤其容易被误解成“存储额外数据的地方”,实际上它只是一段描述文本,在注册参数时作为extraDesc参数传入,最终显示在这一列。你可以把它理解成参数的“脚注”,和我们要操作的那个extra指针没有直接关系。
1.3 配置层面:postgresql.auto.conf 与 ALTER SYSTEM
第三个“extra 数据”落脚点是配置文件层面。ALTER SYSTEM SET产生的配置项会被单独写入$PGDATA/postgresql.auto.conf,而不是postgresql.conf。这种设计把“用户手动改的配置”和“ALTER SYSTEM 写的配置”分开,本质上就是给每个参数增加了“来源”这一层额外信息。
当你同时修改了postgresql.conf和postgresql.auto.conf里同一个参数,后者的优先级更高。pg_settings的sourcefile列会显示这个优先级判断的结果,这是排查“我改了怎么没生效”的第一现场。所以结论已经很清晰:GUC 的 “extra 数据” 在三个层面各有各的含义。真正可编程、可让你自定义挂载状态的是源码里那个void *extra,下面重点讲它。
2. 什么时候会用到 extra 指针:一个扩展开发者的实际场景
很多刚接触 PostgreSQL 扩展开发的人会问:钩子函数里已经有newval了,为什么还要塞一个void **extra参数给我?这要从 GUC 的“一次设置”流程说起。
2.1 GUC 钩子签名里的 extra 到底是怎么传的
扩展注册自定义参数时,需要在DefineCustomStringVariable里提供几个钩子。不同版本签名略有差异,我这里以 PostgreSQL 15 及以上版本为例:
typedef bool (*GucStringCheckHook) (char **newval, void **extra, GucSource source); typedef void (*GucStringAssignHook) (const char *newval, void *extra); typedef char *(*GucShowHook) (void);当一条SET demo.region = 'east'被执行时,set_config_option内部会先调用 check_hook 做校验。注意 check_hook 的第二个参数是void **extra,它可以被写入一个指针,随后这个指针会作为 assign_hook 的第二个参数再传回来,也会在SHOW demo.region的调用链上参与输出。
这个机制解决的实际问题是:一个 GUC 的参数值本身只是布尔、整数或字符串,但扩展可能需要记录跟这个参数相关的更多状态。比如参数被修改了多少次、最后一次修改是通过什么途径进来的、参数值是否触发过某个业务缓存重建、根据参数解析出来的开销很大的内部结构。把这些东西挂到extra上,它们就和 GUC 绑定了,下次任何路径修改参数,扩展都能在钩子里拿到同一份状态。
2.2 典型需求:参数值之外还想记点别的
我做过一个区域路由的示例扩展,该 GUC 只需要 east/west 两个值,但业务方强烈要求能审计“谁在什么时候改了这个参数,以及现在到底该路由到哪个节点”。如果把审计日志塞进参数值字符串,setting列会被污染;如果塞进全局静态变量,又要自己处理 reset、reload、会话上下文切换。最后我用extra挂了一个结构体,记录修改次数、最近来源、以及当前已编译好的路由规则对象,一劳永逸。
如果你的需求只是简单的“参数 A 改了要同步改参数 B”,那么用assign_hook加普通 C 变量就够了,不需要extra。但如果你需要“为每个 GUC 实例维护独立的多字段状态”,extra是最自然的归属地。它比全局变量多了一个优势:当 GUC 系统做事务回滚、配置恢复时,extra会跟随参数栈一起被处理,而全局变量只能靠你自己在钩子里猜当前处于什么状态。
2.3 为什么不建议把所有状态塞进全局变量
以 postmaster + fork 的进程模型来看,每个 backend 有独立的地址空间,全局变量天然就是 per-backend 的。听起来挺适合存状态,但问题在于:一个扩展里可能有十几个参数,如果每个参数都要维护“修改次数+来源+缓存结构”,就会变成十几组命名怪异的全局变量,它们之间还需要在_PG_init里统一初始化,稍不留神就会互相覆盖。
extra的设计初衷就是为了把“参数值”和“参数附属状态”放在同一个对象下面。它虽然没有引用计数和自动释放,但至少给了你一个明确的从属关系:这个状态属于哪个 GUC,一眼就能看出来。项目代码里一旦参数多起来,“归属感”比什么都重要。
3. 手写一个带 extra 状态的扩展(附可运行 C 代码)
纸上谈兵没意思,我直接给出一个可编译的最小扩展骨架。这个扩展注册一个demo.region参数(只接受 east/west),并用extra挂了一个结构体,记录修改次数和最近一次修改的来源。
3.1 扩展初始化与 GUC 注册
先写头部和结构体定义:
#include "postgres.h" #include "fmgr.h" #include "utils/guc.h" PG_MODULE_MAGIC; typedef struct RegionExtra { int change_count; char *last_source; } RegionExtra; static char *region_setting = NULL; static RegionExtra *region_extra = NULL;然后是_PG_init。PostgreSQL 在加载共享库时会调用这个函数,注册 GUC 的入口就放在这里:
void _PG_init(void); void _PG_init(void) { DefineCustomStringVariable("demo.region", "region selector demo", "only accepts east or west", ®ion_setting, "east", PGC_USERSET, 0, check_region, /* check hook */ assign_region, /* assign hook */ show_region); /* show hook */ }DefineCustomStringVariable的第一个参数是参数名,第四、五个参数分别是“保存当前值的 C 变量”和“默认值”。这里有个细节:注册时region_extra这个全局变量并不等于 GUC 的extra字段,GUC 的extra刚开始是 NULL,只有当 check_hook 显式执行*extra = ex;之后才有值。
3.2 check_hook 里初始化 extra 数据
check_hook 的职责有两部分:第一,校验新值是否合法;第二,准备好归属于这个 GUC 的额外状态。我习惯把这两件事放在一个函数里,但顺序有讲究——先校验、再写 extra,避免坏数据污染。
static bool check_region(char **newval, void **extra, GucSource source) { RegionExtra *ex = (RegionExtra *) *extra; /* 先做业务校验,不通过就不碰 extra */ if (strcmp(*newval, "east") != 0 && strcmp(*newval, "west") != 0) { GUC_check_errdetail("only accepts 'east' or 'west'."); return false; } /* 第一次使用时,在 TopMemoryContext 下分配 */ if (ex == NULL) { MemoryContext oldcxt = MemoryContextSwitchTo(TopMemoryContext); ex = (RegionExtra *) palloc0(sizeof(RegionExtra)); MemoryContextSwitchTo(oldcxt); *extra = ex; region_extra = ex; /* 同时留一份在模块级,供 show hook 使用 */ } ex->change_count++; if (ex->last_source != NULL) pfree(ex->last_source); ex->last_source = pstrdup(source_to_text(source)); return true; }这里有个容易被忽略的细节:为什么要把palloc切到TopMemoryContext?因为 check_hook 被调用的环境可能是一个短期内存上下文,比如事务上下文、portal 上下文,如果直接用palloc,上下文销毁时 extra 结构也会被连带释放,下次再读就变成悬空指针。挂在 GUC 上的状态生命周期应该跟随 backend 进程,而不是跟随某一次调用。
低版本的 PostgreSQL(14 之前)中 check hook 没有GucSource source参数,如果你要兼容旧版本,把函数签名改成bool check_region(char **newval, void **extra)即可,注册时传旧签名函数。现在社区里 PG 12 以下还有不少存量,写扩展时版本适配要提前想清楚。
3.3 assign_hook 与 show_hook 消费 extra
check_hook 通过之后,set_config_option会调用 assign_hook,把最终生效的值写入region_setting。注意这里拿到的extra就是刚才 check_hook 塞进去的那个指针:
static void assign_region(const char *newval, void *extra) { RegionExtra *ex = (RegionExtra *) extra; region_setting = (char *) newval; if (ex != NULL) elog(DEBUG1, "demo.region changed, count=%d, source=%s", ex->change_count, ex->last_source); else elog(DEBUG1, "demo.region changed (no extra yet)"); }show_hook 则负责在SHOW demo.region时返回展示文本。借这个机会,把 extra 里的统计信息也带出来,演示一下“参数值+额外状态”的联合输出:
static char * show_region(void) { if (region_setting == NULL) return pstrdup("unset"); if (region_extra != NULL) return psprintf("%s (changes=%d, last_source=%s)", region_setting, region_extra->change_count, region_extra->last_source ? region_extra->last_source : "-"); return pstrdup(region_setting); }这里有一个必须坦白的技术细节:标准 show hook 的签名是char *(*show_hook)(void),它拿不到extra参数。在扩展内部,你要么自己维护一个模块级静态指针(就像我上面的region_extra),要么想办法拿到config_generic结构体再读它的extra字段。后者依赖一些内部接口,不同版本可能有差异。我项目里更常用静态指针同步的方式,因为 show hook 只需要展示,不参与核心逻辑,保持简单更重要。
3.4 用 psql 验证效果
假设你已经把扩展编译成demo.so并放到了$libdir下,那么在 psql 里可以这样测:
LOAD '$libdir/demo'; SET demo.region = 'east'; SHOW demo.region; -- east (changes=1, last_source=session) SET demo.region = 'west'; SHOW demo.region; -- west (changes=2, last_source=session) SET demo.region = 'north'; -- ERROR: value "north" is not acceptable -- DETAIL: only accepts 'east' or 'west'.再查一下pg_settings,你会看到这个参数的source是session,sourcefile为空。这些都是 GUC 自动维护的“额外数据”,因为它们是在会话里用SET修改的,不来自任何配置文件。
如果想让参数来自配置文件,可以临时在postgresql.conf里写一行demo.region = 'west',重启或pg_ctl reload后,pg_settings.sourcefile就会指向postgresql.conf,sourceline也对应到具体行。这是验证 extra 数据之外、GUC 元数据是否正确的常用手段。
4. 内存、reset 与事务回滚:extra 指针的边界条件
代码能跑通只是第一步。真正让 extra 成为“坑”的,是它的生命周期问题。这些边界条件在官方文档里几乎没有展开过,基本都是翻guc.c源码或者被线上问题毒打之后才明白的。
4.1 谁负责释放 extra 指向的内容
明确结论:GUC 框架不负责释放,需要扩展自己负责。
如果你把 extra 指向一个用palloc在 TopMemoryContext 分配的结构体,那么它在 backend 生命周期内不会单独释放,进程退出时由内存上下文统一回收。PostgreSQL 扩展一般不实现_PG_fini,因为模块卸载机制有限,所以常见的选择是:
- 结构体小而简单:用 TopMemoryContext 分配,随 backend 退出由内存上下文回收,不需要显式释放;
- 结构体里持有文件描述符、网络连接、打开的文件等资源:必须自己管理释放逻辑,否则资源会泄漏。更稳妥的做法是避免在 extra 里放这类需要显式释放的资源,只放纯内存数据。
另外要注意显式pfree的时机。如果你在 check_hook 里把旧的last_source字符串pfree之后,再用pstrdup生成新字符串,要确保这些操作都在同一个可控的 MemoryContext 下执行。如果把新字符串分配在了随取随用的临时上下文里,那么 GUC 的 extra 虽然马上写入了,但等到下次读取时可能已经失效。这就是我在 check_hook 里先把上下文切到 TopMemoryContext 再操作的原因。
4.2 SET LOCAL 和事务回滚期间 extra 会怎样
SET LOCAL demo.region = 'west';是事务级设置,事务提交前生效,提交后自动恢复原值。GUC 实现这套机制依赖guc->stack,也就是把修改前的“值+来源+extra”压栈,事务结束时再从栈里弹出来恢复。
这里有一个非常容易踩的坑:如果 check_hook 每次修改都新建一个 extra 结构(新指针),那么事务回滚时 GUC 的 extra 会恢复到事务开始前的旧指针,而你新建的结构就变成了“孤儿对象”。从内存管理的角度,它并不泄漏(TopMemoryContext 会回收),但从业务语义的角度,你可能观察到“计数变了但回滚后又变回去了”,或者“计数和参数值不一致”。
如果你希望 extra 里的计数也跟随事务回滚,那么不要每次 new 一个结构,而是修改结构内部的字段。事务回滚时 GUC 的 extra 指针指回旧结构,旧结构里的字段也就是旧的值。如果你希望即使事务回滚,extra 里的统计也保留(比如审计场景),那就要在每次 check 时把变更记录写到独立于 extra 结构体的外部存储里。这个取舍没有标准答案,取决于业务到底要“和参数一致”还是“记录发生过”。
4.3 SIGHUP reload 与同值短路
通过ALTER SYSTEM SET demo.region='west'; SELECT pg_reload_conf();触发 reload 时,所有 backend 会重新解析配置并尝试应用。这里有个常见误区:并非每次 reload 都会调用 assign_hook。
set_config_option内部有一系列短路判断。如果新解析出来的值和当前值类型、来源都相同,系统可能直接跳过后续的 assign 动作。对内置参数,这是性能优化;对自定义 GUC 和挂在 extra 上的统计逻辑,这会导致“我 reload 了,但 extra 的计数没有增加”。这不是 bug,而是 GUC 框架默认的相等性优化。
因此,如果你的业务逻辑依赖“每次 reload 都要做事”,不要在 assign_hook 里做,放在 check_hook 里而且要接受即使值没变 check 也会被执行的事实。轻量逻辑放 check_hook,重量级联动放 assign_hook,并且默认接受同值情况下不会触发——这个预期先立好,后面能少踩很多坑。
5. DBA 视角:不写代码也要懂的 GUC 额外信息排查
如果你不写扩展,extra 指针可能永远碰不到。但 GUC 参数那一堆“额外元数据”你每天都会遇到,处理不好照样会花掉整个下午。
5.1 用 pg_settings 判断参数到底在哪被改
我排查“参数改了没生效”的习惯是,第一眼先看pg_settings的source列,而不是setting列。source 能直接告诉我参数值的来源层级:default、session、environment、config file、command line、database、user、client、override。
一条实用 SQL:
SELECT name, setting, unit, source, sourcefile, sourceline, pending_restart FROM pg_settings WHERE name IN ('shared_buffers', 'work_mem', 'max_connections') ORDER BY name;如果shared_buffers的 source 是default,但你明明在postgresql.conf里改成了 4GB,那问题几乎可以断定是:连错了实例,或者读取了不同数据目录下的配置文件。尤其是 Docker 和云数据库场景,容器内外配置目录不一致太常见了。
如果 source 是command line,说明参数在启动命令里通过-c传入,文件配置对它无效。这类参数往往表现为“我改了文件却永远不生效”。
5.2 sourcefile/sourceline 与 ALTER SYSTEM 优先级
sourcefile和sourceline联合使用,可以直接定位到具体配置文件的具体行。但要知道ALTER SYSTEM SET产生的配置不在postgresql.conf,而是postgresql.auto.conf,且优先级更高。
举个实际例子:
# postgresql.conf work_mem = 4MB然后执行:
ALTER SYSTEM SET work_mem = '8MB'; SELECT pg_reload_conf();这时pg_settings里看到的 work_mem 是 8MB,sourcefile 指向.../postgresql.auto.conf。如果你改回postgresql.conf里的 4MB 并 reload,参数仍然是 8MB,因为 auto.conf 覆盖了它。这会让很多刚接触 ALTER SYSTEM 的人困惑。
处理方式有两条路:一是ALTER SYSTEM RESET work_mem;,它会从 auto.conf 中删除该项;二是手动编辑 postgresql.auto.conf 并重启实例。我个人推荐前者,逻辑清晰,也不会因为手误破坏文件格式。手动编辑 auto.conf 不是好习惯,尤其当你正在跑生产实例时,一个格式错误可能导致启动失败。
除此之外,pg_file_settings视图值得了解。它显示所有配置文件每一行的解析结果,error列为空代表该行成功解析,非空则说明这一行有问题。我排查“改了很多行配置、但不知道哪行写错”的场景时,直接用这个视图,比逐个打开文件快得多。
5.3 常见翻车现场:改了没生效
最后列几个我实际处理过的翻车现象,供你对号入座:
- 改了
postgresql.conf后忘了pg_ctl reload,重启前所有 backend 都还持有旧值。这种最常见,sourcefile能看出来它指向的确实是文件,但setting没变,说明还没重载。 - 参数单位看错。内存参数比如
shared_buffers有默认单位,你写shared_buffers = 1024可能得到 1024KB 而不是 1GB。查看pg_settings.unit列可以确认当前单位,改之前先查一下文档里这个参数的单位是什么。 - 会话级覆盖。在某个会话里
SET work_mem = '1GB'之后,其他会话看到的仍然是配置文件里的值。如果你在别的连接里执行SELECT setting FROM pg_settings WHERE name='work_mem',看到的是那个连接的会话值,不代表全局配置。 pending_restart = true的参数,比如max_connections、shared_preload_libraries,reload 不会生效,必须重启。这类参数改完记得盯一眼pending_restart列,别白等。
说到底,GUC 的 extra 数据并不神秘。源码层面的extra指针是给扩展开发者准备的自定义挂载点,视图层面的extra_desc、sourcefile、pending_restart是每个人都能用到的排错信息,配置文件层面的postgresql.auto.conf则是 ALTER SYSTEM 带来的覆盖规则。我在实际项目里处理 extra 的心得就三条:check_hook 里先校验后挂载、内存统一放 TopMemoryContext、不依赖同值 reload 触发 assign_hook。把这三条记住,剩下的细节遇到再查源码,基本不会再被它绊住。