4千字长文预警!!java
好,再没有一种序列化方案能像JSON和XML同样流行,自由、方便,拥有强大的表达力和跨平台能力。是通用数据传输格式的默认首选。不过随着数据量的增长和性能要求的提高,这种自由与通用带来的性能问题也不容忽视。python
JSON和XML使用字符串表示全部的数据,对于非字符数据来讲,字面量表达会占用不少额外的存储空间,而且会严重受到数值大小和精度的影响。 一个32位浮点数 1234.5678 在内存中占用 4 bytes 空间,若是存储为 utf8 ,则须要占用 9 bytes空间,在JS这样使用utf16表达字符串的环境中,须要占用 18 bytes空间。 使用正则表达式进行数据解析,在面对非字符数据时显得十分低效,不只要耗费大量的运算解析数据结构,还要将字面量转换成对应的数据类型。android
在面对海量数据时,这种格式自己就可以成为整个系统的IO与计算瓶颈,甚至直接overflow。c++
众多的序列化方案中,按照存储方案,可分为字符串存储和二进制存储,字符串存储是可读的,可是因为以上问题,这里只考虑二进制存储。二进制存储中可分为须要IDL和不须要IDL,或分为自描述与非自描述(反序列化是否须要IDL)。git
须要IDL的使用过程:github
使用该方案所定义的IDL语法编写schemaweb
使用该方案提供的编译器将schema编译成生产方和消费方所用语言的代码(类或者模块)正则表达式
数据生产方引用该代码,根据其接口,构建数据,并序列化编程
消费方引用该代码,根据其接口读取数据json
不须要IDL的使用过程:
生产方与消费方经过文档约定数据结构
生产方序列化
消费方反序列化
etc.
protocol buffers
gRPC所用的传输协议,二进制存储,须要IDL,非自描述
高压缩率,表达性极强,在Google系产品中使用普遍
flat buffers
Google推出序列化方案,二进制存储,须要IDL,非自描述(自描述方案不跨平台)
高性能,体积小,支持string、number、boolean
avro
Hadoop使用的序列化方案,将二进制方案和字符串方案的优点结合起来,仅序列化过程须要IDL,自描述
然而场景受限,没有成熟的JS实现,不适合Web环境,这里不作对比
Thrift
Facebook的方案,二进制存储,须要IDL,非自描述
基本上只集成在RPC中使用,这里不作对比
针对多维数组设计的序列化方案,二进制存储,不须要IDL,自描述
高性能,体积小,支持string、number、boolean
空间优化原理
使用数值类型而非字面量来保存数值,自己就能节约一笔十分可观的空间。 protocol buffer为了实现更高的压缩率,使用varint去压缩数值。(不过下面的测试代表,可使用gzip的环境中,这种方案没有帮助)
时间优化原理
二进制格式用经过特定位置来记录数据结构以及每一个节点数据的偏移量,省去了从字符串中解析数据结构所耗费的时间,避免了长字符串带来的性能问题,在GC语言中,也大大减小了中间垃圾的产生。
在能够进行内存直接操做的环境中(包括JS),还能够经过内存偏移量直接读取数据,而避免进行复制操做,也避免开辟额外的内存空间。DIMBIN和flatbuffers都使用了这种理念来优化数据存储性能。在JS环境中,经过创建DataView或者TypedArray来从内存段中提取数据的耗时基本上能够忽略不计。
二进制方案中存储字符串须要额外的逻辑进行UTF8编解码,性能和体积不如JSON这样的字符串格式。
咱们的数据可视化场景中常常涉及百万甚至千万条数据的实时更新,为解决JSON的性能问题,咱们使用内存偏移量操做的思路,开发了DIMBIN做为序列化方案,并基于其上设计了许多针对web端数据处理的传输格式。
做为一种简单直白的优化思路,DIMBIN已经成为咱们数据传输的标准方案,保持绝对的精简与高效。
咱们刚刚将DIMBIN开源,贡献给社区,但愿能为你们带来一个比 JSON/protocol/flatbuffers 更轻、更快、对Web更友好的解决方案。
针对Web/JS环境中的使用,咱们选择 JSON、protocol buffers、flat buffers、DIMBIN 四种方案,从七个方面进行对比。
Protocolbuffers 和 flatbuffers 表明Google所倡导的完整的workflow。严格、规范、统1、面向IDL,为多端协做所设计,针对python/java/c++。经过IDL生成代码,多平台/多语言使用一致的开发流程。若是团队采用这种工做流,那么这种方案更便于管理,多端协做和接口更迭都更可控。
可是若是离开了这套工程结构,则显得相对繁杂。
JSON/XML 和 DIMBIN 是中立的,不须要IDL,不对工程化方案和技术选型做假设或限制。能够只经过文档规范接口,也能够自行添加schema约束。
Protocolbuffers 和 flatbuffers 须在项目设计的早期阶段加入,并做为工做流中的关键环节。若是出于性能优化目的而加入,会对项目架构形成较大影响。
JSON基本是全部平台的基础设施,无部署成本。
DIMBIN只须要安装一个软件包,可是须要数据结构扁平化,若是数据结构没法被扁平化,将没法从中受益。
在JS中使用时:
使用JSON序列化反序列化的代码行数基本在5之内
使用DIMBIN则10行左右
使用protocol须要单独编写schema(proto)文件,引入编译出的几百行代码,序列化和反序列化时,须要经过面向对象风格的接口操做每一个节点的数据(数据结构上的每一个节点都是一个对象)
使用flatbuffer须要单独编写schema(fbs)文件,引入编译出的几百行代码,序列化过程须要经过状态机风格的接口处理每一个节点,手动转换并放入每一个节点的数据,书写体验比较磨人;反序列化过程经过对象操做接口读取每一个节点的数据
Protocol官网声称性能高于JSON,该测试数据显然不是JS端的,咱们的测试代表其JS端性相对于JSON更差(数据量大的时候差的多)。
全部的二进制方案处理字符串的过程都是相似的:须要将js中的utf16先解码成unicode,再编码成utf8,写入buffer,并记录每一个字符串的数据地址。该过程性能消耗较大,并且若是不使用varint(protocol buffers)的话,体积也没有任何优点。
在处理字符串数据时,JSON的性能老是最好的,序列化性能 JSON > DIMBIN > flatbuffers > proto,反序列化 JSON > proto > DIMBIN > flatbuffers
处理数值数据时 Flatbuffers 和 DIMBIN 性能优点明显,
对于扁平化数值数据的序列化性能 DIMBIN > flatbuffers > JSON > proto,
反序列化 DIMBIN > flatbuffers >十万倍> JSON > proto
使用字符串与数值混合结构或者纯数值时,protocol < DIMBIN < flat < JSON 使用纯字符串时,JSON最小,二进制方案都比较大
Gzip以后,DIMBIN和flat的体积最小且基本一致,protocol反而没有优点,猜想多是varint的反作用。
Protocol 为强类型语言而设计,所支持的类型比JSON要丰富的多,数据结构也能够十分复杂; Flatbuffers 支持 数值/布尔值/字符串 三种基本类型,结构与JSON相似; DIMBIN 支持 数值/布尔值/字符串 三种基本类型,目前只支持多维数组的结构(暂不支持也不鼓励使用键值对),更复杂的结构须要在其上封装。
JSON和DIMBIN都是自描述的,(弱类型语言中)不须要schema,用户能够动态生成数据结构和数据类型,生产方和消费方之间约定好便可,若是须要类型检查则须要在上层封装。
Protocolbuffers 和 flatbuffers 必须在编码前先写好IDL并生成对应的代码,接口修改则需修改IDL并从新生成代码、部署到生产端和消费端、再基于其上进行编码。
Protocolbuffers的C++和java实现中有自描述的特性,能够嵌入.proto文件,可是依然须要编译一个顶层接口来描述这个“自描述的内嵌数据”,基本没有实用性,其文档中也说Google内部历来没有这样用过(不符合IDL的设计原则)。
flatbuffers 有一个自描述版本的分支(flexbuffers),试验阶段,无JS支持,无相关文档。
Protocolbuffers 和 flatbuffers 服务端与客户端语言支持都很是完整。二者优先针对C++/Java(android)/Python开发,JS端缺乏一部分高级功能,无完整文档,须要本身研究example和生成的代码,不过代码不长,注释覆盖完整。
JSON基本上全部编程语言都有对应的工具。
DIMBIN针对JS/TS开发和优化,目前提供c#版本,c++、wasm、java和python的支持在计划中。
咱们生成一份典型的数据,使用扁平化和非扁平化两种结构,使用JSON、DIMBIN、protocol和flat buffers来实现相同的功能,对比各类方案的性能、体积以及便捷程度。
咱们生成两个版本的测试数据:非扁平化(多层键值对结构)数据和等效的扁平化(多维数组)数据
考虑到字符串处理的特殊性,在测试时咱们分开测试了 字符串/数值混合数据、纯字符串数据,和纯数值数据
// 非扁平化数据 export const data = { items: [ { position: [0, 0, 0], index: 0, info: { a: 'text text text...', b: 10.12, }, }, // * 200,000 个 ], } // 等效的扁平化数据 export const flattedData = { positions: [0, 0, 0, 0, 0, 1, ...], indices: [0, 1, ...], info_a: ['text text text', 'text', ...], info_b: [10.12, 12.04, ...], }
const jsonSerialize = () => { return JSON.stringify(data) }
const jsonParse = str => { const _data = JSON.parse(str) let _read = null // 因为flat buffers的读取操做是延后的,所以这里须要主动读取数据来保证测试的公平性 const len = _data.items.length for (let i = 0; i < len; i++) { const item = _data.items[i] _read = item.info.a _read = item.info.b _read = item.index _read = item.position } }
import DIMBIN from 'src/dimbin' const dimbinSerialize = () => { return DIMBIN.serialize([ new Float32Array(flattedData.positions), new Int32Array(flattedData.indices), DIMBIN.stringsSerialize(flattedData.info_a), new Float32Array(flattedData.info_b), ]) }
const dimbinParse = buffer => { const dim = DIMBIN.parse(buffer) const result = { positions: dim[0], indices: dim[1], info_a: DIMBIN.stringsParse(dim[2]), info_b: dim[3], } }
DIMBIN目前仅支持多维数组,不能处理树状数据结构,这里不作对比。
首先须要按照proto3语法编写schema
syntax = "proto3"; message Info { string a = 1; float b = 2; } message Item { repeated float position = 1; int32 index = 2; Info info = 3; } message Data { repeated Item items = 1; } message FlattedData { repeated float positions = 1; repeated int32 indices = 2; repeated string info_a = 3; repeated float info_b = 4; }
使用 protoc 编译器将schema编译成JS模块
./lib/protoc-3.8.0-osx-x86_64/bin/protoc ./src/data.proto --js_out=import_style=commonjs,,binary:./src/generated
// 引入编译好的JS模块 const messages = require('src/generated/src/data_pb.js') const protoSerialize = () => { // 顶层节点 const pbData = new messages.Data() data.items.forEach(item => { // 节点 const pbInfo = new messages.Info() // 节点写入数据 pbInfo.setA(item.info.a) pbInfo.setB(item.info.b) // 子级节点 const pbItem = new messages.Item() pbItem.setInfo(pbInfo) pbItem.setIndex(item.index) pbItem.setPositionList(item.position) pbData.addItems(pbItem) }) // 序列化 const buffer = pbData.serializeBinary() return buffer // 扁平化方案: // const pbData = new messages.FlattedData() // pbData.setPositionsList(flattedData.positions) // pbData.setIndicesList(flattedData.indices) // pbData.setInfoAList(flattedData.info_a) // pbData.setInfoBList(flattedData.info_b) // const buffer = pbData.serializeBinary() // return buffer }
// 引入编译好的JS模块 const messages = require('src/generated/src/data_pb.js') const protoParse = buffer => { const _data = messages.Data.deserializeBinary(buffer) let _read = null const items = _data.getItemsList() for (let i = 0; i < items.length; i++) { const item = items[i] const info = item.getInfo() _read = info.getA() _read = info.getB() _read = item.getIndex() _read = item.getPositionList() } // 扁平化方案: // const _data = messages.FlattedData.deserializeBinary(buffer) // // 读数据(避免延迟读取带来的标定偏差) // let _read = null // _read = _data.getPositionsList() // _read = _data.getIndicesList() // _read = _data.getInfoAList() // _read = _data.getInfoBList() }
首先须要按照proto3语法编写schema
table Info { a: string; b: float; } table Item { position: [float]; index: int; info: Info; } table Data { items: [Item]; } table FlattedData { positions:[float]; indices:[int]; info_a:[string]; info_b:[float]; }
./lib/flatbuffers-1.11.0/flatc -o ./src/generated/ --js --binary ./src/data.fbs
// 首先引入基础库 const flatbuffers = require('flatbuffers').flatbuffers // 而后引入编译出的JS模块 const tables = require('src/generated/data_generated.js') const flatbufferSerialize = () => { const builder = new flatbuffers.Builder(0) const items = [] data.items.forEach(item => { let a = null // 字符串处理 if (item.info.a) { a = builder.createString(item.info.a) } // 开始操做 info 节点 tables.Info.startInfo(builder) // 添加数值 item.info.a && tables.Info.addA(builder, a) tables.Info.addB(builder, item.info.b) // 完成操做info节点 const fbInfo = tables.Info.endInfo(builder) // 数组处理 let position = null if (item.position) { position = tables.Item.createPositionVector(builder, item.position) } // 开始操做item节点 tables.Item.startItem(builder) // 写入数据 item.position && tables.Item.addPosition(builder, position) item.index && tables.Item.addIndex(builder, item.index) tables.Item.addInfo(builder, fbInfo) // 完成info节点 const fbItem = tables.Item.endItem(builder) items.push(fbItem) }) // 数组处理 const pbItems = tables.Data.createItemsVector(builder, items) // 开始操做data节点 tables.Data.startData(builder) // 写入数据 tables.Data.addItems(builder, pbItems) // 完成操做 const fbData = tables.Data.endData(builder) // 完成全部操做 builder.finish(fbData) // 输出 // @NOTE 这个buffer是有偏移量的 // return builder.asUint8Array().buffer return builder.asUint8Array().slice().buffer // 扁平化方案: // const builder = new flatbuffers.Builder(0) // const pbPositions = tables.FlattedData.createPositionsVector(builder, flattedData.positions) // const pbIndices = tables.FlattedData.createIndicesVector(builder, flattedData.indices) // const pbInfoB = tables.FlattedData.createInfoBVector(builder, flattedData.info_b) // const infoAs = [] // for (let i = 0; i < flattedData.info_a.length; i++) { // const str = flattedData.info_a[i] // if (str) { // const a = builder.createString(str) // infoAs.push(a) // } // } // const pbInfoA = tables.FlattedData.createInfoAVector(builder, infoAs) // tables.FlattedData.startFlattedData(builder) // tables.FlattedData.addPositions(builder, pbPositions) // tables.FlattedData.addIndices(builder, pbIndices) // tables.FlattedData.addInfoA(builder, pbInfoA) // tables.FlattedData.addInfoB(builder, pbInfoB) // const fbData = tables.FlattedData.endFlattedData(builder) // builder.finish(fbData) // // 这个buffer是有偏移量的 // return builder.asUint8Array().slice().buffer // // return builder.asUint8Array().buffer }
// 首先引入基础库 const flatbuffers = require('flatbuffers').flatbuffers // 而后引入编译出的JS模块 const tables = require('src/generated/data_generated.js') const flatbufferParse = buffer => { buffer = new Uint8Array(buffer) buffer = new flatbuffers.ByteBuffer(buffer) const _data = tables.Data.getRootAsData(buffer) // 读数据(flatbuffer在解析时并不读取数据,所以这里须要主动读) let _read = null const len = _data.itemsLength() for (let i = 0; i < len; i++) { const item = _data.items(i) const info = item.info() _read = info.a() _read = info.b() _read = item.index() _read = item.positionArray() } // 扁平化方案: // buffer = new Uint8Array(buffer) // buffer = new flatbuffers.ByteBuffer(buffer) // const _data = tables.FlattedData.getRootAsFlattedData(buffer) // // 读数据(flatbuffer是使用get函数延迟读取的,所以这里须要主动读取数据) // let _read = null // _read = _data.positionsArray() // _read = _data.indicesArray() // _read = _data.infoBArray() // const len = _data.infoALength() // for (let i = 0; i < len; i++) { // _read = _data.infoA(i) // } }
Flatbuffers 对字符串的解析性能较差,当数据中的字符串占比较高时,其总体序列化性能、解析性能和体积都不如JSON,对于纯数值数据,相对于JSON优点明显。其状态机通常的接口设计对于复杂数据结构的构建比较繁琐。
测试环境:15' MBP mid 2015,2.2 GHz Intel Core i7,16 GB 1600 MHz DDR3,macOS 10.14.3,Chrome 75
测试数据:上面例子中的数据,200,000条,字符串使用 UUID*2
测试方式:运行10次取平均值,GZip使用默认配置 gzip ./*
单位:时间 ms,体积 Mb
字符串在数据中的占比、单个字符串的长度,以及字符串中unicode的数值大小,都会对测试形成影响。
因为DIMBIN针对扁平化数据而设计,所以非扁平化数据只测试了JSON/protocol/flatbuffers
从测试结果来看,若是你的场景对性能有较高要求,将数据扁平化老是明智的原则。
数据量小、快速迭代、包含大量字符串数据,使用JSON,方便快捷;
数据量小、接口稳定、静态语言主导、多语言协做、集成IDL、依赖gPRC,考虑 protocol buffers。
数据量大、接口稳定、静态语言主导、集成IDL、数据没法扁平化,考虑 flat buffers。
数据量大、快速迭代、性能要求高、数据能够扁平化,不但愿使用重量级工具或修改工程结构,考虑DIMBIN。
原文连接 本文为云栖社区原创内容,未经容许不得转载。