Привет! Я Team Lead в Scalable Solutions. Мы с командой давно работаем над нашей платформой и уже дошли до той точки, когда любые технические решения должны быть обоснованы и согласованы с коллегами. Так исторически сложилось, что у нас есть ряд технических решений, которые были приняты в начале, но никогда не проходили этапы обоснования. К такому решению относится Protobuf. Поэтому я решил сравнить популярные бинарные форматы, чтобы выяснить, какие недостатки есть у каждого, и что сегодня наиболее оптимально с точки зрения эксплуатации. 

В современном зоопарке различных языков программирования и решений для обмена данными есть выбор на любой цвет, вкус и ориентацию. Но большинство предпочитают реализовывать сериализацию через Protobuf.

Давайте разбираться в причинах этого, как до такого доходят, и к чему это всё приводит.

Где эти решения чаще всего используются?

Сейчас серверная разработка переживает эпоху “новой раскрученной” технологии – микросервисной архитектуры MSA. Для реализации комплексного распределённого программного продукта необходимо обеспечить коммуникацию между сервисами.

Одно из преимуществ MSA – это допустимость наличия любого технологического зоопарка. Чтобы обеспечить прозрачную коммуникацию между сервисами, необходимо использовать какое-то кроссплатформенное представление структурированных данных. Чаще всего для этого подходит JSON:

  • Он достаточно компактен по сравнению с XML, который в ранние года развития интернета был стандартом де-факто;

  • Сериализация/десериализация выполняется быстрее для маленьких структур, чем у подобных форматов данных, и медленнее для больших структур;

  • Не имеет ограниченный набор типов данных.

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

На этом уголок духоты закрывается, и мы переходим к сути.

Почему повсеместно используется Protobuf?

В 2008 году компания Google выпустила в свет ранее закрытую технологию для кроссплатформенного бинарного представления данных.

Protobuf не был первым сериализатором, но был одним из первых, который обеспечил кроссплатформенность и гибкость, не будучи монструозным решением.

По сравнению с конкурентами, MessagePack и Gob, Protobuf имеет несколько ключевых преимуществ:

  • Продолжительная история применения (решение уже доросло до третьей мажорной версии, успев пережить много детских болячек);

  • По этому решению доступен огромный объём информации; 

  • Благодаря активной пропаганде со стороны Google, набрал гигантское комьюнити;

  • До сих пор поддерживается компанией Google.

Почему MessagePack непопулярен?

В след за Protobuf в 2009 году на свет появился конкурент – MessagePack.

Это детище OpenSource комьюнити, поэтому изначально развивался силами энтузиастов. Он долгое время обладал большим набором недостатков и поддерживал меньше платформ нежели Protobuf, да и развивался медленнее. Не обладая большим комьюнити и финансовой поддержкой, MessagePack никак не мог перешагнуть статус одного из сериализаторов, с которым сравнивают детище от Google.

Но со временем решению удалось набрать мышечную массу:

  • По числу поддерживаемых языков программирования он превосходит своего старшего брата;

  • Сообщество набирает темп развития;

  • Решение поставляется в виде отдельных пакетов для каждого поддерживаемого языка программирования (без необходимости держать отдельные схемы и их предкомпилировать).

    p.s. Если вы большой поклонник MessagePack, пожалуйста, переведите уже страницу на сайте wikipedia на русский язык.

Почему Gob игнорируется?

В том же 2009 году компания Google опубликовала первый релиз языка программирования Golang, в котором в 2014 году в пакете encoding появился бинарный сериализатор Gob.

Найти информацию, почему разработчики языка решили реализовать новый бинарный формат я не смог (если кто-то знает, буду благодарен за подсказку в комментариях). Могу привести лишь свои домыслы: разработчики хотели без создания схем прямо сериализовать данные, и это должно работать между Golang сервисами как часы, а поддержка других языков и не рассматривалась. Кажется, что они даже не задумывались что кому-то придет в голову поддерживать другие языки.

  • Можно ли этот сериализатор назвать кроссплатформенным? Можно, но с натяжкой: для других языков и платформ есть решения от независимых разработчиков, впрочем как и у MessagePack.

  • Как дела с сообществом, которое поддерживает и популяризирует данный формат? По данным StackOverflow оно пока что в 6 раз меньше, чем у MessagePack.

  • Как и MessagePack, это решение поставляется пакетом без необходимости предкомпилировать схемы.

    Как говорится в поговорке, комар лошадь не повалит, пока медведь не подсобит. А в сообществе поклонников Gob медведей нет, одни суслики :) 

Где действительно оправдано использование Protobuf?

Оправдано использовать в MSA с большим технологическим и языковым зоопарком, требующим сквозную стандартизацию.

У себя в Scalable Solutions мы поддерживаем микросервисы на нескольких технологических стеках: Golang, C/C++, Python, Java, Node.js, Ruby. Поэтому для межсервисной коммуникации нам требуется иметь единый формат описания структуры данных (схемы), которые могут быть легко предкомпилированы под требуемый язык.

