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

Размер и скорость

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

Преимущество aвро в том, что поля записи сохраняются одно-за-другим, без разделителей. Но, имея дело с авро, вам нужно где-то сохранять схему записанных данных. Она может быть прикреплена к сериализированным данным или же храниться отдельно (тогда к данным добавляется идентификатор схемы во внешнем хранилище).

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

UPD: Авро также использует формат переменной длины для записи целых чисел, с чередованием положительных и отрицательные значений (zigzag encoding). Авровский int соответствует протобафовскому sint32, а long - sint64.

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

UPD: Высоконагруженная система или обработка данных в реальном времени может стать тем случаем, когда стоит присмотреться к более специализированным кодекам (ветвь дискуссии).

Типы данных

Примитивные типы, представленные в обоих форматах: bool, string, int32(int), int64(long), float, double, byte[]. Протобаф также поддерживает uint32, uint64. 

В протобафе, по-умолчанию, целые числа кодируются в формате varint, эффективном для небольших положительных чисел.  Вы можете изменить это, указав : sint32, sint64, fixed32, fixed64, sfixed32, sixed64.

Из коллекций вам доступны массивы и отображения (map). Ключом в отображении должна быть строка (в случае протобафа также допускается целое число).

Оба формата поддерживают перечисления (enumerations).

Сложные типы конструируются с помощью алгебраического умножения (records в авро, message в протобафе) и сложения (union в авро, oneof в протобафе).

Для того, чтобы описать опциональное (nullable) поле, в авро, необходимо использовать union с двумя вариантами, один из которых null, а в протобафе - oneof из одного варианта.

UPD: в протобафе nullable также можно задать обернув необходимый тип в message из одного поля. Еще есть экспериментальная поддержка ключевого слова optional, которое работает, как синтаксический сахар над oneof. Дискуссия на stackoverflow.

Оба формата поддерживают механизмы расширения системы типов (logical types в авро и well known types в протобафе). Таким образом обе схемы дополнительно поддерживают сериализацию даты и времени (timestamp) и продолжительности времени (duration).

В отличии от авро, протобаф не поддерживает decimal и UUID. Также авро поддерживает тип fixed - массив байт определенной длины.

Итого, хотя отсутствие поддержки decimal в протобафе - досадное упущение, система типов, тоже не будет определяющей при выборе.

Эволюция данных

Обе схемы поддерживают механизмы обратной совместимости (backward compatibility) за счет заполнения новых полей значениями по-умолчанию. В авро можно указать любое, допустимое значение, в протобафе это значение задано жестко, в зависимости от типа (0, пустая строка, false). В авро также поддерживаются альтернативные имена (aliases) для полей и именованных типов (record, enum, fixed). В протобафе имя поля не используется в двоичной сериализации, но номер поля не может быть изменен.

Для числовых типов, в авро допускаются только преобразования без потери (например int в long, float в double, но не наоборот). Протобаф более толерантен к изменению числовых типов и применяет правила преобразования, идентичные C++. Также протобаф допускает преобразования из bool в число и обратно, из целого числа в enum и обратно.

В случае, когда потребитель данных использует старую схему, а производитель более новую, важно, чтобы новый формат данных не сломал работу потребителя. Такое свойство схемы данных называется упреждающей совместимостью (forward compatibility). 

Неизвестное поле в записи игнорируется обоими форматами.

В случае неизвестного значения enum, авро подставляет значение по-умолчанию, если оно задано, протобаф - нулевое значение.

Неизвестный вариант (case) в объединении (union) протобаф помечает признаком unknownАвро же, в этом случае, выдает исключение и прерывает десериализацию.

Это серьезное ограничение авро. Если вы используете алгебраические типы данных (ADT), то авро, скорее всего, вам не подходит, так как не поддерживает упреждающую совместимость при добавлении нового варианта в объединение.

Представление в Json

Как авро, так и протобаф, поддерживают сериализацию в Json. Это может быть полезно, как для отладочных целей, так и для долгосрочного хранения данных (например, в MongoDB). 

В случае протобафа, нарушится обратная совместимость, если вы поменяете название поля (на самом деле есть трюк, и вы можете переименовать поле один раз, использовав атрибут json_name для сохранения информации о прежнем имени поля). Авро же позволяет давать несколько альтернативных названий (aliases) полям.

Неприятным свойством авро является то, что массив байт (типы bytes, fixed) сохраняется в виде UTF16 строки. Это не только порождает визуальный мусор (псевдографика, переводы строк и т.п), но и может сделать Json нечитаемым, так как не все библиотеки корректно транслируют UTF16. Протобаф же сохраняет массив байт в base64.

Представление в формате Json не должно повлиять на ваш выбор, не забывайте только, что в случае протобафа, вы можете переименовать поле только один раз, а в случае авро, нужно проверить ваш стек на корректность обработки UTF16.

Влияние на архитектуру

Выбрав авро, вы должны будете решить, где хранить схему. В том случае, если вы работаете с большим количеством сообщений малого размера (например, отправляете сообщения через кафку), вам нужно хранить схемы данных в отдельном хранилище (например, использовать Schema Registry). Вам потребуется реализовать кеширование схем, что привнесет в вашу систему состояние (statefullness), будет забирать время на “прогрев” при запуске.

Если ваш язык программирования обладает динамической системой типов (например, python), то протобаф, с его требованием заранее задать типы всех полей, может стать для вас слишком обременительным. ��вро же, наоборот, может стать хорошим решением, так как позволяет десериализировать данные “на лету”, зная лишь схему по которой эти данные были сохранены. Для случаев динамической типизации, в протобафе появился тип Any, но, согласно официального сайта, его поддержка в процессе разработки.

RPC

В обоих форматах есть возможность описать интерфейс для удаленного вызова процедур.

Протокол авро поддерживает синхронный вызов процедуры и вызов без ожидания ответа (one-way). Протокол включает в себя этап рукопожатия (handshake), в время которого стороны обмениваются схемами.

Сервис протобафа допускает как синхронный вызов, так и отправку потока данных (streaming) в обоих направлениях.

Флагманская реализация RPC для протобафа - gRPC. Несмотря на то, что gRPC, по-умолчанию, использует протобаф, теоретически он позволяет использовать и другие механизмы сериализации, включая авро. На практике же, все зависит от конкретной реализации, так, например, в дотнете, Майкрософт прилагает много усилий для того, чтобы выжать максимальную производительность из реализации gRPC в связке с протобафом, но вы вступаете на скользкую дорожку, пытаясь подменить сериалайзер, например, на авро.

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

Kafka

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

Hadoop

Ситуация зеркальна gRPC. Несмотря на то, что Hadoop - это вотчина авро, с помощью библиотеки elephant-bird вы можете использовать протобаф в связке с хадупом.

Комьюнити

Оба продукта доступны в открытом коде.

https://github.com/apache/avro (1.7K звезд, 1.1К форков)

https://github.com/protocolbuffers/protobuf (45K звезд, 12.1К форков)