跳转到主要内容

这是本节的多页打印视图。 .

返回本页常规视图.

工具箱

扩展 pgcli 能力的独立工具,无需 PostgreSQL 实例

在核心实例管理之外扩展 pgcli 能力的工具与使用指南。

  • ZFS 存储 — 安装 ZFS 并管理 pool、dataset 和快照
  • PGMQ 消息队列 — 在 PostgreSQL 内部实现持久化消息队列
  • SQLite + Litestream — 管理宿主机 SQLite 数据库并持续备份到 S3
  • TimescaleDB — 在 PostgreSQL 上使用时序 hypertable 和 time_bucket 分析

1 - ZFS 存储管理

在 pgcli 中安装 ZFS 并管理 pool、dataset 和 snapshot

pg zfs 把一台 Linux 主机变成 ZFS 存储服务器:安装软件包、检查磁盘、创建带冗余的 pool,并管理 dataset 和 snapshot。它直接与内核交互,不读写 pg.yaml,也不依赖任何 PostgreSQL 实例。

命令按“资源 → 动作”组织:pg zfs pool create、pg zfs dataset create、 pg zfs snapshot rollback。没有封装的能力可通过 pg zfs raw 原样透传。

支持的系统

pg zfs install 从 /etc/os-release 识别发行版,并自动准备软件源后完成安装:

发行版 ZFS 来源 实测版本
Debian 12 (bookworm) Debian contrib,DKMS 2.1.11
Debian 13 (trixie) Debian contrib,DKMS 2.3.9
Ubuntu 24.04 (noble) Ubuntu universe 2.2.2,使用 Ubuntu 内核模块包
Ubuntu 26.04 Ubuntu universe 2.4.1
Fedora 44 OpenZFS release RPM,DKMS 2.4.4
Rocky Linux 10 OpenZFS release RPM + EPEL,DKMS 2.2.11

这些是安装器测试时上游仓库提供的版本,实际安装到的版本由仓库决定; pg zfs status 总会打印已安装的用户态与内核模块版本。

其他发行版只会被识别并给出手动安装指引,不会自动安装。仅支持 Linux。

安装

pg zfs install

命令零参数、幂等:

  1. 检查权限(root,或可用的 sudo -n)。
  2. 自动启用所需软件源(Debian contrib、Ubuntu universe、 Fedora/Rocky 的 OpenZFS release 仓库)。
  3. 安装 ZFS、内核头文件/模块和 ZED 守护进程。
  4. 加载模块并把 ZFS 的 systemd 单元设为开机自启。
  5. 打印用户态与内核模块版本。

在健康的机器上重复执行是 no-op。安装不提供 --version、--backports、 --force 等开关——需要特定 ZFS 版本时请用发行版包管理器升级,之后交由 pg zfs 管理。

随时查看状态:

pg zfs status
OS:            Debian GNU/Linux 12 (bookworm)
Family:        debian
ZFS:           installed
Userland:      zfs-2.1.11-1+deb12u1
Kernel module: 2.1.11-1+deb12u1 (loaded)
Kernel:        6.1.0-54-cloud-amd64
Privilege:     root
Pools:         none
Secure Boot:   disabled

RAID 级别与 ZFS 的对应关系

ZFS 没有标准的 RAID2 / RAID3 级别,对应关系如下:

--type ZFS 布局 冗余 类比
stripe(raid0) 条带 vdev 无 RAID0
mirror(raid1) 镜像 1 份以上副本 RAID1
raidz1 单校验 1 块盘 RAID5
raidz2(raid2) 双校验 2 块盘 RAID6
raidz3(raid3) 三校验 3 块盘 三校验 RAID

为方便起见也接受 raid0/raid1/raid2/raid3 别名。

快速开始

# 1. 安装 ZFS
pg zfs install

# 2. 查看哪些磁盘可用
pg zfs disk list

# 3. 两块整盘 -> 自动创建 mirror
pg zfs pool create tank /dev/vdb /dev/vdc

# 4. 确认 pool 在线
pg zfs pool list

# 5. 为备份创建 dataset
pg zfs dataset create tank/backups

# 6. 升级前创建快照
pg zfs snapshot create tank/backups@before-upgrade

# 7. 升级失败时回滚
pg zfs snapshot rollback tank/backups@before-upgrade

pool 挂载在 /tank,dataset 挂载在 /tank/backups。回滚会丢弃快照之后的所有 改动,执行前需要确认。

复用旧 pool 的磁盘

ZFS 会在盘上保留 vdev 分区和标签,export 不会清除它们,因此安全校验会拒绝 这些盘。创建新 pool 前先销毁旧 pool 并清理残留:

# 1. 旧 pool 的数据不再需要时,先导入再销毁
pg zfs pool import fast
pg zfs pool destroy fast --force

# 2. 清除 ZFS 标签和分区表
pg zfs raw zpool labelclear -f /dev/vdd
pg zfs raw zpool labelclear -f /dev/vde
wipefs -a /dev/vdd /dev/vde

# 3. 创建新 pool
pg zfs pool create tank /dev/vdd /dev/vde

如果整盘 labelclear 报错,改清成员分区(/dev/vdd1、/dev/vde1)。旧 pool 已被销毁时跳过第 1 步。

磁盘安全

pg zfs disk list 会列出每块整盘的挂载点、ZFS 归属以及 pgcli 是否认为它可以安全 交给 ZFS 使用。

创建 pool 时拒绝:

  • 根盘,或带有已挂载分区的磁盘;
  • 分区、loop、ROM、CD-ROM 设备;
  • 带有文件系统(ext4、xfs、swap、LVM、LUKS、mdraid 等)的磁盘;
  • 已有 ZFS 标签的磁盘;
  • 只读磁盘;
  • 重复设备(包括通过不同路径指向的同一块盘);
  • 已被其他 pool 使用的磁盘。

--force 只跳过确认提示,不会映射为 ZFS 的 -f,也不绕过任何安全检查。

创建 pool

自动布局

位置参数的数量决定布局:

盘数 布局
1 stripe(告警:无冗余)
2 mirror
3 raidz1
4 及以上 raidz2
pg zfs pool create tank /dev/vdb /dev/vdc              # mirror
pg zfs pool create data /dev/vdb /dev/vdc /dev/vdd     # raidz1
pg zfs pool create bulk /dev/vdb /dev/vdc /dev/vdd /dev/vde   # raidz2

用 --type 覆盖:

pg zfs pool create fast /dev/vdb /dev/vdc /dev/vdd /dev/vde --type mirror
pg zfs pool create safe /dev/vdb /dev/vdc /dev/vdd /dev/vde /dev/vdf --type raidz3

同一 vdev 内请尽量使用容量一致的磁盘(性能也应相近)。ZFS 不会拒绝混合容量, 但 vdev 的可用容量由最小的成员盘决定,大盘多出的空间会被浪费。同一 pool 的不同 vdev 之间可以容量不同。

多个 vdev

使用可重复的 --data 分组,并显式给出 --type。每个分组是一个 vdev,pool 在 这些 vdev 之上做条带:

# 两个 mirror vdev(条带化镜像)
pg zfs pool create fast --type mirror \
  --data /dev/vdb,/dev/vdc --data /dev/vdd,/dev/vde

# 两个 raidz2 vdev
pg zfs pool create bulk --type raidz2 \
  --data /dev/vdb,/dev/vdc,/dev/vdd,/dev/vde \
  --data /dev/vdf,/dev/vdg,/dev/vdh,/dev/vdi

