Как избежать подвешивания системы при долгой работе внешнего сервиса
Что проверяют
Понимаете ли вы, что медленный внешний сервис опаснее упавшего: он держит потоки, соединения и память, пока не встанет вся система. Следом обычно спрашивают, какой таймаут вы бы поставили и что увидит пользователь, когда он сработает.
Как отвечать
Вызов наружу должен быть ограничен по времени и по ресурсам. Дальше перечислите слои защиты:
- таймауты на каждом уровне: на соединение и на чтение. В JDK
HttpClientэтоconnectTimeout()у билдера иtimeout()уHttpRequest, без них запрос ждёт ответа сколько угодно. В SpringRestClientтаймауты задаются черезClientHttpRequestFactory, вWebClientчерезresponseTimeoutу Netty; - изоляция (bulkhead): вызовы к медленному сервису идут в отдельный ограниченный пул потоков или через семафор, чтобы не съесть общий пул Tomcat;
- circuit breaker: после серии ошибок или медленных ответов вызовы на время перестают уходить наружу и сразу получают fallback. Стандартный выбор сейчас Resilience4j, Hystrix давно не развивается;
- ретраи с экспоненциальной задержкой и джиттером: только для идемпотентных операций и с общим бюджетом времени;
- асинхронность:
CompletableFutureсorTimeout()илиcompleteOnTimeout(), а если ответ не нужен сразу, вызов уходит в очередь (Kafka, RabbitMQ), и клиент получает «принято».
Закончите наблюдаемостью: латентность по перцентилям p95 и p99 и состояние circuit breaker в Micrometer и Grafana, чтобы деградация была видна до инцидента.
Где спотыкаются
Отвечают «сделаем асинхронно» и считают задачу решённой. Без таймаута CompletableFuture висит так же вечно, только в другом пуле, по умолчанию в ForkJoinPool.commonPool(), который потом тормозит всё остальное.
Ставят ретраи без ограничения. Три попытки на каждом из трёх уровней вызовов дают 27 запросов на один пользовательский, и медленный сервис окончательно ложится.
Надеются на виртуальные потоки Java 21. Они снимают нехватку потоков, но пул соединений к базе или HTTP-клиента по-прежнему кончается, а таймаутов они не добавляют.