Contexte
JSON/XML est-il mauvais ?
Eh bien, il n'y a pas de schéma de sérialisation aussi populaire que JSON et XML, gratuit, pratique, puissant, expressif et multiplateforme. est la préférence par défaut pour les formats de transfert de données courants. Cependant, avec l'augmentation de la quantité de données et l'amélioration des exigences de performance, les problèmes de performance provoqués par cette liberté et cette généralité ne peuvent être ignorés.
JSON et XML utilisent des chaînes pour représenter toutes les données. Pour les données autres que des caractères, la représentation littérale occupe beaucoup d'espace de stockage supplémentaire et est fortement affectée par la taille et la précision de la valeur. Un nombre à virgule flottante 32 bits 1234.5678 occupe 4 octets d'espace en mémoire. S'il est stocké au format utf8, il doit occuper 9 octets d'espace. Dans l'environnement où utf16 est utilisé pour exprimer des chaînes telles que JS, il doit occuper 18 octets. octets d'espace. L'utilisation d'expressions régulières pour l'analyse des données est très inefficace lorsqu'il s'agit de données autres que des caractères. Non seulement cela consomme beaucoup d'opérations pour analyser la structure des données, mais cela convertit également les littéraux en types de données correspondants.
Face à des données massives, ce format lui-même peut devenir le goulot d'étranglement IO et informatique de l'ensemble du système, voire un débordement direct.
Qu'y a-t-il au-delà de JSON/XML ?
Parmi les nombreux schémas de sérialisation, selon le schéma de stockage, il peut être divisé en stockage de chaînes et stockage binaire. Le stockage de chaînes est lisible, mais en raison des problèmes ci-dessus, seul le stockage binaire est pris en compte ici. Le stockage binaire peut être divisé en ceux qui nécessitent IDL et ceux qui ne nécessitent pas IDL, ou sont divisés en autodescriptifs et non autodescriptifs (si IDL est requis pour la désérialisation).
Le processus d'utilisation d'IDL est requis :
-
Ecrire le schéma en utilisant la syntaxe IDL définie par le schéma
-
Utiliser le compilateur fourni par la solution pour compiler le schéma en code (classe ou module) dans le langage utilisé par les producteurs et les consommateurs
-
Le producteur de données se réfère au code, construit les données selon son interface, et les sérialise
-
Le consommateur se réfère au code et lit les données selon son interface
Le processus d'utilisation d'IDL n'est pas requis :
-
Le producteur et le consommateur s'accordent sur la structure des données à travers le document
-
Sérialisation du producteur
-
Désérialisation du consommateur
etc.
-
tampons de protocole
-
Protocole de transport utilisé par gRPC, stockage binaire, nécessite IDL, non auto-descriptif
-
Taux de compression élevé, très expressif, largement utilisé dans les produits Google
-
tampons plats
-
Google lance un schéma de sérialisation, stockage binaire, nécessite IDL, non auto-descriptif (le schéma auto-descriptif n'est pas multiplateforme)
-
Haute performance, petite taille, chaîne de support, nombre, booléen
-
Avro
-
Le schéma de sérialisation utilisé par Hadoop, qui combine les avantages du schéma binaire et du schéma de chaîne, seul le processus de sérialisation nécessite IDL, auto-descriptif
-
Cependant, la scène est limitée, il n'y a pas d'implémentation JS mature et elle n'est pas adaptée à l'environnement Web. Aucune comparaison n'est faite ici.
-
Épargne
-
Le schéma de Facebook, stockage binaire, nécessite IDL, non auto-descriptif
-
Fondamentalement uniquement intégré et utilisé dans RPC, aucune comparaison n'est faite ici
-
DIMBIN
-
Schéma de sérialisation conçu pour les tableaux multidimensionnels, stockage binaire, aucun IDL requis, autodescriptif
-
Haute performance, petite taille, chaîne de support, nombre, booléen
Envoyez-moi un message privé pour recevoir le matériel d'apprentissage et d'amélioration audio et vidéo C++ le plus récent et le plus complet, y compris ( C/C++ , Linux , FFmpeg , webRTC , rtmp , hls , rtsp , ffplay , srs )
Principe d'optimisation
Principes d'optimisation de l'espace
L'utilisation de types numériques au lieu de littéraux pour stocker des valeurs peut économiser une quantité considérable d'espace en soi. Les tampons de protocole utilisent des variantes pour compresser les valeurs afin d'obtenir des taux de compression plus élevés. (Cependant, les tests ci-dessous montrent que dans les environnements où gzip peut être utilisé, cette solution n'aide pas)
Le principe de l'optimisation du temps
Le format binaire utilise un emplacement spécifique pour enregistrer la structure de données et le décalage de chaque donnée de nœud, ce qui permet de gagner du temps passé à analyser la structure de données à partir de la chaîne et d'éviter les problèmes de performances causés par les chaînes longues. Dans le langage GC, et aussi grandement réduire la production de déchets intermédiaires.
Dans les environnements qui peuvent effectuer des opérations de mémoire directe (y compris JS), les données peuvent également être directement lues via des décalages de mémoire, évitant les opérations de copie et ouvrant de l'espace mémoire supplémentaire. DIMBIN et les flatbuffers utilisent cette idée pour optimiser les performances de stockage des données. Dans l'environnement JS, le temps nécessaire pour extraire les données du segment de mémoire en créant un DataView ou un TypedArray est fondamentalement négligeable.
Le stockage de chaînes dans le schéma binaire nécessite une logique supplémentaire pour l'encodage et le décodage UTF8, et les performances et le volume ne sont pas aussi bons que les formats de chaîne tels que JSON.
Qu'est-ce que DIMBIN ?
Nos scénarios de visualisation de données impliquent souvent des mises à jour en temps réel de millions, voire de dizaines de millions de données.Afin de résoudre le problème de performances de JSON, nous avons utilisé l'idée d'opération de décalage de mémoire pour développer DIMBIN en tant que schéma de sérialisation, et sur cette base, nous avons conçu de nombreux formats de transport pour le traitement des données côté Web.
En tant qu'idée d'optimisation simple et directe, DIMBIN est devenu notre solution standard pour la transmission de données, en maintenant une simplicité et une efficacité absolues.
Nous venons d'ouvrir DIMBIN et avons contribué à la communauté, dans l'espoir de vous apporter une solution plus légère, plus rapide et plus conviviale pour le Web que JSON/protocol/flatbuffers.
Comparaison des schémas
Pour une utilisation dans l'environnement Web/JS, nous choisissons quatre solutions : JSON, les tampons de protocole, les tampons plats et DIMBIN, et nous les comparons sous sept aspects.
Ingénierie
Protocolbuffers et flatbuffers représentent le workflow complet préconisé par Google. Strict, standardisé, unifié, orienté IDL, conçu pour la collaboration multi-terminal, pour python/java/c++. Générez du code via IDL et utilisez un processus de développement cohérent pour plusieurs plates-formes/plusieurs langages. Si l'équipe adopte ce type de flux de travail, cette solution est plus gérable, et la collaboration multi-extrémités et le changement d'interface sont plus contrôlables.
Mais si vous quittez cette structure d'ingénierie, ce sera relativement compliqué.
JSON/XML et DIMBIN sont neutres, ne nécessitent pas d'IDL et ne font pas d'hypothèses ou de restrictions sur les solutions d'ingénierie et la sélection de technologies. Vous pouvez uniquement passer l'interface de spécification de document, ou vous pouvez ajouter vous-même des contraintes de schéma.
Complexité de déploiement/codage
Les tampons de protocole et les tampons plats doivent être ajoutés à un stade précoce de la conception du projet et en tant que lien clé dans le flux de travail. S'il est ajouté à des fins d'optimisation des performances, il aura un impact plus important sur l'architecture du projet.
JSON est essentiellement l'infrastructure de toutes les plates-formes, sans frais de déploiement.
DIMBIN ne nécessite qu'un seul package à installer, mais nécessite un aplatissement de la structure de données, si la structure de données ne peut pas être aplatie, elle n'en bénéficiera pas.
Lorsqu'il est utilisé dans JS :
-
Le nombre de lignes de code désérialisées à l'aide de la sérialisation JSON est essentiellement inférieur à 5
-
L'utilisation de DIMBIN est d'environ 10 lignes
-
Pour utiliser le protocole, vous devez écrire un fichier de schéma (proto) séparément et importer des centaines de lignes de code compilé. Lors de la sérialisation et de la désérialisation, vous devez exploiter les données de chaque nœud via une interface orientée objet (chaque nœud sur la structure de données est un objet)
-
Pour utiliser flatbuffer, vous devez écrire un fichier de schéma (fbs) séparément et introduire des centaines de lignes de code compilé. Le processus de sérialisation doit traiter chaque nœud via une interface de type machine d'état, convertir manuellement et mettre les données de chaque nœud , et l'expérience d'écriture est relativement abrasive. Personnes ; le processus de désérialisation lit les données de chaque nœud via l'interface d'opération d'objet
Performances (environnement JS)
Le site officiel de Protocol affirme que les performances sont supérieures à JSON. Les données de test ne sont évidemment pas côté JS. Notre test montre que ses performances côté JS sont pires que JSON (elles sont bien pires lorsque la quantité de données est importante).
Le processus de traitement des chaînes dans tous les schémas binaires est similaire : utf16 dans js doit être décodé en unicode, puis encodé en utf8, écrit dans le tampon et l'adresse de données de chaque chaîne est enregistrée. Ce processus est coûteux en performances, et si varint (tampons de protocole) n'est pas utilisé, il n'y a aucun avantage en termes de taille.
Lorsqu'il s'agit de données de chaîne, les performances JSON sont toujours les meilleures, performances de sérialisation JSON > DIMBIN > flatbuffers > proto, désérialisation JSON > proto > DIMBIN > flatbuffers
Flatbuffers et DIMBIN présentent des avantages évidents en termes de performances lorsqu'il s'agit de données numériques.
Propriétés de sérialisation pour les données numériques aplaties DIMBIN > flatbuffers > JSON > proto,
Désérialiser DIMBIN > flatbuffers > 100 000 fois > JSON > proto
le volume
Lors de l'utilisation d'une structure mixte de chaînes et de nombres ou de nombres purs, protocole < DIMBIN < plat < JSON Lors de l'utilisation de chaînes pures, JSON est le plus petit et le schéma binaire est relativement grand
Après Gzip, le volume de DIMBIN et flat est le plus petit et fondamentalement le même, mais le protocole n'a aucun avantage. On devine que cela peut être un effet secondaire de varint.
expressivité
Le protocole est conçu pour les langages fortement typés, les types pris en charge sont beaucoup plus riches que JSON et la structure de données peut être très complexe ; Flatbuffers prend en charge trois types de base de numérique/booléen/chaîne, et la structure est similaire à JSON ; DIMBIN prend en charge numérique/ booléen Il existe trois types de base de valeur/chaîne. Actuellement, seules les structures de tableau multidimensionnel sont prises en charge (les paires clé-valeur ne sont pas prises en charge ou encouragées), et les structures plus complexes doivent être encapsulées dessus.
degrés de liberté
JSON et DIMBIN sont auto-descriptifs, (dans les langages faiblement typés) le schéma n'est pas requis, les utilisateurs peuvent générer dynamiquement des structures de données et des types de données, et le producteur et le consommateur peuvent s'entendre dessus. Si la vérification de type est requise, elle doit être encapsulé dans la couche supérieure.
Les tampons de protocole et les tampons plats doivent écrire IDL et générer le code correspondant avant le codage. Pour modifier l'interface, vous devez modifier l'IDL et régénérer le code, le déployer sur le producteur et le consommateur, puis encoder en fonction de celui-ci.
-
Les implémentations C++ et Java de Protocolbuffers ont des fonctionnalités auto-descriptives, qui peuvent être intégrées dans des fichiers .proto, mais doivent toujours compiler une interface de niveau supérieur pour décrire ces "données intégrées auto-descriptives", qui sont fondamentalement inutiles. dit également que l'interne de Google ne l'a jamais utilisé de cette façon (non conforme aux principes de conception d'IDL).
-
flatbuffers a un fork auto-descriptif (flexbuffers), expérimental, pas de support JS, pas de documentation connexe.
Prise en charge multilingue
La prise en charge de la langue du serveur et du client Protocolbuffers et flatbuffers est terminée. Les deux sont préférentiellement développés pour C++/Java(android)/Python. Le côté JS manque de fonctions avancées et n'a pas de documentation complète. Vous devez étudier vous-même l'exemple et le code généré, mais le code n'est pas long et les commentaires sont entièrement couvert.
JSON dispose d'outils pour pratiquement tous les langages de programmation.
DIMBIN est développé et optimisé pour JS/TS, et fournit actuellement la version c#, et la prise en charge de c++, wasm, java et python est prévue.
Cas d'utilisation (uniquement tester l'environnement JS)
Nous générons des données typiques, utilisons des structures plates et non plates, utilisons JSON, DIMBIN, un protocole et des tampons plats pour obtenir la même fonction, et comparons les performances, le volume et la commodité de différentes solutions.
Données de test
Nous générons deux versions de données de test : des données non aplaties (structure clé-valeur multicouche) et des données équivalentes aplaties (tableau multidimensionnel).
Compte tenu de la particularité du traitement des chaînes, nous avons testé séparément les données mixtes chaîne/numérique, les données chaîne pures et les données numériques pures lors des tests.
// 非扁平化数据
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
Sérialisation
const jsonSerialize = () => {
return JSON.stringify(data)
}
désérialiser
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
Sérialisation
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),
])
}
désérialiser
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 ne prend actuellement en charge que les tableaux multidimensionnels et ne peut pas gérer les structures de données arborescentes, il n'y a donc pas de comparaison ici.
Tampons de protocole
schéma
Tout d'abord, vous devez écrire le schéma selon la syntaxe proto3
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;
}
compiler en js
Compiler le schéma dans un module JS à l'aide du compilateur de protocole
./lib/protoc-3.8.0-osx-x86_64/bin/protoc ./src/data.proto --js_out=import_style=commonjs,,binary:./src/generated
Sérialisation
// 引入编译好的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
}
désérialiser
// 引入编译好的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()
}
Tampons plats
schéma
Tout d'abord, vous devez écrire le schéma selon la syntaxe proto3
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];
}
compiler en js
./lib/flatbuffers-1.11.0/flatc -o ./src/generated/ --js --binary ./src/data.fbs
Sérialisation
// 首先引入基础库
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
}
désérialiser
// 首先引入基础库
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)
// }
}
Les flatbuffers ont de mauvaises performances d'analyse pour les chaînes. Lorsque la proportion de chaînes dans les données est élevée, ses performances globales de sérialisation, ses performances d'analyse et son volume ne sont pas aussi bons que JSON. Pour les données numériques pures, il présente des avantages évidents par rapport à JSON. La conception générale de l'interface de sa machine d'état est lourde pour la construction de structures de données complexes.
Performance
Environnement de test : 15' MBP mi-2015, 2,2 GHz Intel Core i7, 16 Go 1600 MHz DDR3, macOS 10.14.3, Chrome 75
Données de test : les données de l'exemple ci-dessus, 200 000, la chaîne utilise UUID*2
Méthode de test : exécutez 10 fois pour obtenir la valeur moyenne, GZip utilise la configuration par défaut gzip ./*
Unité : temps ms, volume Mb
-
La proportion de chaînes dans les données, la longueur d'une seule chaîne et la taille numérique de l'unicode dans la chaîne affecteront toutes le test.
-
Étant donné que DIMBIN est conçu pour les données aplaties, seuls JSON/protocol/flatbuffers sont testés pour les données non aplaties
Performances de sérialisation
Performances de désérialisation
occupation de l'espace
Proposition de sélection
D'après les résultats des tests, si votre scénario a des exigences de performances élevées, il est toujours judicieux d'aplatir les données.
-
Petite quantité de données, itération rapide, y compris un grand nombre de données de chaîne, en utilisant JSON, ce qui est pratique et rapide ;
-
Petite quantité de données, interface stable, dominance de langage statique, collaboration multilingue, IDL intégré, recours à gPRC, pensez aux tampons de protocole.
-
Grande quantité de données, interface stable, langage statique dominant, IDL intégré, les données ne peuvent pas être aplaties, pensez aux tampons plats.
-
Grande quantité de données, itération rapide, exigences de performances élevées, les données peuvent être aplaties, ne souhaitez pas utiliser d'outils lourds ou modifier la structure du projet, pensez à DIMBIN.