--type 作用于每个 --data 组内部,而不是 pool 整体:每个组建成该布局的一个 vdev,pool 在这些 vdev 之上条带。所有组共用同一个 --type;pg zfs 不支持 一次创建中混用 mirror、raidz2 等布局,混合拓扑需使用 pg zfs raw zpool create。

pool 容量为各 vdev 可用容量之和;冗余按 vdev 计算,任一 vdev 失去超出其容错能力 的磁盘都会导致整个 pool 不可用。

SLOG、special、cache 与热备

pg zfs pool create tank /dev/vdb /dev/vdc \
  --type mirror \
  --special /dev/vdd,/dev/vde \
  --log /dev/vdf,/dev/vdg \
  --cache /dev/vdh \
  --spare /dev/vdi

--special 跟随 --type(这里是 mirror)。--log 两块及以上自动 mirror,单块为 stripe 并给出告警。--cache 与 --spare 接受独立的设备列表。

同一块盘只能出现在 data、special、log、cache、spare 中的一处。

建池后也可以用 pool add 追加热备盘:

pg zfs pool add tank --class spare /dev/vdi

默认属性

除非指定 --no-defaults:

对象 属性 值
pool ashift 12
pool autoexpand on
pool autotrim 所有数据盘均为非旋转盘且支持 discard 时为 on
根 dataset compression zstd
根 dataset atime off
根 dataset xattr sa
根 dataset acltype posixacl
根 dataset dnodesize auto

pool 属性用 --prop/-p 覆盖(对应 zpool -o),根 dataset 属性用 --fs-prop/-P 覆盖(对应 zpool -O):

pg zfs pool create tank /dev/vdb /dev/vdc \
  --prop ashift=13 --fs-prop compression=lz4 --fs-prop atime=on

-o 是 pgcli 的全局输出参数,所以 pool/dataset 属性使用 -p/-P。在 pg zfs raw 内部,ZFS 原生参数照常可用。

管理 pool

pg zfs pool list
pg zfs pool status tank
pg zfs pool status --brief          # 只显示有问题的 pool
pg zfs pool scrub tank
pg zfs pool scrub tank --stop

扩容与修复:

# 增加一个 mirror vdev
pg zfs pool add tank /dev/vdd /dev/vde --type mirror

# 把 /dev/vdd 附加到 /dev/vdc 所在的 mirror(2 盘扩成 3 盘)
pg zfs pool attach tank /dev/vdc /dev/vdd

# 再缩回 2 盘
pg zfs pool detach tank /dev/vdd

# 用新盘 /dev/vde 替换故障盘 /dev/vdc
pg zfs pool replace tank /dev/vdc /dev/vde

# 临时下线/上线
pg zfs pool offline tank /dev/vdc
pg zfs pool online tank /dev/vdc

attach 针对 mirror vdev。给 raidz vdev 增加磁盘(raidz expansion)需要 OpenZFS 2.3 及以上;Debian 12(2.1)和 Ubuntu 24.04(2.2)不支持。新版本上可用 pg zfs raw zpool attach 操作。

卸载与彻底删除 pool

pg zfs pool export 会卸载所有 dataset 并把 pool 从主机上分离,但数据仍完整 保存在成员盘上:

pg zfs pool export tank

已 export 的 pool 不会在开机时自动导入。重新接管时先列出可导入的 pool,再导入:

pg zfs pool import                  # 列出可导入的 pool
pg zfs pool import tank
pg zfs pool import tank --dir /dev/disk/by-id --no-mount

pg zfs pool destroy 会删除 pool、所有 dataset 和全部数据:

pg zfs pool destroy tank

destroy 只能作用于已导入的 pool。如果 pool 已 export,需要先导入再销毁:

pg zfs pool import tank
pg zfs pool destroy tank

不加 --force 时这两个命令都会要求确认;EOF 和默认答案均为“否”。非交互自动化 必须显式传 --force。--force 只跳过确认,绝不会映射为 ZFS 的 -f,也不绕过 任何安全校验。export 可逆,destroy 不可逆。

持久化与开机自动导入

pool 和 dataset 默认在重启后自动恢复,无需额外配置:

  • pg zfs install 会启用 zfs-load-module、zfs-import-cache、zfs-mount 和 zfs-zed(单元存在时)。
  • zpool create 会把 pool 记录到 /etc/zfs/zpool.cache;开机时 zfs-import-cache 据此导入。
  • ZFS 不使用 /etc/fstab。每个 dataset 的 mountpoint 是 ZFS 属性,开机时 由 zfs-mount 完成挂载。pool 的根 dataset 挂载在 /<pool>,子 dataset 挂载在其下(例如 /fast/data)。
  • pool 状态保存在成员盘上,异常断电或非正常关机后数据仍可导入。

检查配置:

systemctl is-enabled zfs-load-module zfs-import-cache zfs-mount zfs-zed
pg zfs raw zpool get cachefile fast
pg zfs raw zfs get mountpoint fast

重启后 pg zfs pool list 应能看到该 pool,findmnt /fast 应显示已挂载。

真实硬件上建议用 /dev/disk/by-id/... 等稳定路径创建 pool。/dev/sdX 名称可能在重启后变化,设备找不到时 pool 不会导入。把磁盘移到其他主机前先执行 pg zfs pool export tank,对方即可用 pg zfs pool import tank 接管。

Dataset 与 snapshot

pg zfs dataset create tank/data
pg zfs dataset create tank/a/b/c --parents
pg zfs dataset create tank/logs --prop quota=50G --prop recordsize=128K

pg zfs dataset list
pg zfs dataset list tank/data --props quota,recordsize,compression

pg zfs dataset set tank/data compression=lz4 quota=100G

pg zfs snapshot create tank/data@snap1
pg zfs snapshot create tank/data@weekly --recursive
pg zfs snapshot list tank/data
pg zfs snapshot rollback tank/data@snap1
pg zfs snapshot destroy tank/data@snap1

pg zfs dataset clone tank/data@snap1 tank/clone --prop compression=off
pg zfs dataset destroy tank/clone
pg zfs dataset destroy tank/a --recursive

带子 dataset 的 dataset 必须加 --recursive 才能销毁。

回滚

pg zfs snapshot rollback tank/data@snap1 把 dataset 恢复到快照记录的状态, 之后的所有改动都会被丢弃。如果存在更新的快照,ZFS 会拒绝回滚;加 --recursive 会先删除这些更新的快照。依赖该快照的 clone 也会阻止回滚——先删除 clone,或用 pg zfs raw zfs rollback -R(会一并删除依赖的 clone)。非交互运行需加 --force 跳过确认。

# 升级前创建快照
pg zfs snapshot create tank/data@before-upgrade

# 回滚(需要确认)
pg zfs snapshot rollback tank/data@before-upgrade

# 非交互自动化
pg zfs snapshot rollback tank/data@before-upgrade --force

# 存在更新的快照:--recursive 会一并删除
pg zfs snapshot rollback tank/data@before-upgrade --recursive --force

# clone 阻止回滚:-R 会一并删除依赖的 clone
pg zfs raw zfs rollback -R tank/data@before-upgrade

克隆

pg zfs dataset clone tank/data@snap1 tank/clone 从快照创建一个新的可写 dataset。未改动的数据块按写时复制共享,不会复制整份数据。用 --prop 设置新 dataset 的属性:

# 从快照创建可写副本
pg zfs dataset clone tank/data@snap1 tank/clone

# 为副本设置属性
pg zfs dataset clone tank/data@snap1 tank/clone \
  --prop compression=off --prop mountpoint=/tank/clone

# 提升 clone 断开依赖,之后可删除原快照
pg zfs raw zfs promote tank/clone
pg zfs snapshot destroy tank/data@snap1
pg zfs dataset destroy tank/clone --force

