先讲一件让我印象很深的线上问题。上个月某天晚高峰,权限服务 P99 突然冲到 3 秒,一个普普通通的按钮权限检查,用户点了之后要转好几圈才能看到结果。排查下来发现,不是机器不够,不是连接池打满,而是这条授权链路的关系图太大了——每次 Check 都在对一张巨大的权限关系图做几乎全量的递归扫描。那一刻我才真正理解,为什么 Google 提出 Zanzibar 这套 ReBAC 模型之后,大家的关注点很快就从"怎么表达权限"转移到了"怎么让权限检查不爆炸"上。
这篇文章想把 SpiceDB 在 ReBAC 性能引擎上的重构思路拆开讲清楚。重点不在 API 用法,而在于它怎么把授权检查从一个"暴力扫图的递归问题"重构成一个"可预估成本、可设预算、可调度执行"的查询问题。如果你正在自研权限系统,或者接入了类 Zanzibar 的系统但被性能坑过,这篇文章应该能帮你把体系理清楚。
1. 暴力扫图的反模式:为什么关系权限查询会随规模指数变慢
1.1 先统一认知:关系模型和授权检查到底在查什么
ReBAC 的全称是 Relationship-Based Access Control,核心思想是:权限不是角色表里的一行记录,而是对象与对象之间的关系。SpiceDB 的存储模型里最基本的概念叫 tuple,翻译成大白话就是一条"关系事实"。
举个例子,假设我们组织里有多组织和多文档,schema 大约长这样:
spicedb复制definition user {}
definition group {
relation member: user | group#member
}
definition document {
relation viewer: user | group#member
permission view = viewer
}
这表示"用户能看某个文档"这个权限,可以来源于两类关系:要么用户直接是文档的 viewer,要么用户是某个组的成员,而这个组本身被授予了文档的 viewer 关系。存储层里会看到这样的 tuple:
text复制document:report-1#viewer@group:eng#member
翻译过来就是"engineering 这个组的所有成员,对 report-1 这篇文档具有 viewer 权限"。
一次授权检查(Check)的问题也非常简单:给定用户 alice,文档 report-1,判断 alice 是否有 view 权限。初看这就是一次布尔查询,但真正的问题在于:group:eng#member 背后可能还嵌套着其他组,比如 group:eng 的成员是 group:backend#member,而 group:backend 成员又来自 group:sre#member。一旦关系图出现嵌套,Check 就从一个点查变成了一个图遍历问题。
1.2 最朴素的递归实现是怎么扫的
很多人实现第一版 ReBAC 的时候,逻辑往往是从资源节点出发,向外扩展。伪代码大概长这样:
python复制def check(user, resource, permission):
for rel in get_relations(resource, permission):
if rel.subject == user:
return True
if rel.subject is Group:
if check_member_in_group(user, rel.subject):
return True
return False
def check_member_in_group(user, group):
for rel in get_relations(group, "member"):
if rel.subject == user:
return True
