Pull to refresh
350.98
Конференции Олега Бунина (Онтико)
Конференции Олега Бунина

Protobuf и buf: блеск, нищета и импортозамещение

Reading time11 min
Views7.7K

Если для компиляции proto-файлов вы всё ещё используете protoc, самое время перестать и перейти на buf. Разберём, как это сделать и почему это необходимо. Также рассмотрим проблемы доступа к buf.build.

Меня зовут Эдгар Сипки, я Go-разработчик в Ozon Fintech. buf — мощная утилита для линтинга протофайлов, проверки обратной совместимости API, генерации кода и валидации запросов. Однако, из-за санкций она недоступна в России. Поэтому я расскажу, как мы разрабатывали собственное решение в рамках импортозамещения.

gRPC и его проблемы

Все мы знаем gRPC. Это высокопроизводительный фреймворк RPC. Вот традиционный тулсет для него:

  • Golang

  • Protoc

  • Plugins:

    • protoc-gen-go

    • protoc-gen-go-grpc

Неконсистентное описание API

Это одна из самых важных проблем. Она включает:

  • отсутствие жёсткой структуры описания;

  • вариации наименований (sneak, camel);

  • легко сломать обратную совместимость.

Вот хороший пример:

service ServiceAPI {

  rpc example_one(google.protobuf.Empty)
    returns (google.protobuf.Empty);

  rpc ExampleSecond(google.protobuf.Empty)
    returns (google.protobuf.Empty);

  }

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

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

Ещё оказывается, что для прода не важен стиль. Например:

message Request {
  string example_one = 1;
  string ExampleSecond = 2;
}

message request {
  string example_one = 1;
  string ExampleSecond = 2;
}

Здесь функции будут абсолютно идентичны. При компиляции будет ошибка.

Проблема с зависимостями

Так как отсутствует пакетный менеджер, то все в компании переносят пакеты вручную или используют git submodules. Мы обычно создаём определённые директории, называем third party, закидываем  в них вендоров и прописываем все зависимости. Но это далеко не самый удобный способ работы.

Breaking change

С одной стороны, Protoc создавался как формат, в котором будет очень тяжело сломать обратную зависимость. Однако на деле сломать очень легко. Особенно, если человек элементарно не читал документацию.

Возьмём хороший пример:

syntax = "proto3";
...

service ServiceAPI {
  rpc Auth(AuthInfo) returns
(google.protobuf.Empty);
}
message AuthInfo {
  string username = 1;
  string password = 2;
}

Есть сервис API, отвечающий за авторизацию. Его ручка принимает пароль пользователя. Бизнес сказал нам реализовать функцию, чтобы пользователь мог авторизовываться через почтовый ящик. Мы выкатили pull request и замержили:

syntax = "proto3";
...

service ServiceAPI {
  rpc Auth(AuthInfo) returns
(google.protobuf.Empty);
}
message AuthInfo {
  string username = 1;
  string password = 2;
}
message AuthInfo {
  oneof login {
    string username = 1;
    string email = 2;
  }
  string password = 3;
}

Но всё сломается, потому что нумерация полей парсится неправильно.

Сначала прилетает username и пароль от клиента. Username распарсится нормально, так как индекс совпадает. Затем пароль распарится во второй индекс. Потом прилетит e-mail и тоже попытается распарсится во второй индекс. В итоге мы в логах видели, что человек имел почтовый ящик в виде пароля, а пароль был пустой. Мы не могли никак его авторизовать. Это было тяжело дебажить, пока мы не заметили ситуацию и быстренько не откатили.

Версионирование плагинов

Также возможна такая ситуация: у меня стоит плагин генерации 1.28, а у коллеги — 1.26. И при перегенерации прото-контрактов возникает несколько сотен изменений:

Хотя в реальности эти изменения представляют собой изменения одной строчки в сгенерированном файле, просто меняется название версии. Это объективно всех бесит.

buf

Мы поговорили об основных проблемах toolset’а. Теперь рассмотрим, как решает эти проблемы один из современных инструментов мира Proto — buf.

