- 后端
- 认证鉴权
- 云原生
【免费下载链接】opa
Open Policy Agent (OPA) is an open source, general-purpose policy engine.
Open Policy Agent(OPA)默认将策略与数据保存在内存中,当数据量超出服务器内存配额时,基于 Badger 的持久化磁盘存储(disk storage)提供了把/data映射到嵌入式键值库的解决方案。本文以 storage.md 为主线,结合本仓库中 v1/storage/disk 的实现源码与测试用例,系统讲解磁盘存储的启用方式、分区(Partitions)布局与性能取舍、大 Bundle 的多事务加载机制、磁盘访问指标,以及通过 super flags 细调 Badger 的方法。读完本文,你将掌握如何为自己的策略负载选择合理的分区方案、定位次优分区,并在事务大小受限时正确处置。
磁盘存储的定位:适用场景与重要边界
磁盘存储并不是让 OPA 变成一个数据库,而是一个扩展内存容量的手段。docs/docs/storage.md开篇给出了两条需要牢记的边界:
- 它用于处理无法放入 OPA 服务器内存配额的数据;
- 它不应当作为这些数据的唯一事实来源(primary source of truth)。
磁盘上的数据应当被视为临时(ephemeral)数据:你必须有恢复该数据的手段。截至当前版本,仓库不提供备份/恢复流程,也不提供数据损坏的修复流程。也就是说,磁盘存储的角色是“缓存/加速”,而不是“持久化保险箱”。
从实现上看,磁盘存储是storage.Store接口的一个基于磁盘的实现。包注释(v1/storage/disk/disk.go)明确了它的设计:策略模块以原始字节串保存(一个模块一个键),数据则在调用方提供的“分区”(partitions)协助下映射到底层键值库。操作跨越多个键(例如读取整个/data)比只针对单个键的读取更昂贵,因为存储层必须从若干键值对中重建对象并整体读入内存——这正是分区要解决的问题。
启用磁盘存储:配置项与目录处理
磁盘存储通过storage配置键启用。docs/docs/configuration.md的 Disk Storage 章节给出了完整配置表:
| Field | Type | Required | Description |
|---|---|---|---|
storage.disk.directory | string | Yes | 存放持久化数据库的目录 |
storage.disk.auto_create | bool | No (default:false) | 若为 true,目录不存在时会自动创建 |
storage.disk.partitions | array[string] | No | 用于磁盘数据分区的、互不重叠的data前缀 |
storage.disk.badger | string | No (default: empty) | 传给 Badger 的 “superflags”,用于修改高级选项 |
只要storage.disk被设置为非空值,服务器就会启用磁盘存储,数据写入配置的directory。
配置解析实现在 v1/storage/disk/config.go 的OptionsFromConfig中,几个值得注意的细节:
- 目录不存在时,若
auto_create为 true,会以0700权限调用os.MkdirAll递归创建;否则报错directory <dir> invalid; partitions中的每一项都会用storage.ParsePath校验,非法路径直接返回ErrInvalidPartitionPath;- 解析结果封装为
Options{Dir, Partitions, Badger}(见 v1/storage/disk/disk.go 的Options结构)。
一个最小可用的 YAML 配置(来自原文档示例,稍作展开):
storage: disk: directory: /tmp/disk auto_create: true # 目录不存在时自动创建 partitions: - /users badger: nummemtables=1; numgoroutines=2; maxlevels=3分区(Partitions):决定键值库中的数据如何切分
分区如何决定键的布局
分区决定了 JSON 数据在底层键值库中被切分成多少个键。原文档用一个用户文档示例直观展示了三种分区下的存储结果:
{ "users": { "alice": { "roles": ["admin"] }, "bob": { "roles": ["viewer"] } } }| Partitions | Keys | Values |
|---|---|---|
| (1) none | /users | {"alice": {"roles": ["admin"]}, "bob": {"roles": ["viewer"]}}} |
(2)/users | /users/alice | {"roles": ["admin"]} |
/users/bob | {"roles": ["viewer"]} | |
(3)/users/* | /users/alice/roles | ["admin"] |
/users/bob/roles | ["viewer"] |
从源码结构看(v1/storage/disk/disk.go 包注释),分区支持通配符:/foo/*会把/foo/bar/abc与/foo/buz/def分别写到独立键;也支持多个通配符(如/tenants/*/users/*/bindings)以及通配符出现在分区末尾(如/users/*)。磁盘存储写入的所有键都带前缀/schema_version/partition_version/type:schema_version表示当前 OPA 版本理解的数据结构版本(当前恒为 1),partition_version表示调用方提供的分区布局版本(当前恒为 1),type为data或policies。
分区的性能取舍:多读键 vs 多读字节
分区直接影响一次查询要读多少个键、每个键装多少数据。原文档给出了三种分区下的读取成本对比:
| Query | Partitions | Number of keys read |
|---|---|---|
data.users | (1) | 1 |
| (2) | 2 | |
| (3) | 2 | |
data.users.alice | (1) | 1 withbobdata thrown away |
| (2) | 2 | |
| (3) | 2 |
解读:
- 读取完整
data.users:分区 (1) 只需一次键读取;分区 (2) 需要抓取两个键及其值; - 读取单个用户
data.users.alice:分区 (1) 同样只读一个键,但该键包含全部用户数据,alice之外的数据都要丢弃;分区 (2) 读取两个键、且每个键的值更小。
这个“读一个巨型 JSON 再扔掉大半”的模式正是次优分区的典型信号。因此不存在放之四海皆准的分区设置:好的设置取决于实际使用模式,最终取决于与 OPA 一起使用的策略;通常应该优先优化那些性能关键的查询路径。判定分区是否次优,请观察下一节介绍的指标。
被分区的值必须是对象
分区会把“它指向的值”按成员拆成多个键,因此被拆分的那个层级必须是对象。文档示例中/users可行是因为data.users是以用户名为键的对象;/users/*可行是因为每个用户是以属性名为键的对象。
如果data.users.alice是数组,/users/*会被拒绝,报错:
value at /users/alice cannot be partitioned: expected object, found array解决办法是把分区上调一级——用/users而非/users/*,让数组整体存在/users/alice这个键下。分区值内部的数组没有问题,只有被分区直接切开的那个层级必须为对象。
这一点在测试中得到严格验证:v1/storage/disk/disk_test.go 的TestTruncateNonObjectInPartition覆盖了三种非对象情形,错误消息分别对应expected object, found array、expected object, found null与expected object, found number;对应的错误构造位于 v1/storage/disk/txn.go(消息格式为value at %s cannot be partitioned: expected object, found %s)。
/system 分区:由 OPA 托管、禁止重叠
OPA 会在数据存储中保存一些内部值(例如 bundle 元数据),路径在/system下。该部分数据的分区由 OPA 自己管理:源码中定义常量systemPartition = "/system/*"(v1/storage/disk/disk.go),并在New中无条件追加。因此:
- 用户配置的分区若与
/system重叠,会直接报错system partitions are managed; - 配置的分区之间若互相重叠,也会在启动时被拒绝,错误为
partitions are overlapped(两者都通过pathSet.IsDisjoint()检查)。
此外,分区布局一旦写入数据库元数据,后续变更必须是纯增量且向后兼容的。init与validatePartitions(v1/storage/disk/disk.go)会在启动时对比数据库里已记录的分区与配置的新分区,若发现旧分区缺失(例如从/foo/*改为/foo/bar这种“缩小”),会报partitions are backwards incompatible ... missing: [...]。相关测试见 v1/storage/disk/disk_test.go(partitions are backwards incompatible (old: [...], new: [...], missing: [...]))。实践中这意味着:新增分区要谨慎规划,一旦线上使用,收缩或替换分区布局可能不被允许。
大 Bundle:事务拆分机制
为什么 bundle 会超出一个事务
Badger(OPA 嵌入的键值库)对单个事务能容纳的数据量有上限。由于opa build会把一个 bundle 的所有数据文件合并进单个data.json,一个 bundle 很容易携带超过单事务容量的数据。
bundle service 加载:按需拆成多个事务
对于由 bundle 服务 提供的 bundle,OPA 会在Store.Truncate中把 bundle 拆成尽可能多的事务逐批写入,因此这类 bundle 不受单事务大小限制。核心实现在doTruncateData(v1/storage/disk/disk.go):
- 先通过
partitionWrite计算要写入的键值更新; - 调用
applyUpdates提交;若返回badger.ErrTxnTooBig,就把已成功写入的更新数量n截断,先Commit当前事务,再开新事务继续写剩余部分,循环直至全部写入; - 策略(policies)的写入在
Truncate主循环中有同样的ErrTxnTooBig处理:先提交再开新事务重试。
代价是:激活大 bundle 不是原子的。如果 OPA 在写入过程中途被杀掉,存储中会残留新 bundle 的一部分数据。测试 v1/storage/disk/disk_test.go 的TestTruncateMultipleTxn就通过Badger: "memtablesize=4000;valuethreshold=600"人为缩小事务容量,验证了 20 个 1MB 数据文件跨多个事务成功写入。
命令行 bundle:仍然单事务
通过opa run命令行参数传入的 bundle 则只用一个事务写入,因此大于一个事务容量的 bundle 依然会加载失败。解决途径有两个:
- 改为从 bundle service 提供该 bundle;
- 调大 Badger 的
memtablesizesuper flag(见下文)。
单个键过大:无法拆分的情况
无论怎么拆事务,单个键不能被拆分。如果一个键下存储的值大于事务容量,OPA 会报错:
value at /users/alice is too large to store: Txn is too big to fit into one requestdoTruncateData中专门处理了这种情形:当n == 0且已经开过新事务(replacement != nil),说明“一对键值对放不进刚开的空事务”,此时直接返回value at %s is too large to store错误。对应测试为TestTruncateSingleValueTooLarge(v1/storage/disk/disk_test.go)。
通常的解决办法是在该路径再往下加一层分区,把过大的值摊到更多键上;也可以通过memtablesizesuper flag 提高事务大小上限。
通过指标观察磁盘访问
用 REST API 的 ?metrics 查询
使用 REST API 时,在查询 URL 上加?metrics即可拿到与本次磁盘存储访问相关的指标。原文档示例:
$ curl 'http://localhost:8181/v1/data/tenants/acme1/bindings/user1?metrics' | opa eval -I 'input.metrics' -fpretty { "counter_disk_read_bytes": 339, "counter_disk_read_keys": 3, "counter_server_query_cache_hit": 1, "timer_disk_read_ns": 40736, "timer_rego_external_resolve_ns": 251, "timer_rego_input_parse_ns": 656, "timer_rego_query_eval_ns": 66616, "timer_server_handler_ns": 117539 }可用的 timer 与 counter
timer_disk_*_ns计时器给出不同磁盘操作所花时间。可用的计时器:
timer_disk_read_nstimer_disk_write_nstimer_disk_commit_ns
counter_disk_*计数器:
counter_disk_read_keys:取回的键数量counter_disk_written_keys:写入的键数量counter_disk_deleted_keys:删除的键数量counter_disk_read_bytes:取回的字节数
这些名字直接来自事务实现中的常量(v1/storage/disk/txn.go):disk_read_bytes、disk_read_keys、disk_written_keys、disk_deleted_keys,以及disk_commit、disk_read、disk_write三个计时器(metric 名前缀timer_/counter_由指标框架拼接,_ns表示纳秒单位)。读取路径上,单键读取readOne每次累加 1 个读键计数并累加值字节数;多键读取readMultiple用迭代器遍历前缀下的所有键,逐个累加读键计数与值字节数。
用指标发现次优分区
当某次查询取回的键数和字节数与实际返回的数据量不成比例时,就说明分区设置不佳:查询很可能取回了一个巨大的 JSON 对象,然后把其中大部分数据丢弃。此时应结合策略中真正高频的查询路径,重新设计分区。
对接 Prometheus 的直方图
除了单次请求的?metrics,磁盘存储还把事务粒度的统计转发为 Prometheus 直方图(v1/storage/disk/metrics.go),在Commit时完成转发(v1/storage/disk/txn.go):
keys_read_per_store_read_txn:一次存储读事务发生了多少次数据库读取key_bytes_read_per_store_read_txn:一次存储读事务读取了多少字节keys_read_per_store_write_txn/keys_written_per_store_write_txn/keys_deleted_per_store_write_txn/key_bytes_read_per_store_write_txn:写事务对应的统计
值得注意的实现细节:即使调用方不关心指标,事务也会在提交时把这些直方图更新掉——它们反映的是存储层的真实工作负载,适合长期监控分区健康度。
Debug 日志:启动时的分区统计
给opa run传--log-level debug可以查看底层存储引擎的全部日志。启用 debug 日志后,OPA启动时会输出已配置磁盘分区及其键大小的统计信息:
[DEBUG] partition /tenants/acme3/bindings (pattern /tenants/*/bindings): key count: 10000 (estimated size 598890 bytes) [DEBUG] partition /tenants/acme4/bindings (pattern /tenants/*/bindings): key count: 10000 (estimated size 598890 bytes) [DEBUG] partition /tenants/acme8/bindings (pattern /tenants/*/bindings): key count: 10000 (estimated size 598890 bytes) [DEBUG] partition /tenants/acme9/bindings (pattern /tenants/*/bindings): key count: 10000 (estimated size 598890 bytes) [DEBUG] partition /tenants/acme0/bindings (pattern /tenants/*/bindings): key count: 10000 (estimated size 598890 bytes) [DEBUG] partition /tenants/acme2/bindings (pattern /tenants/*/bindings): key count: 10000 (estimated size 598890 bytes) [DEBUG] partition /tenants/acme6/bindings (pattern /tenants/*/bindings): key count: 10000 (estimated size 598890 bytes)实现位于 v1/storage/disk/disk.go 的diagnostics/logPrefixStatistics:它会遍历数据库键来统计每个分区下的键数与估算大小(key count: %d (estimated size %d bytes),size 为键长加值长之和);对含通配符的分区(如/tenants/*/bindings)则逐个具体前缀分别统计。需要注意:
- 这个过程会遍历数据库全部键;
- 它只在启动时、且 debug 日志开启时执行一次(
diagnostics在New末尾调用,且logger.GetLevel() < logging.Debug时直接跳过); - 若完全没有配置用户分区(只有自动的
/system/*),会额外输出一条警告no partitions configured。
细调 Badger 设置(super flags)
用途与风险警告
分区是调优磁盘存储内存与性能的首要手段;而storage.disk.badger这个 super flag 提供了修改 Badger 内部大量内存/磁盘行为的入口。
必须谨慎使用!原文档对此给出明确警告:
- 通过该特性可以覆盖 OPA 使用的任意Badger 设置;
- 对这些选项没有任何校验;
- 当嵌入的 Badger 版本变化时,这些选项本身也可能随之变化。
从源码看,配置解析(v1/storage/disk/config.go 的badgerConfigFromOptions)的执行顺序是:badger.DefaultOptions("")→FromSuperFlag(opts.Badger)应用用户 flag → 随后强制WithDir、WithValueDir、WithDetectConflicts(false)。也就是说:
以下选项无法被覆盖:
dirvaluedirdetectconflicts
其中冲突检测被强制关闭,是因为 OPA 内部的锁机制不允许并发写:在 v1/storage/disk/disk.go 的NewTransaction中,写事务会持有wmu互斥锁(db.wmu.Lock(),注释// only one concurrent write txn),读事务则持读写锁的读锁;既然同一时刻至多一个写事务,冲突检测自然不需要。
除冲突检测外,其余选项采用当前仓库所嵌入的 Badger(github.com/dgraph-io/badger/v4,见 v1/storage/disk/disk.go 的 import)的默认值,super flag 可覆盖其中绝大多数。
配置示例
原文档给出的 YAML 示例:
storage: disk: directory: /tmp/disk badger: nummemtables=1; numgoroutines=2; maxlevels=3super flag 语法为分号分隔的key=value对。仓库测试中还可见其他组合,例如 v1/storage/disk/config_test.go 用nummemtables=123; numversionstokeep=123验证解析正确性,v1/storage/disk/disk_test.go 用memtablesize=4000; valuethreshold=600缩小事务容量以测试多事务拆分。
结合前文,两个实用场景:
- 提高单事务容量:调大
memtablesize,可让opa run命令行传入的大 bundle 加载成功,也能让“单键过大”的报错消失; - 降低内存占用:通过
nummemtables(内存表数量)、maxlevels(LSM 树最大层级数)、numgoroutines(并发例程数)等调整 Badger 的内存与并发行为。
由于这些选项直接对应 Badger 库的Options结构体字段,且没有任何校验,改动前务必先阅读当前版本 Badger 的选项文档,并在测试环境验证内存与性能表现;对线上数据而言,错误的 Badger 配置可能直接导致启动失败或数据布局异常。
小结:磁盘存储的实践要点
- 定位先行:磁盘存储用于容纳超出内存的数据,但它不是数据源——务必自行保障数据可恢复;
- 分区是关键设计决策:分区决定键的粒度,直接影响读键数与读取字节量的权衡;优先针对策略中性能关键的查询优化;被切分的层级必须是对象;
/system由 OPA 托管,分区布局改动必须向后兼容; - 大 bundle 走 bundle service:它会自动拆成多事务写入(非原子);命令行 bundle 仍是单事务,必要时用
memtablesize提高上限;单个超大键无法拆分,需靠更低层级的分区化解; - 用数据说话:通过 REST API 的
?metrics或 Prometheus 直方图观察timer_disk_*与counter_disk_*,用读键数/字节数是否“超配”来定位次优分区;启动时加--log-level debug可查看各分区的键数与估算大小; - super flags 是最后手段:它能覆盖除
dir、valuedir、detectconflicts外的所有 Badger 选项,但没有任何校验,改动需格外小心。
延伸阅读:配置文档(storage.disk配置表)、bundle 服务文档(大 bundle 的投递方式)、REST API 文档(?metrics查询参数)。
- 后端
- 认证鉴权
- 云原生
【免费下载链接】opa
Open Policy Agent (OPA) is an open source, general-purpose policy engine.
相关推荐
OpenProject 项目所需磁盘存储(Required Disk Storage):统计原理与监控实践
OpenProject 项目所需磁盘存储(Required Disk Storage):统计原理与监控实践 Required disk storage (所需磁
后端前端项目管理企业应用协同办公Numba CUDA 磁盘内核缓存(On-disk Kernel Caching)完全指南
Numba CUDA 磁盘内核缓存(On disk Kernel Caching)完全指南 导读 Numba 的 CUDA 后端在 @cuda.jit 装饰器中
编译器高性能计算Apache Doris磁盘IO优化:存储性能调优指南
Apache Doris磁盘IO优化:存储性能调优指南 在数据密集型应用中,磁盘IO性能往往是制约Apache Doris(一款高性能统一分析型数据库)查询速度
OLAP数据库大数据实时分析
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考