Proto файлы что это

Темная сторона protobuf

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

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

Все последующее изложение касается только реализации protobuf на платформе Java. Также в основном описана версия 2.6.1, хотя в уже выпущенной версии 3.0.0 принципиальных изменений я также не увидел.

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

maven-проект с уже подключенными зависимостями для самостоятельного исследования можно взять на github.

0. Необходимость препроцессинга

Это наименьшая проблема, даже не хотел включать ее в перечень, но для полноты пусть будет упомянута. Для того чтобы получить java-код необходимо запустить компилятор protoc. Некоторая проблема есть в том, что этот компилятор представляет собой нативное приложение и на каждой из платформ исполняемый файл будет своим, поэтому обойтись простым подключением maven-плагина не получится. Как минимум нужна переменная окружения на машинах разработчиков и на CI-сервере, которая будет указывать на исполняемый файл, и после этого его уже можно запускать из maven/ant сценария.

Как вариант, можно сделать maven-pluging, который держит в ресурсах все бинарники и распаковывает из себя нужный под текущую платформу в временную папку, откуда и запускает его. Не знаю, может такой кто-то уже и сделал.

В общем, невелик грех, поэтому простим.

1. Непрактичный код

К сожалению, для платформы Java генератор protoc производит очень непрактичный код. Вместо того, чтобы сгенерировать чистенькие anemic-контейнеры и отдельно сериализаторы к ним, генератор упихивает все в один большой класс с подклассами. Генерируемые бины нельзя ни внедрить в свою иерархию, ни даже банально заимплементировать интерфейс java.util.Serializable для спихивания бинов на куда-нибудь сторону. В общем они годятся только в качестве узкоспециализированных DTO. Если вас это устраивает — то это и не проблема вовсе, только не заглядывайте внутрь.

2. Излишнее копирование — низкая производительность

Собственно вот тут у меня начались уже совершенно объективные проблемы. Генерируемый код для каждой описываемой сущности (назовем ее «Bean») создает два класса (и один интерфейс, но он не важен в данном контексте). Первый класс — это immutable Bean который представляет собой read-only слепок данных, второй класс — это mutable Bean.Builder, который уже можно править и устанавливать значения.

Зачем так сделано, осталось непонятным. Кто-то говорит, что авторы входят в секту адептов ФП; кто-то утверждает что так они пытались избавится от циклических зависимостей при сериализации (как это им помогло?); кто-то говорит, что protobuf первой версии работал только с mutable-классами, а глупые люди стреляли при этом себе в ноги.

Можно было бы сказать, что на вкус и цвет архитектуры разные, но при таком дизайне для того чтобы получить байтовое представление вам нужно создать Bean.Builder, заполнить его, затем вызвать метод build(). Для того чтобы изменить бин, нужно создать его билдер через метод toBuilder(), изменить значение и затем вызвать build().

И все ничего, только при каждом вызове build() и toBuilder() происходит копирование всех полей из экземпляра одного класса в экземпляр другого класса. Если все что вам нужно — это получить байтовый массив для сериализации или изменить пару полей, то это копирование сильно мешает. Кроме того, в этом методе похоже (я сейчас выясняю) присутствует многолетняя проблема, которая приводит к тому, что копируются даже те поля, значения которых даже не были установлены в билдере.

Вы вряд ли заметите это, если у вас мелкие бины с небольшим количеством полей. Однако мне в наследство досталась целая библиотека, количество полей в отдельных бинах которой достигало трех сотен. Вызов метода build() для такого бина занимает около 50мкс в моем случае, что позволяет обработать не более 20000 бинов в секунду.

Ирония в том, что в моем случае другие тесты показывают, что сохранение подобного бина через Jackson/JSON в два-три раза быстрее (в случае если проинициализированы не все поля и большую часть полей можно не сериализовать).

3. Потеря ссылочности

Если у вас есть графоподобная структура, в которой бины ссылаются друг на друга, то у меня для вас плохая новость — protobuf не подходит для сериализации таких структур. Он сохраняет бины по-значению, не отслеживая факт того, что этот бин уже был сериализован.

