背景
JSON/XML很差嗎?
好,再沒有一種序列化方案能像JSON和XML同樣流行,自由、方便,擁有強大的表達力和跨平臺能力。是通用數據傳輸格式的默認首選。不過隨着數據量的增長和性能要求的提高,這種自由與通用帶來的性能問題也不容忽視。java
JSON和XML使用字符串表示全部的數據,對於非字符數據來講,字面量表達會佔用不少額外的存儲空間,而且會嚴重受到數值大小和精度的影響。 一個32位浮點數 1234.5678 在內存中佔用 4 bytes 空間,若是存儲爲 utf8 ,則須要佔用 9 bytes空間,在JS這樣使用utf16表達字符串的環境中,須要佔用 18 bytes空間。 使用正則表達式進行數據解析,在面對非字符數據時顯得十分低效,不只要耗費大量的運算解析數據結構,還要將字面量轉換成對應的數據類型。python
在面對海量數據時,這種格式自己就可以成爲整個系統的IO與計算瓶頸,甚至直接overflow。android
JSON/XML以外還有什麼?
衆多的序列化方案中,按照存儲方案,可分爲字符串存儲和二進制存儲,字符串存儲是可讀的,可是因爲以上問題,這裏只考慮二進制存儲。二進制存儲中可分爲須要IDL和不須要IDL,或分爲自描述與非自描述(反序列化是否須要IDL)。c++
須要IDL的使用過程:
使用該方案所定義的IDL語法編寫schemaweb
使用該方案提供的編譯器將schema編譯成生產方和消費方所用語言的代碼(類或者模塊)正則表達式
數據生產方引用該代碼,根據其接口,構建數據,並序列化編程
消費方引用該代碼,根據其接口讀取數據json
不須要IDL的使用過程:
生產方與消費方經過文檔約定數據結構c#
生產方序列化數組
消費方反序列化
etc.
protocol buffers
gRPC所用的傳輸協議,二進制存儲,須要IDL,非自描述
高壓縮率,表達性極強,在Google系產品中使用普遍
flat buffers
Google推出序列化方案,二進制存儲,須要IDL,非自描述(自描述方案不跨平臺)
高性能,體積小,支持string、number、boolean
avro
Hadoop使用的序列化方案,將二進制方案和字符串方案的優點結合起來,僅序列化過程須要IDL,自描述
然而場景受限,沒有成熟的JS實現,不適合Web環境,這裏不作對比
Thrift
Facebook的方案,二進制存儲,須要IDL,非自描述
基本上只集成在RPC中使用,這裏不作對比
DIMBIN
針對多維數組設計的序列化方案,二進制存儲,不須要IDL,自描述
高性能,體積小,支持string、number、boolean
優化原理
空間優化原理
使用數值類型而非字面量來保存數值,自己就能節約一筆十分可觀的空間。 protocol buffer爲了實現更高的壓縮率,使用varint去壓縮數值。(不過下面的測試代表,可使用gzip的環境中,這種方案沒有幫助)
時間優化原理
二進制格式用經過特定位置來記錄數據結構以及每一個節點數據的偏移量,省去了從字符串中解析數據結構所耗費的時間,避免了長字符串帶來的性能問題,在GC語言中,也大大減小了中間垃圾的產生。
在能夠進行內存直接操做的環境中(包括JS),還能夠經過內存偏移量直接讀取數據,而避免進行復制操做,也避免開闢額外的內存空間。DIMBIN和flatbuffers都使用了這種理念來優化數據存儲性能。在JS環境中,經過創建DataView或者TypedArray來從內存段中提取數據的耗時基本上能夠忽略不計。
二進制方案中存儲字符串須要額外的邏輯進行UTF8編解碼,性能和體積不如JSON這樣的字符串格式。
DIMBIN是什麼?
咱們的數據可視化場景中常常涉及百萬甚至千萬條數據的實時更新,爲解決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)文件,引入編譯出的幾百行代碼,序列化過程須要經過狀態機風格的接口處理每一個節點,手動轉換並放入每一個節點的數據,書寫體驗比較磨人;反序列化過程經過對象操做接口讀取每一個節點的數據
性能(JS環境)
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的支持在計劃中。
用例(僅測試JS環境)
咱們生成一份典型的數據,使用扁平化和非扁平化兩種結構,使用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, ...],
}
JSON
序列化
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 }
}
DIMBIN
序列化
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目前僅支持多維數組,不能處理樹狀數據結構,這裏不作對比。
Protocol Buffers
schema
首先須要按照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;
}
編譯成js
使用 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()
}
Flat buffers
schema
首先須要按照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];
}
編譯成js
./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。