新消息显式设置 count 为零。不认识 count 的二进制转发者保留其未知显式存在性;认识 count 却采用隐式存在性的转发者省略默认零。最终两者取值都为零,但只有一条路径仍保留显式设置状态。

Protobuf 演进只能在明确契约内保留含义

gRPC 滚动发布期间,旧生产方、新消费方与中间服务可能同时运行。Protobuf 通过字段编号和线上类型识别二进制内容,帮助这些版本交换消息。但能读懂格式,并不等于应用已经兼容:消息完全可以解析成功,消费方却误解数值、忽略新指令,或者丢掉“未提供”和“显式设置为零”的区别。审阅时需要把这些承诺分别列出来。 分开编号、含义与字节 二进制标签由字段编号与线上类型组合而成,字段名不在这个标签里传输。保持编号和类型不变而改名,可以保留二进制解码能力,但仍会改变生成代码的访问器,也可能改变 JSON 或文本接口。gRPC 的方法名与服务名属于另一层契约,不能因为字段编号没有变化,就认为任意接口改名都不影响调用。 已经退役的编号绝不能拿来表达另一种意思。不同声明类型可能共享相同线上类型,例如有符号与无符号整数都使用变长整数编码,但负数可能被另一种解释读成很大的正数。解析成功不是语义验证。删除字段后,应在 schema 中保留对应编号与名称的 reserved 声明,让后续声明无法意外占用;它是禁止使用的范围,不是留待日后激活的占位字段。 未知字段可以在二进制解析后重新序列化的转发路径中保留,但只复制已知字段,或中途转换为 JSON,都可能丢掉它们。保留解码信息也不保证每次序列化的字节完全相同,字段顺序与编码选择可能随实现或版本变化。兼容测试应比较必要取值和存在状态,不能把普通重新序列化当成跨版本规范化摘要算法。即使一次小样本碰巧字节相等,也不能推广成所有消息的承诺。 存在性承载另一层含义 在 proto3 中,没有 optional 修饰的单值标量通常采用隐式存在性,正常接口无法区分默认值与未提供。显式可选标量则记录字段是否被设置,即便取值为零也能保留这个事实。消息字段与 oneof 成员本来就具有存在性;重复字段和映射不能区分空集合与没有提供。因此,不能把所有 proto3 字段都归为同一种默认规则。 单值字段在隐式与显式存在性之间变化,可能仍然可以二进制互读,却改变转发后的结果。一个知道该字段、但仍用隐式存在性解释它的旧读方,可以读入显式零,然后在重新编码时把默认值省略。仅在一端加上 optional,不会迫使每个中间节点保留这一区别。需要测试实际修改或转发消息的整条路径,而不是只比较两个最终端点。 坏掉的样子 新版消费方可能要求一个旧写方从未提供的字段。虽然 proto3 不支持 required 声明,应用层校验仍然可以制造同样的滚动升级故障。应先约定缺失时的行为,或安排好发布顺序,再开始强制要求这个值。可选语法本身不会定义合理默认值,不会授权某个操作,也不会让旧服务突然实现新功能。这些决定必须由业务契约给出。 还应覆盖已保存消息、延迟任务以及可能回滚到的版本,它们的存活时间可能远长于发布窗口。只比较紧邻的上一个版本,会漏掉仍然可能重新出现的旧记录。即便当前流量已经不使用某个字段,退役编号仍然需要保留。重新投递旧任务与读取历史数据,都不能被当成无关的边缘情况。 两种转发结果的取值都为零,但未知字段转发路径保留显式存在性,认识字段的隐式存在性转发路径则丢失它。历史 Python 实验在本地范围内通过,未测试 reserved 声明、真实 gRPC 调用或跨语言客户端。本次绘图未重跑示例。 快速验证 在 Python 环境中准备 Google 的 protobuf 7.36.2 包,再运行下面完整程序。本次使用 Python 3.12.14。独立描述符池创建真实的新旧消息类,不依赖编译器,不创建磁盘样本,不启动 gRPC 服务,也不建立网络连接。它验证 Python 二进制运行时,不能代替跨语言互操作或生成客户端兼容性测试。 ...

二月 26, 2028 · 2 分钟 · XB Field Notes
有序通道 1 与 5 继续,通道 3 暂停;它们共享连接信用关口,反方向独立表示。

gRPC 用一条 HTTP/2 连接复用多条流

gRPC 的 HTTP/2 传输允许多个 RPC 尝试共享一条连接,同时保留每条流内部的消息顺序。这对短调用和长时间交换都可能有用,但并不意味着每个调用获得了一条无限且完全独立的传输通道。带宽、TCP 交付、连接级流控以及实现中的资源仍然共享,理解这些边界才能准确判断哪些阻塞能够隔离,哪些会影响其他调用。 让调用形式匹配实际工作 一次基于 HTTP/2 的 RPC 尝试占用一条流;重试会发起另一次尝试,而不是把字节重新写入已经结束的流。复用连接能够摊薄建连和 TLS 的成本,但一个 channel 可以管理多条连接,访问不同后端也不会自动共用同一个 TCP 端点。每次调用仍然包含序列化、元数据、帧处理、调度和业务工作,不能把额外成本说成只剩几个帧头。 gRPC 提供单次请求响应、客户端流、服务器流和双向流四类方法。只有双向流允许双方在同一次 RPC 内独立发送消息序列。每个方向的消息顺序得以保留,但不要求每条请求消息都对应一条回复。应当根据应用契约选择调用形式,而不是认为只要采用 gRPC,所有方法就自然具备双向持续推送能力;什么时候读取和写入,仍需应用合理安排。 HTTP/1.1 可以复用持久连接,WebSocket 本身也支持双向消息,二者都不要求每发送一条应用消息就重新握手。因此,不能用虚构的“一次消息一次连接”来证明 gRPC 总是更省。更有意义的比较包括 RPC 契约、生成接口、状态模型和 HTTP/2 多路复用,再结合完整负载和运维约束测量,才能判断实际延迟、内存和连接开销。 分开调用期限与共享传输 截止时间属于某次 RPC,并不会自动成为同一连接上所有流的统一期限。应通过语言支持的上下文机制向下游传递剩余预算;每经过一层就重新领取完整超时,可能让工作持续到调用方早已不愿等待之后。取消还需要业务代码配合停止工作,已经提交的副作用不会因此自动回滚,客户端未收到成功也不等于服务端没有完成修改。 HTTP/2 对 DATA 帧同时实施流级和连接级流控。某条流的窗口耗尽时,只要连接信用仍足够,其他流就可能继续传输。某个方向的连接信用耗尽,会阻止该方向各条流发送非空 DATA;带 END_STREAM 的零长度 DATA 以及控制帧仍可发送。接收端实现可以独立补充连接信用,所以一个慢读者并不必然拖到整条连接都没有额度;应观察真实实现如何管理缓冲和更新。 ...

一月 16, 2027 · 2 分钟 · XB Field Notes