clone 依赖的快照在 clone 存在期间不能销毁。需要删除旧快照时,先用 pg zfs raw zfs promote tank/clone 提升 clone,使其独立于原 dataset。

raw 透传

pg zfs raw zpool status -v
pg zfs raw zpool set comment="backup pool" tank
pg zfs raw zfs create -o compression=off tank/scratch
pg zfs raw zpool create -f -O compression=gzip temp /dev/vdb

raw 绕过所有安全校验和确认,是未封装参数的逃生通道——请像以 root 身份直接运行 zpool/zfs 一样谨慎。二进制管道也可以工作,例如:

pg zfs snapshot create tank/data@send
pg zfs raw zfs send tank/data@send | pg zfs raw zfs receive tank/recv

权限模型

所有会调用 zpool 或 zfs 的命令都会先检查权限:

  • root:直接执行;

  • 非 root 且 sudo -n 可用:所有 ZFS 命令通过 sudo -n 执行;

  • 否则立即失败:

    ZFS management requires root: run "sudo pg zfs ..." or configure passwordless sudo for this user

pgcli 从不弹出 sudo 密码提示。pg zfs status 和 pg zfs disk list 在无权限时仍会 输出静态信息,并把 pool 详情标记为不可用。

命令参考

命令 用途
pg zfs install 安装 ZFS(零参数、幂等)
pg zfs status 发行版、软件包、模块与 pool 概览
pg zfs disk list [--all] 整盘、挂载、ZFS 归属与安全性
pg zfs pool create NAME DEVICE... 创建 pool
pg zfs pool list 列出 pool
pg zfs pool status [NAME] [--brief] 详细状态
pg zfs pool add NAME DEVICE... 增加 vdev
pg zfs pool attach/detach NAME DEVICE [NEW] 扩大/缩小 mirror
pg zfs pool replace NAME OLD NEW 替换磁盘
pg zfs pool offline/online NAME DEVICE 下线/上线磁盘
pg zfs pool export/import [NAME] 在系统间迁移 pool
pg zfs pool scrub NAME [--stop] 校验数据完整性
pg zfs pool destroy NAME 销毁 pool 及全部数据
pg zfs dataset create/list/set/clone/destroy dataset 生命周期
pg zfs snapshot create/list/rollback/destroy snapshot 生命周期
pg zfs raw zpool/zfs ARGS... 绕过封装的透传

故障排查

pg zfs install 找不到软件包。 检查发行版归档源是否可达、contrib/universe 是否已启用。安装器只修改发行版官方归档源(绝不动第三方源),并在每个被修改的文件旁 留下 *.pgcli.bak。

DKMS 构建失败。 确认当前内核的头文件已安装(Debian/Ubuntu 为 linux-headers-$(uname -r),Fedora/Rocky 为 kernel-devel-$(uname -r))。安装器 会给出构建日志位置;它不会重启或更换内核。内存很小的 VM 请先加 swap。

Secure Boot 已开启。 DKMS 模块需要签名。pgcli 只报告该情况,不会自动登记密钥; 请用 MOK 签名模块,或在测试机上关闭 Secure Boot。

模块未加载。 执行 modprobe zfs 后运行 pg zfs status。安装器会启用 zfs-load-module、zfs-import-cache、zfs-mount 和 zfs-zed,使 pool 开机 自动导入。

磁盘因分区或旧标签被拒绝。 如果确认磁盘空闲,清理残留:

pg zfs raw zpool labelclear -f /dev/vdb
wipefs -a /dev/vdb

在 OpenZFS 2.2 及更新版本上,若标签位于 ZFS 成员分区内,清理整盘会报 failed to clear label for /dev/vdb。此时改为清理该分区,再清分区表:

pg zfs raw zpool labelclear -f /dev/vdb1
wipefs -a /dev/vdb

绝不要对已导入 pool 的成员盘这样做——pg zfs disk list 会显示所属 pool,安全检查 也会拒绝这类磁盘。

SELinux(Fedora/Rocky)。 ZFS 挂载点使用标准文件上下文。若服务无法访问某个 dataset,请查看 ausearch/journalctl 中的 AVC 拒绝记录,而不是全局关闭 SELinux。

重启后 pool 没有自动导入。 以 root 运行 systemctl status zfs-import-cache 和 zpool import。用 /dev/sdX 名称创建的 pool 会受设备重命名影响;真实硬件上 建议使用稳定路径(/dev/disk/by-id/...)创建 pool。

2 - PGMQ 消息队列

用 pgmq 扩展在 PostgreSQL 内部实现持久化消息队列

pgmq 把 PostgreSQL 变成持久化消息队列:不需要 Redis、RabbitMQ 或后台进程, 只有 pgmq schema 下的一组表和 SQL 函数。生产者发送 JSON 消息,消费者按可见性 超时读取,消息在被显式删除或归档之前一直保留在队列里。

安装

pgcli 从 Pigsty 仓库安装扩展包,并在实例的默认数据库执行 CREATE EXTENSION IF NOT EXISTS pgmq:

pg extension install pgmq -i pg01

同一实例的其他数据库需要单独创建扩展:

pg psql -i pg01 -- -d appdb
CREATE EXTENSION IF NOT EXISTS pgmq;

查看扩展版本:

pg exec -i pg01 "SELECT extversion FROM pg_extension WHERE extname = 'pgmq'"

快速开始

-- 创建队列;对已存在的队列重复执行不会报错
SELECT pgmq.create('jobs');

-- 发送一条消息,返回消息 id
SELECT pgmq.send('jobs', '{"task": "resize", "image_id": 42}');

-- 读取最多 10 条,60 秒内对其他消费者不可见
SELECT msg_id, read_ct, message
FROM pgmq.read('jobs', 60, 10);

-- 确认一条消息
SELECT pgmq.delete('jobs', 1);

通过 pgcli 执行同样的语句:

pg exec -i pg01 "SELECT pgmq.send('jobs', '{\"task\": \"resize\"}')"
pg psql -i pg01

队列在磁盘上的形态

每个队列都是 pgmq schema 里的一张表:

对象 用途
pgmq.q_<name> 在队消息
pgmq.a_<name> pgmq.archive 写入的归档表
pgmq.meta list_queues 和 metrics 使用的队列表册

一条在队消息包含 msg_id、read_ct(投递次数)、enqueued_at、vt (再次可见时间)和 message jsonb。较新版本还增加了 last_read_at 和 headers。

消息就是表里的行,因此 pgmq.send 参与事务:随所在语句一起提交或回滚。 队列流量也是普通表流量——高频 send/delete 会像其他高写入表一样产生膨胀。

发送消息

SELECT pgmq.send('jobs', '{"task": "resize"}');

-- 30 秒后才可被读取
SELECT pgmq.send('jobs', '{"task": "cleanup"}', 30);

-- 批量发送:一条语句、一次原子写入
SELECT pgmq.send_batch('jobs', ARRAY[
  '{"task": "a"}',
  '{"task": "b"}',
  '{"task": "c"}'
]::jsonb[]);

send 返回新的 msg_id,send_batch 为每条消息各返回一个 id。批量场景应使用 send_batch,而不是循环调用 send——它只有一次网络往返,且整批原子写入。

消息必须是合法 JSON,存储类型为 jsonb:键会重排、重复键会被去掉。不要把大块 二进制内容放进队列。

较新版本支持 headers jsonb 参数,可携带 trace id、FIFO 分组键等元数据。

读取与确认

SELECT msg_id, read_ct, enqueued_at, vt, message
FROM pgmq.read('jobs', 60, 10);

read(queue, vt, qty) 最多返回 qty 条可见消息,并在 vt 秒内让它们对其他 消费者不可见。读取不会删除消息,每条都必须显式确认:

SELECT pgmq.delete('jobs', 42);                -- 单条
SELECT pgmq.delete('jobs', ARRAY[42, 43, 44]); -- 批量

pgmq.pop('jobs') 读取并删除一步完成;只有在消费者崩溃时丢一条在途消息可以接受时 才使用它。

长期运行的消费者不要用紧凑轮询:

SELECT msg_id, message
FROM pgmq.read_with_poll('jobs', 60, 10, 5, 100);

它最多等待 max_poll_seconds(5 秒),每 poll_interval_ms(100 毫秒)检查一次, 有消息到达就立即返回。

可见性超时与重试

可见性超时是 PGMQ 的投递模型:

  1. read 把 vt 设为 now() + timeout,并递增 read_ct;
  2. 在 vt 之前,其他消费者看不到这条消息;
  3. delete 或 archive 将其移除。若消费者崩溃或没有及时确认,消息在 vt 到期后 重新可见,并被投递给另一个消费者。

因此跨故障场景下投递语义是 at-least-once(至少一次):vt 要大于最坏处理 耗时,消费者要保证幂等。长任务可以在不重新读取的情况下续期:

SELECT pgmq.set_vt('jobs', 42, 300);            -- 单条
SELECT pgmq.set_vt('jobs', ARRAY[42, 43], 300); -- 批量

read_ct 可以直接当重试计数器使用。

归档与死信

delete 是永久删除。archive 把消息移到 pgmq.a_<queue>,便于保留、审计或 重放:

SELECT pgmq.archive('jobs', 42);
SELECT pgmq.archive('jobs', ARRAY[42, 43]);
SELECT * FROM pgmq.a_jobs;

一个简单的死信模式是归档重试次数过多的消息:

-- 把已投递 5 次及以上的消息移入归档表
SELECT pgmq.archive('jobs', ARRAY(
  SELECT msg_id FROM pgmq.q_jobs WHERE read_ct >= 5
));

没有这类规则时,毒消息会被无限重投。

监控

SELECT * FROM pgmq.list_queues();
SELECT * FROM pgmq.metrics('jobs');
SELECT * FROM pgmq.metrics_all();

metrics 返回 queue_length、queue_visible_length、最新/最老消息的年龄以及 total_messages。较旧的扩展版本返回的列更少。

也可以直接查询队列表:

SELECT count(*) FROM pgmq.q_jobs;

SELECT msg_id, read_ct, enqueued_at, vt, message
FROM pgmq.q_jobs
ORDER BY msg_id DESC
LIMIT 5;

queue_length 持续增长或 oldest_msg_age_sec 不断变大时应当告警:要么消费者 挂了,要么消费速度跟不上。

维护

SELECT * FROM pgmq.list_queues();

SELECT pgmq.create_unlogged('scratch');                    -- 不写 WAL,崩溃后丢失
-- 分区队列依赖 pg_partman:pg extension install pg_partman -i pg01
SELECT pgmq.create_partitioned('events', '10000', '100000');
SELECT pgmq.purge_queue('jobs');                           -- 清空消息,保留队列
SELECT pgmq.drop_queue('jobs');                            -- 删除队列和归档表
  • create_unlogged 用崩溃安全性换写入速度,只用于可重新生成的数据。
  • create_partitioned 依赖 pg_partman 扩展。它避免超高吞吐队列把单表撑大; 旧分区会自动丢弃。
  • purge_queue 清空但保留队列;drop_queue 彻底删除。
  • 队列名最长 47 个字符。

普通队列在高频 send/delete 下会产生 dead tuple。确认 autovacuum 能跟上;写入量 很大时优先考虑归档代替删除,或使用分区队列。

权限

PGMQ 函数以调用者的权限执行。应用角色需要 schema、函数和队列表的访问权限:

GRANT USAGE ON SCHEMA pgmq TO app;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA pgmq TO app;
GRANT SELECT, INSERT, UPDATE, DELETE ON pgmq.q_jobs TO app;
GRANT SELECT, INSERT ON pgmq.a_jobs TO app;   -- 仅归档时需要

pgmq.create 应以队列所有者身份执行;建表权限通常不应授予应用角色。新建队列需要 单独授权,或预先配置 default privileges。

函数参考

函数 用途
pgmq.create(name) 创建队列
pgmq.create_unlogged(name) 创建 unlogged 队列
pgmq.create_partitioned(name, ...) 创建分区队列
pgmq.send(name, msg [, delay]) 发送单条消息,返回 msg_id
pgmq.send_batch(name, msgs jsonb[]) 原子批量发送
pgmq.read(name, vt, qty) 读取并隐藏消息
pgmq.read_with_poll(name, vt, qty, ...) 长轮询读取
pgmq.pop(name [, qty]) 读取并删除
pgmq.delete(name, id) / pgmq.delete(name, ids[]) 确认并移除消息
pgmq.archive(name, id) / pgmq.archive(name, ids[]) 移入归档表
pgmq.set_vt(name, id(s), seconds) 延长可见性超时
pgmq.list_queues() / pgmq.metrics(name) 查看队列状态
pgmq.purge_queue(name) 删除全部消息,保留队列
pgmq.drop_queue(name) 删除队列及其归档表

故障排查

extension "pgmq" is not available。 实例镜像里没有扩展包,或该数据库没有 对应构建。执行 pg extension install pgmq -i <instance>,并在目标数据库中创建 扩展。

消息被重复投递。 处理时间超过 vt,或消费者在 delete 前退出。增大 vt、 在长任务中调用 set_vt,并保证处理逻辑幂等。

队列长度不下降。 消费者失败或太慢。检查 read_ct、应用日志和 metrics().oldest_msg_age_sec。PGMQ 会无限重投失败消息,除非归档或删除它们。

性能随时间下降。 普通队列就是普通表。检查 dead tuple 与 autovacuum,批量 发送和读取;持续高吞吐场景考虑 create_partitioned。

permission denied for schema pgmq。 给应用角色授予 schema 的 USAGE、 函数的 EXECUTE,以及每个队列表的相应表权限。

3 - SQLite + Litestream

在宿主机上管理 SQLite 数据库,并用 Litestream 将 WAL 变更复制到 S3 兼容对象存储

pg sqlite 在 Linux 宿主机上管理 SQLite 数据库,并可选地用 Litestream 把 WAL 变更持续复制到 S3 兼容对象存储。 SQLite 直接以宿主机进程运行:不使用容器、不引入 Go SQLite driver,也不依赖 cgo。 所有 SQL 都由 sqlite3 命令行执行;每个数据库对应一个独立的 Litestream 后台进程。

复制是异步的(默认 sync interval 下 RPO 约 1 秒)。它属于持续备份,不是高可用: SQLite 仍然只有一个写入者,故障切换意味着要在别处恢复备份。

目前仅支持 Linux。

工作原理

  • 每个受管数据库在 pg.yaml 中有一条记录,并在 <base>/sqlite/<name>/ 下有独立 的状态目录。
  • pg sqlite create 会启用 journal_mode=WAL,该设置持久化在数据库文件头中。
  • pg sqlite replication enable 生成专属的 litestream.yml(权限 0600),并在 后台启动 litestream replicate。Litestream 跟随 WAL,把 LTX 对象上传到指定 bucket。
  • S3 凭据只写入该 litestream.yml,绝不会进入 pg.yaml,任何 pg sqlite 命令也不会打印它。

安装

pg sqlite install