Когда MessagePack рациональнее Protobuf?

Так как MessagePack поставляется как отдельные пакеты для разных языков программирования, его использование оправдано, когда архитектура содержит минимальное число отличных друг от друга технологических стеков.

Почему так? Описание структуры данных делается индивидуально в каждом языке программирования для конкретного пакета, который реализует данную сериализацию. Изменение в одном сервисе, если оно нарушает обратную совместимость, потребует доработки всех связанных с ним сервисов.

Конечно и в случае с Protobuf потребуется сделать те же изменения, но, так как потребуется изменить лишь схему, а после её предкомпилировать под нужные языки, то работы потребуется значительно меньше.

Когда стоит смотреть на Gob?

На текущий момент использование Gob оправдано в архитектуре, где работают сервисы, написанные только на Golang. Так как поддержка данной сериализации в других языках осуществляется только энтузиастами и актуализация пакетов может идти со значительной задержкой.

Почему стоит:

  • Это нативный сериализатор, не требующий никаких внешних зависимостей;

  • Операции производятся над теми же структурами, которые могут использоваться в логике приложения, без необходимости как-то преобразовывать структуры/данные, как это необходимо в случае с Protobuf и некоторыми реализациями MessagePack;

  • Этот формат позволяет работать с ним в потоке, без необходимости разделения сообщений добавлением границ для сообщений;

  • Когда хочется иметь функциональность, которая позволит преобразовывать поля в момент сериализации/десериализации благодаря интерфейсам GobEncoder и GobDecoder.

Очевидные недостатки Protobuf

  • Необходимость предкомпиляции в структуры конкретного языка программирования;

  • Разработчику сложно расширять предкомпилированные структуры. Например, для добавления тегов, которые значительно увеличивают гибкость обработки и проверки данных из структур. Нужно настраивать и использовать дополнительные плагины (технически решаемая задача, но доставляет неудобства);

  • Необходимо писать конверторы ошибок, если требуется пробросить их в журнал или в сервис с другим форматом/структурой;

  • Поддерживает не все типы Golang;

  • Если вы используете язык программирования высокого уровня (Java, C#, Kotlin, Swift), у которого есть такие типы данных как decimal32/decimal64/decimal128, то воспользоваться этими типами в Protobuf не сможете, так как эти данные не поддерживаются, и придётся использовать Protobuf суррогаты, которые более ресурсозатратные.

Практические недостатки MessagePack

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

  • Сложно обеспечивать лёгкую переносимость структур без стандартизации используемых пакетов в разработке. Тут не работает идея о том, что написал схему один раз и предкомпилируешь под каждый сервис отдельно, придётся поработать руками.

  • Не поддерживает все типы, доступные в Golang.

  • Работа с decimal типами здесь реализована простой конвертацией в строку и парсингом обратно – с точки зрения ресурсозатратности тоже очень сомнительное решение.

Специфика работы с Gob

  • Для передачи типов в структурах, которые реализуют интерфейсы, должны быть предварительно зарегистрированы. Казалось, что это небольшая сложность и легко выполнимое правило, но на практике часто доставляет много неудобств, когда ведётся командная разработка, и когда один из разработчиков, написав интерфейс к структуре, не обратил внимание на то, что она используется в обмене данными.

  • Так как в Golang типа decimal нет, то в Gob задача передачи подобных данных решена по аналогии с MessagePack – простой конвертаций в строчку и парсингом обратно.

Итоги первой части

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

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

  • Правильно ли мы выбрали Protobuf в качестве сериализатора,

  • Стоит ли менять сериализатор, обладая новыми знаниями о проекте и команде,

  • Будем ли мы менять бинарный формат обмена данными в ближайшем будущем, и, если будем, то на что.

Да начнется холивар в комментариях :)

