» » Сериализация данных или диалектика общения: простая сериализация

 

Сериализация данных или диалектика общения: простая сериализация

Автор: admin от 11-10-2016, 23:10, посмотрело: 320

Сериализация данных или диалектика общения: простая сериализация Доброго времени суток, уважаемые. В данной статье мы рассмотрим наиболее популярные форматы сериализации данных и проведем с ними небольшое тестирование. Это первая статья на тему сериализации данных и в ней мы рассмотрим простые сериализаторы, которые не требуют от разработчика больших изменений в коде для их интеграции.

Рано или поздно, но вы, как и наша компания, можете столкнуться с ситуацией, когда количество используемых в вашем продукте сервисов, резко возрастает, да и все они к тому же оказываются очень «говорливыми». Произошло ли это из-за перехода на «хайповую» нынче микросервисную архитектуру или вы просто получили пачку заказов на небольшие доработки и реализовали их кучкой сервисов — неважно. Важно то, что начиная с этого момента, ваш продукт обзавелся двумя новыми проблемами — что делать с увеличившимся количеством данных, гоняемых между отдельными сервисами, и как не допустить хаоса при разработке и поддержке такого количества сервисов. Немного поясню про вторую проблему: когда количество ваших сервисов вырастает до сотни или более, их уже не может разрабатывать и сопровождать одна команда разработчиков, следовательно, вы раздаете пачки сервисов разным командам. И тут главное, чтобы все эти команды использовали один формат для своих RPC, иначе вы столкнетесь с такими классическими проблемами, когда одна команда не может поддерживать сервисы другой или просто два сервиса не стыкуются между собой без обильного уплотнения места стыка костылями. Но об этом мы поговорим в отдельной статье, а сегодня мы обратим внимание на первую проблему возросших данных и подумаем, что мы можем с этим сделать. А делать нам в силу нашей православной лени ничего не хочется, а хочется добавить пару строчек в общий код и получить сразу профит. С этого мы и начнем в данной статье, а именно — рассмотрим сериализаторы, встраивание которых не требует больших изменений в нашем прекрасном RPC.

Вопрос формата, на самом деле, для нашей Компании довольно болезненный, потому как наши текущие продукты для обмена информацией между компонентами используют xml-формат. Нет, мы не мазохисты, мы прекрасно понимаем, что использовать xml для обмена данными стоило лет 10 назад, но в этом как раз и причина — продукту уже 10 лет, и он содержит много legacy-архитектурных решений, которые довольно трудно быстро «выпилить». Немного поразмыслив и похоливарив, мы решили, что будем использовать JSON для хранения и передачи данных, но нужно выбрать какой-то из вариантов упаковки JSON, так как для нас критичен размер передаваемых данных (ниже я поясню, почему так).

Мы накидали список критериев, по которым будем выбирать подходящий нам формат:


  • Эффективность компрессии данных. Наш продукт будет обрабатывать огромное количество событий на вход из различных источников. Каждое событие вызвано некоторыми действиями пользователя. В основном, события небольшие и содержат метаинформацию о происходящем — послал письмо, чатнул что-то в Facebook и т.д. — но могут также содержать и данные, причем немаленького размера. Кроме того, количество таких событий очень большое, в сутки легко может быть передано несколько десятков ТБ, поэтому экономия размера событий критически важна для нас.


  • Возможность работы из различных языков. Поскольку наш новый проект написан с использованием C++, PHP и JS, нас интересовала поддержка только этих языков, но с учетом того, что микросервисная архитектура допускает гетерогенность среды разработки, поддержка дополнительных языков будет кстати. Скажем, для нас довольно интересен язык go, и не исключено, что часть сервисов будет реализовано на нем.


  • Поддержка версионности/эволюционирования структуры данных. Так как наши продукты довольно долго живут без обновления у клиентов (процесс обновления совсем не прост), то в какой-то момент на поддержке будет слишком много разных версий, и важно, чтобы мы смогли легко развивать формат хранения, не теряя при этом совместимость с уже упакованными данными.


  • Простота использования. У нас есть опыт использования протокола Thrift для построения общения между компонентами. Честно говоря, разработчикам не всегда просто разобраться, как работает RPC и как добавить что-то в уже существующий код, не сломав ничего в старом. Поэтому чем проще будет использовать формат сериализации, тем лучше, так как уровень С++ разработчика и JS разработчика в таких вещах совсем разный :)


  • Возможность произвольного чтения данных (Random-access reads/writes). Так как мы подразумеваем использование выбранного формата и для хранения данных, то было бы здорово, если бы он поддерживал возможность частичной десериализации данных, чтобы не вычитывать каждый раз весь объект, который зачастую бывает совсем не маленьким. Кроме чтения данных, большим плюсом была бы возможность изменения данных без вычитки всего содержимого.