В начале он представлял собой следующую экосистему:

  • lint

  • breaking checker

  • generate

  • pkg manager

  • formatter

Но сейчас он представляет собой уже полноценный линтер для протофайлов. Он может автоматически за вас проверять поломку обратной совместимости. Он же является и компилятором вместо Protoc. В Protobuf  реализовали собственный внутренний компилятор, он же представляет собственный пакетный менеджер, форматор. А сейчас ещё и альтернативу для работы с вебом. Вместо GRPC Gateway можно использовать их buf коннект. GRPC-хендлеры смогут автоматически работать с вебом.

Линтер

Давайте вспомним, что такое линтер:

  • инструмент анализа кода;

  • помощь в нахождении и исправлении ошибок;

  • выявление несоответствий стилей кодирования;

  • обнаружение подозрительных конструкций в коде;

  • единообразие.

Рассмотрим такой пример:

syntax = "proto3";

package api;

import 'google/protobuf/empty.proto';

service ServiceAPI {
  rpc Auth(AuthInfo) returns
(google.protobuf.Empty);
}

message AuthInfo {
  string username = 1;
  string password = 2;
}

И давайте натравим на него линтер:

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

→ Версионирование

В нашем примере мы не указали никакое версионирование. Исправить это можно вот так:

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

→ Документация

Следующие пункты касались уже документации. Она позволяет понять, что происходит в коде:

Так мы  поняли, почему, к примеру, АuthAPI не возвращал ничего в своём теле. Ведь в документации  описано, что он возвращает token в metadata. Мы описали кейсы по bad request и not found, потому что это бизнесовая фича. А когда расписали, тестировщики нас поблагодарили —  им стало гораздо проще с этим работать. И клиентским разработчикам стало гораздо проще интегрироваться.

Но есть интересный момент. У нас было тело возврата Google Protobuf empty, а стал AuthResponse. И если мы посмотрим, AuthResponse не содержит никаких полей. Это особенность линтера.

Если вы используете Google Protobuf empty, и, к примеру, в ручке авторизации решили возвращать тело пользователя, то полностью сломаете новую совместимость. Нужно делать полную ручку. Иначе на клиентской стороне попросту не смогут с этим работать. Единственный вариант — это создавать полностью пустое тело у мессенджа. Так рекомендует линтер и многие крупные компании. Потому что если вам нужно будет расширяться: добавить юзера, вы просто добавляете новое поле в тело. Это решит проблему.

Дополнительно, документирование упрощает взаимодействие и интеграцию с кодом.

Настройка

Настраивается линтер невероятно просто. Мы просто перечисляем все правила, которые нам нужны:

Их там несколько десятков, в реальности их можно все включить одной строчкой: правилом all.

Breaking change

Вот простой пример проверки поломки обратной совместимости:

buf breaking --against '.git#branch=main'

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

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

А вот это пример команды, как мы используем это в CI:

buf breaking --against \

"../../.git#branch=${CHANGE_TARGET},recurse_submodules
=true,depth=1,subdir=api/proto"

У нас есть возможность очень легко настраивать команду для проверки. К примеру, здесь в CHANGE_TARGET просто подсовывается ветка, в которую был отправлен pull request. Таким образом, автоматически поломать продакшн не получится.

Плагины

Сам protoc — чистый компилятор для языка описания интерфейсов Protocol Buffers (protobuf). Чтобы генерировать код под разные языки, фреймворки и прочее ему нужны плагины. К примеру, основные плагины для Go — это proto gen go и proto gen grpc. Они вдвоём генерируют нужный нам код для запуска GRPC-сервера и генерации GRPC-клиента.

Устанавливаются они достаточно легко, так как сами эти плагины чаще всего написаны на Go. Поэтому спокойно через go install можно поставить новую версию:

В свою очередь это превращается в следующее:

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

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

С каждым новым плагином или расширением конфигурации, мы будем получать огромную простыню текста. С одной стороны, можно закинуть всё в shell-скрипт и спокойно использовать, однако гораздо проще сделать так:

Мы указываем нужные нам плагины и версию, что позволяет решить проблему, при которой люди генерируют разными версиями. И объективно Yml-файл гораздо проще редактировать и настраивать, чем огромную bush-команду. В конфигурации всё передаётся достаточно просто через те же параметры.

Дополнительно buf имеет возможность удалённо генерировать ваш код, отправляя к себе на сервера. То есть, если вы используете локальный плагин, то генерируете и получаете код. А если используете удалённый код, то отправляете ваши protoc-контракты на сервера буфа, там генерируете это всё за счёт docker-контейнера. На выходе вы получаете нужные proto-файлы. Gроисходит это в GRPC. Всё добавляется легко и просто:

Самая интересная часть — это пакетный менеджер:

Всё, что мы должны сделать, это:

  1. прописать адрес до самого пакета;

  1. вызвать команду buf mod update.

Нужные пакеты скачиваются локально. Причём все популярные IDE: Visual Studio Code и JetBrain имеют плагин для работы с buf. Также автоматически идёт подсветка и настройки всего, что нужно. Вы просто дальше работаете с proto-файлами также хорошо, как работаете с Go.

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

Конфигурации

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

  • buf.work.yml — позволяет разделять отдельные окружения друг от друга и дополнительно расширять конфигурации нон-спейсами;

  • buf.yml — содержит конфигурации для правила линтинга, наименования и прочего;

  • buf.gen.yml — позволяет генерировать всё, что нужно.

Проблемы buf

Можно подумать, что buf — наш грааль. Он действительно решает огромное количество проблем, возникающих при работе с GRPC. Но одновременно с этим он завозит другие проблемы.

Декоммит

Почему-то ребята из buf решили сделать свой отдельный хостинг на домене buf.build. Закинули туда proto-контракты Google, валидации. Другие прото-контракты, которые хотите расшаривать как пакеты, также нужно заливать к ним на хостинг, а не на привычный Github или Gitlab.

Это создаёт ряд проблем, потому что этот Git-хостинг имеет API в виде proto-контракта, то есть всё передаётся в виде GRPC-месседжа. Получается, что коммита попросту нет. Они просто дропнули всё, что было.

Удалённая генерация

С одной стороны, это потрясающий инструмент, потому что вам локально больше не надо ничего ставить.  Вы просто ставите buf и спокойно генерируете всё, что нужно. Но одновременно с этим вы передаёте данные к ним на сервера, в том числе и прото-контракты. Они на своей стороне могут сделать с ними всё, что захотят, сгенерировать и выслать обратно.

С одной стороны можно сказать, что тот же Github имеет доступ ко всем данным, однако он хотя бы не заблокирован в РФ.

Блокировка в РФ и РБ

Это означает, что нам нельзя использовать пакетный менеджер, удалённую генерацию, registry. И это основная проблема с учётом удаления коммитов и с тем, что они могут получить доступ к нашим внутренним контрактам. Причём все, кто использует proto, знают, что часто GRPC используется для межсерверной коммуникации. Соответственно, мы предоставляем полный доступ нашей внутренней системе. Причём ещё с комментариями в коде, с описанием, как это работает. И это всё в условиях, когда нас могут в любой момент забанить и дропнуть наши данные.

Решение

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

VPN и proxy

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

У нас всё заработало, но мы поняли, что нужно генерировать прото-контракты в CI и через Go Get подтягивать их к библиотеке. Мы подошли к DevOps и сказали, что нам нужно просунуть прокси до сервиса в иностранную компанию, которая нас забанила. Они просто посмеялись и отказались это делать, потому что ситуация комичная. Поэтому этот вариант пришлось отмести.

Prototool

Это альтернативный buf инструмент, который обладает тем же самым функционалом — только местами хуже. Но есть одна большая проблема: они ещё в 2018-2019 годах, когда buf только зарождался, стали сами рекомендовать посмотреть в его сторону, а где-то в марте 2022 полностью заблокировали свой репозиторий в Github.