命令是幂等的,会安装两个宿主机二进制:

  1. 如果 sqlite3 不在 PATH 上,则通过发行版包管理器安装(apt-get、dnf、yum、 pacman、zypper 或 apk)。只有这一步可能需要 root;pgcli 使用免密 sudo -n,不会交互式索要密码,失败时会给出明确提示。
  2. Litestream 0.5.17,从 GitHub 下载并校验官方 SHA256。可写时安装到 /usr/local/bin,否则安装到 ~/.local/bin。

常用参数:

pg sqlite install --install-dir /opt/bin      # 指定 Litestream 安装目录
pg sqlite install --force-litestream          # 强制重新下载受支持版本

Litestream 0.3.x 与生成的配置不兼容;安装器会忽略 PATH 上的旧版本,直接安装 0.5.x。

创建与管理数据库

# 默认路径:<base>/sqlite/appdb/appdb.db
pg sqlite create appdb
pg sqlite create analytics --path /srv/data/analytics.db

pg sqlite list
pg sqlite info appdb
pg sqlite info appdb --integrity full

pg sqlite exec appdb "CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT)"
pg sqlite exec appdb "INSERT INTO items (name) VALUES ('first')"

# 从 stdin 读取 SQL(不受命令行长度限制)
pg sqlite exec appdb - <<'SQL'
INSERT INTO items (name) VALUES ('second');
INSERT INTO items (name) VALUES ('third');
SQL

pg sqlite shell appdb
pg sqlite shell appdb -- -header -column

create 不会截断已有文件:指向一个已存在的 SQLite 数据库时会接管该文件、启用 WAL,并保留原有内容。--path 必须是绝对路径,且只移动数据库文件——配置、日志、 pid 和 Litestream 本地状态始终留在 <base>/sqlite/<name>/ 下。

info 读取 journal_mode、page size/count 和 freelist,默认执行 PRAGMA quick_check。大库可用 --integrity none 跳过检查,或 --integrity full 执行完整 integrity_check。

remove 只注销数据库并停止复制,数据库文件保留。加 --clean-data 才会删除 数据库、WAL 附属文件、Litestream 本地状态和日志(交互确认,--force 跳过)。 远端 S3 备份永远不会被自动删除。

启用 S3 复制

bucket 必须已经存在——Litestream 不会创建 bucket。请先用 mc mb、云控制台或 其他 S3 工具创建好。

本地 MinIO(测试用)

pgcli 可以直接用 MinIO addon 在宿主机上起一个对象存储:

# 安装名为 sqlite-s3 的 MinIO 实例;命令输出会打印 S3 API 端点。
# 端口空闲时默认端点为 http://127.0.0.1:9000。
pg addon install minio --name sqlite-s3

# root 用户默认为 admin,密码在安装时生成。
S3_SECRET=$(pg addon password minio --name sqlite-s3)

# 用内置的 mc 包装器注册端点并创建 bucket。
pg mc alias set local http://127.0.0.1:9000 admin "$S3_SECRET"
pg mc mb local/sqlite-backups

如果 9000 端口已被占用,请改用 pg addon list 中显示的实际端点。然后把复制指向 这个本地存储:

pg sqlite replication enable appdb \
  --s3-endpoint http://127.0.0.1:9000 \
  --s3-bucket sqlite-backups \
  --s3-access-key admin \
  --s3-secret-key "$S3_SECRET" \
  --s3-uri-style path

其他 MinIO 或 S3 兼容存储使用相同的 flags,替换成自己的端点和凭据:

pg sqlite replication enable appdb \
  --s3-endpoint http://127.0.0.1:9000 \
  --s3-bucket sqlite-backups \
  --s3-access-key minioadmin \
  --s3-secret-key minioadmin \
  --s3-uri-style path

AWS S3,使用默认凭据链(命令行不传 key):

pg sqlite replication enable appdb \
  --s3-bucket my-org-sqlite-backups \
  --s3-region eu-west-1 \
  --s3-uri-style virtual-host
参数 默认值 说明
--s3-bucket 必填 必须已存在
--s3-endpoint AWS 默认 完整 http(s)://host[:port],不能带路径或凭据
--s3-path sqlite/<name> 对象 key 前缀;前导 / 会被去掉
--s3-region us-east-1
--s3-access-key / --s3-secret-key 默认凭据链 必须同时提供或同时省略
--s3-uri-style path path 或 virtual-host
--s3-ca-file 无 通过 SSL_CERT_FILE 传给 Litestream,用于私有 CA
--s3-skip-verify false 关闭 TLS 校验(自签名端点)
--sync-interval 1s WAL 上传间隔
--snapshot-interval 24h 全量快照间隔
--retention 24h 快照保留时间
--no-start false 只写配置,不启动 Litestream
--force false 覆盖已有配置

enable 会先确认数据库处于 WAL 模式,解析可用的 0.5.x Litestream(或用 --litestream-binary PATH 指定),以 0600 写入 litestream.yml,清理本地 Litestream 旧状态,启动进程并检查它没有立即退出。若立即退出,会打印日志最后一行 并报错。

在复制运行中,用完全相同的非敏感配置再次执行 enable 是幂等的。配置不同则默认 拒绝;--force 会停止旧进程、重写配置、reset 本地状态并重启。

凭据存放位置

路径 内容 权限
<base>/pg.yaml 名称、路径、binary/version、endpoint、bucket、前缀、region、URI style、CA file、skip-verify —— 不含 key 0644
<base>/sqlite/<name>/<name>.db 数据库文件 应用默认
<base>/sqlite/<name>/litestream.yml 完整 S3 配置,含 access/secret key 0600
<base>/sqlite/<name>/litestream.log 进程日志 0644
<base>/sqlite/<name>/litestream.pid 运行中的 pid 0644
<base>/sqlite/<name>/.<db>-litestream/ Litestream 本地 LTX 状态 应用默认

备份或复制 litestream.yml 时要像对待其他凭据文件一样谨慎。 replication disable 会删除它,因此下次 enable 需要重新提供凭据。

复制日常操作

pg sqlite replication status appdb
pg sqlite replication status              # 所有数据库
pg sqlite replication logs appdb -n 100
pg sqlite replication logs appdb -f
pg sqlite replication stop appdb          # 最终 sync,最长 30s
pg sqlite replication start appdb
pg sqlite replication disable appdb       # 停止并删除 litestream.yml

status 结合进程状态与 litestream status -json(本地事务 id 和 WAL 大小), 并显示日志中最近的错误行。即使状态查询失败,仍会展示进程状态并正常退出。

检查远端备份文件

pg mc 是内置 MinIO 客户端的包装器,mc 自己的 flags 必须写在 -- 之后。使用 「本地 MinIO」示例中创建的 alias,可以列出 Litestream 上传到 --s3-path 下的对象:

pg mc ls local/sqlite-backups/sqlite/appdb -- --recursive

有数据时类似这样:

0000/0000000000000001-0000000000000001.ltx
0000/0000000000000002-0000000000000002.ltx
0001/0000000000000001-0000000000000002.ltx
0009/0000000000000001-0000000000000002.ltx

0000/ 存放逐事务的 WAL 帧;编号更大的层级(0001/、0009/ 等)是合并、 压缩后的文件。刚 enable 完时前缀为空只说明还没有写入被同步——插入一行数据, 等待至少一个 --sync-interval(默认 1s)后再列一次。replication status 显示的本地事务 id 应与这些文件名中的最大 txid 一致。

disable 保留数据库、本地 LTX 状态和日志,只删除 pg.yaml 中的 replication 块并删除 litestream.yml。远端备份不受影响。

开机自启

Litestream 是独立的宿主机进程,不会在主机重启后自行恢复。把数据库注册到 pgcli 的开机服务:

pg autostart enable --sqlite --name appdb
pg autostart status

