Дисклеймер: пример вымышленный, проблема реально встречающаяся 🐤
Вы разрабатываете сервис приема заказов
order-service. В зависимости от состава заказа и истории покупателя вы можете предлагать разные виды оплаты: предоплату, частичную предоплату, постоплату. Логика анализа состава заказа реализована в order-service, а за информацией по покупателю он обращается в client-service другой команды.Фронт вызывает:
POST /orders{
"clientUid": "some-uuid",
"products": [{...}, {...}]
}
Ответ:
{
"orderUid": "some-uid",
"orderStatus": "WAITING_FOR_CONFIRMATION", //ожидает подтверждения
"paymentType": "ON_DELIVERY" //оплата при получении
}На основании ответа фронт показывает страницу подтверждения заказа с типом оплаты.
Обычно
client-service отвечал order-service за 500 мс. Но что-то пошло не так, и теперь он отвечает по 20 секунд Таймаут между фронтом и
order-service 30 секунд, сам order-service отрабатывает не более 500 мс, но часть запросов от фронта стала падать по таймауту.Почему так происходит, ведь 20 cек client-service + 500 мс order-service < 30 сек таймаута?
Потому что эти 20,5 сек - это время выполнения запроса, только если он сразу получил все необходимые ресурсы, то есть "чистое" время выполнения. А при нагрузке запрос может сначала ждать свободный поток, соединение с БД или другой ограниченный ресурс и только потом начать свои 20 секунд ожидания
client-service.Допустим, код
order-service работает так:1️⃣открывается транзакция с БД
2️⃣сохраняются данные заказа
3️⃣вызывается по REST client-service
4️⃣после его ответа сохраняются еще данные в БД
5️⃣транзакция закрывается
6️⃣идет ответ фронту
Если приложение держит транзакцию, пока ждет ответ client-service, то все это время соединение с БД остается занятым, а размер пула соединений ограничен.
Например, в пуле 10 соединений.
Первые 10 запросов заняли все соединения и на 20 секунд ушли ждать client-service.
11-й запрос уже не может начать работу с БД и ждет свободное соединение.
Через ~20 секунд оно освобождается, 11-й запрос сохраняет заказ, но теперь ему предстоит еще 20 секунд ждать ответ client-service.
Итого около 40 секунд end-to-end: 20 сек ждал в очереди к БД + 20 сек ждал ответ от client-service.
Для фронта 11-й запрос упадет по таймауту, хотя сам client-service для каждого отдельного вызова отвечает за 20 секунд.
✅ Поэтому первым делом стоит проверить, не держим ли мы долгую транзакцию во время ожидания внешнего сервиса. И если вдруг держим, то позволяет ли бизнес-логика изменить границы транзакции так, чтобы вынести из нее внешний вызов.
У долгих транзакций есть и другие минусы. Когда в PostgreSQL мы делаем UPDATE строки, создается новая версия строки, а старая сразу не удаляется. Очисткой устаревших строк занимается фоновый процесс VACUUM. Он очищает версии, которые больше не нужны активным транзакциям. Долгоживущие транзакции могут мешать очистке старых версий строк, их накопление ведет к разрастанию таблиц и индексов и ухудшению производительности запросов.
А если долгих транзакций нет или БД вообще не используется, что тогда?
✅ Если погуглить, в таких ситуациях обычно предлагается использовать паттерн Circuit breaker: если внешний сервис деградировал, временно перестать отправлять в него новые запросы, чтобы не допустить каскадного отказа.
Это поможет сохранить доступность order-service, но возникает бизнес-вопрос: что делать с самим заказом, если без client-service мы не можем определить paymentType?
Варианты:
✅ Возвращать ошибку пользователю, но делать это быстро, не удерживая ресурсы десятки секунд
✅ Кэшировать ответы
client-service - подойдет, если много заказов от одних и тех же клиентов, но надо уточнять, как инвалидировать кэш, как часто обновляется ответ по клиенту (может он исчерпает лимит на постоплату, а мы из-за кэша не заметим)✅ Договориться с бизнесом, что при недоступности
client-service paymentType определять только по локальной логике🐼 И конечно разговаривать с командой
client-service, почему сервис вместо 500 мс стал отвечать 20 секунд 😅