gRPC

Задача

Спроектировать сервис со следующими свойствами:

  • обмен между фронтендом и прикладной логикой может осуществляеться через шину
  • доступный извне АПИ и структура обмена данными документируются программно
  • клиенты могут работать по протоколам SOAP, JSON, Websockets
  • сервис поддерживает метрики (prometheus) и трассировку (jaeger)

Технические особенности решения

  • Формат описания сервиса: protobuf
  • Шина: nats-streaming
  • Базовый протокол взаимодействия: gRPC

Структура сервиса

Структура сервиса

Компоненты сервиса

Внешние интерфейсы

  • HTML Frontend - страницы вебсайта
  • JSON API / Swagger - доступ к АПИ для собственного и клиентского javascript, см. grpc-gateway
  • WebSocket - работа в режиме “1 соединение - 1 запрос” позволяет использовать документацию JSON API, см. также - grpc-websocket-proxy
  • SOAP - gRPC proxy + генерация WSDL, см. soap-proxy

Генерируемый код

  • JSON proxy - конвертация запросов JSON в gRPC
  • WS proxy - конвертирует (на лету) запросы по протоколу ws в JSON API
  • SOAP proxy - WSDL + конвертация SOAP-запросов в запросы gRPC
  • gRPC Server - сервис получает gRPC запросы и передает их в соответствующий gRPC handler
  • MQ Client - заменяет gRPC handler и передает запросы в MQ
  • MQ handler - получает запросы из MQ и передает их в соответствующий gRPC handler
  • [docs.proto.html] - документация по файлу main.proto, см. protoc-gen-doc

Используемое ПО

  • PG - Postgresql - СУБД, которая неизбежно будет использоваться как минимум до миграции всего старого кода
  • MQ (RPC, pub/sub) - шина с поддержкой RPC и pub/sub, для которой есть библиотеки интеграции с gRPC, например NATS и AMQP. Я начинал с Rabbit, но в конфигурации по умолчанию он даже при бездействии давал избыточную нагрузку на CPU, поэтому дальнейшая разработка велась с nats-streaming. Однако, даже в первом приближении есть альтернативы
  • Trace server (Jaeger) - тут не будет привязки к конкретной реализации, т.к. jaeger использует opentracing

Универсальный код собственной разработки

  • Template renderer - формирование html из шаблонов с возможностью вызова методов gRPC
  • PG NOTIFY -> gRPC Message proxy - конверсия событий PG по заданному .proto и передача (publish) их в NATS

Код, реализующий конкретный сервис

  • service.proto - спецификация конкретного сервиса, для сервисов, основанных на прикладной логике в хранимом коде БД, в перспективе может генерироваться по описаниям функций Postgresql
  • [Templates] - HTML-разметка страниц web-интерфейса с данными, полученными из вызовов gRPC-методов
  • gRPC Handler - прикладная логика сервиса
  • PG client - методы, вызывающие хранимый код, в перспективе могут генерироваться по описаниям функций Postgresql

Пилотная версия

Комментарии к выбранным технологиям

RPC

Главная цель проектируемого решения - избавиться от устаревших (CGI / SUXX / mod_perl) технологий и построить работу на новом стеке, чтобы можно было переделывать подсистемы в предсказуемые сроки (без реверс-инжиниринга) и набирать под этот стек персонал. Один из трех используемых в текущей системе фреймворков (PGWS, в продакшене с 2010г) основан на RPC. Если мы перенесем на RPC остальной код, цель уже можно считать, в целом, достигнутой. Дополнительно, новое решение, будучи близким к PGWS, позволит использовать уже проверенные практикой решения с минимальными изменениями.

Protobuf

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

Выбор этого протокола для межсервисного обмена позволяет

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

gRPC

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

MQ-RPC

  • Взаимодействие между фронтендом и прикладной логикой через шину позволяет в процессе эксплуатации без участия разработчиков менять соотношение “фронтенды:обработчики” для нагруженных сервисов.
  • Реализация MQ-клиента с сигнатурой gRPC-обработчика позволяет, для ненагруженных сервисов, убрать MQ из цепочки и вызывать gRPC-обработчик непосредственно из gRPC-сервера (красная стрелка “Вариант без MQ RPC” на схеме). Т.е. такой вариант использования шины не добавляет ее в число зависимостей.

