更快的 Protovalidate
Protovalidate 的标准规则一直是 CEL 表达式。这种设计让每种语言实现都拥有唯一的真理来源,即 validate.proto。虽然这让新实现更易于构建且行为保持一致,但也导致验证速度未达到我们的预期。
如今,Go、Java 和 TypeScript 在评估大多数标准规则时已绕过 CEL。与 CEL 路径相比,Go 的验证速度提升约 3 倍,Java 提升 10 倍,TypeScript 提升 13 倍。Python 的性能优化路径有所不同:protovalidate-py 2.0 用原生扩展取代了纯 Python 核心,使复杂架构基准测试的速度提升了 485 倍。它还切换到了新的 protobuf-py 库。
原生规则
Protovalidate 的标准规则在 Protovalidate 架构 中作为 CEL 表达式编写一次,所有实现均从中读取。例如,uint32.lte 规则由以下 CEL 表达式定义:
!has(rules.gte) && !has(rules.gt) && this > rules.lte ? 'must be less than or equal to %s'.format([rules.lte]) : ''
在 Go 中,原生上限检查 如下所示:
func (n nativeNumericCompare[T]) aboveHi(v T) bool {
if n.upper == upperBoundLt {
return v >= n.hi
}
return v > n.hi
}由于宿主语言无需为 CEL 程序运行虚拟机环境,Go 版本的速度 显著 更快。
在构建校验器时,Go、Java 和 TypeScript 会先检查每条规则是否有原生实现(类似上面展示的 Go 版本),如果没有就回退到 CEL 表达式。因此不支持的标率规则和自定义规则仍会走 CEL。Python 和 C++ 目前所有规则都还在用 CEL,后续会再介绍。
每次提交都会跑两遍完整的一致性测试套件:一遍开启原生规则,一遍关闭。只要与 CEL 的结果有任何差异,构建就会失败。我们在 ConnectRPC 等其他多语言项目中也采用了同样的做法。
Go
protovalidate-go v1.3.0 默认启用原生规则。如果出于某些原因需要回到 CEL 路径,在创建校验器时传入 WithDisableNativeRules 即可。
Go、Java 和 TypeScript 原生支持的标率规则范围一致:全部十二种数值类型、bool、bytes、enum、string(含所有知名格式)、repeated 和 map 字段的集合规则,以及 StringValue、Int32Value 等 wrapper 类型。
从这些基准测试看,求值耗时大约降到了原来的三分之一。以下是九项结果中的六项:
Scalar2.4×
CELnativeRepeated/Scalar2.9×
CELnativeMap2.8×
CELnativeInt32GT3.3×
CELnativeComplexSchema2.9×
CELnativeTestByteMatching6.5×
CELnative内存分配也减少了,简单的标量场景甚至降到了零。完整的 benchstat 输出见 #316。
Java
Java 的实现(#469)是从 Go 版本移植来的,方案和规则覆盖范围完全相同。
原生规则同样可以选择关闭:
// Only if you want the CEL path back.
Config config = Config.newBuilder().setEnableNativeRules(false).build();
Validator validator = ValidatorFactory.newBuilder().withConfig(config).build();由于 Java 的 CEL 基线开销更高,相对提升也更明显。以下是其中六项基准测试:
validateBoolConst 8.4×
CELnativevalidateBytesConst 6.4×
CELnativevalidateEnumRules 10.7×
CELnativevalidateBytesIn 11.8×
CELnativevalidateComplexSchema 11.3×
CELnativevalidateInt32GT 33.2×
CELnative运行时分配量大幅减少:`validateComplexSchema` 降低 93%,`validateInt32GT` 降低 99%。校验器构建效率提升更为显著:`buildBenchComplexSchema` 从 15 毫秒降至 37 微秒。完整数据详见 #469。
以上特性已随 protovalidate-java v1.3.0 发布。
TypeScript
@bufbuild/protovalidate v1.3.0 是三个实现中最新发布的(#162)。原生规则默认启用;使用 `disableNativeRules` 可强制所有规则通过 CEL 执行:
const validator = createValidator({ disableNativeRules: true });protovalidate-es 使用 TypeScript 解释 CEL,因此其 CEL 基准性能低于 Go 或 Java:
Scalar 16.0×
CELnativeRepeated/Scalar 9.0×
CELnativeMap 9.4×
CELnativeInt32GT 13.5×
CELnativeComplexSchema 14.9×
CELnativeTestByteMatching 16.9×
CELnative上述结果来自运行 protovalidate-es 基准测试套件 获得。
protovalidate-py 2.0
protovalidate-py v1 通过纯 Python CEL 解释器执行所有规则。复杂 schema 场景下,每条消息需耗时 36 毫秒。这意味着一个完整 API 请求的延迟预算,仅够校验单条消息。
v2.0.0 用基于 protovalidate-cc(C++ 实现)的原生扩展替代了解释器。创建和调用校验器的 API 保持不变。原本 36 毫秒的复杂场景现在仅需 75 微秒:
repeated_message 856×
v1v2scalar 626×
v1v2wrapper_testing 615×
v1v2complex_schema 485×
v1v2int32_gt 365×
v1v2string_matching 217×
v1v2repeated_scalar 104×
v1v2map26×
v1v2以上是首个基准测试表中列出的 13 种情形中的 8 种,详见 #507。
尽管依赖 Rust 和 C++,安装 v2 并不要求用户配置 C++ 或 Rust 工具链。pip install protovalidate 将直接拉取针对 Linux(glibc 和 musl)、macOS 或 Windows 上 x86-64 和 arm64 架构的预编译 wheel 包。
从 1.x 升级
仓库名称已从 protovalidate-python 改为 protovalidate-py,以与 protobuf-py 和 connect-py 保持一致。PyPI 包名仍为 protovalidate,旧仓库 URL 会自动重定向。
Version 2 使用我们 七月发布 的 protobuf-py 作为主要 Protobuf 运行时,取代了 google.protobuf。你仍可以校验 google.protobuf 消息,但库返回的 Violation 消息将是 protobuf-py 消息。该包也不再依赖 protobuf;如果你之前通过 protovalidate 间接获取了该依赖,现在需直接声明它。
大多数读取违规信息的代码保持不变。有两处差异:嵌套消息可能为 None 而非自动变为空消息,且 JSON 序列化采用 protobuf-py 的 API。
# 之前
for violation in validator.collect_violations(message):
print(violation.proto.rule_id)
for element in violation.proto.field.elements:
print(element)
print(MessageToJson(violation.proto))
# 之后
for violation in validator.collect_violations(message):
print(violation.proto.rule_id)
if (field := violation.proto.field) is not None:
for element in field.elements:
print(element)
print(violation.proto.to_json())
代码生成器现在会随库一起生成 buf.validate 类型。与 google.protobuf 不同,protobuf-py 没有进程级的全局注册表,因此这些内置类型不会与你的应用自行生成的副本冲突。你自己生成的类型仍依赖 buf.validate,所以暂时请保持 include_imports 开启。完整的导入支持正在 开发中。
下一步
protovalidate-py 2.0 不再在 Python 层解释执行 CEL,但标准规则在原生层仍然会经过 CEL。下一步就是把这条路径上的 CEL 也绕过去。
当前发布的版本:
- Go:
buf.build/go/protovalidatev1.4.0,原生规则默认开启 - Python:
pip install protovalidatev2.0.0,现已由 protovalidate-cc 驱动 - Java:
build.buf:protovalidatev1.3.0,原生规则默认开启 - TypeScript:
@bufbuild/protovalidatev1.3.0,原生规则默认开启
想试用 Protovalidate,可以访问 protovalidate.com 查看快速上手指南和 在线演练场。如果你正从 protoc-gen-validate 迁移,请先阅读迁移指南。
下一篇文章