3130 字
约 10 分钟
15
S06-A12 · 序列化格式的选型代价:一选定终身

三年前选了一个二进制格式,今天每接一个新工具,都得先为它写一遍适配。问题不在格式本身有多差,而在于序列化格式不是库,是协议:一旦有第二个进程、第二份历史数据、第二个团队读它,替换成本就从"改一个依赖"变成"迁移全部存量与全部参与方"。

选型时看起来在比较编解码速度,实际在决定一件更长寿的事:未来几年里,谁能读、谁能改、改错了会不会静默。


1. 格式是协议:寿命与代码不匹配

对象 寿命量级 更换成本 谁承担
代码 一两个发布周期 一次重构 本团队
库实现 跟随依赖升级 换依赖、跑一轮回归 本团队
格式与 schema 数年,常长于代码库本身 存量数据 + 全部读写方 所有下游
对外的线协议 与商业约定同寿 双版本并行一整个过渡期 双方
─── 落盘与上网都带版本:这是事后唯一能区分"老数据"的东西
{"v": 3, "id": 10231, "payload": {...}}

判据只有一条:有没有第二个读方。只有本进程自己读、生命周期跟着进程走,格式就是实现细节;一旦落盘、上网、给下游,它就是协议,此后所有决定都要按协议的标准做,包括命名、编号、默认值、错误语义。


2. 四个维度的矛盾:不能同时取强

维度 取强的代价 取弱的代价 常见落点
可读性 文本体积大、解析慢 二进制需要工具与 schema 才能看 对外接口、配置、日志走文本
体积 压缩换 CPU、调试变难 大包挤带宽与页缓存 高频内部调用走二进制
演进能力 强 schema 要维护 IDL 与生成链 靠口头约定,改一次崩一次 跨团队走强 schema
生态 强生态绑定语言与版本 弱生态要自己写适配 多语言场景先查各语言实现质量
─── 文本格式的两个隐性损失:精度,与"缺失"
{"amount": 0.1}                // 二进制浮点表示,不是精确的 0.1
{"amount": null}  对比  {}      // null 与"没有这个字段"多数实现不区分

四维不能同时最优,只能回答"哪个维度坏了更要命"。对外可读性坏了,排查成本永久上升;体积坏了,代价主要在流量账上,通常可买。把体积当第一判据的团队,往往是在用一个可测的小数换一个不可测的大风险。


3. "可演进"的真正含义:加、删、改各自能不能安全做

操作 安全条件 不安全时的表现
加字段 读方忽略未知字段,新字段有默认值或可选 读方遇到未知字段直接反序列化失败
删字段 旧数据不再被读,且名字与编号永不复用 复用编号 → 旧数据被解析成新语义
改类型 新类型是旧类型的超集且编码兼容 跨编码(如数值与字符串)旧读方直接报错
改语义(类型不变) 必须靠版本号或改名 最难发现:能解析,值全错
// 删除字段的正确姿势:编号与名字都别再回来
message Order {
  reserved 7, 9;                  // 曾用编号,永久退役
  reserved "channel";             // 曾用名字
  optional string channel_code = 12;
}

最危险的是最后一行。类型没变、结构没变,含义变了——把"金额单位从元改成分"这类改动,任何 schema 机制都检测不到,只有版本字段加双读比对能拦住。同样的坑还有时区(本地时间换 UTC)与精度(分改厘),它们都表现为"解析成功、数值不对"。

所以真正该被评审的不是"改了什么字段",而是"这次改动属于哪一类"。把四类改动写成一份检查清单挂进 schema 的 MR 模板,比任何工具都省事:

"可演进"不等于"随便改":它等于每一类改动都有明确的安全条件,且这些条件被文档化、被评审。没有这份规则的强 schema,只是把错误从运行期提前到了"生成链报错"。


4. schema 的三档严格度,与它们各自的失效方式

严格度 谁来保证 失效方式 适合
无 schema 人 拼写、缺字段、类型漂移全部在运行期暴露 一次性脚本、内部临时数据
弱 schema 运行期校验(DTO 校验、校验库) 校验漏一处即静默通过;文档与实现不同步 单团队、中等寿命
强 schema 编译器 + IDL + 编号规则 规则被执行了但语义错了;生成链成为发布瓶颈 跨团队、长期
// 弱 schema 的真实边界:校验只覆盖被声明的字段
class OrderDto { String channelCode; BigDecimal amount; }   // 无约束
// 新增字段默认放行,字段名拼错也默认放行——两者在日志里长得一样

强 schema 不是"更安全",而是把错误提前到编译期,代价是新增一条生成链:IDL 版本、插件版本、生成代码是否入库、CI 里跑不跑生成检查。这条链的维护成本最常被算漏,它会在换构建工具、升编译器、加一门语言时集中爆发。


