Два сервиса обновляются в разное время: как менять protobuf без потери смысла

В сервисе доставки появился новый формат адреса. Разработчик меняет поле в proto-файле, генерирует Go-код, запускает тесты и выпускает сервер. Старый клиент продолжает работать ещё несколько дней. В одиночном репозитории всё выглядит согласованным, но в системе одновременно живут два понимания одного сообщения. Поэтому совместимость схемы нужно обсуждать как переход между состояниями, а не как успешный запуск генератора.

За полем стоят старые сообщения и старые читатели

Допустим, старый запрос содержал address одной строкой. Новый сервер хочет получать отдельные city и street, чтобы рассчитывать доступность доставки. Простая замена одного поля двумя оставляет открытым вопрос: откуда новый сервер возьмёт город у старого клиента? Даже если сообщение успешно декодируется, бизнес-операция может перестать выполняться. Совместимость байтов и совместимость поведения отвечают на разные вопросы.

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

Добавление рядом с прежним контрактом

syntax = "proto3";
package shipping.v1;

message CreateDeliveryRequest {
  string address = 1 [deprecated = true];
  optional string city = 2;
  optional string street = 3;
}

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

Для новых строк здесь выбрано optional: приложению важно отличать отсутствие поля от переданной пустой строки. Правило выбора между форматами нужно записать в контракте. Например, новые поля имеют приоритет только когда присутствуют оба; смешанный ввод отклоняется. Без этого разные языки и разные команды могут реализовать несовпадающую логику поверх одной схемы.

Выпуск в несколько шагов

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

Удалённый номер нельзя отдавать новому смыслу. Следующая схема после окончания поддержки может выглядеть так:

message CreateDeliveryRequest {
  reserved 1;
  reserved "address";
  optional string city = 2;
  optional string street = 3;
}

Резервирование делает намерение проверяемым при будущих изменениях. Номера — часть бинарного контракта. Имена также имеют значение при работе с JSON-представлением; его правила отличаются от бинарного обмена. Если ваш gRPC-сервис доступен через JSON-шлюз, нужно отдельно проверить фактический маршрут сериализации. Перед выпуском проверьте совместимость изменения полей для используемого формата обмена.

Какая проверка обнаружит проблему до выпуска

Сохраните реальные по форме примеры старых сообщений с обезличенными данными. Пусть новый сервер прочитает их и выполнит выбранную бизнес-ветку. Второй тест отправляет новое сообщение старому читателю и проверяет допустимое поведение, если такая комбинация возможна при развёртывании. Отдельный тест проверяет частичный ввод city без street. Компиляция сгенерированного кода не заменяет эти сценарии: компилятор не знает, от какого адреса служба обязана принять заказ.

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

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

Материал о переходе от Go API к микросервисам помогает связать контракт с отказами между процессами. Если рядом используется JSON, отдельно проверьте сохранение больших идентификаторов в Go: совместимость схемы не устраняет потери при другом формате передачи.

Научиться выпускать сервисы независимо

Платный курс «Микросервисы на Go» включает контракты HTTP и gRPC, границы сервисов и планирование совместимого выпуска. Затем вы разберёте, что меняется при добавлении PostgreSQL и Kafka, как читать сценарии отказов и восстанавливать обработку. Это следующий шаг для разработчика, у которого одна версия API уже работает, а несколько версий должны сосуществовать. Откройте программу курса и переходите от локального запроса к взаимодействию процессов.

Продолжить на практике

  • Микросервисы на Go: HTTP, PostgreSQL, gRPC, Kafka и Docker ↗

    От работающего Go API — к взаимодействию сервисов, которому можно доверять. Разберитесь, как проектировать контракты, сохранять согласованность данных и восстанавливать обработку после сбоев. HTTP, PostgreSQL, gRPC, Kafka и Docker — через код и наглядные разборы.