SpiceDB PostgreSQL Datastore 深度解析:版本要求、Watch 配置与双层 MVCC 实现原理
2026/9/17 13:38:36 网站建设 项目流程

SpiceDB PostgreSQL Datastore 深度解析:版本要求、Watch 配置与双层 MVCC 实现原理

【免费下载链接】spicedbOpen Source, Google Zanzibar-inspired database for scalably storing and querying fine-grained authorization data项目地址: https://gitcode.com/GitHub_Trending/sp/spicedb

本文以 SpiceDB 仓库中 internal/datastore/postgres/README.md 为骨架,结合internal/datastore/postgres/目录下的驱动源码、迁移脚本与仓库内的 Docker Compose 配置,系统讲解 PostgreSQL 作为 SpiceDB 持久化数据存储的版本要求、track_commit_timestamp配置对 Watch API 的影响,以及驱动如何基于 PostgreSQL 原生 MVCC 之上再叠加一层"应用层 MVCC"来实现显式版本跟踪与任意时间点(point-in-time)快照查询。读完本文,你将掌握 PostgreSQL 驱动在生产环境中的配置要点、核心参数含义,以及其实现 Zanzibar 式一致性语义的底层机制。

概述:PostgreSQL 在 SpiceDB 架构中的定位

SpiceDB 是 Google Zanzibar 思想的开源实现,用于规模化地存储与查询细粒度授权数据。Zanzibar 模型的核心诉求之一是对所有授权数据做"版本化"处理——每一次写入关系元组(relationship tuple)或 schema 变更都对应一个可比较、可追溯的版本(revision),并据此支持"过去某时刻的数据长什么样"这样的时间点查询,以及增量变化的订阅(Watch)。

PostgreSQL 作为传统的关系型数据库管理系统(RDBMS)非常流行,因此 SpiceDB 官方提供了一个完整的 PostgreSQL 数据存储驱动,让用户可以直接把 PostgreSQL 用作 SpiceDB 的后端持久化存储(backing durable storage)。从 internal/datastore/postgres/postgres.go 中可以看到驱动引擎注册名为postgres

const ( Engine = "postgres" ) func init() { datastore.Engines = append(datastore.Engines, Engine) }

README 给出的推荐使用场景是:当你愿意把所有权限数据存放在单个区域(single region)时,PostgreSQL 是一个合适的选择。换句话说,该驱动面向的是单主写入、数据集中在同一区域的部署形态;如果你的数据必须跨区域分布或需要更高的横向扩展能力,则更适合考虑仓库中面向分布式数据库的其他驱动。

最低支持版本与版本定义

README 明确指出:驱动的最低支持版本(Minimum required version)在version.go中以MinimumSupportedPostgresVersion常量定义。查看 internal/datastore/postgres/version/version.go:

// MinimumSupportedPostgresVersion is the minimum version of Postgres supported for this driver. const MinimumSupportedPostgresVersion = "14" // LatestTestedPostgresVersion is the latest version of Postgres that has been tested with this driver. const LatestTestedPostgresVersion = "18"

以此为准:

  • 最低支持版本为 PostgreSQL 14:低于 14 的版本不被该驱动支持;
  • 最新实测版本为 PostgreSQL 18:源码注释特别说明,这两个常量必须与 Docker Hub 上postgres镜像的 tag 保持一致,因为测试与开发环境会直接以镜像 tag 拉取对应版本的数据库。

仓库中的 docker-compose.postgres.yaml 使用的即是postgres:16镜像(image: "postgres:16"),处于支持区间内。这意味着你可以放心在 14、15、16、17、18 等版本上运行该驱动,但需要注意:若使用低于 14 的版本,驱动所依赖的某些 PostgreSQL 特性将无法工作。

配置:track_commit_timestamp与 Watch API

README 中给出的唯一一条显式配置要求是:

track_commit_timestamp必须设置为on,Watch API 才能启用。

该参数是 PostgreSQL 服务端参数(postgresql.conf或启动命令行),用于决定数据库是否为已提交事务记录提交时间戳(commit timestamp)。SpiceDB 的 Watch 功能需要利用提交时间戳来按提交顺序消费变更,因此必须在服务端开启它。

驱动侧的检查逻辑