Другими словами если у вас есть bean1 и bean2, которые ссылаются друг на друга, то при сериализации-десериализации вы получите bean1, который ссылается на бин bean3; а также bean2, который ссылается на бин bean4.

Уверен, что в подавляющем большинстве случаев такая функциональность не нужна и даже противопоказана в простых DTO. Однако эта проблема проявляется и в более естественных случаях. Например, если вы добавите один и тот же бин в коллекцию 100 раз, он будет сохранен все 100 раз, а не одиножды. Или вы сериализуете список лотов (товаров). Каждый из лотов представляет собой мелкий бин с описанием (количество, цена, дата), а также со ссылкой на развесистое описание продукта. Если сохранять в лоб, то описание продукта будет сериализовано столько раз, сколько существует лотов, даже если все лоты указывают на один и тот же продукт. Решением этой проблемы будет отдельное сохранение продуктов в виде словаря, но это уже дополнительные действия — и при сериализации, и при десереализации.

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

4. Компактность под вопросом

Бытует мнение, что protobuf является суперкомпактным форматом. На самом деле компактность сериализации обеспечивается всего несколькими факторами:

Предположим, приложение генерирует такие данные, что в процентном соотношении в байтовом представлении строки занимают 75%, а примитивы занимают 25%. В таком случае, даже если наш алгоритм оптимизации примитивов сократит необходимое для их хранения место до нуля, мы получим экономию всего в 1/4.

В некоторых случаях компактность сериализация является весьма критичной, например для мобильных приложений в условиях плохой/дорогой связи. В таких случаях без дополнительной компрессии поверх protobuf не обойтись, иначе мы будем впустую гонять избыточные данные в строках. Но тогда вдруг выясняется, что аналогичный комплект [JSON+GZIP] при сериализации дает несильно больший размер по сравнению с [PROTOBUF+ZIP]. Конечно, вариант [JSON+GZIP] будет также потреблять больше ресурсов CPU при работе, но в тоже время, он зачастую также является еще и более удобным.

protoc v3

В protobuf третьей версии появился новый режим генерации «Java Nano». Его еще нет в документации, а runtime этого режима еще в стадии alpha, но пользоваться им можно уже сейчас при помощи переключателя «—javanano_out».

В этом режиме генератор создает анемичные бины с публичными полями (без сеттеров и без геттеров) и с простыми методами сериализации. Лишнего копирования нет, поэтому проблема #2 решена. Остальные проблемы остались, более того при наличии циклических ссылок сериализатор выпадает в StackOverflowError.

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

protostuff

Альтернативная реализация протокола protobuf. В бою не испытывал, но на первый взгляд выглядит очень добротно. Не требует proto-файлов (однако умеет с ними работать, если это необходимо), поэтому решены проблемы #0, #1 и #2. Кроме этого умеет сохранять в свой собственный формат, а также в JSON, XML и YAML. Также интересной является возможность перегонять данные из одного формата в другой потоком, без необходимости полной десериализации в промежуточный бин.

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

Источник

Работаем с Google Protocol Buffer в РНР

Proto файлы что это. Смотреть фото Proto файлы что это. Смотреть картинку Proto файлы что это. Картинка про Proto файлы что это. Фото Proto файлы что этоВ проекте, который я сейчас разрабатываю, возникла необходимость смены протокола, который используется для обмена данными между частями приложения. Сейчас, на уровне внутренних сервисов, обмен происходит через передачу сериализированных массивов РНР поверх TCP сокетов. Так как по обе стороны находятся приложения на РНР, проблем не возникает, формат пакета данных также стандартный, поэтому особых сложностей нет. Разве что часто меня не удовлетворяет скорость обработки, а также то, что мы сильно завязаны на язык и платформу. Если придется стыковать с другой системой или же переписать что-либо, будут сложности — ведь сериализированный формат поймет лишь родной язык, а писать парсер мне не очень хочется. Первоначальный выбор был более чем оправданным — скорость разработки и отладки были приоритетными, сейчас есть немного времени и желания посмотреть на архитектуру с высока и другим взглядом.