--name 必填(SQLite 没有默认数据库名)。复制必须已经启用且 litestream.yml 仍然存在——开关命令会检查这两点,否则报错并提示重新 enable。开机时 pg start --autostart 会在对象存储 addon 之后重启 Litestream,因此同机的 MinIO/silo/rustfs/garage/SeaweedFS 已经先开始监听。开机路径只做启动:绝不 重写 litestream.yml,也不修改数据库文件。

pg autostart disable --sqlite --name appdb

pg sqlite replication disable 会自动清除该标志。systemd/launchd 服务细节见 开机自启。

恢复

Litestream 可以恢复最新状态或指定时间点。支持两种模式:

# 恢复到另一个路径;复制继续运行
pg sqlite replication restore appdb --target /tmp/appdb-restored.db

# 按 RFC3339 时间戳恢复(UTC,使用 Z 后缀)
pg sqlite replication restore appdb --target /tmp/appdb-old.db \
  --timestamp 2026-01-01T12:00:00Z --integrity full

# 按 Litestream 事务 id 恢复(十六进制,可带 0x 前缀)
pg sqlite replication restore appdb --target /tmp/appdb-tx.db --txid 00000000000000ff

# 只展示将要恢复的内容,不写任何文件
pg sqlite replication restore appdb --dry-run

--timestamp 与 --txid 互斥;都不传则恢复最新状态。--timestamp 表示绝对 时间点,请写成带 Z 后缀的 UTC 时间(如 2026-01-01T12:00:00Z),避免恢复 时间点受宿主机时区影响。--target 不能与 --restart 同用;目标已存在时必须加 --force,此时旧的 target 及其 -wal/-shm 会先被移入 <target>.pre-restore-<UTC>/。

时间点恢复演练

在做出想撤销的变更之前,先记录一个事务边界:

# 当前本地事务 id(输出中的 `local: ... txid=...` 行)
pg sqlite replication status appdb

# 或查看 S3 上已上传的 LTX 范围
pg mc ls local/sqlite-backups/sqlite/appdb -- --recursive

LTX 对象名为 <min-txid>-<max-txid>.ltx。按 --txid 恢复会包含该事务及之前 的状态。记录变更前的 txid(或一个安全早于变更的 RFC3339 UTC 时间戳),然后恢复 一份副本并验证:

pg sqlite replication restore appdb \
  --target /tmp/appdb-pitr.db \
  --txid 0000000000000004 \
  --integrity full

sqlite3 -readonly /tmp/appdb-pitr.db \
  "SELECT COUNT(*) FROM items; PRAGMA journal_mode;"

时间戳方式流程相同:

pg sqlite replication restore appdb \
  --target /tmp/appdb-pitr-ts.db \
  --timestamp 2026-01-01T12:00:00Z \
  --integrity full

不确定时先加 --dry-run:它只打印将要使用的事务范围,不写任何文件。恢复到 --target 不会停止复制,因此可以在检查副本的同时继续处理线上流量。建议再恢复 一次最新状态(不传 --timestamp/--txid),确认较新的数据存在;验证无误后, 再用原地恢复把副本正式替换上线。

原地恢复

不传 --target 时会替换配置中的数据库:

pg sqlite replication restore appdb --force

流程刻意保守:

  1. 停止 Litestream(会执行最终 sync);其他打开该数据库的应用必须提前自行停止。
  2. 当前数据库及 WAL 附属文件移入 <db>.pre-restore-<UTC>/(不加 --force 时 会先确认)。
  3. 备份先恢复到数据库同目录下的临时目录,因此恢复失败也不会影响已经保留的旧副本。
  4. 检查恢复出的数据库,必要时切回 WAL,然后通过同一文件系统内的原子 rename 移动到最终位置。
  5. 重置 Litestream 本地状态;若 reset 失败,则删除旧的本地状态目录。
  6. 默认保持复制停止;--restart 会重新启动。

恢复后把复制重启到相同的 --s3-path,可能把恢复出的时间线与远端旧数据混在一起。 优先用新的 --s3-path 重新 enable;使用 --restart 前请确认理解旧时间线。

如果数据库文件完全丢失,原地恢复不需要 --force;但每个数据库的 litestream.yml 必须仍然存在。

WAL 与应用侧设置

CLI 只把 PRAGMA journal_mode=WAL 持久化到数据库文件中。以下设置按连接在应用侧 调整:

PRAGMA busy_timeout=5000;   -- 等待锁而不是立即失败
PRAGMA synchronous=NORMAL;  -- WAL 下的推荐值(在持久性上有所取舍)
PRAGMA wal_autocheckpoint=1000;

注意事项:

  • WAL 模式要求本地文件系统,不要把数据库放在 NFS 或 SMB 上。
  • 同一时刻仍然只有一个写入者。长写事务会阻塞其他写入者;长读事务会阻止 WAL checkpoint,可能让 WAL 无限增长。
  • Litestream 复制的是 WAL,不是应用一致快照。涉及多条语句的不变量请使用事务。

故障排查

litestream exited during startup —— 执行 pg sqlite replication logs <name> -n 50。常见原因是 bucket 不存在、凭据错误, 或宿主机无法访问 endpoint。

no Litestream 0.5.x binary found —— 执行 pg sqlite install。PATH 上的 0.3.x 会被报告但不会使用。

连接 MinIO/RustFS 的 TLS 错误 —— 纯 HTTP 端点要写完整 http://host:port;自签名 HTTPS 用 --s3-ca-file 或 --s3-skip-verify。

database ... uses journal_mode=delete —— 数据库不是由 pg sqlite create 创建的,或被其他工具改过模式。在没有其他连接时执行 PRAGMA journal_mode=WAL;,再重试 enable。

litestream.yml not found —— start、status 和 restore 都依赖 enable 写出的配置;请重新执行 enable(凭据不在 pg.yaml 中)。

限制

  • 仅支持 Linux;macOS 的 host-binary 路径未实现。
  • Litestream 是异步备份,不是同步复制或高可用。默认 RPO 约 1 秒, stop/disable 时的最终 sync 最长可能耗时 30 秒。
  • 每个数据库一个 Litestream 进程、一个 WAL 写入者。
  • --clean-data 和原地恢复具有破坏性;默认会确认,保留的 .pre-restore-* 目录需要手动清理。

4 - TimescaleDB

在 PostgreSQL 内使用 TimescaleDB hypertable 和 time_bucket 处理时序数据

TimescaleDB 是面向时序负载的 PostgreSQL 扩展。它把普通表变成 hypertable: 数据按时间自动切分成 chunk,应用仍然使用普通 SQL、索引和事务。

pgcli 安装的是 Pigsty 的 TSL 构建(TimescaleDB 2.31.0)。Hypertable、 time_bucket、chunk 管理、列存/压缩、连续聚合和保留策略均可使用。 TSL 允许免费内部使用,但禁止对外提供时序数据库服务。

使用场景

TimescaleDB 适合以时间为主要访问维度、以追加写入为主的数据:

  • IoT 与设备遥测 — 按设备采集读数、查询最新值和按时间窗口聚合。
  • 基础设施与应用监控 — CPU、内存、延迟、请求量等指标,用于看板和告警。
  • 工业与能源计量 — 高频传感器数据流,保留周期长。
  • 金融行情数据 — tick、报价和成交;用 time_bucket 和 first/last 生成 OHLC 类聚合。
  • 产品与事件分析 — 用户事件、会话和 API 调用,按最近时间窗口统计。
  • 智能家居与楼宇自动化 — 传感器历史数据和状态变化。

典型形态是 (entity_id, time) 的追加型数据:查询过滤最近一段时间,过期数据按 整个 chunk 删除。

