Зоопарк в Golang MSA. Protobuf, MessagePack, Gob – что выбрать?
Привет! Я 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 в качестве сериализатора,
Стоит ли менять сериализатор, обладая новыми знаниями о проекте и команде,
Будем ли мы менять бинарный формат обмена данными в ближайшем будущем, и, если будем, то на что.
Да начнется холивар в комментариях :)