Следует сказать, что данные передаются самые простые — строки (различной длины, на практике длинее килобайта или десятка почти нет, обычно это сотни байт), целые числа (в том числе и unix timestamp), некоторый набор констант, true/false флаги, только в одном случае передаются значения с плавающей точкой. В принципе, все сводится к трем типам данных — строка, целое число, число с плавающей точкой. Если хотеть, можно выделить еще поле кода команды, которое можно отнести к перечисляемому виду (количество команд ограничено и конечно, хоть и растет с ростом системы). В сериализированном виде такой пакет занимает достаточно много места, и хоть передаются данные по сокетах в пределах локальной машины, это все равно не выход — изначально система такая, что должна допускать динамическое расширение на несколько узлов кластера.

Навскидку приходит мысль использовать вместо PHP-массива сразу JSON, этим мы решим вопрос понимания протокола другими языками. Но здесь есть подводный камень в виде, насколько я понимаю, кодирования символов строковых, особенно кириллицы, которая преобразуется в UTF и представлена в виде \u3490, что значительно увеличивает объем передаваемых данных.

Так что я начал (в который раз, эх) исследовать различные форматы для взаимодействия между сервисами приложения, которые бы позволили обмениваться в будущем и между разными платформами, прозрачно передаваться по сети и быть, по возможности, максимально компактными. Очень приглянулся мне протокол Hessian (по тестам, его вторая реализация просто отличная), но слишком он замкнутый и очень мало документации. Поэтому в основном я рассматривал Facebook/Apache Thrift и Google Protocol Buffer.

Thrift теперь открытый и передан Apache Fundation, но его реализация достаточно сложна и запутана, вместе с минимумом документации и примеров (чтобы не сказать отсутствием), он отпал сразу, к тому же заставить работать вариант для РНР мне так и не удалось.

А вот Google Protocol Buffer (далее, для сокращения — PB) оказался очень даже приятным и интересным. Однако сложности начались сразу, так как моя задача была работать с ним в РНР, а не Python или Java, как предлагают нам разработчики. Так как материалов по этой теме нет, решил описать свои шаги, на случай, если кому придется такое же делать. Сразу оговорюсь, что сам протокол я не буду описывать, если вы не знакомы с ним — хорошее введение есть на сайте проекта (например: Developer Guide ).

И так, первое, что радует — для работы с PB нет необходимости устанавливать дополнительное ПО или компилировать расширение для РНР, а значит вы сможете провести эксперименты и на домашней машине и на виртуальном хостинге. Пока единственным средством работы в РНР является проект pb4php, который находится в ранней стадии разработки (судя по номеру версии, 0.25, при этом разработка ведется одним человеком и начала больше года назад, хотя коммиты время от времени появляются, но активности очень небольшая). Его мы и будем использовать, но советую трезво оценить свои потребности — если вам достаточно основных возможностей формата и операций создания/чтения/записи/сериализации, то все хорошо, более продвинутые операции не поддерживаются.

Для примера представим самый простой вариант — у нас есть некоторый сервис, который получает новости, например, извлекает из RSS-лент, и есть сервер, который рассылает новости подписчикам. Мы хотим свести обмен данными между обоими сервисами к обмену данными через Protocol Buffer, при этом один из сервисов, или оба, у нас на РНР. Что и как делать?

Сначала необходимо согласовать формат сообщения, пусть это будет самый простой вариант:

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

Указанный файл сохраняем в виде обычного текстового файла в формате *.proto. Далее, используя компилятор (загрузить можно здесь для различных платформ) мы компилируем сообщение в бинарный формат, по сути, шаблон для последующего использования.

Стандартный компилятор сразу генерирует враппер для сообщения, объекты и служебные методы для поддерживаемых языков — Java, C++, Python. Однако мы же работает с РНР, которого пока нет в списке.

В пакете pb4php есть скрипт (лежит в директории example/protoc.php), который делает то же самое (хоть и коряво, честно) для РНР — загружает указанный proto файл и генерирует структуру РНР-класса для работы с сообщением (да-да, код на РНР). Обратите внимание, этот компилятор работает с текстовым описанием сообщения, тем самым файлом *.proto, что вы создали выше.

