gRPC или REST: клиенты, контракт и выгрузка данных — это выбор между двумя контрактами для двух разных потребителей одного сервиса телеметрии. Браузерной панели нужен последний замер одного датчика через обычный GET и JSON. Внутреннему обработчику истории нужна ограниченная выборка измерений несколькими сообщениями с явной границей последовательности. Ниже — полный контракт, правило выборки и проверка результата.
Исходные данные и правило выборки
Учебный сервис телеметрии мастерской хранит четыре измерения. Имена, записи и границы сочинены для примера.
| seq | sensor_id | temp_milli_c |
|---|---|---|
| 41 | S-17 | 22800 |
| 42 | S-17 | 22900 |
| 43 | S-18 | 25100 |
| 44 | S-17 | 23125 |
temp_milli_c — целые тысячные градуса Цельсия в нашей модели. seq уникален в этом наборе.
Правило выборки для внутреннего обработчика: совпадение sensor_id и строгое неравенство after_seq < seq <= through_seq, порядок по возрастанию seq. Поток заканчивается после последней подходящей записи. Это ограниченная выгрузка.
gRPC сам это правило не реализует: нужен код сервиса и согласованное хранилище. Транспорт передаёт сообщения, а фильтрацию по датчику и границам выполняет приложение.
Два потребителя:
- A — браузерная панель. Нужен последний замер одного датчика через обычный GET и JSON.
- B — внутренний Go/Python-обработчик истории. Получает ограниченную выборку измерений несколькими сообщениями, оба конца контролируются, нужна явно заданная граница последовательности.
Требований по гарантированной скорости нет. Выбор опирается на контракт, клиентов и семантику выгрузки.
Полный telemetry.proto
Контракт для B описан в proto-файле. Все входные и выходные типы определены. Схема не является готовой реализацией сервера.
syntax = "proto3";
package workshop.telemetry;
service TelemetryService {
rpc ReadLatest (SensorRequest) returns (Sample);
rpc ExportSamples (ExportRequest) returns (stream Sample);
}
message SensorRequest { string sensor_id = 1; }
message ExportRequest {
string sensor_id = 1;
uint64 after_seq = 2;
uint64 through_seq = 3;
}
message Sample {
string sensor_id = 1;
uint64 seq = 2;
sint32 temp_milli_c = 3;
}
Два RPC: ReadLatest возвращает один Sample, ExportSamples возвращает поток Sample. В документации gRPC на grpc.io серверный поток описан так: сервер возвращает поток сообщений в ответ на запрос клиента, после отправки всех сообщений передаёт статус и завершает обработку. Клиент завершается, получив все сообщения сервера.
Protobuf — выбранный формат по умолчанию в этом нативном gRPC-маршруте. В документации gRPC на grpc.io указано, что по умолчанию gRPC использует protocol buffers как язык описания интерфейса и как формат обмена сообщениями. API-данные ниже записаны в нормализованной читательской форме и не изображают Protobuf wire bytes.
Порядок сообщений внутри одного RPC gRPC гарантирует. Это свойство транспорта, и оно не заменяет прикладную фильтрацию.
OpenAPI paths-фрагмент для панели
Для A достаточно одного GET. Ниже — фрагмент paths.
paths:
/v1/sensors/{sensorId}/latest:
get:
operationId: readLatestSample
parameters:
- name: sensorId
in: path
required: true
schema: {type: string}
responses:
'200':
description: Latest sample in the selected dataset
content:
application/json:
schema:
type: object
required: [sensor_id, seq, temp_milli_c]
properties:
sensor_id: {type: string}
seq: {type: integer}
temp_milli_c: {type: integer}
'404':
description: No sample for this sensor
Для GET /v1/sensors/S-17/latest ручной эталон: 200 и {"sensor_id":"S-17","seq":44,"temp_milli_c":23125}. Для неизвестного S-99 в нашем REST-контракте 404.
В спецификации OpenAPI 3.1 на spec.openapis.org параметр пути помечается required: true, а ответы описываются в объекте responses. Схема не является готовым сервером: обработчик, хранилище и политику кэша пишет команда.
Политика HTTP-кэша задаётся отдельно по допустимой давности, заголовкам и доступу. Сам GET не гарантирует актуальность измерения.
Карта выбора A/B
Карта объединяет восемь критериев сравнения и отдельную строку итогового выбора.
| Поле | A: браузерная панель | B: внутренняя ограниченная выгрузка |
|---|---|---|
| Контракт | Один REST GET и JSON | ReadLatest/ExportSamples из telemetry.proto |
| Выбранный транспорт | Обычный HTTP для REST | Нативный gRPC HTTP/2 в выбранной архитектуре |
| Сообщения | JSON-объект последнего замера | Protobuf Sample, каждое сообщение имеет seq |
| Ответ | Один объект GET в этом сценарии | server streaming конечной выборки |
| Кэш/повтор | HTTP-политика отдельно; проверить давность origin | Повтор по after_seq только по нашей прикладной семантике, gRPC не хранит курсор |
| Распределение нагрузки | Выбранная инфраструктура HTTP | Resolver и поддерживаемая клиентская политика, например round_robin; L7-прокси как вариант |
| Отладка | DevTools/curl и данный контракт | Совместимый gRPC-инструмент со схемой; reflection отдельно включается при необходимости |
| Эволюция | Согласовать поля REST-представления | Сохранить номера Sample; удалённые номера/имена резервировать |
| Первый кандидат | REST | gRPC |
Клиентская балансировка gRPC существует. В документации gRPC на grpc.io описано, что политика получает список IP-адресов от resolver и отвечает за соединения к серверам. По умолчанию используется pick_first, который не распределяет обращения по всем адресам, а берёт первый доступный. Политику round_robin можно включить через service config; набор политик зависит от языка.
L7-прокси — вариант. Если B ходит к разным backend, одинаковый запрос должен опираться на согласованный набор данных; балансировщик не создаёт такую согласованность сам.
Точные результаты выборки
Для ExportSamples(sensor_id=S-17, after_seq=41, through_seq=44) ручной эталон — два сообщения: (S-17,42,22900), затем (S-17,44,23125), затем завершение.
Запись 43 не проходит фильтр датчика, хотя попадает в числовой интервал: у неё sensor_id=S-18. Запись 41 исключена строгой нижней границей.
| Запрос | Результат |
|---|---|
| after_seq=41, through_seq=44 | [42, 44] |
| after_seq=42, through_seq=44 | [44] |
| after_seq=44, through_seq=44 | [] |
Повтор с after_seq=42 даёт [44], с after_seq=44 даёт пустую завершённую выборку. Если 43 попал в S-17 или 41 повторился, неверна прикладная фильтрация.
through_seq фиксирует верхнюю границу. Автоматическую транзакционную snapshot-изоляцию и надёжный replay из факта наличия gRPC не выводим: сохранение курсора и возобновление после сбоя требуют отдельной реализации.
Конечный поток и прикладной курсор
Серверный конечный поток завершается после последней подходящей записи. Бесконечная подписка — другой сценарий: она держит соединение открытым и ждёт новые измерения. В нашем примере B получает ограниченную выгрузку и завершение.
Прикладной курсор живёт в коде сервиса. gRPC не хранит значение after_seq между вызовами. Клиент сам решает, с какого значения продолжить, и передаёт его в следующем запросе.
В документации gRPC на grpc.io описаны четыре вида методов: unary, server streaming, client streaming и bidirectional streaming. Server streaming подходит для конечной выборки: клиент читает из потока, пока сообщения не закончатся.
Другие HTTP API тоже могут передавать потоки. Streaming — не исключительная возможность gRPC, а общий приём, реализуемый разными транспортами.
Эволюция контракта и reserved
При изменении Sample удаляем temp_milli_c=3 и в следующем варианте того же сообщения резервируем номер 3 и имя temp_milli_c.
reserved 3;
reserved "temp_milli_c";
string sensor_id = 1;
uint64 seq = 2;
Пример относится только к изменяемому телу и хранится отдельно от рабочего файла. Номер 3 новому полю не отдаём. seq=2 остаётся идентификатором порядка.
В руководстве proto3 на protobuf.dev указано: номера полей уникальны внутри сообщения, диапазон 19000–19999 зарезервирован реализацией, а удалённый номер добавляют в список reserved, чтобы будущие разработчики не переиспользовали его. Там же отмечено, что резервирование имени защищает JSON- и TextFormat-кодировки.
Проверка: после правки protoc должен принять файл, а seq=2 сохранить роль идентификатора порядка. Компиляцию в этом примере не заявляем.
Нативный gRPC, gRPC-Web и другие HTTP API
Нативный браузерный API отличается от gRPC-Web. В README проекта gRPC-Web на GitHub описано, что клиенты gRPC-Web подключаются к gRPC-сервисам через специальный прокси, по умолчанию Envoy. Там же указано, что gRPC-Web поддерживает unary и server-side streaming, причём server streaming работает в режиме grpcwebtext; client-side и bidirectional streaming не поддерживаются.
Браузер не объявляем неспособным работать с gRPC-Web. Для A уже задан обычный GET, и это основание первого выбора.
Выбор A соответствует обычному GET/JSON и своей политике кэша. Выбор B — полному контракту, поддерживаемому HTTP/2 gRPC и конечному потоку. Обязательный L7 не требуется, а производительность по имени протокола не обещаем. При нескольких backend отдельно сверяем одинаковый результат по этому набору на каждом backend; общий cursor store gRPC не предоставляет.
Как читать проверку и что делать при расхождении
Проверку удобно вести от ожидаемого результата к причине расхождения. Сначала зафиксируйте, какой именно набор должен вернуться для пары границ, затем сравните с ним фактический ответ и только после этого ищите место, где выборка отклонилась от правила. Такой порядок отделяет ошибку контракта от ошибки данных: если типы и номера полей совпадают со схемой, а набор записей всё равно неверен, причина почти всегда в прикладном фильтре или в хранилище.
Для диапазона after_seq=41, through_seq=44 ожидается последовательность [42, 44]. Если в ответе появилась запись 43, фильтрация по sensor_id дала неверный результат. Проверьте условие совпадения датчика в коде сервиса. Если появилась запись 41, значит нижняя граница трактуется как нестрогая. Оба случая указывают на код сервиса.
Повторный запрос с after_seq=42 служит отдельной проверкой курсора. Ожидается [44]; пустой ответ здесь означает, что курсор сдвинулся дальше, чем нужно, либо верхняя граница была сужена. Запрос с after_seq=44 при той же верхней границе должен вернуть пустую завершённую выборку — это корректный результат.
При нескольких backend сверяйте один и тот же диапазон на каждом из них. При расхождении сравните наборы данных и реализацию фильтра на каждом backend; согласованность хранилища обеспечивает команда.
Если расхождение найдено, действие читателя — вернуться к правилу выборки, проверить порядок условий фильтра и только затем пересматривать контракт. Менять схему сообщений при верном наборе записей не требуется.
Частые вопросы
Чем gRPC отличается от REST в этом примере?
REST обслуживает браузерную панель одним GET и JSON. gRPC обслуживает внутренний обработчик потоком Protobuf-сообщений по контракту telemetry.proto. Различие лежит в контракте и клиентах.
Почему запись 43 не попала в выборку S-17?
У записи 43 sensor_id=S-18, поэтому фильтр по датчику её исключает, хотя seq=43 попадает в числовой интервал 41–44. Проверка границ и фильтра датчика выполняется приложением.
Что даёт повторный запрос с after_seq=42?
Повтор с after_seq=42 и through_seq=44 возвращает только запись 44. Запись 42 исключена строгой нижней границей. Это позволяет продолжить выгрузку с последнего полученного значения.
Зачем резервировать номер 3 при удалении temp_milli_c?
Резервирование номера и имени защищает от переиспользования: будущий разработчик не займёт номер 3 под другое поле. В руководстве proto3 на protobuf.dev это описано как обязательная практика при удалении поля.
Можно ли вызвать gRPC напрямую из браузера?
Нативный браузерный API отличается от gRPC-Web. gRPC-Web работает через прокси, по умолчанию Envoy, и поддерживает unary и server streaming в режиме grpcwebtext. Для панели в этом примере выбран обычный GET.
