PostgreSQL 18 扩展容器化:文件解耦了,运行时依赖未必

2026-07-31 28 预计阅读时间: 1 分钟
来源: postgr.es AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:10 分钟

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 是否保留 $systemdynamic_library_path 是否保留 $libdir
  • 镜像是否使用 digest 固定,并纳入漏洞扫描、签名和回滚流程。

extension_control_path 完成了扩展文件外置所需的机制,但“文件可独立交付”不等于“扩展可脱离服务器运行”。对 pgvector、PostGIS 这类按需加载扩展,容器化能带来最完整的收益;对 Hook 和后台 Worker 扩展,价值主要是独立发布;对复制或分布式系统扩展,真正的耦合仍位于共享内存、WAL、进程树和拓扑之中。先识别扩展改变了服务器的哪些部分,再决定是否增加一层镜像管理,通常比单纯追求容器化更可靠。


相关推荐