PostgreSQL 18 新增 extension_control_path,补上了扩展外置的最后一块拼图:控制文件和 SQL 脚本不再必须安装到服务器自身的 sharedir/extension。配合已有的 dynamic_library_path,以及 Kubernetes ImageVolume 或 Docker 的镜像挂载能力,扩展可以被打包成独立 OCI 镜像,在运行时挂进 PostgreSQL 容器。
这让“一个精简 PostgreSQL 基础镜像,加若干独立版本的扩展镜像”成为可行的交付方式。不过,镜像边界只解决文件分发问题。扩展是否真正与服务器解耦,取决于它是否介入启动流程、共享内存、后台进程、WAL 和复制拓扑。
PostgreSQL 18 真正补上的能力
此前,dynamic_library_path 已经允许 PostgreSQL 从安装目录之外加载 .so 动态库,但扩展的 .control 文件和版本化 SQL 脚本仍然必须位于编译时确定的 sharedir/extension。因此,即使动态库已经挂载成功,CREATE EXTENSION 仍可能报告找不到扩展。
PostgreSQL 18 的 extension_control_path 为控制文件和 SQL 文件增加了独立搜索路径。两个参数配合后,一个扩展可以完全位于 PostgreSQL 安装目录之外:
extension_control_path = '/opt/extensions/pgvector/share:$system'
dynamic_library_path = '/opt/extensions/pgvector/lib:$libdir'
这里有两个容易踩坑的细节:
extension_control_path中填写的是sharedir层级目录,PostgreSQL 会自动追加extension。如果文件位于/opt/extensions/pgvector/share/extension/vector.control,应填写/opt/extensions/pgvector/share,而不是完整的.../share/extension。$system指向 PostgreSQL 编译时的系统共享目录,必须保留,否则plpgsql等内置扩展也可能无法解析。类似地,dynamic_library_path应保留$libdir。
多个扩展需要逐项加入搜索路径。PostgreSQL 不会枚举挂载目录并自动发现所有扩展,而是按顺序查找第一个匹配项:
extension_control_path = '/opt/extensions/vector/share:/opt/extensions/postgis/share:/opt/extensions/pgaudit/share:$system'
dynamic_library_path = '/opt/extensions/vector/lib:/opt/extensions/postgis/lib:/opt/extensions/pgaudit/lib:$libdir'
顺序本身也是配置的一部分。如果两个镜像提供同名控制文件,排在前面的目录会胜出,因此生产环境应固定镜像摘要,并避免含糊的覆盖关系。
扩展镜像应包含什么
可以将每个扩展镜像整理为三个可移植目录。下面是一个建议布局,不要求服务器镜像采用相同的 PostgreSQL 安装前缀:
/extension/
├── lib/postgresql/ # 扩展自身的 .so
├── share/extension/ # .control 和版本化 SQL 文件
└── dependencies/ # libgeos、libjansson 等运行时依赖
扩展的 .so 只与对应 PostgreSQL 大版本兼容。为 PostgreSQL 17 构建的二进制不能假定可在 PostgreSQL 18 中加载,因此镜像标签至少应包含扩展版本和 PostgreSQL 大版本,例如:
registry.example.com/pgvector:0.8.0-pg18
registry.example.com/pgvector:0.8.0-pg17
如果扩展还依赖 GEOS、PROJ 或 Jansson 一类共享库,仅配置 dynamic_library_path 并不够。该参数帮助 PostgreSQL 找到扩展入口库,而入口库依赖的其他系统库仍由操作系统动态链接器解析。需要把依赖目录加入 LD_LIBRARY_PATH,或者在服务器镜像中通过系统链接器配置提供它们。
可以这样实践:用 ImageVolume 挂载扩展
下面是一个可改造的 Kubernetes 1.33+ 示例。它假设扩展镜像采用上面的 /extension 布局,并且集群已启用 ImageVolume;实际生产中应将示例镜像替换为内部仓库地址和不可变 digest。
apiVersion: v1
kind: Pod
metadata:
name: postgres18-vector
spec:
containers:
- name: postgres
image: postgres:18
env:
- name: POSTGRES_PASSWORD
value: change-me
- name: LD_LIBRARY_PATH
value: /opt/extensions/vector/dependencies
args:
- postgres
- -c
- extension_control_path=/opt/extensions/vector/share:$system
- -c
- dynamic_library_path=/opt/extensions/vector/lib/postgresql:$libdir
volumeMounts:
- name: vector
mountPath: /opt/extensions/vector
readOnly: true
ports:
- name: postgres
containerPort: 5432
volumes:
- name: vector
image:
reference: registry.example.com/pgvector:0.8.0-pg18
pullPolicy: IfNotPresent
Pod 启动后,可以验证 PostgreSQL 是否能看到并安装扩展:
kubectl exec -it postgres18-vector -- \
psql -U postgres -d postgres -c \
"SELECT name, default_version FROM pg_available_extensions WHERE name = 'vector';"
kubectl exec -it postgres18-vector -- \
psql -U postgres -d postgres -c \
"CREATE EXTENSION vector; SELECT extname, extversion FROM pg_extension WHERE extname = 'vector';"
对于 CloudNativePG,应使用其扩展镜像元数据和集群声明机制管理挂载、搜索路径及 LD_LIBRARY_PATH,而不是直接套用普通 Pod。具体字段会随 operator 版本变化,但底层检查点相同:控制文件路径、动态库路径、传递依赖和 PostgreSQL 大版本必须一致。
PG 16 和 PG 17 只能把动态库放到外部路径,控制文件仍需进入服务器的扩展目录。可以用 init container 把文件复制到共享卷,再通过 subPath 挂载,但这种方案依赖服务器镜像内部路径,每次 Pod 启动都要执行复制,也容易与 operator 对系统目录的管理发生冲突。若目标是干净、可审计的扩展镜像交付,PG 18 才是更合适的起点。
四类扩展,四种收益边界
| 类型 | 典型扩展 | 是否需要重启 | 容器化的主要收益 |
|---|---|---|---|
| 按需加载 | pgvector、PostGIS、SQL-only pg_partman | 通常不需要 | 可独立升级,并具备接近热挂载的能力 |
| Hook 型 | pgaudit、pg_stat_statements、auto_explain | 需要 | 独立版本管理,服务器镜像保持不变 |
| 后台 Worker 型 | pg_cron、启用 worker 的 pg_partman | 需要 | 文件独立交付,但进程生命周期仍绑定服务器 |
| 系统深度集成型 | Spock、pglogical、TimescaleDB、Citus | 通常需要,且涉及更多配置 | 独立构建和扫描,不代表运行时解耦 |
按需扩展最适合这种模式。它们通常在执行 CREATE EXTENSION 或首次调用函数时才加载,不要求写入 shared_preload_libraries,也不注册常驻后台进程。升级 pgvector 镜像无需重建 PostgreSQL 基础镜像,多个扩展还可以分别扫描、发布和回滚。
Hook 型扩展必须在服务器启动时通过 shared_preload_libraries 进入执行链路。更换镜像后仍要滚动重启,但独立版本管理依然有价值,尤其适用于只有部分集群需要审计或统计功能的环境。
后台 Worker 扩展与 postmaster 的进程树绑定。挂载解决了文件来源,却无法让 worker 脱离数据库生命周期。扩展崩溃、重启策略和资源消耗也会扩大服务器的运维面。
Spock、pglogical 等系统集成型扩展的约束更深:它们可能要求启动时分配共享内存、启用 wal_level=logical、开启 track_commit_timestamp、维护多个后台进程和复制槽。复制槽还可能因订阅端长期掉线导致 WAL 持续累积。这些都不是改变文件安装路径能够解决的问题。
采用前检查清单
决定是否为扩展单独制作 OCI 镜像时,可以逐项确认:
- 是否必须配置
shared_preload_libraries,以及变更是否需要滚动重启。 - 是否注册后台 worker、申请共享内存或安装 executor/planner hook。
- 是否改变
wal_level、提交时间戳、复制槽或其他集群级持久状态。 - 是否包含 PostgreSQL 大版本相关的
.so,镜像标签是否明确标出 PG 版本。 - 是否携带传递共享库,并正确设置动态链接器搜索路径。
extension_control_path是否保留$system,dynamic_library_path是否保留$libdir。- 镜像是否使用 digest 固定,并纳入漏洞扫描、签名和回滚流程。
extension_control_path 完成了扩展文件外置所需的机制,但“文件可独立交付”不等于“扩展可脱离服务器运行”。对 pgvector、PostGIS 这类按需加载扩展,容器化能带来最完整的收益;对 Hook 和后台 Worker 扩展,价值主要是独立发布;对复制或分布式系统扩展,真正的耦合仍位于共享内存、WAL、进程树和拓扑之中。先识别扩展改变了服务器的哪些部分,再决定是否增加一层镜像管理,通常比单纯追求容器化更可靠。