Protobuf vs Avro. Как сделать выбор?
В статье перечислены особенности двух популярных форматов сериализации, которые следует учитывать архитектору, выбирая один из них.
Размер и скорость
В сети можно найти сравнительные тесты форматов сериализации. Не стоит придавать значение конкретным числам, так как скорость сериализации/десериализации, как и размер получающихся двоичных данных, зависит от конкретной схемы данных и от реализации сериализатора. Отметим лишь, что авро и протобаф занимают лидирующие позиции в подобных тестах.
Преимущество aвро в том, что поля записи сохраняются одно-за-другим, без разделителей. Но, имея дело с авро, вам нужно где-то сохранять схему записанных данных. Она может быть прикреплена к сериализированным данным или же храниться отдельно (тогда к данным добавляется идентификатор схемы во внешнем хранилище).
Фишка протобaфа в том, что, при сериализации целых чисел, по-умолчанию, используется формат переменной длины (varint), который занимает меньше места для небольших положительных чисел. Протобаф добавляет в бинарный поток номер поля и его тип, что увеличивает итоговый размер. Также, если в сообщение входят поля типа запись (nested message в терминологии протобафа), предварительно нужно вычислить итоговый размер записи, что усложняет алгоритм сериализации и занимает дополнительное время.
В целом, можно сказать, что вы будете удовлетворены размером и скоростью обоих форматов. В большинстве случаев это не тот фактор, который будет определять ваш выбор.
Типы данных
Примитивные типы, представленные в обоих форматах: bool, string, int32(int), int64(long), float, double, byte[]. Протобаф также поддерживает uint32, uint64.
В протобафе, по-умолчанию, целые числа кодируются в формате varint, эффективном для небольших положительных чисел. Вы можете изменить это, указав : sint32, sint64, fixed32, fixed64, sfixed32, sixed64.
Из коллекций вам доступны массивы и отображения (map). Ключом в отображении должна быть строка (в случае протобафа также допускается целое число).
Оба формата поддерживают перечисления (enumerations).
Сложные типы конструируются с помощью алгебраического умножения (records в авро, message в протобафе) и сложения (union в авро, oneof в протобафе).
Для того, чтобы описать опциональное поле, в авро, необходимо использовать union с двумя вариантами, один из которых null, а в протобафе — oneof из одного варианта.
Оба формата поддерживают механизмы расширения системы типов (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К форков)