ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Apache Spark Connect 协议扩展指南:Proto3 消息中 Required 与 Optional 字段的约定与实战

Apache Spark Connect 协议扩展指南:Proto3 消息中 Required 与 Optional 字段的约定与实战 Apache Spark Connect 协议扩展指南Proto3 消息中 Required 与 Optional 字段的约定与实战【免费下载链接】sparkApache Spark - A unified analytics engine for large-scale data processing项目地址: https://gitcode.com/gh_mirrors/sp/sparkSpark Connect 将客户端与执行引擎解耦为基于 gRPC 的独立协议而协议本身完全由 sql/connect/common/src/main/protobuf/spark/connect/ 下的.proto文件定义。由于协议演进必须保持向前兼容新增字段的语义标注方式直接决定了服务端的行为边界。本文基于 sql/connect/docs/adding-proto-messages.md 这一面向 Spark Connect 开发者的官方约定文档系统讲解 proto3 时代如何表达「必填」与「可选」并结合仓库中的真实 proto 定义、生成脚本与校验工具给出可落地的新增消息开发流程。读完本文你将掌握 Spark Connect 协议字段的注释规范、服务端校验原则以及修改 proto 文件后的代码再生成与兼容性检查方法。一、背景为什么 proto3 中没有required关键字Spark Connect 协议选择的是proto3语法各 proto 文件首行均声明syntax proto3。与 proto2 相比proto3 做了两项直接影响字段语义的设计取舍不再支持required约束proto3 中所有字段除oneof内字段外默认都是「可选」的字段是否存在完全由调用方决定协议层无法强制要求某个字段必须被填充非 message 类型字段没有has_field_name()探测方法在 proto2 里可以通过has_format()之类的生成方法判断字段是否被显式设置而 proto3 对string、int32等标量类型不再生成这类方法服务端只能通过「值是否为默认值如空串、0」间接推断。这两点决定了「必填」与「可选」在 Spark Connect 中不再是一个编译期强制约束而必须成为开发者之间的显式约定。为了让这种约定在代码评审、多语言客户端实现中保持一致Spark Connect 制定了统一的注释标记体系即文档标题所说的 Required, Optional and default values。二、Required 字段用(Required)注释显式声明2.1 约定规则对于语义上必须存在、服务端无法正确处理的字段开发者必须在字段注释中以(Required)作为标记前缀在标记后补充说明该字段的取值约束或用途。标量字段服务端不做任何额外的输入校验信任客户端传入的值复合字段message 类型服务端只做最轻量的检查避免空指针异常不做语义层面的校验。也就是说(Required)是一种「文档级 代码级约定」而非运行时强校验。真正的一致性依赖客户端实现者自觉遵守注释以及评审者在审查新增消息时核对标记。2.2 文档中的示例message DataSource { // (Required) Supported formats include: parquet, orc, text, json, parquet, csv, avro. string format 1; }注意这里format是普通的string字段未加optional但因为它在语义上是必填的所以注释中明确写下了(Required)和允许的取值集合。2.3 仓库中的真实案例在 sql/connect/common/src/main/protobuf/spark/connect/base.proto 中AnalyzePlanRequest的session_id字段就是典型的(Required)标量字段// (Required) // // The session_id specifies a spark session for a user id (which is specified // by user_context.user_id). The session_id is set by the client to be able to // collate streaming responses from different queries within the dedicated session. // The id should be an UUID string of the format 00112233-4455-6677-8899-aabbccddeeff string session_id 1;该字段注释不仅标注(Required)还进一步约束了取值格式UUID 字符串。同理UserContext user_context 2被标注为(Required) User context属于「复合字段只做最小检查」的实例——服务端只会保证其非空引用不会校验内部字段的合法性。再看oneof中的子消息字段message Explain { // (Required) The logical plan to be analyzed. Plan plan 1; // (Required) For analyzePlan rpc calls, configure the mode to explain plan in strings. ExplainMode explain_mode 2; ... }这印证了约定在嵌套消息中的一致性凡是服务端处理该请求所必需的输入一律以(Required)标注。三、Optional 字段用optional关键字 (Optional)注释表达「缺省分支」3.1 约定规则语义上可选的字段必须显式标记为optional。服务端拿到请求后会依据该字段是否存在来分支进入不同的处理逻辑。但这里有一个关键细节由于标量类型在 proto3 中无法配置自定义默认值「字段是否存在」本身并不等于「字段的默认值」。例如optional int32 level 2被设置与否服务端拿到的值都存在0这个天然默认值。因此约定要求服务端实现必须依据自身的规则来解释观察到的值而不是假定 proto 层会注入业务默认值。换言之默认值的解释权在服务端代码而不在 proto 文件或生成的客户端代码中。3.2 文档中的示例message DataSource { // (Optional) If not set, Spark will infer the schema. optional string schema 2; }客户端不设置schema时服务端自动推断 schema设置后则以显式 schema 为准——这正是「依据字段是否存在来分支」的典型语义。3.3 仓库中的真实案例DataSource消息的完整形态文档示例并非孤立存在sql/connect/common/src/main/protobuf/spark/connect/relations.proto 中的Read.DataSource消息第 239 行起就是这套约定在真实协议中的落地版本且注释信息比文档示例更加完整message DataSource { // (Optional) Supported formats include: parquet, orc, text, json, parquet, csv, avro. // // If not set, the value from SQL conf spark.sql.sources.default will be used. optional string format 1; // (Optional) If not set, Spark will infer the schema. // // This schema string should be either DDL-formatted or JSON-formatted. optional string schema 2; // Options for the data source. The context of this map varies based on the // data source format. This options could be empty for valid data source format. // The map key is case insensitive. mapstring, string options 3; // (Optional) A list of path for file-system backed data sources. repeated string paths 4; // (Optional) Condition in the where clause for each partition. // // This is only supported by the JDBC data source. repeated string predicates 5; ... }可以观察到几个有意思的实践细节format的默认值来自服务端配置注释明确说明「不设置时使用spark.sql.sources.default配置项」这正是「服务端按自身规则解释缺省值」的直接例证schema的默认行为是「自动推断」并进一步限定了取值必须是 DDL 格式或 JSON 格式字符串map与repeated字段天然具备「可选/可为空」特性它们的缺省语义空 map、空列表由服务端自行处理注释还承担了限制适用场景的职责如predicates仅 JDBC 数据源支持。同样在 base.proto 中(Optional)标记广泛用于client_type、client_observed_server_side_session_id、last_response_id等「存在与否会影响服务端行为但不影响请求基本可处理性」的字段例如// (Optional) // // Server-side generated idempotency key from the previous responses (if any). Server // can use this to validate that the server side session has not changed. optional string client_observed_server_side_session_id 17;client_type则被明确注释为「仅用于日志服务端不会解释其含义」——说明(Optional)字段也不一定参与业务分支是否解释完全由服务端实现决定。四、oneof互斥字段的特殊语义容器除 Required/Optional 之外Spark Connect 协议大量使用oneof表达互斥选择。例如AnalyzePlanRequest中的oneof analyze聚合了Schema、Explain、TreeString、IsLocal、DDLParse等十余种分析操作一次请求只能选择其中一种。proto3 中oneof字段本身自带「已设置」语义生成的代码会提供getAnalyzeCase()之类的方法因此它天然解决了「该字段是否被显式选择」的判定问题——这与普通标量字段在 proto3 中「无法探测是否设置」的痛点形成互补。新增消息时若语义上要求「多选一」应优先考虑oneof而非多个(Optional)标量字段。五、实战流程新增/修改 proto 消息后如何同步生成代码文档与 README 的约定最终要落到构建流程中。Spark Connect 的 proto 变更涉及多语言生成的代码实际开发流程如下5.1 修改 proto 定义在 sql/connect/common/src/main/protobuf/spark/connect/ 目录含base.proto、relations.proto、expressions.proto、commands.proto、catalog.proto、ml.proto、types.proto等中新增或修改消息并严格遵循上文注释约定。需要特别提醒协议演进要保证向后兼容——新增字段使用新编号即可禁止修改、删除或复用已有字段编号否则会导致旧客户端解析错乱。5.2 重新生成客户端代码Python 客户端的 proto 生成由 dev/connect-gen-protos.sh 完成其依赖与使用方式记录在 sql/connect/README.md 中# 1. 准备 Python 环境并安装依赖如 ruff 及 Spark Connect proto 生成插件 pip install --group dev # 2. 安装 bufproto 编译工具链 brew install bufbuild/buf/buf # 3. 生成 Python 客户端代码 dev/connect-gen-protos.shJava/Scala 端的 proto 代码则随 Maven/sbt 构建自动生成。仓库还提供了一键生成脚本 dev/gen-protos.sh。若构建环境无法使用官方protoc/protoc-gen-grpc-java二进制例如 CentOS 6/7 上 glibc 版本低于 2.14可显式指定自编译的工具路径export SPARK_PROTOC_EXEC_PATH/path-to-protoc-exe export CONNECT_PLUGIN_EXEC_PATH/path-to-protoc-gen-grpc-java-exe ./build/mvn -Phive -Puser-defined-protoc clean package # 或 ./build/sbt -Puser-defined-protoc clean package5.3 兼容性检查与验证dev/check-protos.py 负责校验 proto 文件的格式与一致性dev/protobuf-breaking-changes-check.sh 用于在 CI 中检测破坏性变更如删除字段、修改编号防止协议不兼容被合入协议对应的服务端解析逻辑位于sql/connect/common与sql/connect/server的 Java/Scala 源码中本仓库的 sql/connect/common/src/main/java 等目录修改 proto 后需同步实现(Required)/(Optional)字段的服务端行为并补齐测试。六、总结给 Spark Connect 协议贡献者的字段标注速查语义proto 写法注释标记服务端行为必填标量普通字段如string format 1;// (Required) 取值说明不做额外输入校验必填复合普通 message 字段// (Required) 说明仅做最小检查避免 NPE不做语义校验可选字段optional关键字// (Optional) 缺省行为说明依据字段是否存在分支默认值由服务端自行解释多选一oneof依据内部字段语义标注天然具备「是否被选择」的探测能力一句话概括本约定的核心proto3 丢掉了编译期强制Spark Connect 用统一的(Required)/(Optional)注释体系补上了「语义契约」——注释既是给多语言客户端实现者看的接口文档也是代码评审中核对字段语义的检查清单而默认值、校验逻辑、分支行为永远以服务端实现为准。遵循这套约定就能在保持协议向前兼容的前提下安全、清晰地为 Spark Connect 增加新的 proto 消息。【免费下载链接】sparkApache Spark - A unified analytics engine for large-scale data processing项目地址: https://gitcode.com/gh_mirrors/sp/spark创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进