Задача
Спроектировать сервис со следующими свойствами:
- обмен между фронтендом и прикладной логикой может осуществляеться через шину
- доступный извне АПИ и структура обмена данными документируются программно
- клиенты могут работать по протоколам 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, и логично реализовывать их поддержку с самого начала.
Связанность сервисов
Если сервис А использует функционал сервиса Б, эта связь может быть реализована одним из способов:
- Генерация через шину - программная генерация клиентской (А) и серверной (Б) части для отправки в шину сообщений заданного protobuf формата и прием сообщений в этом формате
- Примитив через шину - п.1. только без автогенерации - ручное формирование и парсинг сообщений через шину сервисом А
- Генерация gRPC - обмен через gRPC посредством программно сгенерированных по .proto клиента (А) и сервера (Б) на заданном языке
- Генерация JSON - обмен через HTTP/JSON посредством программно сгенерированных по .proto клиента (А) и сервера (Б) на заданном языке
- Примитив через JSON - ручное формирование и парсинг JSON сообщений сервисом А
Для обмена между микросервисами можно использовать любой протокол из списка
- MQ
- gRPC
- JSON / Websockets
- SOAP
Перспективы
На основе проекта pgmig можно построить решение, которое запросит в БД описания хранимых функций и по ним сгенерит файл .proto и pg-клиента для сервиса предложенной архитектуры. В результате разработка сервисов будет аналогична PGWS и сведется к
- созданию пакета в БД
- верстке страницы и ее js-коду
Это позволит перенести в новую архитектуру экспертизу текущего проекта.
Вопросы и ответы
1. Не выйдет ли по данному архитектурному решению так, что во главе всего станет база или какой-то из микросервисов, который будет диктовать "правила игры" другим микросервисам?
Нет, Правила игры - это формат обмена между сервисами. Варианты обмена перечислены в п. "Связанность сервисов"
2. При таком подходе - возможно ли разработка отдельных микросервисов разными командами, на разных технологиях?
Да. См. п. "Связанность сервисов"
3. Как продолжение предыдущего вопроса - по технике - смогут ли решения от этих разных команд отгружаться на бой в разное время, в разном объеме независимо друг от друга?
Да, кроме прочего, protobuf позволяет добавления в структуры новых версий сервера без изменения (и перекомпиляции) клиентов. Однако, может иметь смысл перед релизом сервиса протестировать его внутренних клиентов. Это включает задачу "определить всех внутренних клиентов заданного сервиса". В предлагаемой архитектуре эта задача сводится к определению сервисов, использующих файл .proto заданного сервиса.