SpiceDB PostgreSQL Datastore 深度解析:版本要求、Watch 配置与双层 MVCC 实现原理
发布时间:2026/9/17 13:39:23来源:尧图网络
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 中可以看到驱动引擎注册名为postgresconst ( 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 APIREADME 中给出的唯一一条显式配置要求是track_commit_timestamp必须设置为onWatch 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_timestampon) } }从这段代码可以提炼出两条事实Watch 的启用条件是track_commit_timestamp on且未通过配置显式禁用watchDisabled为 false若参数未开启且未显式禁用驱动会打印告警日志明确提示 postgres must be run with track_commit_timestampon随后 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_timestampon 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_USERspicedb \ -e POSTGRES_PASSWORDspicedb \ -e POSTGRES_DBspicedb \ -p 5432:5432 \ postgres:16 \ -c track_commit_timestampon也可以在postgresql.conf中设置track_commit_timestamp on修改后需重启 PostgreSQL该参数为启动时只读不能通过pg_reload_conf()热加载。注意如果不需要 Watch API也可以保持该参数关闭SpiceDB 其余读写功能不受影响只是 Watch 会告警并停用。实现要点为什么需要第二层 MVCCREADME 的 Implementation Caveats 一节解释了该驱动最重要的设计决策虽然 PostgreSQL 借助 MVCC 实现了 ACID 属性但在不安装扩展的前提下它并不允许用户读取脏数据dirty data。因此PostgreSQL 数据存储驱动实现了第二层 MVCC——由驱动手动控制所有对数据库的写入。这使得驱动能够显式跟踪数据库的所有版本并执行任意时间点的快照查询。理解这段话需要先理解 PostgreSQL 原生 MVCC 与 SpiceDB 需求的错位PostgreSQL 的 MVCC 是并发控制机制它为每个事务分配 xid事务 ID并通过快照snapshot让不同事务看到不同的一致性视图。但普通用户只能通过BEGIN ... REPEATABLE READ等隔离级别拿到相对一致性视图而无法直接、廉价地拿到任意指定事务 xid 之后的数据视图。SpiceDB 需要的是一种显式、可长期引用、可对外暴露的版本号ZedTokenSpiceDB 的版本令牌需要跨请求、跨客户端传递任何一次CheckPermission或ReadRelationships都可能携带一个之前拿到的 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 中的liveDeletedTxnIDnamespace_configschema 表命名空间配置同样以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_xid与deleted_xid从而在数据库层面直接完成该 revision 下哪些行存活的判断。这第二层 MVCC 与 PostgreSQL 原生 MVCC 在这里形成了精巧的结合应用层负责版本记账与快照编码数据库层负责高效可见性判定。Revision 的表示Postgres Snapshot 即版本SpiceDB 的 revision 在 PostgreSQL 驱动中被实现为 internal/datastore/postgres/snapshot.go 中的postgresRevision其内部持有一个pgSnapshottype 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序列化时对xmax与xipList采用相对编码以xmin为基准做差再打包进 protobuf 消息后 base64 编码以压缩令牌体积。写入路径手动记账的事务所有写事务都走 internal/datastore/postgres/postgres.go 的ReadWriteTx。其关键步骤为在relation_tuple_transaction表中插入一行通过createNewTransaction见 revisions.go拿到newXID新事务 ID与newSnapshot新快照在该事务内执行用户写入写元组、写 namespace 等见 readwrite.go提交后返回postgresRevision{snapshot: newSnapshot.markComplete(newXID.Uint64), optionalTxID: newXID, ...}作为本次写入的 revision。其中markCompletesnapshot.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 heartbeatpostgres.go 的startRevisionHeartbeat后台协程通过数据库 advisory lock 选举出唯一的 leader按量化周期周期性向事务表插入心跳事务确保在写负载很低时量化窗口内始终存在一个可用的最新版本避免客户端拿到过旧版本。相关配置参数与默认值从 internal/datastore/postgres/options.go 可以整理出驱动级默认值参数Option默认值含义RevisionQuantization5s版本量化窗口向外通告的版本按此取整MaxRevisionStalenessPercent0.110%上一个量化版本可被继续通告的时间占量化窗口的比例GCWindow24h客户端可读取的最老版本更老的版本视为过期staleGCInterval3min后台垃圾回收运行间隔仅影响磁盘占用不影响可读版本GCMaxOperationTime1min单次 GC 的最大执行时间WatchBufferLength128Watch 变更缓冲通道长度WatchBufferWriteTimeout1s向缓冲写入超时超时则断开 Watch 调用方MaxRetries10可重试写事务的最大客户端重试次数FollowerReadDelay0读取副本时从当前时间回退的量仅使用读副本时设置WatchDisabledfalse是否禁用 WatchGCEnabledtrue是否启用 GCReadStrictModefalse是否为读取开启严格模式从主库读取时默认关闭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只读副本强制开启严格读模式ReadStrictMode(true)被硬编码进副本选项且 postgres.go 明确禁止 primary 开启严格模式strict read mode is not supported on primary instances。严格模式下所有读查询的 WHERE 条件都会额外断言目标版本在读取连接上可用副本连接数上限ReadReplicaURIs数量不得超过datastorecfg.MaxReplicaCount。配置读取副本时需配合--datastore-follower-read-delay-durationflag 注释明确Postgres/MySQL 驱动用它保证副本数据追平主库后再读例如仓库的 docker-compose.postgres.yaml 中SPICEDB_DATASTORE_FOLLOWER_READ_DELAY_DURATION2000ms并同时配置SPICEDB_DATASTORE_READ_REPLICA_CONN_URI指向只读副本。该 compose 文件还演示了完整的 PostgreSQL 主从拓扑postgres-primary开启wal_levelreplica、max_wal_senders10、max_replication_slots10、hot_standbyonpostgres-replica通过 development/postgres/primary-init.sh 创建replicator复制用户与物理复制槽随后migrate服务执行datastore migrate head完成建表与迁移最后两个 SpiceDB 实例共享主库写入、副本读取。连接池与可靠性细节options.go 中还提供了丰富的连接池调优选项读写各一套ReadConnsMinOpen/ReadConnsMaxOpen、WriteConnsMinOpen/WriteConnsMaxOpen池的最小/最大连接数ReadConnMaxIdleTime/WriteConnMaxIdleTime空闲连接自动关闭阈值默认无上限ReadConnMaxLifetime/WriteConnMaxLifetime连接最大存活时长默认无上限配合MaxLifetimeJitter默认存活时长的 20%避免连接同时老化ReadConnHealthCheckInterval/WriteConnHealthCheckInterval异步健康检查频率默认 30sReadConnPingTimeout/WriteConnPingTimeout对空闲连接做 liveness ping 的超时默认 5s。连接串可直接使用标准 PostgreSQL URI如postgres://spicedb:spicedblocalhost:5432/spicedb?sslmodedisable驱动通过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_USERspicedb \ -e POSTGRES_PASSWORDspicedb \ -e POSTGRES_DBspicedb \ -p 5432:5432 \ postgres:16 \ -c track_commit_timestampon # 2. 执行迁移到最新版本 spicedb datastore migrate head \ --datastore-engine postgres \ --datastore-conn-uri postgres://spicedb:spicedblocalhost:5432/spicedb?sslmodedisable # 3. 启动服务 spicedb serve \ --datastore-engine postgres \ --datastore-conn-uri postgres://spicedb:spicedblocalhost:5432/spicedb?sslmodedisable \ --grpc-preshared-key super-secret-key其中datastore migrate head会按 internal/datastore/postgres/migrations 目录下的 25 个迁移脚本zz_migration.0001至zz_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_timestampon驱动启动时会自动探测并告警internal/datastore/postgres/postgres.go双层 MVCCPostgreSQL 原生 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),仅供参考
网站建设高端定制企业官网