Gateway (Hub) и масштабирование

Вопросы маршрутизации и масштабирования сервисов не требуют проработки на первом этапе, т.к. планируется иметь несколько точек масштабирования (далее - ТМ) и будет использовано стороннее ПО. Первично ситуация видится такой:

  • Каждый сервис (версия сервиса) обслуживает некоторый адрес ://host/prefix
  • Клиенты обращаются по этому адресу по протоколу http/https/ws[/gRPC]
  • (ТМ-1) Маппинг запроса на исполнителя (или пул исполнителей) осуществляется сторонним ПО (nginx/traefik/HAproxy)
  • (ТМ-2) Отправленный в MQ запрос может попадать к одному из нескольких обработчиков, при этом один это будет сервер или несколько - определяется функционалом MQ
  • (ТМ-3) Отправленный в БД запрос может направляться к одному из шардов (одной из реплик) БД

Swagger (openapi)

Выбран потому, что это - один из стандартных способов построения интерфейсов для HTTP клиентов (любых, не только javascript) и позволяет встроить вызов АПИ даже без использования каких-либо фреймворков (пример вызова: curl -X POST http://localhost:8081/v1/sample/ping)

По аналогичным причинам не был выбран Google gRPC-Web - на клиенте необходима js-библиотека и на сервере - envoy/nginx. В прототипе этапа 1 клиентский код работы через websockets - 89 строк без зависимостей.

SOAP

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

Trace server (Jaeger)

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

Связанность сервисов

Если сервис А использует функционал сервиса Б, эта связь может быть реализована одним из способов:

  1. Генерация через шину - программная генерация клиентской (А) и серверной (Б) части для отправки в шину сообщений заданного protobuf формата и прием сообщений в этом формате
  2. Примитив через шину - п.1. только без автогенерации - ручное формирование и парсинг сообщений через шину сервисом А
  3. Генерация gRPC - обмен через gRPC посредством программно сгенерированных по .proto клиента (А) и сервера (Б) на заданном языке
  4. Генерация JSON - обмен через HTTP/JSON посредством программно сгенерированных по .proto клиента (А) и сервера (Б) на заданном языке
  5. Примитив через JSON - ручное формирование и парсинг JSON сообщений сервисом А

Для обмена между микросервисами можно использовать любой протокол из списка

  • MQ
  • gRPC
  • JSON / Websockets
  • SOAP

Перспективы

На основе проекта pgmig можно построить решение, которое запросит в БД описания хранимых функций и по ним сгенерит файл .proto и pg-клиента для сервиса предложенной архитектуры. В результате разработка сервисов будет аналогична PGWS и сведется к

  • созданию пакета в БД
  • верстке страницы и ее js-коду

Это позволит перенести в новую архитектуру экспертизу текущего проекта.

Вопросы и ответы

1. Не выйдет ли по данному архитектурному решению так, что во главе всего станет база или какой-то из микросервисов, который будет диктовать "правила игры" другим микросервисам?
Нет, Правила игры - это формат обмена между сервисами. Варианты обмена перечислены в п. "Связанность сервисов"
2. При таком подходе - возможно ли разработка отдельных микросервисов разными командами, на разных технологиях?
Да. См. п. "Связанность сервисов"
3. Как продолжение предыдущего вопроса - по технике - смогут ли решения от этих разных команд отгружаться на бой в разное время, в разном объеме независимо друг от друга?
Да, кроме прочего, protobuf позволяет добавления в структуры новых версий сервера без изменения (и перекомпиляции) клиентов. Однако, может иметь смысл перед релизом сервиса протестировать его внутренних клиентов. Это включает задачу "определить всех внутренних клиентов заданного сервиса". В предлагаемой архитектуре эта задача сводится к определению сервисов, использующих файл .proto заданного сервиса.