Комментарии 29
Мы поняли, что большая часть наших проблем — из-за микросервисов. Недостаток ресурсов возникал из-за микросервисных проблем, а не сложных вычислений или бизнес-логики.
Ну хорошо, хоть поняли. Сначала автор создает себе массу проблем, потом пытается их героически решить. Одно хорошо - с опытом приходит понимание, как действительно нужно разрабатывать софт.
То есть один микросервис посылал во второй данные, а тот был либо более старой версии, либо данные от первого второму не доехали, а второй микросервис уже переслал что-то в третий.
Тут проблема не в архитектуре, а в том, что после выпуска кода в продакшен, кто-то начал бездумно менять контракты.
В примере описано большое количество ошибок и ни одна из них не связана с архитектурой. Видимо, в команде ни у кого не было опыта работы с микросервисами.
Золотое правило микросервисов: если можно не использовать микросервисы, не используй микросервисы.
К сожалению, с такими требованиями (петабайт фильмов, 100500 RPS) без микросервисов будет очень сложно, если вообще возможно. Но есть и хорошая новость, ни Руслан, ни Савва за полгода это не сделают. Разве что, недавно они делали тоже самое другому заказчику.
Если каждый фильм "весит" хотя бы по гигабайту — то петабайт фильмов — это всего лишь миллион фильмов. Это уже не тот размер БД, где можно делать любую чушь — но за рамки возможностей традиционных СУБД ни разу не выходит.
Тут нормальным решением будет выделение непосредственно хранения и раздачи фильмов в отдельный шардируемый (микро)сервис — в то время как остальную систему можно без всяких проблем оставить монолитом.
Исходя из моей практики, монолитом называют то приложение, которое во время разработки сложно запустить на локальной машинеКрайне некорректное мнение.
Распределенные данные
Что насчёт JOIN?
Нам нужно сходить в сервис пользовательских данных, в сервис авторизации и аутентификации, подтянуть его комментарии, историю просмотров — и отдать. Вместо JOIN мы выполняем какое-то число HTTP-вызовов.
Вопрос без иронии.
Описанный механизм работает хорошо, когда он про одну сущность детализацию собирает.
Как этот механизм выглядит, когда для управленческих инструментов требуется разнообразными способами агрегировать информацию по тысячам сущностей, в реальном времени по актуальным данным?
В монолите был бы один хранимый view, который джойнит и агрегирует десяток таблиц, всё внутри одной базы, на злостно оптимизированных фильтрованных индексах, отдавая только результат в несколько сотен КБ.
Здесь же, получается, запрашиваем 100К строк с одного микросервиса, 200К с другого, 15М с третьего, передаём эти гигабайты JSON по сети, парсим у себя, стараясь не упасть по памяти, и джойним вручную на самописных алгоритмах?
Как этот механизм выглядит, когда для управленческих инструментов требуется разнообразными способами агрегировать информацию по тысячам сущностей, в реальном времени по актуальным данным?
Это плохо выглядит везде - хоть в какой архитектуре. Запросы в оперативную базу с разлапистыми джойнами - совершенно не сахар и для самой базы данных, и требуют уже специальных решений - типа отдельной базы данных для аналитики с денормализованными данными или хотя бы отдельной реплики базы. И вот когда эта самая реплика/отдельная база появляются... тут над ними легко и создать микросервис поставки данных.
Да, это круто, но что ты будешь делать, когда твоё хранилище перестанет влезать на один сервер или в один дата-центр? Тогда ты захочешь распределённое хранилище. Хорошо, если Django умеет поддерживать транзакционный механизм с распределённым хранилищем.
Преждевременная оптимизация - зло. А если такой проблемы в будущем не будет?
Как люди жили до этого? Как решали такие проблемы?
Почему такую проблему должен решать Django. Да, он фреймворк, но он не все силен. Это уже задача/проблема масштабирования Базы Данных.
Распределенные транзакции
Поэтому мы используем очереди. Мы списываем у Васи деньги и кладём это сообщение в очередь.
Вот, ещё такой вопрос.
Очередь ведь не часть какого-то микросервиса, она на все микросервисы одна.
Как обеспечивается транзакционная целостность между очередью и кладущим в неё микросервисом?
Если в микросервисе полагается иметь что-то типа
begin tran;
списать у Васи;
положить в очередь;
commit tran;
то возможна ситуация, когда в очередь на присвоение Пете-то ушло, а у Васи не списалось, потому что транзакция не смогла закомиттиться.
А если полагается иметь
begin tran;
списать у Васи;
commit tran;
положить в очередь;
тогда возможна ситуация, когда у Васи-то списалось, а в очередь на присвоение Пете не ушло, потому что клалка в очередь не сработала.
Мне приходит в голову только взять пример 2, обернуть положить в очередь;
в try
, и сделать вернуть Васе;
, если catch
— но и здесь остаются вопросы вида "А если не удалось вернуть Васе?".
Тут два варианта. Вариант первый — всё та же распределённая транзакция.
Вариант второй — идемпотентные операции:
- (локальная транзакция) списать у Васи, отметить присвоение Пете как начатое
- (пока присвоение начато) положить информацию в очередь
- (когда придёт ответ) отметить присвоение как законченное
А как ограничивать по нижней границе баланса для всех не завершенных операций ?
К примеру. В очереди на списание стоит 10 операций каждая по -10, но суммарно -100. А на балансе всего +30. Ведь имеется race condition между GetBalance и списанием.
Так же, как и без микросервисов делаете — проверять баланс в одной транзакции со списанием и одной операцией.
Разве оверхед на простой транзикционный механизм между микросервисами не создаёт эффект бутылочного горлышка ?
Количество открытых портов, память на каждое соединение, задержки сетевого хранилища когда там начинает работать сборщик мусора или дедупликация. Дополнительная синхронизация между несколькими инстансами в разных подах. И т.д.
Откуда у вас взялось "между микросервисами"? Почему вы вообще рассматриваете ситуацию, когда получение баланса и списание с него же реализуется в разных микросервисах?
Баланс это лишь пример
Ну так ответ в общем случае я уже давал — бить сложную операцию на части, так чтобы каждая часть происходила в транзакции.
А как это делать конкретно в каждом случае — вопрос конкретных случаев.
Разве оверхед на простой транзикционный механизм между микросервисами не создаёт эффект бутылочного горлышка ?У вас же в требованиях заложено это бутылочное горлышко, по факту. Можно сделать шардирование там какое-то, максимум. Ну и локализовывать транзакции исправлением границ между сервисами.
Разве я где-то писал требования ?
В очереди на списание стоит 10 операций каждая по -10, но суммарно -100. А на балансе всего +30. Ведь имеется race condition между GetBalance и списанием.Правда, мне пришлось додумать возможную проблему с этим: нарушенные границы BC, довольно типичная беда, плюс BC разнесены по сети, как это делается с микросервисами. А то без этого по одному лишь ТЗ и вправду нет проблем.
Если у вас нет message bus или queue и прочих характерных для микросервисов инструментов, то наверняка у вас монолит.У монолита может быть несколько интерфейсов — CLI, API, Web, воркеры. Монолит может скейлиться сколь угодно, независимо скейлить каждый интерфейс.
Монолит — это
1) Единый для приложения деплой. Основная проблема, которая возникает при росте команды. Может решаться выделением сервисов или библиотек.
2) Синхронность коммуникаций между составляющими его компонентами (если т.н. «микросервисы» общаются через синхронный HTTP, то есть имеют temporal coupling, то это просто distributed monolith, не имеющий ничего общего с распределёнными вычислениями/concurrent computing). Хотя асинхронность тоже легко достижима, но используется лишь от случая к случаю.
Индустрия на редкость быстро забыла опыт 1980-1990-х, когда после некоторого энтузиазма обнаружились "Fallacies of distributed computing".
Микросервисы vs. Монолит