不适合没有时间维度的 OLTP、键值/文档存储和大对象存储。TSL 构建包含压缩、 连续聚合和保留策略,但 TSL 不允许对外提供时序数据库服务。

安装

pg extension install timescaledb -i pg01 --auto-restart

pgcli 会使用 Pigsty 的 TSL 包构建扩展镜像,把 timescaledb 放到 shared_preload_libraries 的首位,重启 PostgreSQL,并在实例的默认数据库执行 CREATE EXTENSION IF NOT EXISTS timescaledb。

同一实例的其他数据库需要单独创建扩展:

pg psql -i pg01 -- -d appdb
CREATE EXTENSION IF NOT EXISTS timescaledb;

查看版本和许可证:

pg exec -i pg01 "SELECT extversion FROM pg_extension WHERE extname = 'timescaledb'"
pg exec -i pg01 "SHOW timescaledb.license"

Pigsty 包返回 timescale。

快速开始

1. 创建实例

pg create -i tsdb
pg start -i tsdb

pg create 写入配置并打印管理员密码;pg start 启动 PostgreSQL 和备份容器。 默认数据库为 tsdb_db。

2. 安装扩展

pg extension install timescaledb -i tsdb --auto-restart

pgcli 构建扩展镜像,预加载 timescaledb,重启 PostgreSQL,并在 tsdb_db 中 执行 CREATE EXTENSION IF NOT EXISTS timescaledb。

3. 创建 hypertable

连接实例:

pg psql -i tsdb

创建普通表并转换为 hypertable:

CREATE TABLE sensor (
    time      timestamptz NOT NULL,
    sensor_id text NOT NULL,
    value     double precision
);

SELECT create_hypertable('sensor', by_range('time', INTERVAL '1 day'));
CREATE INDEX ON sensor (sensor_id, time DESC);

也可以一步创建;此时使用默认 chunk 间隔,之后仍可调整:

CREATE TABLE sensor (
    time      timestamptz NOT NULL,
    sensor_id text NOT NULL,
    value     double precision
) WITH (tsdb.hypertable);

SELECT set_chunk_time_interval('sensor', INTERVAL '1 day');

chunk 间隔按查询和保留窗口选择;后续调整只影响新 chunk。

4. 写入数据

INSERT INTO sensor
SELECT now() - (i * INTERVAL '1 minute'),
       'sensor_' || (i % 3 + 1),
       random() * 100
FROM generate_series(1, 1440) AS i;

SELECT time_bucket('1 hour', time) AS bucket,
       sensor_id,
       round(avg(value)::numeric, 2) AS avg_value
FROM sensor
WHERE time > now() - INTERVAL '24 hours'
GROUP BY bucket, sensor_id
ORDER BY bucket DESC, sensor_id;

5. 日常运维

追加数据、查看 chunk 和后台任务、启用自动维护:

INSERT INTO sensor VALUES (now(), 'sensor_1', 42.0);

SELECT show_chunks('sensor');
SELECT * FROM timescaledb_information.jobs;

ALTER TABLE sensor SET (
    timescaledb.compress,
    timescaledb.compress_segmentby = 'sensor_id',
    timescaledb.compress_orderby = 'time DESC'
);
CALL add_columnstore_policy('sensor', INTERVAL '7 days');
SELECT add_retention_policy('sensor', INTERVAL '365 days');

常用运维命令:

pg status -i tsdb               # 实例与备份状态
pg logs -i tsdb -n 100          # 查看日志尾部
pg export -i tsdb -o sensor.dump

压缩、保留策略、连续聚合和 chunk 的完整操作见下文对应章节。

InfluxDB 兼容 API:influx2tsdb 插件

如果现有工具只支持 InfluxDB 协议,可用 influx2tsdb 插件 在 TimescaleDB 之上运行一个受管的 无状态代理,提供 InfluxDB 1.x HTTP API(Line Protocol 写入与 InfluxQL 查询):

  • 每个 InfluxDB 数据库对应一个 PostgreSQL schema,每个 measurement 对应一个 hypertable,time 为分区列;
  • CREATE / ALTER / DROP RETENTION POLICY 会被翻译成 TimescaleDB 自带的 add_retention_policy / add_compression_policy 任务——不需要 pg_cron, 且代理停机期间 chunk 清理照常进行;
  • 代理本身不存数据,备份、PITR、复制与 HA 仍然完全走 PostgreSQL。
pg addon install influx2tsdb --pg-name tsdb

代价是吞吐:请求需要翻译成 SQL,纯时序写入/查询性能低于原生 InfluxDB。代理 没有认证和 TLS,需要 nginx 前置或做网络隔离。参数、数据 模型、保留语义和测试脚本见 插件文档。

常用查询

每条序列的首末值:

SELECT sensor_id,
       first(value, time) AS first_value,
       last(value, time) AS last_value
FROM sensor
GROUP BY sensor_id;

每条序列的最新一行:

SELECT DISTINCT ON (sensor_id) sensor_id, time, value
FROM sensor
ORDER BY sensor_id, time DESC;

按指定时区做天级分桶:

SELECT time_bucket('1 day', time, 'Asia/Shanghai') AS day,
       count(*)
FROM sensor
GROUP BY day
ORDER BY day DESC;

补齐时间桶序列中的空档:

SELECT time_bucket_gapfill('1 hour', time) AS bucket,
       avg(value)
FROM sensor
WHERE time BETWEEN now() - INTERVAL '6 hours' AND now()
GROUP BY bucket
ORDER BY bucket;

time_bucket_gapfill 需要明确的起点和终点:可以作为第二、三个参数传入,或在 WHERE 中给出时间范围。

压缩与列存

较早的 chunk 可以转入列存(columnstore),压缩数据并加速分析查询:

ALTER TABLE sensor SET (
    timescaledb.compress,
    timescaledb.compress_segmentby = 'sensor_id',
    timescaledb.compress_orderby = 'time DESC'
);

-- 压缩 7 天前的 chunk,由后台任务执行
CALL add_columnstore_policy('sensor', INTERVAL '7 days');

查看压缩状态和空间占用:

SELECT * FROM hypertable_columnstore_stats('sensor');
SELECT * FROM chunk_columnstore_stats('sensor');

保留策略

超出保留窗口的 chunk 会自动删除:

SELECT add_retention_policy('sensor', INTERVAL '365 days');

-- 查看或移除策略
SELECT * FROM timescaledb_information.jobs
WHERE proc_name = 'policy_retention';

SELECT remove_retention_policy('sensor');

这些任务由 TimescaleDB 自带的后台调度器执行,不需要 pg_cron。 influx2tsdb 插件 使用同一机制:其 CREATE / ALTER / DROP RETENTION POLICY 会被自动翻译成这些任务,代理停机期间 清理照常进行。

连续聚合

预先聚合最近的数据,并由后台任务持续刷新:

CREATE MATERIALIZED VIEW sensor_hourly
WITH (timescaledb.continuous) AS
SELECT time_bucket('1 hour', time) AS bucket,
       sensor_id,
       avg(value) AS avg_value
FROM sensor
GROUP BY bucket, sensor_id;

SELECT add_continuous_aggregate_policy('sensor_hourly',
    start_offset      => INTERVAL '3 hours',
    end_offset        => INTERVAL '1 hour',
    schedule_interval => INTERVAL '1 hour');

像普通表一样查询聚合结果:

SELECT * FROM sensor_hourly
ORDER BY bucket DESC
LIMIT 10;

策略只自动刷新 start_offset 到 end_offset 之间的近期窗口;回填更早的数据后 需要手动刷新。

管理 chunk

