找回密码
 立即注册
查看: 211|回复: 5

有 Protocol buffer 这种轻便的序列化反序列化工具,Json 为 ...

[复制链接]
发表于 2023-2-22 09:10 | 显示全部楼层 |阅读模式
当然json的格式清晰,但是数据量显然大于protocol  buffer
发表于 2023-2-22 09:12 | 显示全部楼层
软件工程也是一种工程学(engineering)。现实世界做工程设计,一定会面对各式各样不同的约束条件。即使是看起来好像一模一样的问题,在不同的约束条件下也可能产生迥然不同的解决方案。
Protobuf是一个非常典型的,针对特定约束,在设计上做出特定妥协(tradeoff)的例子,它的某些设计甚至是反直觉的。例如:

  • 零值/空值字段,在序列化以后,完全不存在于payload里。这意味着反序列化时,这些字段都会被自动填充零值。你无法分辨某个字段是发送端没有(或忘了)赋值,还是赋值为零。
  • 从Protobuf 2.0到3.0,取消了optional字段支持,所有字段都是必需。当然,如上一条所述,在构造消息时,所有字段你都可以不赋值,因为它们都会(在反序列化时)自动获得零值填充。
  • 综上两条所述,如果你确实需要表示一个optional的primitive类型字段,比如一个整数,你需要区分它是没有设置还是设置成0,你就得把它包装在某个wrapper类型里,例如Google官方的wrappers。
更正: @祖与占指出pb 3在release 3.15.0里已经把optional支持加回来了。这个修改是2021年初发布的,但Google官方的pb3 spec到目前(2022.11)都还没把optional加回来d。我猜在实现上是给官方的wrapper类型做了个语法糖,简化使用。
<hr/>很多新接触Protobuf的人会在这些设计上吃到苦头。但为什么Google要这么设计?
如果你在同一个企业内部服务上工作过足够长的时间,你一定会知道,经过一段时间的演变,那怕是最初设计很简单的API,请求/响应消息中字段数量也可能会大量膨胀,而其中一定又会有大量的可选字段,平时根本不会用到。因为gRPC/Protobuf是Google内部的RPC调用标准,减小payload 10%的大小,就意味着节省10%的网络带宽和存储容量。按照Google的规模,你可以想像一下这样的设计能带来的成本节省。
默认0值填充,还突显了另一个特点(或者说workaround):在消息schema的演进上,尽可能强迫API(或者说API实现者)向后/向前兼容。例如v1版的消息有a, b两个字段,v2版有a, b, c三个字段。v1版客户端向v2版服务端发请求,v2版仍然能正常解出一个c为零的v2版消息。而反过来,则是v1版接受v2版消息时也不会出错,只是不能直接看到新字段c。
如果你用过Thrift,你会知道protobuf在应对schema变化时要宽容得多。在Thrift中新增字段,如果另一端IDL没有更新,就会在运行时出错。
换句话说,因为数据合法性的验证常常不能独立于业务逻辑完成,那还不如不要在传输和序列化层面去强求,而是留给业务逻辑层去做。
服务的向后/向前兼容性是个非常大的话题,但凡因为breaking API吃过苦头的人,看到protobuf的这些设计决定,可能会心一笑,原来Google的人也不是神仙,也是这么一步一步踩坑过来的。鄙司内部有个老服务还在用JSON而不是强类型的IDL,前阵子就因为改服务的人没这个意识,搞出API breaking change的严重incident。用Thrift或Protobuf这样的IDL,至少API接口层面的breaking change更容易在code review时发现。
<hr/>其实是想说,protobuf固然有序列化快速,payload小的优点,但并不是适合所有场景。它的学习曲线一定比JSON来得陡峭,有很多坑需要开发者去踩,而它的优点未必适合所有人。
比如,如果你的服务访问量不大,和/或payload不大,用JSON就省掉了踩protobuf坑的风险。即使消息很大,还有gzip transfer-encoding不是。schema和API兼容嘛,就加强测试吧……
没有放之四海而皆准的解决方案,做工程就要做tradeoff。选择一些东西,常常意味着要放弃一些别的东西。
发表于 2023-2-22 09:17 | 显示全部楼层
Protobuf和Thrift其实优势不在性能,而在于强类型,或者说接口的一致性。
JSON虽然也可以有Schema,但是用的地方不多。在超大规模的项目上,弱类型或者没有类型是很糟糕的。最典型的例子是前后端类型统一。这也就是为什么JS代码里诸如下面这种模式很不好。
function handleResponse(responseString) {
  try {
    const response = JSON.parse(responseString);
    if (response.fieldA != null) {
      // do something with field A
    }
  } catch (error) {
    // ...
  }
}
因为没法保证response里一定有fieldA,并且fieldA的类型和预期的一致。即便用TypeScript, 也只能在编译期间保证客户端代码本身的类型一致,但是无法保证和服务端保持一致。一种解决方法是,所有RESTful API都要在后端写好类型定义,并且通过工具自动生成前端的代码的类型定义(TypeScript或者Flow),然后前端就可以直接用。
当然,因为HTTP协议本身是文本协议,所以在HTTP协议上面用Protobuf就不太合适了。但是如果是一个C++服务和一个Python的客户端之间通讯,直接走TCP协议,那么Protobuf就是一个不错的选择。又或者传输的数据本身包含二进制的内容,用JSON得先用Base64等编码,再来压缩的话,性能就比Protobuf差了。
还有一些答主提到的开发效率/开发体验的问题,那都可以通过工具来解决的。只是小一点的公司可能没有足够的资源投入到提高开发体验上。比如二进制数据不可读,那么只要在调试的框架里加一个解析器就行了。
发表于 2023-2-22 09:20 | 显示全部楼层
两边生成辅助类,每个对象都要有,这是重不是轻。
好比是说,坐飞机比地铁快,为什么上下班大家不坐飞机呢?