Однако я предпочел генерировать класс-обертку для сообщения вручную, в частности, обнаружил ошибку в самих файлах примеров. Компилятор неверно генерирует указатель на тип данных строка — константы PBMessage::WIRED_STRING нет в исходниках, хотя она присутствует во всех примерах, вам придется заменить ее на PBMessage::WIRED_LENGTH_DELIMITED.

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

Наш класс очень простой и будет хранится в файле pb_news_interface.php:

В конструкторе мы описываем формат данных, используя предопределенные имена для типов данных, pb4php отображает их на классы основных типами данных протокола — PBInt, PBBool (наследуется от PBInt), PBSignedInt, PBEnum (перечисления), PBString и PBBytes. Такая обертка нужна, так как далеко не все типы данных протокола можно напрямую отобразить на встроенные типы данных языка, а другие просто дублируются, например sint32/int32 в C++ отображаются на один и тот же тип int32 (хотя вообще вопрос типов данных непростой и таблица отображения из документации не дает исчерпывающего ответа). pb4php реализует лишь несколько базовых типов, так что вам придется выбирать самые общие типы для описания своего формата.

Кстати, сам код обертки далеко не оптимален, его вполне можно заменить более простым и коротким, используя магические методы __get/__set, видимо код писался еще и для совместимости с устаревшими версиями РНР и ООП-возможности там используются далеко не в полную силу.

Ок, давайте дальше. Для использования протокола в программе необходимо подключить два служебных файла — основной класс для работы с сообщениями ( /message/pb_message.php ) и наш сгенерированный класс-обертку для сообщений (pb_news_interface.php). После этого мы работает далее как с обычным РНР-классом.

Для дальнейшей работы нам необходимо получить сериализированное значение нашего сообщения, например, для дальнейшей передачи через сеть. Для этого есть встроенный метод SerializeToString, который кодирует сообщение в строку (в HEX).

Кстати, автор позаботился даже о встроенном методе передачи объектов по сети — просто вызовите метод Send и передайте ему URL и экземпляр сообщения, далее через cURL будет сделан POST-запрос с параметром message, который содержит сериализированное PB-сообщение. Хотя в реальности я бы не стал использовать встроенный механизм передачи данных, скорее просто для тестирования возможностей.

Еще есть интересная константа MODUS, которая отвечает за формат хранения — в двоичном виде или строковом. Двоичный эффективней для передачи по сети и экономит трафик, строковый удобный для тестирования и чтения разработчиком.

Проводя исследование, я взял типовый набор данных, которые гоняются в моем приложении, и попробовал сравнить вариант с родной сериализацией РНР и Protocol Buffer в варианте pb4php. Выигрыш на размере данных получился около 30% (127 байт против 186 в обычном виде), хотя это никак не претендует на серьезное исследование, просто было интересно сравнить эффективность на моем реальном наборе данных.

Полученную строку можно записать в файл или передать любым другим способом, для восстановления первоначальной формы объекта достаточно создать экземпляр и вызвать метод ParseFromString.

Кстати, о некоторых расширенных возможностях PB, которые поддерживаются в библиотеке — посмотрите в директории /examples/nested_mess/, там есть пример работы как раз с преобразованием RSS в Protocol Buffer с использованием сообщений, которые содержат внутри себя, кроме полей с обычными типами, так же и вложенные классы.

И так, в итоге. Используя возможности pb4php мы можем:

Конечно, сериализация отнимает ресурсы и она обычно всегда дольше обычной нативной, хотя на простых сообщениях (где несколько полей) это почти никак не заметно. Основной выиграш происходит из-за сокращения объема данных (особенно критично при передаче по сети и большом потоке сообщений), а также легкой возможности строить сервисы, которые не зависят от языка реализации и особенностей платформы. Единственное, что не радует, это практически замороженное развитие проекта (имеется ввиду pb4php, а не сам Protocol Buffer) и его уникальность — для многих языков есть по несколько реализаций. Интересно было бы иметь также вариант с С-расширением для РНР, это бы значительно ускорило операции сериализации/восстановления, а также хотелось бы поддержки от основных фреймворков, вроде Zend Framework.