Chunk 间隔

创建时指定间隔,或之后调整(只影响新 chunk):

SELECT create_hypertable('sensor', by_range('time', INTERVAL '1 day'));

-- 已有 chunk 保持原范围;新 chunk 使用 6 小时
SELECT set_chunk_time_interval('sensor', INTERVAL '6 hours');

SELECT time_interval
FROM timescaledb_information.dimensions
WHERE hypertable_name = 'sensor';

timestamptz 列的 chunk 边界按 UTC 对齐。间隔要保证单个 chunk 能较轻松地放进 内存;间隔过小会产生大量 chunk,并增加查询规划开销。

查看 chunk

SELECT show_chunks('sensor');
SELECT show_chunks('sensor', older_than => INTERVAL '30 days');
SELECT show_chunks('sensor', newer_than => INTERVAL '1 day');

SELECT chunk_name, range_start, range_end
FROM timescaledb_information.chunks
WHERE hypertable_name = 'sensor'
ORDER BY range_start DESC
LIMIT 10;

older_than / newer_than 按 chunk 的时间范围过滤,而不是按创建时间。

查看 chunk 大小

SELECT chunk_name,
       pg_size_pretty(pg_total_relation_size(
           format('%I.%I', chunk_schema, chunk_name)::regclass)) AS total_size
FROM timescaledb_information.chunks
WHERE hypertable_name = 'sensor'
ORDER BY range_start DESC;

SELECT pg_size_pretty(hypertable_size('sensor'));
SELECT * FROM hypertable_detailed_size('sensor');

拆分 chunk

在 chunk 的时间范围内选一个时间点进行拆分;chunk 名称来自 show_chunks('sensor'):

CALL split_chunk('_timescaledb_internal._hyper_1_2_chunk',
                 '2026-10-26 12:00:00+00'::timestamptz);

split_chunk 是 procedure,必须用 CALL,不能用 SELECT。

删除 chunk

-- 完全早于 90 天、或晚于明天的 chunk
SELECT count(*) FROM drop_chunks('sensor', older_than => INTERVAL '90 days');
SELECT count(*) FROM drop_chunks('sensor', newer_than => now() + INTERVAL '1 day');

drop_chunks 直接删除文件,不产生 dead tuple。不要用大范围 DELETE 清理 历史数据。自动过期优先使用保留策略;只有需要自定义调度时才安装 pg_cron:

pg extension install pg_cron -i pg01 --auto-restart
SELECT cron.schedule('drop-sensor-chunks', '0 3 * * *',
  $$SELECT drop_chunks('sensor', older_than => INTERVAL '90 days')$$);

手动转换列存

配置好压缩参数后(见上文「压缩与列存」),可以不等策略而手动转换单个 chunk:

CALL convert_to_columnstore('_timescaledb_internal._hyper_1_1_chunk');
SELECT chunk_name, is_compressed
FROM timescaledb_information.chunks
WHERE hypertable_name = 'sensor'
ORDER BY range_start DESC;

CALL convert_to_rowstore('_timescaledb_internal._hyper_1_1_chunk');

这两个 procedure 接收 chunk 名称,而不是 hypertable 名称。

元数据视图

SELECT * FROM timescaledb_information.hypertables;
SELECT * FROM timescaledb_information.dimensions;
SELECT * FROM timescaledb_information.chunks;
SELECT * FROM timescaledb_information.jobs;

jobs 中可以看到上面创建的列存、保留策略和连续聚合任务,以及内部的 job 历史 任务。

功能可用性

功能 状态
Hypertable、time_bucket、first/last、show_chunks、drop_chunks、hypertable_size、元数据视图 ✅ Apache
用 add_dimension / by_hash 做空间分区 ✅ Apache
列存与压缩(ALTER TABLE ... SET (timescaledb.compress)、CALL add_columnstore_policy) ✅ TSL
连续聚合(CREATE MATERIALIZED VIEW ... WITH (timescaledb.continuous)) ✅ TSL
保留策略(add_retention_policy) ✅ TSL
time_bucket_gapfill ✅ TSL

标记为 ✅ Apache 的功能遵循 Apache-2.0;标记为 ✅ TSL 的功能来自 Timescale License,允许免费内部使用,但禁止对外提供时序数据库服务。

备份与恢复

逻辑备份和 pg export/pg import 正常工作。物理恢复(pg backup/pg restore 使用的 pgBackRest)还需要 TimescaleDB 的恢复钩子:

SELECT timescaledb_pre_restore();   -- 替换数据文件之前,条件允许时执行
-- 恢复实例
SELECT timescaledb_post_restore();  -- 恢复后的实例启动之后执行

pg restore 不会自动调用这两个函数,恢复完成后需要手动执行 timescaledb_post_restore()。正式依赖前请先演练一次。

权限

CREATE EXTENSION 需要超级用户;pg exec 以实例管理员身份连接。表的所有者 可以对自己的表执行 create_hypertable 并管理 chunk。应用角色只需要普通的表 权限。

故障排查

extension "timescaledb" is not available。 实例镜像里没有扩展包,或目标 数据库没有创建扩展。执行 pg extension install timescaledb -i <instance>,并在 该数据库中创建扩展。

timescaledb is not preloaded。 扩展已在 shared_preload_libraries 中,但 PostgreSQL 没有重启。pg extension install 会自动处理;可用 SHOW shared_preload_libraries; 检查并重启实例。

函数报许可证错误。 实例镜像中还是 Apache 构建(旧版 pgcli 或手动安装的 PGDG 包)。执行 pg extension install timescaledb -i <instance> --auto-restart 切换到 Pigsty TSL 包。

cannot create a unique index without the column "time"。 hypertable 上的 唯一索引和主键必须包含分区列:

CREATE TABLE sensor (
    time      timestamptz NOT NULL,
    sensor_id text NOT NULL,
    value     double precision,
    PRIMARY KEY (sensor_id, time)
);

chunk 数量过多。 用 set_chunk_time_interval 调大间隔;已有 chunk 不会 重写。

旧数据没有被清理。 用 add_retention_policy 添加保留策略,或手动执行 drop_chunks;可在 timescaledb_information.jobs 中检查策略任务。

函数参考

函数 用途 状态
create_hypertable('t', by_range('time')) 把表转换为 hypertable ✅
by_range('time', INTERVAL '1 day') 指定时间分区和 chunk 间隔 ✅
set_chunk_time_interval('t', INTERVAL ...) 调整新 chunk 的间隔 ✅
time_bucket('1 hour', time [, timezone]) 时间分桶聚合 ✅
first(value, time) / last(value, time) 分组内首末值 ✅
show_chunks('t' [, older_than/newer_than => ...]) / drop_chunks('t', ...) 查看、过滤和删除 chunk ✅
hypertable_size('t') / hypertable_detailed_size('t') hypertable 总大小及明细 ✅
add_dimension('t', by_hash('col', N)) 添加空间分区 ✅
add_retention_policy(...) / remove_retention_policy(...) 自动过期清理 ✅ TSL
CALL add_columnstore_policy(...) 列存/压缩策略 ✅ TSL
CALL convert_to_columnstore(chunk) / CALL convert_to_rowstore(chunk) 手动转换 chunk 存储格式 ✅ TSL
hypertable_columnstore_stats('t') / chunk_columnstore_stats('t') 列存状态与空间统计 ✅ TSL
CREATE MATERIALIZED VIEW ... WITH (timescaledb.continuous) 连续聚合 ✅ TSL
add_continuous_aggregate_policy(...) 连续聚合的自动刷新策略 ✅ TSL
time_bucket_gapfill(...) 补齐缺失的时间桶 ✅ TSL