Проанализировав приличное количество вариантов, мы отобрали для себя таких кандидатов:


  • JSON

  • BSON

  • Message Pack

  • Cbor


  • Данные форматы не требуют описание IDL схемы передаваемых данных, а содержат схему данных внутри себя. Это сильно упрощает работу и позволяет в большинстве случаев добавить поддержку, написав не более 10 строчек кода.

    Также мы прекрасно отдаем себе отчет, что некоторые факторы протокола или сериализатора сильно зависят от его реализации. То, что отлично пакует на C++, может плохо паковать на javascript. Поэтому для наших экспериментов будем использовать реализации для JS и Go и будем гонять тесты. JS реализацию для верности будем гонять в браузере и на nodejs.

    Итак, приступим к рассмотрению.

    JSON


    Самый простой из рассматриваемых нами форматов взаимодействия. При сравнении других форматов мы будем использовать его как эталонный, так как в текущих наших проектах он и показал свою эффективность, и проявил все свои минусы.

    Плюсы:


    • Поддерживает почти все необходимые нам типы данных. Можно было бы придраться к отсутствию поддержки бинарных данных, но тут можно обойтись base64.

    • Легко читаем человеком, что дает простоту отладки

    • Поддерживается кучей языков (хотя те, кто использовал JSON в Go поймут, что тут я лукавлю)

    • Можно реализовать версионирование через JSON Scheme


    Минусы:


    • Несмотря на компактность JSON по сравнению с хml, в нашем проекте, где за сутки передаются гигабайты данных, он все же довольно расточителен для каналов и для хранения данных в нем. Единственный плюс нативного JSON нам видится только в использовании для хранения PostgreSQL (с её возможностями работы с jsob).

    • Нет поддержки частичной десериализации данных. Чтобы достать что-то из середины JSON-файла, придется сначала десериализовать все, что идет перед нужным полем. Также это не позволяет использовать формат для stream-обработки, что может быть полезно при сетевом взаимодействии.


    Давайте посмотрим что у нас с производительностью. При рассмотрении мы сразу постараемся учесть недостаток JSON в его размере и сделаем тесты с запаковкой JSON с помощью zlib. Для тестов мы будем использовать следующие библиотеки:


    Исходники и все результаты тестов вы можете найти по следующим ссылкам:

    Go — https://github.com/KyKyPy3/serialization-tests
    JS (node) — https://github.com/KyKyPy3/js-serialization-tests
    JS (browser) — http://jsperv.com/serialization-benchmarks/5

    Опытным путем мы установили, что данные для тестов нужно брать как можно более приближенные к реальным, потому что результаты тестов с разными тестовыми данными различаются кардинально. Так что если вам важно не промахнутся с форматом, всегда тестируйте его на наиболее приближенных к вашим реалиям данных. Мы же будем тестировать на данных, приближенных к нашим реалиям. Вы можете посмотреть на них в исходниках тестов.

    Вот что мы получили для JSON по скорости. Ниже приведены результаты бенчмарков для соответствующих языков:




























    JS (Node)
    Json encode21,507 ops/sec (86 runs sampled)
    Json decode9,039 ops/sec (89 runs sampled)
    Json roundtrip6,090 ops/sec (93 runs sampled)
    Json compres encode1,168 ops/sec (84 runs sampled)
    Json compres decode2,980 ops/sec (93 runs sampled)
    Json compres roundtrip874 ops/sec (86 runs sampled)













    JS (browser)
    Json roundtrip5,754 ops/sec
    Json compres roundtrip890 ops/sec





















































    Go
    Json encode5000391100 ns/op24.37 MB/s54520 B/op1478 allocs/op
    Json decode3000392785 ns/op24.27 MB/s76634 B/op1430 allocs/op
    Json roundtrip2000796115 ns/op11.97 MB/s131150 B/op2908 allocs/op
    Json compres encode3000422254 ns/op0.00 MB/s54790 B/op1478 allocs/op
    Json compres decode3000464569 ns/op4.50 MB/s117206 B/op1446 allocs/op
    Json compres roundtrip2000881305 ns/op0.00 MB/s171795 B/op2915 allocs/op

    А вот что получили по размерам данных:












    JS (Node)
    Json9482 bytes
    Json compressed 1872 bytes













    JS (Browser)
    Json9482 bytes
    Json compressed 1872 bytes

    На данном этапе можно сделать вывод, что хоть сжатие JSON и дает отличный результат, потеря в скорости обработки просто катастрофическая. Еще один вывод: с JSON прекрасно работает JS, чего нельзя сказать, например, про go. Не исключено, что обработка JSON в других языках покажет результаты не сравнимые с JS. Пока откладываем результаты JSON в сторону и смотрим, как будет с другими форматами.

    BSON


    Этот формат данных пришел из MongoDb и активно ими продвигается. Формат изначально был разработан для хранения данных и не предполагался для их передачи по сети. Честно говоря, после недолгих поисков в интернете мы не нашли ни одного серьезного продукта, использующего внутри себя BSON. Но давайте посмотрим, что нам может дать данный формат.

    Плюсы:


    • Поддержка дополнительных типов данных.
      Согласно спецификации формат BSON, помимо стандартных типов данных формата JSON, BSON поддерживает еще такие типы как Date, ObjectId, Null и бинарные данные (Binary data). Некоторые из них (например, ObjectId) чаще используются в MongoDb и не всегда могут быть полезны другим. Но некоторые дополнительные типы данных дают нам следующие бонусы. Если мы храним в нашем объекте дату, то в случае формата JSON у нас есть только один вариант хранения — это один из вариантов ISO-8601, и в строковом представлении. При этом, если мы хотим отфильтровать нашу коллекцию JSON-объектов по датам, при обработке нам нужно будет превратить строки в формат Date и только после этого их сравнивать между собой.?BSON же хранит все даты как Int64 (так же, как и тип Date) и берет на себя всю работу по сериализации/десериализации в формат Date. Поэтому мы можем сравнивать даты без десериализации — просто как числа, что явно быстрее, чем вариант с классическим JSON. Именно это преимущество активно используется в MongoDb.


    • BSON поддерживает так называемый Random read/write к своим данным.
      BSON хранит длины для строк и бинарных данных, позволяя пропускать атрибуты, которые нам не интересны. JSON же последовательно считывает данные и не может попускать элемент, не прочитав его значение до конца. Таким образом, если мы будем хранить большие объемы бинарных данных внутри формата, данная особенность может сыграть для нас важную роль.


    Минусы:


    • Размер данных.
      Что касается размера конечного файла, то тут все неоднозначно. В каких то ситуациях размер объекта будет меньше, а в каких-то — больше, все зависит от того, что лежит внутри Bson объекта. Почему так получается — нам ответит спецификация, в которой сказано, что для скорости доступа к элементам объекта формат сохраняет дополнительную информацию, такую как размер данных для больших элементов.


    Так например JSON объект

    {«hello": "world»}

    превратится вот в такое:

    x16x00x00x00                  // total document size
    x02                               // 0x02 = type String
    hellox00                          // field name
    x06x00x00x00worldx00          // field value
    x00                               // 0x00 = type EOO ('end of object')
    

    В спецификации сказано, что BSON разрабатывался, как формат с быстрой сериализацией / десериализацией, как минимум, за счет того, что числа он хранит как тип Int, и не тратит время на парсинг их из строки. Давайте проверим. Для тестирования нами были взяты следующие библиотеки:


    И вот какие результаты мы получили (для наглядности я добавил также и результаты для JSON):








































    JS (Node)
    Json encode21,507 ops/sec (86 runs sampled)
    Json decode9,039 ops/sec (89 runs sampled)
    Json roundtrip6,090 ops/sec (93 runs sampled)
    Json compres encode1,168 ops/sec (84 runs sampled)
    Json compres decode2,980 ops/sec (93 runs sampled)
    Json compres roundtrip874 ops/sec (86 runs sampled)
    Bson encode93.21 ops/sec (76 runs sampled)
    Bson decode242 ops/sec (84 runs sampled)
    Bson roundtrip65.24 ops/sec (65 runs sampled)

















    JS (browser)
    Json roundtrip5,754 ops/sec
    Json compres roundtrip890 ops/sec
    Bson roundtrip374 ops/sec













































































    Go
    Json encode5000391100 ns/op24.37 MB/s54520 B/op1478 allocs/op
    Json decode3000392785 ns/op24.27 MB/s76634 B/op1430 allocs/op
    Json roundtrip2000796115 ns/op11.97 MB/s131150 B/op2908 allocs/op
    Json compres encode3000422254 ns/op0.00 MB/s54790 B/op1478 allocs/op
    Json compres decode3000464569 ns/op4.50 MB/s117206 B/op1446 allocs/op
    Json compres roundtrip2000881305 ns/op0.00 MB/s171795 B/op2915 allocs/op
    Bson Encode10000249024 ns/op40.42 MB/s70085 B/op982 allocs/op
    Bson Decode3000524408 ns/op19.19 MB/s124777 B/op3580 allocs/op
    Bson Roundtrip2000712524 ns/op14.13 MB/s195334 B/op4562 allocs/op

    А вот что получили по размерам данных:
















    JS (Node)
    Json9482 bytes
    Json compressed1872 bytes
    Bson112710 bytes

















    JS (Browser)
    Json9482 bytes
    Json compressed1872 bytes
    Bson9618 bytes

    Хоть BSON и дает нам возможность дополнительных типов данных и, что самое главное, возможности частичного чтения / изменения данных, в плане компрессии данных у него все совсем печально, поэтому мы вынуждены продолжить поиски дальше.

    Message Pack


    Следующий формат, который попал на наш стол, это Message Pack. Данный формат довольно популярен последнее время и лично я о нем узнал, когда ковырялся с tarantool.

    Если заглянуть на сайт формата, то можно:


    • Узнать, что формат активно используется такими продуктам как redis и fluentd, что внушает доверии к нему.

    • Увидеть громкую надпись It’s like JSON. but fast and small


    Придется проверить, насколько это правда, но сначала давайте посмотрим, что же нам предлагает формат.

    По традиции начнем с плюсов:


    • Формат полностью совместим с JSON
      При конвертации данных из MessagePack в JSON мы не потеряем данные, чего нельзя сказать, например, про формат BSON. Правда, есть ряд ограничений, накладываемых на различные типы данных:


    • Значение типа Integer ограничено от -(263) до (264)–1;

    • Максимальная длина бинарного объекта (232)–1;

    • Максимальный размер байт строки (232)–1;

    • Максимальное количество элементов в массиве не больше (232)–1;

    • Максимальное количество элементов в ассоциативном массиве не больше (232)–1;



    • Довольно неплохо жмет данные.
      Например, {«a»:1,«b»:2} занимает 13 байт в JSON, 19 байт в BSON и всего лишь 7 байт в MessagePack, что довольно неплохо.

    • Есть возможность расширять поддерживаемые типы данных.
      MsgPack позволяет расширять его систему типов собственными. Так как тип в MsgPack кодируется числом, а значения от –1 до –128 зарезервированы форматом (об этом сказано в спецификации формата), то для использования доступны значения от 0 до 127. Поэтому мы можем добавлять расширения, которые будут указывать на наши собственные типы данных.

    • Имеет поддержку у огромного количества языков.

    • Есть RPC пакет (но это не так важно для нас).

    • Можно использовать streaming API.


    Минусы:


    • Не поддерживает частичное изменение данных.
      В отличие от формата BSON, даже при условии, что MsgPack хранит размеры каждого поля, изменять в нем данные частично не получится. Предположим, что у нас есть сереализованное представление JSON {«a»:1, «b»:2}. Bson использует для хранения значения ключа ‘a’ 5 байт, что позволит нам изменить значение с 1 на 2000 (занимает 3 байта) без проблем. А вот MessagePack для хранения использует 1 байт, и так как 2000 занимает 3 байта, то без сдвига данных о параметре ‘b’ мы не можем изменить значение параметра ‘a’.


    Теперь давайте посмотрим, насколько он производительный и как же он сжимает данные. Для тестов использовались следующие библиотеки:


    Результаты мы получили следующие:




















































    JS (Node)
    Json encode21,507 ops/sec (86 runs sampled)
    Json decode9,039 ops/sec (89 runs sampled)
    Json roundtrip6,090 ops/sec (93 runs sampled)
    Json compres encode1,168 ops/sec (84 runs sampled)
    Json compres decode2,980 ops/sec (93 runs sampled)
    Json compres roundtrip874 ops/sec (86 runs sampled)
    Bson encode93.21 ops/sec (76 runs sampled)
    Bson decode242 ops/sec (84 runs sampled)
    Bson roundtrip65.24 ops/sec (65 runs sampled)
    MsgPack encode4,758 ops/sec (79 runs sampled)
    MsgPack decode2,632 ops/sec (91 runs sampled)
    MsgPack roundtrip1,692 ops/sec (91 runs sampled)





















    JS (browser)
    Json roundtrip5,754 ops/sec
    Json compres roundtrip890 ops/sec
    Bson roundtrip374 ops/sec
    MsgPack roundtrip1,048 ops/sec





































































































    Go
    Json encode5000391100 ns/op24.37 MB/s54520 B/op1478 allocs/op
    Json decode3000392785 ns/op24.27 MB/s76634 B/op1430 allocs/op
    Json roundtrip2000796115 ns/op11.97 MB/s131150 B/op2908 allocs/op
    Json compres encode3000422254 ns/op0.00 MB/s54790 B/op1478 allocs/op
    Json compres decode3000464569 ns/op4.50 MB/s117206 B/op1446 allocs/op
    Json compres roundtrip2000881305 ns/op0.00 MB/s171795 B/op2915 allocs/op
    Bson Encode10000249024 ns/op40.42 MB/s70085 B/op982 allocs/op
    Bson Decode3000524408 ns/op19.19 MB/s124777 B/op3580 allocs/op
    Bson Roundtrip2000712524 ns/op14.13 MB/s195334 B/op4562 allocs/op
    MsgPack Encode5000306260 ns/op27.36 MB/s49907 B/op968 allocs/op
    MsgPack Decode10000214967 ns/op38.98 MB/s59649 B/op1690 allocs/op
    MsgPack Roundtrip3000547434 ns/op15.31 MB/s109754 B/op2658 allocs/op

    А вот что получили по размерам данных:




















    JS (Node)
    Json9482 bytes
    Json compressed1872 bytes
    Bson112710 bytes
    MsgPack 7628 bytes





















    JS (Browser)
    Json9482 bytes
    Json compressed1872 bytes
    Bson9618 bytes
    MsgPack7628 bytes

    Конечно, MessagePack не так круто жмет данные, как нам бы хотелось, но по крайней мере, ведет себя довольно стабильно как в JS, так и в Go. Пожалуй, на текущий момент это наиболее привлекательный кандидат для наших задач, но осталось рассмотреть нашего последнего пациента.

    Cbor


    Честно говоря, формат очень похож на MessagePack по своим возможностям, и складывается впечатление, что формат разрабатывался как замена MessagePack. В нем также есть поддержка расширения типов данных и полная совместимость с JSON. Из различий я заметил только поддержку массивов/строк произвольной длины, но, на мой взгляд, это очень странная фича. Если Вы хотите узнать больше про данный формат, то по нему была отличная статья на Хабре — habrahabr.ru/post/208690. Ну а мы посмотрим, как у Cbor с производительностью и сжатием данных.

    Для тестов были использованы следующие библиотеки:


    И, конечно же, вот финальные результаты наших тестов с учетом всех рассматриваемых форматов:
































































    JS (Node)
    Json encode21,507 ops/sec ±1.01% (86 runs sampled)
    Json decode9,039 ops/sec ±0.90% (89 runs sampled)
    Json roundtrip6,090 ops/sec ±0.62% (93 runs sampled)
    Json compres encode1,168 ops/sec ±1.20% (84 runs sampled)
    Json compres decode2,980 ops/sec ±0.43% (93 runs sampled)
    Json compres roundtrip874 ops/sec ±0.91% (86 runs sampled)
    Bson encode93.21 ops/sec ±0.64% (76 runs sampled)
    Bson decode242 ops/sec ±0.63% (84 runs sampled)
    Bson roundtrip65.24 ops/sec ±1.27% (65 runs sampled)
    MsgPack encode4,758 ops/sec ±1.13% (79 runs sampled)
    MsgPack decode2,632 ops/sec ±0.90% (91 runs sampled)
    MsgPack roundtrip1,692 ops/sec ±0.83% (91 runs sampled)
    Cbor encode1,529 ops/sec ±4.13% (89 runs sampled)
    Cbor decode1,198 ops/sec ±0.97% (88 runs sampled)
    Cbor roundtrip351 ops/sec ±3.28% (77 runs sampled)

























    JS (browser)
    Json roundtrip5,754 ops/sec ±0.63%
    Json compres roundtrip890 ops/sec ±1.72%
    Bson roundtrip374 ops/sec ±2.22%
    MsgPack roundtrip1,048 ops/sec ±5.40%
    Cbor roundtrip859 ops/sec ±4.19%





























































































































    Go
    Json encode5000391100 ns/op24.37 MB/s54520 B/op1478 allocs/op
    Json decode3000392785 ns/op24.27 MB/s76634 B/op1430 allocs/op
    Json roundtrip2000796115 ns/op11.97 MB/s131150 B/op2908 allocs/op
    Json compres encode3000422254 ns/op0.00 MB/s54790 B/op1478 allocs/op
    Json compres decode3000464569 ns/op4.50 MB/s117206 B/op1446 allocs/op
    Json compres roundtrip2000881305 ns/op0.00 MB/s171795 B/op2915 allocs/op
    Bson Encode10000249024 ns/op40.42 MB/s70085 B/op982 allocs/op
    Bson Decode3000524408 ns/op19.19 MB/s124777 B/op3580 allocs/op
    Bson Roundtrip2000712524 ns/op14.13 MB/s195334 B/op4562 allocs/op
    MsgPack Encode5000306260 ns/op27.36 MB/s49907 B/op968 allocs/op
    MsgPack Decode10000214967 ns/op38.98 MB/s59649 B/op1690 allocs/op
    MsgPack Roundtrip3000547434 ns/op15.31 MB/s109754 B/op2658 allocs/op
    Cbor Encode2000071203 ns/op117.48 MB/s32944 B/op12 allocs/op
    Cbor Decode 3000432005 ns/op19.36 MB/s40216 B/op2159 allocs/op
    Cbor Roundtrip3000531434 ns/op15.74 MB/s73160 B/op2171 allocs/op

    А вот что получили по размерам данных:
























    JS (Node)
    Json9482 bytes
    Json compressed1872 bytes
    Bson112710 bytes
    MsgPack7628 bytes
    Cbor7617 bytes


























    JS (Browser)
    Json9482 bytes
    Json compressed1872 bytes
    Bson9618 bytes
    MsgPack7628 bytes
    Cbor7617 bytes

    Комментарии, я думаю, тут излишни, все прекрасно видно из результатов — CBor оказался самым медленным форматом.

    Выводы


    Какие выводы мы сделали из этого сравнения? Немного подумав и посмотрев на результаты, мы пришли к выводу, что нас не удовлетворил ни один из форматов. Да, MsgPack показал себя совсем неплохим вариантом: он прост в использовании и довольно стабилен, но посоветовавшись с коллегами, мы решили свежее взглянуть на другие бинарные форматы данных, не на основе JSON: Protobuf, FlatBuffers, Cap’n proto и avro. О том, что у нас получилось и что же в конечном итоге мы выбрали, расскажем в следующей статье.

    Автор: Роман Ефременко KyKyPy3uK

    Источник: Хабрахабр

    Категория: Программирование

    Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь.
    Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.

    Добавление комментария

    Имя:*
    E-Mail:
    Комментарий:
    Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
    Введите два слова, показанных на изображении: *