在 internal/datastore/postgres/postgres.go 的newPostgresDatastore中,驱动初始化时会直接向数据库发送SHOW track_commit_timestamp;查询来探测该参数的真实值:

// Verify that the server supports commit timestamps var trackTSOn string if err := readPool. QueryRow(initializationContext, "SHOW track_commit_timestamp;"). Scan(&trackTSOn); err != nil { return nil, err } watchEnabled := trackTSOn == "on" && !config.watchDisabled if !watchEnabled { if config.watchDisabled { log.Warn().Msg("watch API disabled via configuration") } else { log.Warn().Msg("watch API disabled, postgres must be run with track_commit_timestamp=on") } }

从这段代码可以提炼出两条事实:

  1. Watch 的启用条件是track_commit_timestamp == "on"未通过配置显式禁用(watchDisabled为 false);
  2. 若参数未开启且未显式禁用,驱动会打印告警日志,明确提示 "postgres must be run with track_commit_timestamp=on",随后 Watch 处于关闭状态。

Watch 被禁用时的行为

在 internal/datastore/postgres/watch.go 的Watch方法中,当watchEnabled为 false 时,调用方会立刻收到一个WatchDisabledErr错误,并且变更通道被关闭:

if !pgd.watchEnabled { close(updates) errs <- datastore.NewWatchDisabledErr("postgres must be run with track_commit_timestamp=on for watch to be enabled. See " + sharederrors.PostgresEnableWatchErrorLink) return updates, errs }

特性上报(Features)

驱动还会通过OfflineFeatures()把 Watch 支持状态上报给上层,便于服务端能力探测:

watchStatus := datastore.FeatureUnsupported if pgd.watchEnabled { watchStatus = datastore.FeatureSupported } return &datastore.Features{ Watch: datastore.Feature{Status: watchStatus}, ... WatchEmitsImmediately: datastore.Feature{Status: datastore.FeatureUnsupported}, ... }, nil

其中WatchEmitsImmediately(立即发射策略)在 PostgreSQL 驱动中恒为不支持——Watch方法里对EmitImmediatelyStrategy会直接返回错误,因为 PostgreSQL 驱动只支持轮询型 Watch(默认策略)。

如何开启该参数

以 Docker 启动 PostgreSQL 为例,可以通过-c命令行参数直接传入:

docker run -d \ --name spicedb-postgres \ -e POSTGRES_USER=spicedb \ -e POSTGRES_PASSWORD=spicedb \ -e POSTGRES_DB=spicedb \ -p 5432:5432 \ postgres:16 \ -c track_commit_timestamp=on

也可以在postgresql.conf中设置:

track_commit_timestamp = on

修改后需重启 PostgreSQL(该参数为启动时只读,不能通过pg_reload_conf()热加载)。注意:如果不需要 Watch API,也可以保持该参数关闭,SpiceDB 其余读写功能不受影响,只是 Watch 会告警并停用。

实现要点:为什么需要第二层 MVCC

README 的 "Implementation Caveats" 一节解释了该驱动最重要的设计决策:

虽然 PostgreSQL 借助 MVCC 实现了 ACID 属性,但在不安装扩展的前提下,它并不允许用户读取"脏数据"(dirty data)。因此,PostgreSQL 数据存储驱动实现了第二层 MVCC——由驱动手动控制所有对数据库的写入。这使得驱动能够显式跟踪数据库的所有版本,并执行任意时间点的快照查询。

理解这段话需要先理解 PostgreSQL 原生 MVCC 与 SpiceDB 需求的错位:

  • PostgreSQL 的 MVCC 是并发控制机制:它为每个事务分配 xid(事务 ID),并通过快照(snapshot)让不同事务看到不同的一致性视图。但普通用户只能通过BEGIN ... REPEATABLE READ等隔离级别拿到"相对"一致性视图,而无法直接、廉价地拿到"任意指定事务 xid 之后的数据视图"。
  • SpiceDB 需要的是一种显式、可长期引用、可对外暴露的版本号:ZedToken(SpiceDB 的版本令牌)需要跨请求、跨客户端传递,任何一次CheckPermissionReadRelationships都可能携带一个之前拿到的 token,并期望数据视图严格等于该 token 对应的时间点。
  • 此外,PostgreSQL 会在事务提交后逐步回收旧版本数据(VACUUM),因此驱动还必须实现自己的垃圾回收(GC)来管理"过期版本",避免无限膨胀。

