Вот раньше сидишь себе, кодишь какой-нибудь монолит на 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 втаскивать — оверкилл, а асинхронное взаимодействие нужно. Да еще и с гарантиями и не нарушая порядок доставки.
По архитектуре получилось следующее:
Функции ядра просты. Мы предоставляем внешнюю API для работы с задачами. Мы сохраняем полученные задачи в БД, передаем их на обработку и сохраняем обработанные результаты.
Процессор еще проще. То, что получили от Ядра, мы должны обработать по разным механизмам и отослать обратно.
В первой реализации мы сделали взаимодействие по обычным однонаправленным стримам. На ядре был сделан метод AddEvent, который принимал события, писал в Postgres и закидывал в гошный канал.
Гошный канал мы использовали дальше при передаче ивентов в процессор. Процессор поднимает стрим до ядра, получает данные из Postgres и дальше получает то, что приходит в канал
Гарантии доставки
Когда мы строим распределенную систему (у нас всегда распределенная система, если у нас есть больше одного сервиса, которые общаются между собой), то у нас может возникнуть множество проблем
Когда мы собираемся гарантировать доставку сообщений с одного сервиса на другой, то мы не можем положиться на сеть, потому что сеть ненадежна и порядок доставки сообщений в ней не гарантирован, так еще и сообщения могут теряться и дублироваться.
Когда мы собираемся строить гарантии на уровне приложения, то приходится решать проблемы сети с помощью алгоритмов внутри приложения.
- Если мы не доставили сообщение — его нужно переотправить
- Если мы переотправляем сообщение — то возможны дубликаты
А еще нам нужно определиться со способом доставки сообщений. Сейчас есть минимум два вида доставки — это at least once delivery и at most once delivery.
at least once delivery — доставка сообщений хотя бы до одной ноды в сети. Сообщения мы можем потерять при этом методе доставки
at most once delivery — обеспечит доставку до всех нод, но при этом могут быть дубликаты сообщений.
Для реализации любого из механизмов доставки нам нужен протокол. После небольших раздумий, родилось следующее:
Этим протоколом мы покрываем реализацию at most once delivery, но для нашего случая хватает at least once, потому что сервисы запущены по одной копии на сервис, однако, дубликаты в системе еще никто не отменял.
Для проверки на дубликаты на стороне процессора была сделана обычная мапа с мутексом. А со стороны ядра мы отсылаем сообщения под ретраером с экспоненциальным бэкоффом.
Заключение
Хоть я и не любитель велосипедов и своих решений, но иногда свое решение можно сделать достаточно быстро и оно даже будет работать. А если вы выбрали gRPC в качестве инструмента для коммуникации между микросервисами, то вы можете достаточно долго жить без Кафки и не знать особых проблем, например с балансировкой. Но об этом мы поговорим в следующей статье :)
Ранее статья была опубликована тут.