← 文章 / 未分类
buf 19小时前 · 2026-09-17 23:17:59 · 2 阅读

更快的 Protovalidate

Protovalidate 的标准规则一直是 CEL 表达式。这种设计让每种语言实现都拥有唯一的真理来源,即 validate.proto。虽然这让新实现更易于构建且行为保持一致,但也导致验证速度未达到我们的预期。

如今,GoJavaTypeScript 在评估大多数标准规则时已绕过 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 原生支持的标率规则范围一致:全部十二种数值类型、boolbytesenumstring(含所有知名格式)、repeatedmap 字段的集合规则,以及 StringValueInt32Value 等 wrapper 类型。

从这些基准测试看,求值耗时大约降到了原来的三分之一。以下是九项结果中的六项:

Scalar2.4×

CELnative

Repeated/Scalar2.9×

CELnative

Map2.8×

CELnative

Int32GT3.3×

CELnative

ComplexSchema2.9×

CELnative

TestByteMatching6.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×

CELnative

validateBytesConst 6.4×

CELnative

validateEnumRules 10.7×

CELnative

validateBytesIn 11.8×

CELnative

validateComplexSchema 11.3×

CELnative

validateInt32GT 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×

CELnative

Repeated/Scalar 9.0×

CELnative

Map 9.4×

CELnative

Int32GT 13.5×

CELnative

ComplexSchema 14.9×

CELnative

TestByteMatching 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×

v1v2

scalar 626×

v1v2

wrapper_testing 615×

v1v2

complex_schema 485×

v1v2

int32_gt 365×

v1v2

string_matching 217×

v1v2

repeated_scalar 104×

v1v2
v1v2

map26×

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-pyconnect-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/protovalidate v1.4.0,原生规则默认开启
  • Python:pip install protovalidate v2.0.0,现已由 protovalidate-cc 驱动
  • Java:build.buf:protovalidate v1.3.0,原生规则默认开启
  • TypeScript:@bufbuild/protovalidate v1.3.0,原生规则默认开启

想试用 Protovalidate,可以访问 protovalidate.com 查看快速上手指南和 在线演练场。如果你正从 protoc-gen-validate 迁移,请先阅读迁移指南

下一篇文章
原始来源: buf

评论 (0)