Что такое proto файл

Как работать с protobuf в Go

E-commerce давно перестали быть сайтами с картинками — сегодня это огромные онлайн-платформы с множеством высоконагруженных сервисов. В Ozon порядка 60% сервисов — от инфраструктурных проектов до пользовательских — написано на Go, в IT-лаборатории компании сейчас одна из самых больших golang-команд России. Об инструментах разработки и трендах в развитии языка рассказывает Владимир Сердюков, ведущий разработчик группы «Личный кабинет» Ozon.

Что такое proto файл. Смотреть фото Что такое proto файл. Смотреть картинку Что такое proto файл. Картинка про Что такое proto файл. Фото Что такое proto файл

ведущий разработчик группы «Личный кабинет» Ozon

Как-то ко мне пришел тестировщик и показал тестовый смартфон, на котором было запущено наше приложение — но вместо текста и элементов интерфейса на нем был белый экран. Мы начали разбираться.

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

Почему эту проблему стоит обсуждать?

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

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

proto 2 vs proto 3

Для работы с контрактами мы в Ozon используем protobuf — механизм, придуманный Google для сериализации структур данных. Чтобы из proto-файлов сгенерировать код, мы используем proto 3 с набором плагинов, один из которых — gogoproto, призванный упростить этот процесс и частично забороть особенности proto v3.

Предыдущая версия протокола (proto 2) позволяла реализовывать обязательные поля при помощи тега optional и задавать стандартное значение. Однако это приводило к проблемам портирования возможности генерации кода на другие языки программирования.

Это стало причиной появления proto 3. В новой версии протокола поменялось отношение к обязательным полям: все поля стали необязательные, а значения по умолчанию просто не отправляются. Кроме того, были исправлены enum, улучшен декодинг в json и внесены другие мелкие изменения.

Как сделать поля обязательными в proto 3

Чтобы сделать поля обязательными, можно использовать Well-known типы данных. Это может быть строка, булево значение, число, timestamp и тому подобное.

Что такое proto файл. Смотреть фото Что такое proto файл. Смотреть картинку Что такое proto файл. Картинка про Что такое proto файл. Фото Что такое proto файл

Если появляется такая строка, значит поле отправится вне зависимости от того, что вы в него записали. Однако сложные типы данных (proto-сообщение) все равно остаются необязательными. Тут на помощь приходит плагин gogoproto, где можно указать, что ваша структура не nullable.

Что такое proto файл. Смотреть фото Что такое proto файл. Смотреть картинку Что такое proto файл. Картинка про Что такое proto файл. Фото Что такое proto файл

Кроме того, можно сделать свой собственный форк jsonpb. Это библиотека, которая proto-сообщения генерирует в json. В нее добавлены три вида тегов — написав их в любом месте файла, можно не переживать, что сообщение не будет отправлено.

Что такое proto файл. Смотреть фото Что такое proto файл. Смотреть картинку Что такое proto файл. Картинка про Что такое proto файл. Фото Что такое proto файл

Как проверить, что новые изменения не ломают контракты?

Теперь понятно, как можно сделать поля обязательными, но что делать, если вдруг поле перестало быть обязательным или произошло что-то еще?

Для этого есть несколько инструментов для валидации proto-файлов, например buf.build и uber/prototool.

Оба инструмента работают по схожей схеме: чтобы запустить валидацию, создайте yaml-файл, в нем укажите конфигурацию, какие proto-файлы надо проверить, а какие нет. Также можно указать, какую ветку из репозитория брать. В обоих случаях работа идет только с git.

uber/protool пока не поддерживает apiv2, поэтому стоит приглядеться внимательнее к buf.build.

Что такое apiv2?

Поддержка работы с protobuf в Go впервые была анонсирована в 2010 году, первая версия Go вышла только спустя два года. С тех пор прошло много времени, изменились требования.

В итоге, несколько месяцев назад, 2 марта 2020 года, была анонсирована apiv2. Вот что изменилось по сравнению с прошлой версией:

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

Что такое proto файл. Смотреть фото Что такое proto файл. Смотреть картинку Что такое proto файл. Картинка про Что такое proto файл. Фото Что такое proto файл

Дальше в функции-обработчике где-нибудь в середине формирования сообщения делаем следующее:

Что такое proto файл. Смотреть фото Что такое proto файл. Смотреть картинку Что такое proto файл. Картинка про Что такое proto файл. Фото Что такое proto файл

