Где резать монолит: границы, контракты и события
Монолит режут не потому, что микросервисы модно, а потому что выкатка одной кнопки не должна останавливать всю систему. Вопрос только в том, где проводить границы — и именно здесь ошибаются чаще всего.
Зачем вообще резать
Единственная честная причина — независимость. Независимая выкатка, независимый отказ, независимое масштабирование. Если ни одно из трёх вам сейчас не мешает, распил принесёт только распределённые транзакции и новые способы сломаться.
Проверка на необходимость: если правка в одной части системы заставляет тестировать всю систему целиком — вот вам первая граница.
Где проводить границы
Резать по слоям — отдельно «работа с базой», отдельно «логика», отдельно «API» — самый популярный способ получить распределённый монолит: сервисов стало больше, а выкатывать по-прежнему приходится всё вместе.
Резать надо по зонам ответственности, то есть по тому, что система делает для пользователя: авторизация, файлы, отзывы, статистика. Проверка простая: если для одной пользовательской задачи меняются три сервиса — границы проведены неверно.
Контракт важнее реализации
Как только сервисов больше одного, главным становится договор между ними. Держите его в отдельном репозитории: тогда изменение контракта — это отдельный запрос на слияние, который видно, а не строчка в коде, которую заметят на проде.
syntax = "proto3";
package files;
// Контракт лежит отдельно от реализации: поменять его незаметно нельзя.
service FileService {
rpc Upload(stream UploadRequest) returns (UploadResponse);
rpc ListFiles(ListFilesRequest) returns (ListFilesResponse);
rpc DeleteFile(DeleteFileRequest) returns (DeleteFileResponse);
}
message UploadRequest {
bytes chunk = 1; // файл идёт потоком, а не одним куском в памяти
string folder = 2;
}Потоковая загрузка здесь не украшение: файл на сотни мегабайт, принятый одним куском, съедает память сервиса и роняет его вместе со всеми остальными запросами.
Что делать синхронно, а что событием
Правило, которое экономит больше всего нервов: синхронно — только то, без чего нельзя ответить пользователю. Всё остальное уходит событием в очередь.
- Проверить права — синхронно, ответ нужен сейчас
- Отдать список файлов — синхронно
- Пересчитать статистику после загрузки — событием
- Отправить уведомление — событием
- Записать в аналитику — событием
// Потребитель события: пересчёт статистики после загрузки.
// Обработчик обязан быть идемпотентным — брокер доставляет
// «хотя бы один раз», и одно и то же событие придёт дважды.
func (c *Consumer) Handle(ctx context.Context, msg Message) error {
if c.seen.Has(msg.ID) {
return nil // уже обработали, тихо подтверждаем
}
if err := c.stats.Recalc(ctx, msg.UserID); err != nil {
return fmt.Errorf("recalc stats: %w", err)
}
c.seen.Add(msg.ID)
return nil
}Выкатка: состояние описано в git
Пока выкатка — ручная операция, она остаётся редкой и страшной, а значит правки копятся и приезжают на прод пачками. Лечится описанием желаемого состояния в репозитории: инструмент сам приводит кластер к тому, что записано.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: root-prod
namespace: argocd
spec:
source:
path: apps/prod # что должно быть развёрнуто
targetRevision: HEAD
directory:
recurse: true
syncPolicy:
automated:
prune: true # убрать то, чего больше нет в git
selfHeal: true # вернуть состояние, если правили рукамиПункт «вернуть состояние, если правили руками» важнее, чем кажется: он превращает ручную правку на проде из незаметного расхождения в то, что откатится само и заставит внести изменение нормальным путём.
Чего ждать после распила
- Выкатка одной части перестаёт трогать остальные
- Нагруженные части масштабируются отдельно от спокойных
- Отказ одного сервиса перестаёт быть отказом всей платформы
- Появляются новые проблемы: сетевые задержки, согласованность данных, отладка через границы сервисов
Последний пункт не повод не резать, но повод не резать всё сразу. Начинать стоит с той части, которая меняется чаще остальных и мешает выкатывать всё прочее.
Думаете про распил
Начать стоит с разбора: какие части системы меняются чаще других и что мешает выкатывать их отдельно. Часто выясняется, что резать нужно не всё и не сразу.