所以pb通常用在解决对性能要求特别高的场景。

json被大量使用到原因就是易用性,pb是注重性能,两者之间的选择是一个权衡。

易用性和性能和夸平台和成本都是考虑的因素,不然大家为嘛都不买房要租房呢。。

不说了我坐飞机从西直门到东直门去了。
发表于 2023-2-22 09:20 | 显示全部楼层
我提一点,如果你想多个动态库模块用同一份proto,不好意思,会直接Crash
原因就在于谷歌这shit一样的代码生成器认为你的同一份proto必须在同一个模块,而且生成的代码动辄上千上万行,就这还好意思叫轻便?
发表于 2023-2-22 09:24 | 显示全部楼层

  • 神特么轻便,随便一个proto文件生成的cpp动辄上万行,蛐蛐一个序列化库本身的dll/so/lib/a都有好几兆。
  • 而且我项目里仅存的warning全部来自prototobuf,无论用msvc还是gcc还是clang都是铺天盖地的警告,感情谷歌你家用的火星编译器?
  • 而且protobuf那辣鸡性能。。。也就五十步笑百步嘲讽下json罢了,而且还未必干的过rapidjson/simdjson,更别说还有一堆类json格式的高性能序列化库了。
  • 我就没见过地球上还有比protobuf兼容性更差的序列化库。数据文件和库文件版本相差哪怕一个patch号,都直接编译不过。当然原因也可以理解,狗家是mono repo,也不存在需要适配客户版本的问题,根本不需要关心老版本的兼容问题。
  • 更阴间的是,如果有某个库间接依赖了protobuf而你并不知道,然后你用项目里的proto版本写了个程序,那么乐子来了,程序启动后,三方库里间接依赖的那个根本没被你的程序用到的protobuf,会检测到你用的这个另一个版本,然后把你的程序强行terminate掉,开心吧?
  • 案例一:ubuntu默认的mesa库会依赖libmir,然后libmir会依赖一个古董版本protobuf,鬼故事来了。而且mesa库写gui/opengl基本都会间接依赖上。解决方案:装nvidia驱动,替换掉默认的mesa。
  • 案例二:vcpkg安装opencv时,[dnn]这个feature会依赖protobuf,鬼故事又来了。然而软件源里直接把protobuf写在顶层依赖了,哪怕你不装dnn模块,只是拿来处理下图片和矩阵,也会引入protobuf。解决方案:修改软件源配置文件,把依赖挪到dnn模块里——这个我今天刚提了pr。(更新:pr已合并,可以无脑vcpkg install ooencv3/4了)
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-24 06:39 , Processed in 0.874535 second(s), 25 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表