Но даже в таком виде проектом можно пользоваться и если перед вами стоит задача быстро научиться обрабатывать сообщения в формате Google Protocol Buffer на РНР — используйте pb4php!

Источник

Структурированный протокол обмена данных Protobuf или JSON во фронтенде?

Proto файлы что это. Смотреть фото Proto файлы что это. Смотреть картинку Proto файлы что это. Картинка про Proto файлы что это. Фото Proto файлы что это

В новом проекте в нашей команде мы выбрали frontend framework VUE для нового продукта, бэкенд написан на PHP, и уже как 17 лет успешно работает.

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

Про бэкенд

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

Про фронтенд

4мес. разработки фронта мы использовали JSON в качестве ответа от сервера, мапили в State Vuex в удобном нам формате. Но для отдачи на сервер нам требовалось преобразовывать в обратную сторону, чтобы сервер смог прочитать и замапить свои DTO объекты (может показаться странным, но так надо 🙂 )

Проблемы

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

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

После некоторых поисков, мы пришли к двум вариантам:

Хватит болтовни, давайте посмотрим как все это выглядит

Как это выглядит на стороне PHP я не буду описывать, там примерно все тоже самое, объекты те же.

Покажу на примере простого клиентского JS и мини сервера на Node.js.

Для начала описываем структуры данных которые нам потребуется. Дока.

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

Далее скачивается генератор кода на основании структур.

И запускается команда генерации под JS.

После генерации появляется 3 JS файла, в которых уже все приведено к объектам, с функционалом сериализации в буфер и десериализации из буфера.

price_pb.js
product_pb.js
service_pb.js

Далее описываем уже JS код.

В принципе клиент готов.

На сервере заюзаем Express

Что мы имеем в итоге

Я взял lorem ipsum на 10 абзацев, получилось 5.5кб данных с учетом заполненных объектов Price, Product. И погонял данные по Protobuf и JSON (все тоже самое только заполненные JSON схемы, вместо Protobuf объектов)

Источник

Protocol Buffers

Протокол сериализации для гетерогенных систем

Контекст

Основная идея

Язык общения

Общие приемущества

Недостатки

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

Пример

Шаг 1. Определяем формат протокола

Модификаторы дают нам больше представления о том как поле используется, например, модификатор required позволяет описать обязательное поле в сообщении, если десериализатор не обнаружит этого поля, то весь процесс десериализации закончится с ошибкой. Это важно учитывать при проектировании API (снова взгляните на второй абзац в разделе “Недостатки” этой статьи). Модификатор optional, говорит о том, что поле может быть, а может отсутствовать, своего рода nullable поле. Модификатор repeated используется для работы с множеством значений для одного поля (аналогично коллекциям в Java).

Вы можете вкладывать messages друг в друга, использовать перечисления enum, в общем очень похоже на Java. Кроме того, есть возможность определить значения по умолчанию.

*Шаг 2. Компилируем файл

* опциональный, для понимания

Шаг 3. Собираем проект

Прежде всего хочу сказать, что не смотря на то, что все примеры на java, работа на других платформах с protobuf аналогична.

Поигрались с терминалом и хватит, перейдем к практическому применению. Создадим gradle проект, цель которого будет перегнать через массив байт группу со студентами. Для автоматизации рутинной деятельности нам поможет инструмент автоматизации сборки gradle. Для вашего случая инструмент может отличаться, но идея должна быть понятна. Для того, чтобы добавить поддержку protocol buffers в цикле сборки нашего проекта, дополним типичный build.gradle файл следующими настройками:

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

Шаг 4. Взаимодействуем со сгенерированным кодом

Как вы уже поняли, создавать студентов мы можем аналогично:

Итак, данные мы создали, получили заполненный объект типа Group, теперь необходимо перегнать его в массив байт. Сделать это можно следующим образом:

Для того, чтобы проверить результат выведем его на экран (компилятор создает человекопонятные методы toString для классов, так что с отладкой нет проблем).

В результате, в консоли мы видим:

За ширмой, для полноты примера, я добавил еще одного студента к группе.

Заключение

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *