Найти тему
Mad Devs

Гарантированная доставка по gRPC стримам

Оглавление

Вот раньше сидишь себе, кодишь какой-нибудь монолит на PHP, деплоишь его по FTP и было счастье. А потом люди придумали всякое разделение, потому что с ростом нагрузки сервера не справлялись и нужны были варианты для масштабирования. Так появились всякие Event sourcing, CQRS, SOA и тому подобные. И вот у тебя уже требования немного повысились и нужно знать еще про сети, про событийную целостность, про CAP теорему и прочие вещи.

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

А еще придумали SOAP, REST для API. В общем-то прогресс не стоял на месте и сейчас у нас для старта проекта есть несколько архитектурных паттернов, вроде SOA/CQRS и, прости господи, микросервисы, которые в последнее время очень популярны.

А для организации API у нас есть три подхода. RPC/REST/GraphQL. И вроде бы выбор достаточно понятен. RPC у нас для API, где мы можем писать свои методы и называть так, как удобно нам, при этом ответы более или менее стандартизированы. REST — у нас для всего того, что можно описать существительными, а действия над ними методами HTTP. А GraphQL — это некая попытка взять и сделать что-то вроде SQL поверх HTTP с одним эндпоинтом, отсылая какие-то запросы в жсоне и получая ответы обратно.

Что выбрать — не понятно. Ну и при всем изобилии подходов пришел такой Google, со словами, вы не верно понимаете RPC и сделал gRPC. И на первый взгляд все получилось у них хорошо, вместо XML протоколбуфера, работа поверх HTTP/2, но, отсутствует поддержка в браузерах, которую не страшно тащить в прод. Отсюда мы приходим к тому, что применение его сейчас — это либо микросервисы, либо мобильные приложения.

При этом, на gRPC можно строить вполне себе хорошие API, которые умеют в REST. В мобильных приложениях оно чувствует себя вполне хорошо, однако, в микросервисах у нас возникают некоторые сложности выбора.

В микросервисах мы можем строить коммуникацию как синхронно, так и асинхронно. Синхронно — это чаще всего REST, а когда нам нужно асинхронности, мы втаскиваем NATS/Kafka или другой MQ.

А что делать, если у нас такой случай, что втаскивать NATS — это оверкилл, не говоря уже о Kafka, а асинхронное взаимодействие с гарантиями нам сделать нужно?

Асинхронное взаимодействие между микросервисами без MQ

И вот, в очередном проекте у нас возникла такая ситуация. MQ втаскивать — оверкилл, а асинхронное взаимодействие нужно. Да еще и с гарантиями и не нарушая порядок доставки.

По архитектуре получилось следующее:

-2

Функции ядра просты. Мы предоставляем внешнюю API для работы с задачами. Мы сохраняем полученные задачи в БД, передаем их на обработку и сохраняем обработанные результаты.

Процессор еще проще. То, что получили от Ядра, мы должны обработать по разным механизмам и отослать обратно.

В первой реализации мы сделали взаимодействие по обычным однонаправленным стримам. На ядре был сделан метод AddEvent, который принимал события, писал в Postgres и закидывал в гошный канал.

-3

Гошный канал мы использовали дальше при передаче ивентов в процессор. Процессор поднимает стрим до ядра, получает данные из Postgres и дальше получает то, что приходит в канал

-4

Гарантии доставки

-5

Когда мы строим распределенную систему (у нас всегда распределенная система, если у нас есть больше одного сервиса, которые общаются между собой), то у нас может возникнуть множество проблем

Когда мы собираемся гарантировать доставку сообщений с одного сервиса на другой, то мы не можем положиться на сеть, потому что сеть ненадежна и порядок доставки сообщений в ней не гарантирован, так еще и сообщения могут теряться и дублироваться.

Когда мы собираемся строить гарантии на уровне приложения, то приходится решать проблемы сети с помощью алгоритмов внутри приложения.

  • Если мы не доставили сообщение — его нужно переотправить
  • Если мы переотправляем сообщение — то возможны дубликаты

А еще нам нужно определиться со способом доставки сообщений. Сейчас есть минимум два вида доставки — это at least once delivery и at most once delivery.

at least once delivery — доставка сообщений хотя бы до одной ноды в сети. Сообщения мы можем потерять при этом методе доставки

at most once delivery — обеспечит доставку до всех нод, но при этом могут быть дубликаты сообщений.

Для реализации любого из механизмов доставки нам нужен протокол. После небольших раздумий, родилось следующее:

-6

Этим протоколом мы покрываем реализацию at most once delivery, но для нашего случая хватает at least once, потому что сервисы запущены по одной копии на сервис, однако, дубликаты в системе еще никто не отменял.

Для проверки на дубликаты на стороне процессора была сделана обычная мапа с мутексом. А со стороны ядра мы отсылаем сообщения под ретраером с экспоненциальным бэкоффом.

Заключение

Хоть я и не любитель велосипедов и своих решений, но иногда свое решение можно сделать достаточно быстро и оно даже будет работать. А если вы выбрали gRPC в качестве инструмента для коммуникации между микросервисами, то вы можете достаточно долго жить без Кафки и не знать особых проблем, например с балансировкой. Но об этом мы поговорим в следующей статье :)

Ранее статья была опубликована тут.