Комментарии (21)


  1. beho1der
    23.06.2022 08:44

    Что-то не нашел для Gob библиотеки для js


    1. orange_from_scalable Автор
      23.06.2022 13:34

      Для javascript решения пока нет. IMHO наличие такого решения, возможно, могло увеличить интерес к этому формату


  1. lorus
    23.06.2022 09:58

    Cap'n proto не рассматривали? Вроде бы от одного из авторов Protobuf. Как работа над ошибками. Позиционируется как самый быстрый сериализатор.


    1. ErgoZru
      23.06.2022 10:48

      Быстрее flatbuffers от того же гугла?


      1. pda0
        23.06.2022 14:46

        Возможно или сопоставим. Пошёл почитать, пишут что у них нет стадии сериализации/десериализации.


    1. orange_from_scalable Автор
      23.06.2022 13:36

      Внутри команды рассматривали flatbuffers. Он действительно самый быстрый, как пишет ErgoZru, но и достаточно сыроват/нестабилен для комбинации flatbuffers + gRPC. Поэтому пока следим за развитием.


  1. ErgoZru
    23.06.2022 10:53
    -1

    Очень странное сравнение... Сравнивать protobuf, который является IDL, то есть просто описанием структур и методов, и конкретные сериализаторы... Эта статья выглядит по сути как сравнение инструкции (protobuf) по которой собирают машину и гаечных ключей, которыми собирают ту же машину. Короче теплое с мягким. Ну и никто не мешает написать ген-плагин к протобафу для перегонки прото файлов в те же MsgPack методы и структуры. Да можно даже готовые взять, тот же генератор документации и подсунуть нужный шаблон, или gotemplate генератор, и подсунуть полноценный шаблон и генерировать вообще что хочешь. Да и gob прикрутить при желании можно. Ч не говорю о совместимости с протобафом в котором есть своя реализация, но дополнить ее никто не мешает.


    1. orange_from_scalable Автор
      23.06.2022 13:45

      Всё верно, сравнение не совсем "нормально" с математической точки зрения. Но мы живём не в идеальном мире: любые решения будут иметь свои наборы свойств и особенностей, которые нельзя будет привести к общему знаменателю.
      Статья разбирает решения не только с технической точки зрения, но и со стороны эксплуатации/разработки ("административная" стороны медали).

      Можно и реализовать своё решение со схемами, дебагом и сусликами, которое будет соответствовать всем требованиям. Но такое решение будет технико-экономически неконкурентоспособно, так как на него нужно ещё до начала эксплуатации затратит много человекочасов и не факт, что результат будет лучше.


    1. lueurxax
      23.06.2022 15:33
      +2

      Это скорее сравнение разных подходов, с одной стороны Protobuf с подходом к кросплатформенности и кодогенерацией со стороны документации к коду. И messagepack c gob, как вариант для ситуаций когда мы движемся от кода к документации. Так же хорошо прослеживается что автор намекает на то что все 3 варианта могли бы быть популярны при наличии заинтересованной компании в виде пиара и разработчиков.


      1. orange_from_scalable Автор
        23.06.2022 15:35

        Да, хорошо подмечено: proto - это schema-first решение, msgpack и gob - это всё таки code-first решение.


  1. panter_dsd
    23.06.2022 15:08
    +1

    Еще один аргумент в пользу protobuf - его знает большинство разработчиков и не будет тратиться время на обучение вновь пришедших в команду.

    Жду вторую часть с конкретными цифрами, ибо при выборе решения важно опираться на такие показатели, как размер сообщения, нагрузка на проц при кодировке/раскодировке и потребление памяти при этом.


    1. orange_from_scalable Автор
      23.06.2022 15:13

      Это второе решение после JSON, которое не требует отдельного изучения для вновь пришедших в команду. Это серьёзное преимущество для коммерческой разработки.


    1. orange_from_scalable Автор
      23.06.2022 15:14
      -1

      Вторая часть уже почти готова. Не стал делать long-long-story. Тем более теперь появились новые мысли, что нужно бы добавить/доправить, чтобы цифры были интереснее.


  1. leprosus
    23.06.2022 15:10
    +1

    Вы тут пишите в комментариях про технико-экономическое обоснование (про конкурентность решений). Экономическое - вижу, хотя и без ваших человекочасов. А где технические цифры? Можно же сравнить их по производительности, компактности, сколько ресурсов жрут, что там аллокациями. Было бы вообще неплохо сравнить с JSON (мне кажется, его куда чаще используют).


    1. orange_from_scalable Автор
      23.06.2022 15:15

      Как писал уже выше, вторая часть будет чуть позже.
      Спасибо про аллокации, я их не сравнивал, добавлю в следующей части.


  1. gohrytt
    24.06.2022 01:43

    Занудство мод on: пакет json от goccy все ещё работает быстрее существующих реализаций msgpack и gob на кодировании и декодировании, с protobuf не сравнивал. Так что если сетевые издержки не являются узким местом - звучит будто их использование - пустая трата времени в текущих реалиях.


    1. orange_from_scalable Автор
      24.06.2022 11:48

      Всё верно, до момента, когда размер передаваемых данных увеличивается критично (постараюсь в сл части показать этот момент, если получится реализовать это в программном стенде). Если обмениваться маленькими JSON структурами, то статью можно не читать. Если структуры увеличиваются, то скорость JSON сериализации/десериализации падает ощутимо.


  1. Kekmefek
    24.06.2022 08:58
    +1

    Весьма кстати статья. Как раз искал готовое решение для UNIX RPC сокетов, для обмена между двумя процессами и Gob пока подходит.

    Жаль что статью заминусовали.


    1. orange_from_scalable Автор
      24.06.2022 11:56

      Если нужно нативное решение, то можно использовать https://pkg.go.dev/encoding/gob https://pkg.go.dev/net/rpc Для своих pet проектов отлично работает.


      1. Kekmefek
        24.06.2022 12:27

        Да. Я на них и ориентируюсь.


    1. leprosus
      24.06.2022 12:04

      у меня есть небольшой покет, которая может пригодиться для p2p коммуникации TCP+Gob https://github.com/leprosus/golang-p2p (imho лучше, чем нативный RPC)