DEV Community

Sergey Shinder
Sergey Shinder

Posted on

Ретраи, которые сложили внешний сервис вместо того чтобы спасти

Интеграция с внешним платёжным провайдером однажды преподала мне урок про то, что благие намерения в распределённых системах опаснее всего. У нас была логика ретраев: если запрос к провайдеру не прошёл — повторить. Разумно же? Пока однажды у провайдера не начались подтормаживания, и наши ретраи не превратили лёгкое недомогание в полноценный отказ.

Механика была такая. Провайдер начал отвечать медленно, наши запросы упирались в таймаут, и мы их ретраили. Каждый ретрай — новый запрос поверх уже перегруженного сервиса. Мы, по сути, устроили ему DDoS из лучших побуждений. Он лёг окончательно, а вместе с ним встали все наши платежи. Классический retry storm.

Первое, что мы починили — сами ретраи. Не тупой повтор сразу же, а экспоненциальная задержка с джиттером: каждая следующая попытка ждёт дольше, плюс случайный разброс, чтобы все наши инстансы не долбили провайдера синхронно в одну секунду. И жёсткий лимит на число попыток — три, не бесконечность.

Второе и главное — circuit breaker. Если провайдер стабильно отвечает ошибками, мы перестаём его дёргать вовсе на какое-то время и сразу отдаём управляемую ошибку. Это даёт внешнему сервису шанс встать на ноги, а не добивает его. Пусть лучше часть операций честно отвалится сразу, чем вся система захлебнётся в ожидании.

Третий урок был про идемпотентность, и он оказался тонким. Когда вы ретраите платёж, вы обязаны быть уверены, что не спишете деньги дважды. Мы стали слать в каждый запрос idempotency-ключ, чтобы провайдер понимал: это повтор той же операции, а не новая. Без этого ретраи из спасения превращаются в двойные списания и злых пользователей.

И ещё одно, о чём часто забывают: таймауты. У нас они были выставлены слишком щедро, запрос висел десятки секунд, занимая пул соединений. Пока он висел, приходили новые запросы, и пул исчерпывался — отказ расползался по нашей стороне ещё до внешнего сервиса. Мы ужали таймауты до разумных значений и изолировали пул соединений к провайдеру.

Вывод простой: любой вызов за границу вашего процесса может подвести, зависнуть или подтормаживать. Проектируйте интеграцию не под счастливый путь, а под тот день, когда партнёр по ту сторону API чувствует себя плохо. Ретраи без backoff, breaker и идемпотентности — это не отказоустойчивость, это ускоритель катастрофы.

– Sergey Shinder

Top comments (0)