
Тут могла бы быть история о том, как команда решила перейти на микросервисы ради «масштабируемости» и «гибкости», но на самом деле всё началось с того, что обновление одного модуля монолита ломало половину продакшна. Мы устали. И однажды CTO сказал: «Кажется, пора». Что было дальше — расскажем без прикрас.
Монолит, который нас довёл
Начнём с лирики. Монолитная система, написанная на Spring Boot, существовала с 2017 года. В ней было 8 команд разработчиков, около 2 миллионов строк кода, и каждая третья фича обязательно ломала две старых.
Вот, например, как выглядел один из сервисов (ну, модулей в монолите):
@RestController
@RequestMapping("/api/v1/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public ResponseEntity<UserDto> getUser(@PathVariable Long id) {
return ResponseEntity.ok(userService.getUserById(id));
}
}
А теперь угадайте, что происходит, если UserService
вызывает BillingService
, который, в свою очередь, зависит от AuthService
, а тот от LoggerService
, который из-за багов пишет по 100 МБ логов в секунду? Правильно, ад.
План атаки: распилить аккуратно
Резать монолит сразу — значит убить всё. Поэтому мы начали с анализа зависимостей. Самое важное — понять, какие куски можно оторвать с минимальной болью. Использовали для этого комбинацию:
Spring Boot Actuator (для метрик)
Jaeger + OpenTracing (для трассировки зависимостей)
jQAssistant + ArchUnit (для статического анализа кода)
Визуализация зависимостей выглядела как лапша. Но кое-что было понятно: User, Billing, Notifications и Reporting — кандидаты на миграцию.
Первый микросервис: Auth
Начали с самого «изолированного» (ха-ха) сервиса — AuthService. Он уже почти не зависел от других модулей, кроме User. Мы вынесли его в отдельный Spring Boot сервис, завернули в Docker, деплоили в k8s, настроили Kafka для асинхронной коммуникации.
Пример конфигурации Kafka consumer:
spring:
kafka:
consumer:
bootstrap-servers: kafka:9092
group-id: auth-group
auto-offset-reset: earliest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
Ничего сложного. Кроме того, что он развалился под нагрузкой в 200 RPS, потому что мы забыли про connection pool. Ну и, конечно, забили на retry-политику, из-за чего Kafka messages уходили в никуда. Учли, переделали.
Вторая волна: Billing и Notifications
Эти сервисы мы вынесли параллельно. Billing работал с PostgreSQL, Notifications — с RabbitMQ. Появился первый общий контракт — мы ввели protobuf-схемы, генерируемые через CI. Это был первый шаг к контрактному тестированию.
Фрагмент proto-файла:
syntax = "proto3";
message UserCreated {
string id = 1;
string email = 2;
string name = 3;
}
Мы добавили lint для схем, автогенерацию в build.gradle.kts
и пайплайн в GitLab:
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.19.1"
}
plugins {
id("grpc") {
artifact = "io.grpc:protoc-gen-grpc-java:1.42.1"
}
}
}
Логика миграции: синхрон → асинхрон
Одна из больших проблем была в том, что все модули раньше общались через синхронные вызовы. Мы перешли на event-driven архитектуру. Но это потребовало:
Переобучить команды мыслить событиями, а не методами.
Ввести event versioning.
Создать свой Event Gateway — агрегатор событий с трассировкой.
Мысли и баги на проде
Самое неприятное — момент, когда старый монолит и новые микросервисы живут одновременно. Например:
AuthService уже обрабатывает регистрацию,
но UserService в монолите всё ещё создаёт пользователя в базе.
Мы временно ввели sync proxy, чтобы Auth дергал старый метод внутри монолита. Грязно? Да. Зато работало.
CI/CD и DevOps ад
Новые микросервисы = новые пайплайны. Мы использовали:
GitLab CI
Helm + ArgoCD
ECR + Kustomize
Каждый сервис теперь имеет helm/
и kustomize/overlays/
с параметрами для stage/prod.
Вот пример deployment.yaml
:
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth-service
spec:
replicas: 3
selector:
matchLabels:
app: auth-service
template:
metadata:
labels:
app: auth-service
spec:
containers:
- name: auth
image: registry.example.com/auth:latest
ports:
- containerPort: 8080
Что не взлетело
Feature Flags — пытались внедрить через LaunchDarkly, но потом сделали свой велосипед.
Service Mesh — Istio был хорош на демо, но в проде всё начало лагать.
Distributed tracing — почти никто не смотрел в Jaeger, пока не стало совсем плохо.
Что получилось
У нас теперь 16 микросервисов.
90% старого монолита выключено.
Обновление одного сервиса больше не валит всё остальное.
Команды перешли к DevOps-культуре, стали автономными.
Заключение
Если вы думаете мигрировать с монолита — не делайте это, пока не будете уверены, что сможете жить в хаосе как минимум полгода. Это тяжело, это больно, но в итоге — это свобода.