
Эффективные форматы данных JSON
JSON стал повсеместным форматом для передачи данных между приложениями и веб-API. При всей его популярности есть немало минусов в его эффективном использовании. В частности, популярное использование может быть весьма неэффективным. Эта предполагаемая неэффективность привела ко многим двоичным форматам, таким как MessagePack, BSON, PROTOBuff, Thrift и многим другим.
Было бы неплохо, если бы мы могли повысить эффективность JSON, не добавляя при этом новую библиотеку, которая может вызвать нежелательные побочные эффекты, такие как зависимость от версии протокола, зависимость от версии приложения, удобочитаемость и другие проблемы, которые обычно возникают. связаны с двоичными форматами. Мы хотели бы принять меры для сохранения универсального и удобного для пользователя характера самого JSON при одновременном повышении эффективности за счет нескольких факторов. Кроме того, снижение времени синтаксического анализа и обработки приложений является дополнительным преимуществом, которое станет очевидным, по большей части, из-за уменьшения размера данных. В частности, эти меры имеют много общего с типами баз данных и общим дизайном систем баз данных. В целях этой статьи мы будем называть меры или форматы JSON соответственно как объектно-ориентированные, ориентированные на строки и ориентированные на столбцы. В конце мы произведем некоторые тестовые данные, чтобы проиллюстрировать возможные преимущества.
Объектно-ориентированный
Это популярный способ получения данных для веб-API, который обычно выглядит следующим образом:
[
{
name1:value_11,
name2:value_12,
name3:value_13,
...,
namek:value_1k
},
{
name1:value_21,
name2:value_22,
name3:value_23,
...,
namek:value_2k
},
{...},
{
name1:value_n1,
name2:value_n2,
name3:value_n3,
...,
namek:value_nk
}
]
Некоторые из примеров хорошо известных общедоступных веб-API, поддерживающих этот стиль:
https://developers.facebook.com/docs/marketing-api/using-the-api
https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/DBActivityStreams.html
https://code.msdn.microsoft.com/REST-in-Bing-Maps-Windows-6d547d69 / sourcecode? fileId = 82515 & pathId = 1683851973
Более полный список общедоступных API можно найти здесь:
https://github.com/n0shake/Public-APIs
С ориентацией на ряд
Более эффективный способ представления данных в виде строкового шаблона:
{
meta: {“name1”,“name2”, “name3”, .., "namek"}
data:
[
[ value_11, value_12, value_13, ..., value_1n ],
[ value_21, value_22, value_23, ..., value_2n ],
[ value_31, value_32, value_33, ..., value_3n ],
[...],
[ value_k1, value_k2, value_k3, ..., value_kn ]
]
}
Примечание: при анализе этого типа ориентации будет создано гораздо меньше индивидуальных массивов по сравнению с объектно-ориентированным форматом. Это станет очевидным в тестах на задержку обработки.
Столбец ориентированный
Более эффективный способ представления данных в виде шаблона, ориентированного на столбцы:
{
meta: {“name1”,“name2”, “name3”, .., "namek"}
data:
[
[ value_11, value_12, value_13, ..., value_1n ],
[ value_21, value_22, value_23, ..., value_2n ],
[ value_31, value_32, value_33, ..., value_3n ],
[...],
[ value_k1, value_k2, value_k3, ..., value_kn ]
]
}
Примечание: при анализе этого типа ориентации будет создано гораздо меньше отдельных массивов по сравнению с форматом, ориентированным на строки, хотя общий размер данных может быть ненамного меньше. Это станет очевидным в тестах на задержку обработки.
Тесты
Для тестов мы будем использовать node js и javascript в качестве нашей песочницы. Мы выбрали javascript для его оптимизированной нативной реализации парсера json. Это также очень популярная среда как для использования, так и для создания json API. Тест имитирует задачу передачи набора строк базы данных. Мы будем записывать количество строк, время создания json, время анализа json и размер передаваемых данных.
Исходный код можно найти в Приложении A в конце этого документа.
И, наконец, результаты.
Type Object Row Column Best v Worst
Ratio
-------------------- --------- --------- --------- --------------
Row Count 10000 10000 10000
Data Size (KiB) 1190 565 487 2.44
Parsing Time (ms) 8 5 3 2.67
Creation Time (ms) 7 3 1 7
Row Count 100000 100000 100000
Data Size (KiB) 11316 5750 4969 2.28
Parsing Time (ms) 84 55 27 3.11
Creation Time (ms) 47 26 15 3.13
Row Count 1000000 1000000 1000000
Data Size (KiB) 120613 58485 50672 2.38
Parsing Time (ms) 1075 616 388 2.77
Creation Time (ms) 750 342 266 2.82
Становится ясно, что примерно на 100000 строках общая эффективность находится на оптимальном уровне. Мы можем только догадываться, что это влияние размера кэша ЦП и окончательного размера данных, которое вызывает этот эффект. Кажется, что количество строк можно точно настроить для оптимальной эффективности в каждом конкретном случае.
Заключение
Этот тест является чисто показателем того, какие возможные улучшения можно внести в хорошо известные форматы JSON. Если ваш формат JSON уже включает такие оптимизации, лучше следовать двоичному пути. Однако, если ваше приложение следует популярному объектно-ориентированному шаблону JSON для сериализации объектов, то многое можно получить, если сначала изменить шаблон формата без необходимости переписывать большие части вашей инфраструктуры.
Приложение
Тестовый код
Тест может быть выполнен с использованием движка v8 в узле js.
function createMetaData(){
console.debug("createMetaData")
return [
'user',
'sessionId',
'command',
'statement',
'transactionId',
'time'
]
}
function createData(count){
console.debug("createData: %i",count)
var data = []
var meta = createMetaData()
for(var d = 0; d < count; ++d){
var object = {}
object[meta[0]] = 'test'
object[meta[1]] = 1
object[meta[2]] = 'SELECT'
object[meta[3]] = 'SELECT * from mydata'
object[meta[4]] = d
object[meta[5]] = new Date().getMilliseconds()
data.push(object)
}
return {data:data}
}
function createRowData(count){
console.debug("createRowData %i",count)
var meta = createMetaData()
var data = []
for(var d = 0; d < count; ++d){
for(var d = 0; d < count; ++d){
var row = []
row.push('test')
row.push(1)
row.push('SELECT')
row.push('SELECT * from mydata')
row.push(d)
row.push(new Date().getMilliseconds())
data.push(row)
}
}
return {data:data, meta:meta}
}
function createColData(count){
console.debug("createColData: %i",count)
var meta = createMetaData()
var cols = {}
for(var r = 0; r < meta.length; ++r){
cols[meta[r]] = []
}
for(var d = 0; d < count; ++d){
cols[meta[0]].push('test')
cols[meta[1]].push(1)
cols[meta[2]].push('SELECT')
cols[meta[3]].push('SELECT * from mydata')
cols[meta[4]].push(d)
cols[meta[5]].push(new Date().getMilliseconds())
}
var data = []
for(var d = 0; d < meta.length; ++d){
data.push(cols[meta[d]]);
}
return {data:data, meta:meta}
}
function bench(data){
console.log("bench %i",data.data.length)
var start = new Date()
var serialized = JSON.stringify(data)
var endSerialized = new Date()
console.info("JSON Size: %f KiB Time to serialize %dms",serialized.length/1024.0,(endSerialized-start))
start = new Date()
var deserialized = JSON.parse(serialized)
var endDeSerialized = new Date()
console.info("Time to deserialize %dms Deserialized size %i ",(endDeSerialized-start),deserialized.data.length)
}
var counts = [10000, 100000, 1000000]
console.info(" ----------------- Object oriented ----------------------")
for (var c in counts){
var data = createData(counts[c])
bench(data)
}
console.info(" ----------------- Row oriented -----------------------")
for (var c in counts){
var rowData = createRowData(counts[c])
bench(rowData)
}
console.info(" ----------------- Col oriented -----------------------")
for (var c in counts){
var colData = createColData(counts[c])
bench(colData)
}