空间优先的 protobuffer / json 解码器
今天同事给我转了一个帖子 ,说的是 golang 在处理大量并发 json / protobuffer unmarshaling 事务时,可能产生大量的 (10GB) 临时内存,无法及时回收的问题。
我的观点是:如果一个系统的某个模块有可能使用 10G 这个量级的内存,那么它必然是一个核心问题需要专门对待。核心问题应该有核心问题的考量方法,这不是 GC 之错,也并非手动管理内存一条解决之道。即使手工管理内存,也无非是把内存块之管理转嫁到一个你平常不太想关心的“堆”这个数据结构上,期待有人实现了一个通用方案尽可能的帮你解决好。如果随意使用,一样有类似内存碎片无法合并之类的问题,吃掉你额外的内存。如果它存在于你的核心模块,你一样需要谨慎考量。
在这个具体问题上,我认为还是应该先简化数据结构,让核心数据结构变得易管理。显然、固定大小的数据结构是最容易被管理的。因为如果你的数据结构大小固定,就不再需要堆管理,而是一个固定数组滚动处理临时数据。
json 这样的弹性数据结构看起来不太好用固定大小的数据结构去描述,但稍微想点办法也不难做到。C 在这方面的库不少,我就不一一列举。核心想法一般是:用一个固定数据结构去引用编码数据中的切片。解码后的结构只保留元素的类型以及在原始数据中的位置。如果你能大致知道需要解码的数据元素的数量级,那么就可以用一个固定大小的结构去承载解码结果,不需要任何动态的内存分配。
只不过大多数库把目标都放在时间因素上,让解码更快;少放在空间因素上,让解码过程少使用内存;所以这样的解码方式并不常用。如果你的核心问题于此,你就得去考虑这样的方案。
protobufffer 会比 json 的结构更适合这样做。因为它已经把数据的 key 部分转换成数组 id ,且整体数据结构是明确的。但只是因为 protobuffer 编码更繁杂,不如json 那么简单,反而相关的库更少。
第二、如果你真的有这样的需求:要解码成千上万的数据结构类似的数据包。就应该考虑下列模式:
假设你要解码的数据块的类型是 X ,可以预先生成一个叫做 X 的 accessor 访问器。当你的业务逻辑需要访问 X.a.b 这个字段的时候,应该去调用 X.a.b(binary) ,此处的 binary 指编码过的 json/protobuffer 数据块,X.a.b() 是一个预生成好的访问器函数,它可以用最优化的方法从数据块中提取出需要的数据。
我们还可以进一步的为 X 做一个索引函数,可以预处理 binary ,制作一个固定内存大小的索引信息结构,为具体字段的访问加速。
这个 accessor 对象不太在乎多复杂,因为它可能在整个程序中只初始化一次。