easyp

Тогда мы стали сидеть и думать по поводу написания велосипеда: своей инструменты  с нуля. Плюс в том, что можно его адаптировать под свою компанию и, если выйдет успешно, заопенсорсить. Но хоть мы и любим писать всё с нуля, это большой объём работы, а ресурсы на разработку ограничены. Кроме того все в команде и так нагружены своими задачами. Тем не менее мы стали копать в эту сторону. Мы залезли в исходники buf и стали смотреть, как он устроен.

Во-первых, нас удивило, что они используют большое количество велосипедов внутри: большие собственные библиотеки и технологии. С этим всё равно можно было жить, поэтому мы задумались над вариантом форка. Но в таком случае, нам бы пришлось обязать как минимум всех в нашей компании переезжать на наш форк, начать его использовать. Если это произойдёт успешно, то уговаривать остальных. Этим нам заниматься не хотелось.

Мы продолжили думать и заметили исходники proto. У них там чуть ли не сотни контрактов. У нас появилась идея создать обратно совместимый сервер, который будет работать с их CLI притворяться им же. Мы взяли proto-контракты и начали с ними экспериментировать. Первое, что стали делать, это реверс сервера:

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

Экспериментировали и с Google API — поэтапно разобрались, что при подтягивании пакетов Google API использует три сервиса. В первую очередь, подтягивает информацию о том, где находится сам пакет. Однако это, оказывается, адрес не для самого пакета, а для резолвера, который получает адрес и узнаёт, где реально находится пакет. CLI с этой информацией идёт в репозиторий сервиса, подтягивает оттуда метаинформацию о репозитории: кто владелец, есть ли доступ и прочее. Мы со всем этим разобрались, тоже получили доступ.

Последний шаг — самый сложный. Нам нужно было из нашего CLI пойти в download-services и скачать proto-контракты. Там было всё зашифровано, поэтому пришлось с этим разбираться, проверять хэш-суммы, их CLI и повторять их алгоритм.

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

Дальше мы стали задумываться, что с этим делать.

→ Git-Hosting

Первый вариант — реализовать Git-hosting, как сделал это buf. Но компании в этом случае придётся затягивать его к себе вместе с Gitlab. Мы решили, что это плохой вариант. Тем более, что мы не хотели наступать на те же  грабли, на которые наступил buf — речь про полный vendor-locking в нашем софте.

→ Git Proxy

Мы получали запрос в proto-формате и перенаправили его на Git-сервер. А полученные пакеты возвращали обратно клиенту в нужном формате для buf через proto. Всё заработало. Но, если proto-validate весит несколько мегабайт, с этим можно как-то работать, то Google AP занял у нас 1,5 гигобайта оперативки. Это достаточно большое количество крупных пакетов, с которыми не так легко работать.

→ Git proxy to disk

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

Дальше мы точно так же делаем buf mod update, и всё работает.

Вам не придётся заменять CLI и вообще что-либо. Просто меняйте url. Важная деталь: вам нужно очистить кэш. Если вы не очистите буфовский кэш, у вас это не заработает.

Заключение

В целом мы сохранили CLI-функционал. Вы можете так же использовать buf, как и раньше. Кроме того, мы решили самую важную для нас проблему с пакетным менеджером. Теперь его можно спокойно использовать в рамках РФ и Республики Беларусь.

Сейчас у нас версия 0.1.0. Пока он поддерживает только package message support, то есть пакетный менеджер. Но следующим шагом — 0.2.0 — мы будем поддерживать уже удаленную генерацию (remote generation support).

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

Кодовую базу можете посмотреть здесь. Будем рады любым pullrequests и issue.


Вы можете посмотреть мой выступление по этой теме на Golang Conf:

Tags:
Hubs:
Total votes 19: ↑17 and ↓2+17
Comments11

Articles

Information

Website
www.ontico.ru
Registered
Founded
Employees
11–30 employees
Location
Россия