Protobuf оказался не таким монолитным

TL;DR: Яндекс выложил в open source YaFF, и это очень красивое архитектурное решение.

Я впервые услышал про YaFF больше года назад на одном из мероприятий компаний про бэкенд. А недавно увидел пост у Димы Александрова, руководителя разработки в Лавке, где он поделился подробной статьей на Хабре о том, зачем формат создавали и как он устроен, и ссылкой на исходники.

С Protobuf я работал ещё в Яндексе: мы строили на нём cross-backend-протокол. А в «Чёрном поясе по C++» объясняли, как им пользоваться и для чего он вообще нужен.

Для меня Protobuf всегда был решением по умолчанию для бинарной сериализации. Описываешь сообщение в .proto, генерируешь код и дальше работаешь с обычными классами. Не думаешь, в каком порядке разложить байты, что делать с little-endian и big-endian и как корректно развивать протокол.

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

Но у любого универсального решения есть границы применимости.

В сценарии, который описывается на Хабре, через runtime проходят огромные объемы данных. Protobuf-сообщение нельзя просто положить в память и начать читать как готовый объект. Его нужно разобрать, выделить память под поля и переложить туда данные. Когда объектов очень много, постоянная десериализация начинает съедать заметную долю CPU.

Эту проблему уже решают zero-copy-форматы, например FlatBuffers. Они позволяют читать поля прямо из сериализованного буфера, не создавая отдельное представление объекта в памяти.

Но если большая система уже построена вокруг Protobuf, перейти на FlatBuffers — не значит просто заменить одну функцию сериализации другой. Появляются новая схема, другие сгенерированные типы и другой API. Если часть системы продолжает использовать Protobuf, приходится синхронизировать два представления и писать конвертеры между ними.

И вот здесь появляется YaFF — Yet Another Flat Format.

Он использует привычный .proto как источник схемы и генерирует Protobuf-подобный интерфейс. Но сериализует данные уже не в стандартный Protobuf wire format, а в собственное представление, из которого их можно читать без обычной десериализации. Именно это показалось мне самым интересным.

Я всегда воспринимал Protobuf как неделимую экосистему: .proto-схема, сгенерированные классы, правила совместимости и бинарный формат. А авторы YaFF провели внутри неё границу. Они сохранили контракт, интерфейс и привычный способ развивать схемы, но заменили физическое представление данных.

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

В устройство самого YaFF я пока до конца не погрузился. Несколько важных тезисов по YaFF можно найти в посте у Димы, а в самой статье подробно разбираются разные способы раскладки полей внутри буфера — на эту часть меня уже не хватило. Зато сама архитектурная идея кажется очень красивой: самое интересное в YaFF — не zero-copy само по себе, а то, как его авторы отделили полезный контракт Protobuf от ставшего дорогим способа хранения данных.


В этом посте были ссылки, но мы их удалили по правилам Сетки