5. 字段编号与命名:最难事后更改的决定

绑定对象 典型改动 后果
二进制格式的编号 复用已删编号 旧数据被读成新字段,静默错值
JSON 的键名 改名 旧读方取不到值,拿到 null 或缺省
数据库列名 改名 依赖列名的 SQL、映射、报表同时断
类字段名 改名 反射与映射框架按名匹配时断
// 编号台账:退役编号永不回收,回收即数据损坏
message: Order
next_free: 13
assigned:
  1: id
  2: amount
  7: channel        // deprecated 2026-03,禁止复用

文本协议里主要成本是命名,二进制协议里主要成本是编号。选二进制格式时,编号台账必须进仓库、与 schema 同一次评审——它比字段名更难事后修,因为字段名至少还能在代码里全局替换,编号一旦写进存量字节就固化在数据里了。


6. 兼容方向、跨语言成本、性能权重与包裹层

方向 定义 谁必须先升级
向后兼容 新代码能读旧数据 数据先老,代码后新,随时可发
向前兼容 旧代码能读新数据 旧读方必须先学会忽略未知字段
语言 实现来源 隐性成本
JVM 官方或社区 生成代码与构建插件版本互相绑定
Go 官方 零值与可选语义的差异要单独处理
Python 纯实现慢,C 扩展装不上 依赖编译环境,镜像里常缺
前端 需要额外运行时 包体积进入首屏预算

两个方向常被混为一谈。向后兼容坏了,表现是"历史数据读不出来";向前兼容坏了,表现是"灰度期间旧实例遇到新写入就崩"。灰度发布真正需要的是后者。

// 端口在内、格式在外:换格式只改适配器
interface OrderCodec { byte[] encode(Order o); Order decode(byte[] b); }
// 业务代码只依赖接口,字节只在 codec 边界出现

包裹层常被当成"以后换格式很便宜"的理由,这一点要打折:它确实让替换实现变便宜,却对迁移存量毫无帮助。存量数据是按旧格式写的,换与不换都要读一遍、写一遍。所以包裹层的真实价值是隔离变化与支持双读,不是抹掉迁移。

判据是存量的可弃性:存量能重建(缓存、派生表、日志),包裹层的价值接近满分;存量不可重建(订单、账单),包裹层只是一层缓冲,真正决定成本的是迁移工程。

包裹层能给你 不能给你
换格式不动调用方 存量数据仍要迁移
双读双写、灰度过渡 语义差异(时间精度、null 与缺省)
统一的版本与错误语义 对外线协议的兼容性(内部换了、对外没变才成立)

性能在这个决策里的权重经常被高估:端到端耗时里序列化通常是位数百分比量级(按自己的 profile 结果核对,不要按基准文章),真正的瓶颈多在网络往返次数与数据库。可读性相反,常被低估——线上排查时能直接用文本看一眼,与需要工具加 schema 才能解码,差的是一整次定位时长。


排错:五种现场

现象 原因 处理
旧数据读不出来 向后兼容坏了:字段被删或编号被复用 用版本字段分支解码,禁止复用编号
灰度期间旧实例崩 向前兼容坏了:遇到未知字段报错 打开忽略未知字段,先升级读方
解析成功但值全错 语义漂移(单位、精度、含义) 版本号 + 双读比对,类型检查救不了
某语言实现慢一个数量级 走了纯解释实现或未走二进制通道 换官方实现,按 profile 定位
前端包体积超预算 引入了完整运行时 只在必要端引入,或改文本格式

边界:这些情况选最省事的即可

场景 做法 理由
进程内或单次任务的数据 内存结构、随手 JSON 没有第二读方,协议属性不存在
生命周期短于一年 用团队最熟的格式 迁移成本不会真正发生
单语言、单团队 语言原生序列化 没有跨语言适配面
同一制品写、同一制品读回 带一个版本字段即可 无第三方兼容约束
数据量极小且以人看为主 可读性优先 体积差在噪声里

判据是读方的数量与存量的寿命。读方只有自己、存量几个月就过期,格式就是实现细节,选最顺手的最划算;读方包含未知的第三方、存量要留几年,它就已经是协议,此时省下的每一分钟都要在未来几年里还回去。


自测

  1. 判断一个序列化格式是"实现细节"还是"协议",判据是什么?
  2. 加字段、删字段、改类型各自的安全条件是什么,哪一类最危险?
  3. 弱 schema 与强 schema 的失效方式有什么本质不同?
  4. 为什么"复用已删除的字段编号"比改字段名危险得多?
  5. 灰度发布更需要向后兼容还是向前兼容,为什么?
  6. 把格式包在接口后面,哪些成本被消掉了,哪些仍在包裹层之外?
S06-A12 · 序列化格式的选型代价:一选定终身
https://www.clxhxhhr.top/posts/6339/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。