Таким нехитрым способом можно реализовать разные бизнес-кейсы.

Источник

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 для классов, так что с отладкой нет проблем).

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

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

Заключение

Источник

Использование Google Protocol Buffers (protobuf) в Java

Привет, хабровчане. В рамках курса «Java Developer. Professional» подготовили для вас перевод полезного материала.

Что такое proto файл. Смотреть фото Что такое proto файл. Смотреть картинку Что такое proto файл. Картинка про Что такое proto файл. Фото Что такое proto файл

Недавно вышло третье издание книги «Effective Java» («Java: эффективное программирование»), и мне было интересно, что появилось нового в этой классической книге по Java, так как предыдущее издание охватывало только Java 6. Очевидно, что появились совершенно новые разделы, связанные с Java 7, Java 8 и Java 9, такие как глава 7 «Lambdas and Streams» («Лямбда-выражения и потоки»), раздел 9 «Prefer try-with-resources to try-finally» (в русском издании «2.9. Предпочитайте try-с-ресурсами использованию try-finally») и раздел 55 «Return optionals judiciously» (в русском издании «8.7. Возвращайте Optional с осторожностью»). Но я был слегка удивлен, когда обнаружил новый раздел, не связанный с нововведениями в Java, а обусловленный изменениями в мире разработки программного обеспечения. Именно этот раздел 85 «Prefer alternatives to Java Serialization» (в русском издании «12.1 Предпочитайте альтернативы сериализации Java») и побудил меня написать данную статью об использовании Google Protocol Buffers в Java.

В разделе 85 «Prefer alternatives to Java Serialization» (12.1 «Предпочитайте альтернативы сериализации Java») Джошуа Блох (Josh Bloch) выделяет жирным шрифтом следующие два утверждения, связанные с сериализацией в Java:

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

«Нет никаких оснований для использования сериализации Java в любой новой системе, которую вы пишете».

После описания в общих чертах проблем с десериализацией в Java и, сделав эти смелые заявления, Блох рекомендует использовать то, что он называет «кроссплатформенным представлением структурированных данных» (чтобы избежать путаницы, связанной с термином «сериализация» при обсуждении Java). Блох говорит, что основными решениями здесь являются JSON (JavaScript Object Notation) и Protocol Buffers (protobuf). Мне показалось интересным упоминание о Protocol Buffers, так как в последнее время я немного читал о них и игрался с ними. В интернете есть довольно много материалов по использованию JSON (даже в Java), в то время как осведомленность о Protocol Buffers среди java-разработчиков гораздо меньше. Поэтому я думаю, что статья об использовании Protocol Buffers в Java будет полезной.

На странице проекта Google Protocol Buffers описывается как «не зависящий от языка и платформы расширяемый механизм для сериализации структурированных данных». Также там есть пояснение: «Как XML, но меньше, быстрее и проще». И хотя одним из преимуществ Protocol Buffers является поддержка различных языков программирования, в этой статье речь пойдет исключительно про использование Protocol Buffers в Java.

album.proto

Несмотря на простоту приведенного выше определения формата протокола, в нем присутствует довольно много информации. В первой строке явно указано, что используется proto3 вместо proto2, используемого по умолчанию, если явно ничего не указано. Две строки, начинающиеся с option, указывают параметры генерации Java-кода (имя генерируемого класса и пакет этого класса) и они нужны только при использовании Java.

Ключевое слово «message» определяет структуру «Album», которую мы хотим представить. В ней есть четыре поля, три из которых строки (string), а одно — целое число (int32). Два из них могут присутствовать в сообщении более одного раза, так как для них указано зарезервированное слово repeated. Обратите внимание, что формат сообщения определяется независимо от Java за исключением двух option, которые определяют детали генерации Java-классов по данной спецификации.

Что такое proto файл. Смотреть фото Что такое proto файл. Смотреть картинку Что такое proto файл. Картинка про Что такое proto файл. Фото Что такое proto файл

Сгенерированный Java-класс AlbumProtos.java содержит более 1000 строк, и я не буду приводить его здесь, он доступен на GitHub. Среди нескольких интересных моментов относительно сгенерированного кода я хотел бы отметить отсутствие выражений import (вместо них используются полные имена классов с пакетами). Более подробная информация об исходном коде Java, сгенерированном protoc, доступна в руководстве Java Generated Code. Важно отметить, что данный сгенерированный класс AlbumProtos пока никак не связан с моим Java-приложением, и сгенерирован исключительно из текстового файла album.proto, приведенного ранее.