于是该驱动采取了"应用层自行记账"(manual book-keeping)的策略:所有写入都经过驱动控制,驱动为每个写事务记录一条显式的"事务记录",从而把 PostgreSQL 的 xid 转译成 SpiceDB 的 revision。

核心表结构

驱动初始化时通过 internal/datastore/postgres/migrations 目录下的迁移脚本建表。以最早的迁移 zz_migration.0001_1eaeba4b8a73_initial.go 为例,可见三张核心表:

CREATE TABLE relation_tuple_transaction ( id BIGSERIAL NOT NULL, timestamp TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, CONSTRAINT pk_rttx PRIMARY KEY (id) ); CREATE TABLE namespace_config ( namespace VARCHAR NOT NULL, serialized_config BYTEA NOT NULL, created_transaction BIGINT NOT NULL, deleted_transaction BIGINT NOT NULL DEFAULT '9223372036854775807', CONSTRAINT pk_namespace_config PRIMARY KEY (namespace, created_transaction) ); CREATE TABLE relation_tuple ( id BIGSERIAL NOT NULL, namespace VARCHAR NOT NULL, object_id VARCHAR NOT NULL, relation VARCHAR NOT NULL, userset_namespace VARCHAR NOT NULL, userset_object_id VARCHAR NOT NULL, userset_relation VARCHAR NOT NULL, created_transaction BIGINT NOT NULL, deleted_transaction BIGINT NOT NULL DEFAULT '9223372036854775807', CONSTRAINT pk_relation_tuple PRIMARY KEY (id), CONSTRAINT uq_relation_tuple_living UNIQUE (namespace, object_id, relation, userset_namespace, userset_object_id, userset_relation, deleted_transaction) );

对应到 internal/datastore/postgres/schema/schema.go 中的常量定义,这套表结构的核心思路是:

  • relation_tuple_transaction(事务表):每笔写事务在其中插入一行,作为显式的"版本刻度";
  • relation_tuple(关系元组表):每行关系元组带有created_transaction(创建于哪个版本)与deleted_transaction(删除于哪个版本,9223372036854775807表示"当前仍存活",见 postgres.go 中的liveDeletedTxnID);
  • namespace_config(schema 表):命名空间配置同样以created_transaction/deleted_transaction区间记录版本。

这种"created/deleted 区间"设计正是典型的时间点快照存储形态:任何 revision 都可以通过"创建区间包含该 revision 且删除区间不包含该 revision"来判定某行在该时间点是否可见。

读取时的存活过滤

在 internal/datastore/postgres/reader.go 中,pgReader携带一个aliveFilter,而该过滤器由 postgres.go 的buildLivingObjectFilterForRevision构造:

func buildLivingObjectFilterForRevision(revision postgresRevision) queryFilterer { createdBeforeTXN := sq.Expr(fmt.Sprintf( snapshotAlive, schema.ColCreatedXid, ), revision.snapshot, true) deletedAfterTXN := sq.Expr(fmt.Sprintf( snapshotAlive, schema.ColDeletedXid, ), revision.snapshot, false) return func(original sq.SelectBuilder) sq.SelectBuilder { return original.Where(createdBeforeTXN).Where(deletedAfterTXN) } }

其中snapshotAlive的 SQL 模板为:

snapshotAlive = "pg_visible_in_snapshot(%[1]s, ?) = ?"

也就是说,每次读取都会借助 PostgreSQL 原生函数pg_visible_in_snapshot(column, snapshot),用目标 revision 对应的 PG snapshot 去过滤created_xiddeleted_xid,从而在数据库层面直接完成"该 revision 下哪些行存活"的判断。这第二层 MVCC 与 PostgreSQL 原生 MVCC 在这里形成了精巧的结合:应用层负责版本记账与快照编码,数据库层负责高效可见性判定。

Revision 的表示:Postgres Snapshot 即版本

SpiceDB 的 revision 在 PostgreSQL 驱动中被实现为 internal/datastore/postgres/snapshot.go 中的postgresRevision,其内部持有一个pgSnapshot

