PGFS:将数据库作为文件系统
前几天,我收到了一条来自 Odoo 社区的需求, 对方苦恼于:“数据库能做PITR(Point-in-Time Recovery),那文件系统有没有办法一起回滚呢?”
为什么会有“PGFS”这个想法?
从数据库老司机的角度来看,这是个颇具挑战性又让人兴奋的问题。 我们都知道,像 Odoo 这类 ERP 系统,最宝贵的确实是数据库中的核心业务数据,放在一套 PostgreSQL 里。
不过,许多“企业级应用”,多少也要接触一些文件操作,比如上传附件、存储图片和文档等等。 虽然这些文件没有数据库那样“关键到能决定生死”,但如果能和数据库一起回到某个时间点, 不论是从安全性/数据完整性/便利性等各个维度上来说,都是极好的。
这就把我带入了一个有趣的思考:有没有一种办法,让文件系统也具有类似数据库的PITR能力? 传统做法大多指向昂贵复杂的CDP(Continuous Data Protection)方案,需要硬件设备或底层块存储做日志级捕获。 可我又想:对于 “穷人” 来说,能不能更巧妙地用开源技术把这个难题解决了?
思考良久,最终浮现出一个让我“拍案叫绝”的组合:JuiceFS + PostgreSQL。 通过将PG变身文件系统,文件的所有写入也都进入数据库里,从而实现和数据库共用同一个WAL日志,随时回溯到任何历史时间点。 这听起来有点天马行空,可是别着急——它确实“能跑”。让我们来看看JuiceFS是怎么做到的。
初识JuiceFS:让数据库“化身”文件系统
JuiceFS 是一款高性能、云原生的分布式文件系统, 能够把对象存储(如S3/MinIO)挂载成一个本地POSIX文件系统。它安装与使用非常轻量,只需几行命令即可完成格式化、挂载、读写。
比如以下命令,就能把SQLite 作为JuiceFS的元数据存储,并把本地路径当作对象存储来测试:
juicefs format sqlite3:///tmp/jfs.db myjfs # 使用SQLite3存储元数据,本地FS存储数据
juicefs mount sqlite3:///tmp/jfs.db ~/jfs -d # 将这个文件系统挂载到 ~/jfs
妙就妙在:JuiceFS 还支持使用PostgreSQL 作为元数据和对象数据的存储后端! 也就是说,你只需要把JuiceFS的后端改成一个已经安装好的PostgreSQL实例,就能得到一个基于数据库的“文件系统”。
于是,如果你有现成的PostgreSQL数据库(例如通过 Pigsty 单机安装),就能一键拉起一套 “PGFS”:
METAURL=postgres://dbuser_meta:DBUser.Meta@:5432/meta
OPTIONS=(
--storage postgres
--bucket :5432/meta
--access-key dbuser_meta
--secret-key DBUser.Meta
${METAURL}
jfs
)
juicefs format "${OPTIONS[@]}" # 创建一个 PG 文件系统
juicefs mount ${METAURL} /data2 -d # 后台挂载到 /data2 目录
juicefs bench /data2 # 测试性能
juicefs umount /data2 # 停止挂载
如此一来,任何写到/data2目录的数据,其实都会存进 PG 中的 jfs_blob
这张表里。换言之,这个文件系统和PG数据库已经融为一体!
PGFS实战:文件系统也能PITR
设想我们有一套 Odoo,它需要在/var/lib/odoo
之类的目录存放文件数据。
传统上,如果需要把Odoo的数据库回溯到过去,虽然数据库能通过WAL日志进行时间点恢复,可文件系统依然得靠外部快照或CDP。
而现在,如果把/var/lib/odoo
挂载到PGFS上,所有对文件系统的写操作就变成了对PG数据库的写操作。
数据库再也不是单纯保存SQL数据,它还同时承载了文件系统的信息。
这就意味着:当我做PITR时,不仅数据库能回到某个时间点,文件也能够瞬间“随数据库”一起回到同一时刻。
有人可能会问,ZFS 不也能快照吗?是的,ZFS能做快照并回滚,但那依然是基于具体快照点, 想要精细到某一秒或某几分钟前,则需要真正的日志式方案或CDP功能。 JuiceFS+PG的组合,就等同于把文件操作日志写进了数据库的WAL里,而这可是PostgreSQL天生便擅长的一件事。
下面这段实验流程可以说明一切。我们先写个循环往文件系统写时间戳,再持续往数据库里插入心跳记录:
while true; do date "+%H-%M-%S" >> /data2/ts.log; sleep 1; done
/pg/bin/pg-heartbeat # 生成数据库心跳记录
tail -f /data2/ts.log
然后,通过 PostgreSQL 校验一下 JuiceFS 所属的那张表:
postgres@meta:5432/meta=# SELECT min(modified),max(modified) FROM jfs_blob;
min | max
----------------------------+----------------------------
2025-03-21 02:26:00.322397 | 2025-03-21 02:40:45.688779
当我们下定决心,要回滚到比如一分钟前(2025-03-21 02:39:00
),只需执行:
pg-pitr --time="2025-03-21 02:39:00" # 使用 pgbackrest 回滚至特定时刻,实际命令如下:
pgbackrest --stanza=pg-meta --type=time --target='2025-03-21 02:39:00+00' restore
什么?你问 PITR 和 pgBackRest 是哪里来的? Pigsty 已经为你配置好开箱即用的监控,备份,高可用,直接用就好了!自己手搓也行,不过会有点麻烦。
然后当我们再看文件系统中的日志和数据库心跳表,两者都停留在了 02:39:00 这个时间点前:
$ tail -n1 /data2/ts.log
02-38-59
$ psql -c 'select * from monitor.heartbeat'
id | ts | lsn | txid
---------+-------------------------------+-----------+------
pg-meta | 2025-03-21 02:38:59.129603+00 | 251871544 | 2546
这意味着这种玩法是可行的!我们成功通过 PGFS 实现了 FS/DB 一致的 PITR!
性能表现如何?
那么功能是有了,但性能怎么样呢?
我找了台开发服务器,SSD,用自带的 juicefs bench
测试了一下,结果如下,看着还行,对 Odoo 这种应用肯定富余太多了。
$ juicefs bench ~/jfs # 简单测试单线程性能
BlockSize: 1.0 MiB, BigFileSize: 1.0 GiB,
SmallFileSize: 128 KiB, SmallFileCount: 100, NumThreads: 1
Time used: 42.2 s, CPU: 687.2%, Memory: 179.4 MiB
+------------------+------------------+---------------+
| ITEM | VALUE | COST |
+------------------+------------------+---------------+
| Write big file | 178.51 MiB/s | 5.74 s/file |
| Read big file | 31.69 MiB/s | 32.31 s/file |
| Write small file | 149.4 files/s | 6.70 ms/file |
| Read small file | 545.2 files/s | 1.83 ms/file |
| Stat file | 1749.7 files/s | 0.57 ms/file |
| FUSE operation | 17869 operations | 3.82 ms/op |
| Update meta | 1164 operations | 1.09 ms/op |
| Put object | 356 operations | 303.01 ms/op |
| Get object | 256 operations | 1072.82 ms/op |
| Delete object | 0 operations | 0.00 ms/op |
| Write into cache | 356 operations | 2.18 ms/op |
| Read from cache | 100 operations | 0.11 ms/op |
+------------------+------------------+---------------+
另一个样本:阿里云ESSD PL1乞丐盘测试结果
+------------------+------------------+---------------+
| ITEM | VALUE | COST |
+------------------+------------------+---------------+
| Write big file | 18.08 MiB/s | 56.64 s/file |
| Read big file | 98.07 MiB/s | 10.44 s/file |
| Write small file | 268.1 files/s | 3.73 ms/file |
| Read small file | 1654.3 files/s | 0.60 ms/file |
| Stat file | 7465.7 files/s | 0.13 ms/file |
| FUSE operation | 17855 operations | 4.28 ms/op |
| Update meta | 1192 operations | 16.28 ms/op |
| Put object | 357 operations | 2845.34 ms/op |
| Get object | 255 operations | 327.37 ms/op |
| Delete object | 0 operations | 0.00 ms/op |
| Write into cache | 357 operations | 2.05 ms/op |
| Read from cache | 102 operations | 0.18 ms/op |
+------------------+------------------+---------------+
虽然与原生FS相比吞吐性能肯定逊色,但对于那些文件量不大、访问频次较低的应用场景已经足够了。 毕竟用“数据库充当文件系统”,本身就不是为了跑大型存储和高并发写入, 而是为了让数据库和文件系统能“同步回到过去”,能用就行。
补完拼图:一键“企业级”交付
接下来,让我们把这套玩意儿放进一个实践场景 —— 比如一键部署“企业级”的 Odoo ,让文件“自动”具备CDP能力。
Pigsty 提供了外部高可用、自动备份、监控、PITR等能力的PG,想要安装它非常容易:
curl -fsSL https://repo.pigsty.cc/get | bash; cd ~/pigsty
./bootstrap # 安装 Pigsty 依赖
./configure -c app/odoo # 使用 Odoo 配置模板
./install.yml # 安装 Pigsty
上面是 Pigsty 的标准安装流程,下面使用剧本安装 Docker,创建挂载 PGFS,并使用 Docker Compose 拉起无状态的 Odoo
./docker.yml -l odoo # 安装 Docker 模块,拉起 Odoo 无状态部分
./juice.yml -l odoo # 安装 JuiceFS 模块,PGFS 挂载到 /data2
./app.yml -l odoo # 拉起 Odoo 无状态部分,使用外部 PG/PGFS
是的,就是这么简单,所有东西就准备好了,不过,命令虽然简单,但这里的关键是配置文件。
这里的配置文件 pigsty.yml
大概会是这个样子,唯一的修改就是增加了 JuiceFS 的配置,将 PGFS 挂载到了 /data/odoo
:
odoo:
hosts: { 10.10.10.10: {} }
vars:
# ./juice.yml -l odoo
juice_fsname: jfs
juice_mountpoint: /data/odoo
juice_options:
- --storage postgres
- --bucket :5432/meta
- --access-key dbuser_meta
- --secret-key DBUser.Meta
- postgres://dbuser_meta:DBUser.Meta@:5432/meta
- ${juice_fsname}
# ./app.yml -l odoo
app: odoo # specify app name to be installed (in the apps)
apps: # define all applications
odoo: # app name, should have corresponding ~/app/odoo folder
file: # optional directory to be created
- { path: /data/odoo ,state: directory, owner: 100, group: 101 }
- { path: /data/odoo/webdata ,state: directory, owner: 100, group: 101 }
- { path: /data/odoo/addons ,state: directory, owner: 100, group: 101 }
conf: # override /opt/<app>/.env config file
PG_HOST: 10.10.10.10 # postgres host
PG_PORT: 5432 # postgres port
PG_USERNAME: odoo # postgres user
PG_PASSWORD: DBUser.Odoo # postgres password
ODOO_PORT: 8069 # odoo app port
ODOO_DATA: /data/odoo/webdata # odoo webdata
ODOO_ADDONS: /data/odoo/addons # odoo plugins
ODOO_DBNAME: odoo # odoo database name
ODOO_VERSION: 18.0 # odoo image version
完成这些后,就在同一台服务器上跑起了一套“企业级” Odoo:后端数据库由 Pigsty 管理、文件系统由JuiceFS挂载,而JuiceFS的底层又连接在PG上。 一旦出现“回退需求”,只要对PG执行 PITR,就能把文件和数据库一起“回到指定时刻”。这对于有相似需求的应用,比如Dify、Gitlab、Gitea、MatterMost等,都同样适用。
回顾这一切,你会发现:本来需要花大价钱、依赖高端存储硬件才能实现的CDP, 如今用一套轻量级开源组合就能搞定。虽然带有“穷人工程”的DIY痕迹,但它确实简单、稳定且足够实用,值得在更多场景中探索和尝试。