Прежде чем двигаться дальше, нам понадобится простой Java-класс для демонстрации Protocol Buffers. Для этого я буду использовать класс Album, который приведен ниже (код на GitHub).

Album.java

Создадим экземпляр Album с помощью следующего кода:

Чтение массива byte[] обратно в экземпляр Album может быть выполнено следующим образом:

Здесь мы видим, что в обоих экземплярах соответствующие поля одинаковы и эти два экземпляра действительно разные. При использовании Protocol Buffers, действительно, нужно сделать немного больше работы, чем при «почти автоматическом» механизме сериализации Java, когда надо просто наследоваться от интерфейса Serializable, но есть важные преимущества, которые оправдывают затраты. В третьем издании книги Effective Java («Java: эффективное программирование») Джошуа Блох обсуждает уязвимости безопасности, связанные со стандартной десериализацией в Java, и утверждает, что «Нет никаких оснований для использования сериализации Java в любой новой системе, которую вы пишете».

Смотреть открытый вебинар на тему «gRPC для микросервисов или не REST-ом единым».

Источник

Темная сторона 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 будет сохранять все поля объекта подряд, в независимости от того были они проинициализированы значением или нет, а это снова сильно бьет по компактности в случае, когда заполнены не все поля. Но насколько я вижу, такое поведение при желании можно подправить, переопределив пару классов.

Источник

Реверс-инжиниринг сообщений Protocol Buffers

Подготовка

Для начала я создам объекты для исследования. Нам понадобятся 2 файла:

Сохраняем их и собираем все вместе. Если вы не знаете, что такое protoc, то Вам нужно прочесть введение в библиотеку Protobuf для вашего языка программирования.

Удаляем или закомментируем вторую строку файла addressbook.proto и выполняем команду:

После выполнения вышеупомянутых команд мы имеем два исполняемых файла tut.lite.exe и tut.exe, с LITE и полной сборкой библиотеки libprotobuf соответственно. Обе программы делают одно и тоже: создаётся protobuf сообщение, которое выводится в std::cout. Так же у нас появилось два бинарных файла с именами A и B. Первый сгенерирован lite версией, второй — полной версией программы. Содержимое их идентично. На скриншоте ниже можно увидеть бинарное представление этого сообщения и его текстовый вид:

Что такое proto файл. Смотреть фото Что такое proto файл. Смотреть картинку Что такое proto файл. Картинка про Что такое proto файл. Фото Что такое proto файл

Удаляем addressbook.proto и попытаемся его восстановить.

Восстановление схемы сообщений из Descriptor данных исполнимого файла

Глянем содержимое файла adressbook.pb.cc, сгенерированного ранее утилитой protoc. Нас должна заинтересовать функция protobuf_AddDesc_addressbook_2eproto. Одним из первых действий в ней — вызов функции ::google::protobuf::DescriptorPool::InternalAddGeneratedFile, первый аргумент которой и есть Descriptor protobuf сообщение с информацией о структуре оригинальных сообщений.

В ней сохранена информацией о перечислениях, списке импорта, сообщениях, имена и типы данных их полей и т.д. Формат не является секретом и поставляется вместе с исходным кодом; его можно глянуть в google/protobuf/descriptor.proto. Эти данные используется при рефлексии, для отладочного вывода содержимого сообщений и т.д.

В ответ увидим что-то такое:

Что такое proto файл. Смотреть фото Что такое proto файл. Смотреть картинку Что такое proto файл. Картинка про Что такое proto файл. Фото Что такое proto файл

Восстановление схемы из байт сообщения

Программа protodec может восстановить схему указанного protobuf сообщения с их типами, загруженного из файла. Для этого запустим команду:

Что такое proto файл. Смотреть фото Что такое proto файл. Смотреть картинку Что такое proto файл. Картинка про Что такое proto файл. Фото Что такое proto файл

Этот вывод означает, что в данном protobuf сообщении (загруженном из файла A), было обнаружено 3 сообщения. Если мы взглянем на оригинальный addressbook.proto, то несомненно угадывается общее: MSG1 это Person::PhoneNumber, MSG2 это Person, ну а MSG3 это AddressBook. Опишу бросающиеся в глаза несоответствия:

Что такое proto файл. Смотреть фото Что такое proto файл. Смотреть картинку Что такое proto файл. Картинка про Что такое proto файл. Фото Что такое proto файл

Проверка

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

Источник

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

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