type pgSnapshot struct { xmin, xmax uint64 xipList []uint64 // Must always be sorted }

这恰好是 PostgreSQL 官方快照的三种组成:xmin(最早仍在进行的事务)、xmax(下一个待分配事务)、xipList(进行中事务列表)。快照的字符串编码遵循 PostgreSQL 官方格式xmin:xmax:xip_list,例如0:4:2,3

一个 revision 本质上就是一个 PostgreSQL 快照:它精确刻画了"截至某个时间点,哪些事务已提交、哪些仍在进行"。驱动还实现了快照之间的偏序比较Equal/GreaterThan/LessThan),其比较逻辑(snapshot.go 的compare方法)基于"哪个快照掌握更多事务结局信息"来判断先后:若 A 快照能确定 B 快照中仍在进行(或尚未看到)的事务已经尘埃落定,则 A 比 B 更新。当两个快照对同一批事务掌握相互冲突的信息时,则判定为"并发"(concurrent)而非简单的大小关系——这正是 Zanzibar 语义中"并发写版本不可简单排序"的体现。

为了让 revision 可以放入 ZedToken 跨请求传递,postgresRevision实现了MarshalBinary/String编码(revisions.go):序列化时对xmaxxipList采用相对编码(以xmin为基准做差),再打包进 protobuf 消息后 base64 编码,以压缩令牌体积。

写入路径:手动记账的事务

所有写事务都走 internal/datastore/postgres/postgres.go 的ReadWriteTx。其关键步骤为:

  1. relation_tuple_transaction表中插入一行,通过createNewTransaction(见 revisions.go)拿到newXID(新事务 ID)与newSnapshot(新快照);
  2. 在该事务内执行用户写入(写元组、写 namespace 等,见 readwrite.go);
  3. 提交后返回postgresRevision{snapshot: newSnapshot.markComplete(newXID.Uint64), optionalTxID: newXID, ...}作为本次写入的 revision。

其中markComplete(snapshot.go)会把刚提交的自身事务从 xip 列表里移除并调整 xmin/xmax,得到"当前已提交版本"的精确快照。此外事务采用Serializable 隔离级别(除非通过WithRelaxedIsolationLevel放宽为 Repeatable Read),并在遇到可重试错误时按maxRetries进行客户端重试。ReadWriteTx只会运行在 primary 实例上——从 postgres.go 可见,若在只读副本上调用写事务会直接返回MustBugf("read-write transaction not supported on read-only datastore")

版本号生成、量化与 GC

为了让大量并发请求不必每次都落库取"最新版本",驱动引入了版本量化(revision quantization)与缓存机制。

优化版本查询

revisions.go 中的querySelectRevision会把数据库当前时间向下取整到最近的量化周期(quantization period),再找到该时间点之后的第一笔事务及其快照作为"优化的版本"返回;若量化周期内没有新事务,则退回最新事务。这保证了在量化窗口内所有客户端拿到的是同一个稳定版本,从而最大化缓存命中率。核心片段:

WITH selected AS (SELECT ( (SELECT %[1]s FROM %[2]s WHERE %[3]s >= TO_TIMESTAMP(FLOOR((EXTRACT(EPOCH FROM NOW() AT TIME ZONE 'utc') * 1000000000 - %[6]d)/ %[4]d) * %[4]d / 1000000000) AT TIME ZONE 'utc' ORDER BY %[3]s ASC LIMIT 1) ) as xid) SELECT selected.xid, ...

同时,驱动还支持修订心跳(revision heartbeat)(postgres.go 的startRevisionHeartbeat):后台协程通过数据库 advisory lock 选举出唯一的 leader,按量化周期周期性向事务表插入心跳事务,确保在写负载很低时量化窗口内始终存在一个可用的最新版本,避免客户端拿到过旧版本。

相关配置参数与默认值

从 internal/datastore/postgres/options.go 可以整理出驱动级默认值:

参数(Option)默认值含义
RevisionQuantization5s版本量化窗口,向外通告的版本按此取整
MaxRevisionStalenessPercent0.1(10%)上一个量化版本可被继续通告的时间,占量化窗口的比例
GCWindow24h客户端可读取的最老版本;更老的版本视为过期(stale)
GCInterval3min后台垃圾回收运行间隔(仅影响磁盘占用,不影响可读版本)
GCMaxOperationTime1min单次 GC 的最大执行时间
WatchBufferLength128Watch 变更缓冲通道长度
WatchBufferWriteTimeout1s向缓冲写入超时,超时则断开 Watch 调用方
MaxRetries10可重试写事务的最大客户端重试次数
FollowerReadDelay0读取副本时从当前时间回退的量(仅使用读副本时设置)
WatchDisabledfalse是否禁用 Watch
GCEnabledtrue是否启用 GC
ReadStrictModefalse是否为读取开启严格模式(从主库读取时默认关闭)
RelaxedIsolationLevelfalse是否把 Serializable 放宽为 Repeatable Read(会削弱强一致性保证)

其中generateConfig还会做参数校验:若revisionQuantization >= gcWindow会直接报错(errQuantizationTooLarge),因为量化窗口不能大于 GC 窗口。

这些选项在命令行中的对应 flag 定义于 pkg/cmd/datastore/datastore.go,例如:

  • --datastore-gc-window
  • --datastore-gc-interval
  • --datastore-gc-max-operation-time
  • --datastore-revision-quantization-interval
  • --datastore-revision-quantization-max-staleness-percent
  • --datastore-follower-read-delay-duration
  • --datastore-max-tx-retries
  • --datastore-watch-buffer-length
  • --datastore-disable-watch-support
  • --datastore-relaxed-isolation-level(仅 PostgreSQL 驱动)

上述 flag 均可通过环境变量(SPICEDB_前缀 + 大写 + 连字符转下划线)或配置文件传入。

垃圾回收

internal/datastore/postgres/gc.go 实现了GarbageCollector接口:GC 通过 advisory lock 保证同时只有一个实例在执行,先查询 GC 窗口之前的最新事务(TxIDBefore),再批量删除(gcBatchDeleteSize = 1000)该版本之前已不再需要的元组与命名空间配置行,同时清理关系计数器等衍生数据。GC 只回收物理数据,而"版本是否可读"由 GC 窗口决定——这正是GCIntervalflag 注释中"affects disk usage only, never which revisions are readable"的含义。

读取副本(Read Replica)与严格读模式

README 虽未展开,但从驱动结构可以推断出该驱动支持"主 + 只读副本"的读取扩展形态。enginebuilder.go 的newDatastoreFromConfig会读取ReadReplicaURIs列表,为每个副本构造NewReadOnlyPostgresDatastore,再通过proxy.NewStrictReplicatedDatastore(primary, replicas...)组合成带读取负载均衡的存储。

值得注意的两个实现细节(见 enginebuilder.go):

  1. 只读副本强制开启严格读模式ReadStrictMode(true)被硬编码进副本选项,且 postgres.go 明确禁止 primary 开启严格模式(strict read mode is not supported on primary instances)。严格模式下,所有读查询的 WHERE 条件都会额外断言"目标版本在读取连接上可用";
  2. 副本连接数上限ReadReplicaURIs数量不得超过datastorecfg.MaxReplicaCount

配置读取副本时需配合--datastore-follower-read-delay-duration(flag 注释明确:Postgres/MySQL 驱动用它保证副本数据追平主库后再读),例如仓库的 docker-compose.postgres.yaml 中SPICEDB_DATASTORE_FOLLOWER_READ_DELAY_DURATION=2000ms,并同时配置SPICEDB_DATASTORE_READ_REPLICA_CONN_URI指向只读副本。该 compose 文件还演示了完整的 PostgreSQL 主从拓扑:postgres-primary(开启wal_level=replicamax_wal_senders=10max_replication_slots=10hot_standby=on)+postgres-replica(通过 development/postgres/primary-init.sh 创建replicator复制用户与物理复制槽),随后migrate服务执行datastore migrate head完成建表与迁移,最后两个 SpiceDB 实例共享主库写入、副本读取。

连接池与可靠性细节

options.go 中还提供了丰富的连接池调优选项,读写各一套:

  • ReadConnsMinOpen/ReadConnsMaxOpenWriteConnsMinOpen/WriteConnsMaxOpen:池的最小/最大连接数;
  • ReadConnMaxIdleTime/WriteConnMaxIdleTime:空闲连接自动关闭阈值(默认无上限);
  • ReadConnMaxLifetime/WriteConnMaxLifetime:连接最大存活时长(默认无上限),配合MaxLifetimeJitter(默认存活时长的 20%)避免连接同时老化;
  • ReadConnHealthCheckInterval/WriteConnHealthCheckInterval:异步健康检查频率(默认 30s);
  • ReadConnPingTimeout/WriteConnPingTimeout:对空闲连接做 liveness ping 的超时(默认 5s)。

连接串可直接使用标准 PostgreSQL URI,如postgres://spicedb:spicedb@localhost:5432/spicedb?sslmode=disable,驱动通过pgxpool.ParseConfig解析(见 postgres.go)。此外驱动支持动态凭证提供方(CredentialsProviderName)、查询拦截器(WithQueryInterceptor)、Prometheus 连接池指标(WithEnablePrometheusStats)以及 OTEL 追踪参数(IncludeQueryParametersInTraces)。

快速上手:本地跑起 PostgreSQL 数据存储

仓库提供了完整的开发用 Docker Compose 编排(docker-compose.postgres.yaml),包含主从 PostgreSQL、迁移、双 SpiceDB 实例、Envoy 负载均衡以及 Prometheus/Grafana/Tempo/Loki 可观测性栈。最小化地验证 PostgreSQL 数据存储,可按以下步骤:

# 1. 仅启动 PostgreSQL 主库(开发用途) docker run -d --name spicedb-postgres \ -e POSTGRES_USER=spicedb \ -e POSTGRES_PASSWORD=spicedb \ -e POSTGRES_DB=spicedb \ -p 5432:5432 \ postgres:16 \ -c track_commit_timestamp=on # 2. 执行迁移到最新版本 spicedb datastore migrate head \ --datastore-engine postgres \ --datastore-conn-uri "postgres://spicedb:spicedb@localhost:5432/spicedb?sslmode=disable" # 3. 启动服务 spicedb serve \ --datastore-engine postgres \ --datastore-conn-uri "postgres://spicedb:spicedb@localhost:5432/spicedb?sslmode=disable" \ --grpc-preshared-key super-secret-key

其中datastore migrate head会按 internal/datastore/postgres/migrations 目录下的 25 个迁移脚本(zz_migration.0001zz_migration.0025,涵盖初始建表、反向索引、唯一存活元组、GC 索引、caveat 表、xid8 列迁移、schema 表等演进)把数据库升级到最新结构。迁移后可用--datastore-bootstrap-files之类的引导参数写入初始 schema 与元组。

总结

PostgreSQL 数据存储驱动是 SpiceDB 在"单区域部署"场景下的主力持久化后端,其设计要点可归纳为:

  • 版本要求:最低 PostgreSQL 14,最新实测 18(见 internal/datastore/postgres/version/version.go);
  • 唯一硬性配置:启用 Watch API 必须在服务端设置track_commit_timestamp=on,驱动启动时会自动探测并告警(internal/datastore/postgres/postgres.go);
  • 双层 MVCC:PostgreSQL 原生 MVCC 之上叠加应用层手动记账的 MVCC——以relation_tuple_transaction记录每个写事务,以created_transaction/deleted_transaction区间描述每行数据的生命周期,用 PostgreSQL snapshot 作为可比较、可编码进 ZedToken 的 revision,实现任意时间点快照查询与 Watch 增量订阅;
  • 版本量化 + 心跳 + GC:通过量化窗口稳定版本、心跳保证低负载下的新鲜版本、后台 GC 在 GC 窗口之外回收物理数据;
  • 扩展形态:支持"主库写入 + 只读副本读取"的严格读模式部署,并可通过--datastore-follower-read-delay-duration保证副本一致性。

理解这套"第二层 MVCC"机制,是正确调优 SpiceDB 一致性参数(量化窗口、GC 窗口、follower 延迟)以及排障(如 Watch 不可用、版本过期)的前提。

【免费下载链接】spicedbOpen Source, Google Zanzibar-inspired database for scalably storing and querying fine-grained authorization data项目地址: https://gitcode.com/GitHub_Trending/sp/spicedb

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询