Как реализовать обработку ошибок в микросервисах
Что проверяют
Не знание try/catch, а понимание, что ошибка в распределённой системе это часть контракта: как её увидит вызывающий сервис, как её найдут в логах и как она не утащит за собой соседей.
Как отвечать
Разложить ответ по трём уровням.
1. Внутри сервиса. Исключения делятся на бизнесовые (OrderNotFoundException) и технические (таймаут БД, сбой сериализации). Ловить точечно там, где есть что сделать; остальное отдавать наверх в единую точку. В Spring это @RestControllerAdvice с @ExceptionHandler, а не try/catch в каждом контроллере и не самодельный аспект.
2. Контракт наружу. Стабильная структура тела ответа: машинночитаемый code, текст для человека, traceId. В Spring 6 для этого есть ProblemDetail и ErrorResponse по RFC 9457 (бывший 7807), свой формат изобретать не нужно.
@RestControllerAdvice
class ApiExceptionHandler {
@ExceptionHandler(OrderNotFoundException.class)
ProblemDetail handle(OrderNotFoundException e) {
var pd = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage());
pd.setProperty("code", "ORDER_NOT_FOUND");
return pd;
}
}Коды: 400 невалидный запрос, 401/403 доступ, 404 нет ресурса, 409 конфликт состояния, 422 семантика, 429 лимит, 503 зависимость недоступна. Главное разделение для вызывающей стороны: «повторять бессмысленно» (4xx) против «повторить можно» (5xx, 429, 503). Стектрейсы, SQL и внутренние адреса наружу не отдавать.
3. Между сервисами. Таймаут на каждый исходящий вызов, без него ретраи только усугубляют. Retry применять к идемпотентным операциям, с экспоненциальной задержкой, джиттером и ограниченным числом попыток; неидемпотентные защищать ключом идемпотентности. Circuit breaker и bulkhead через Resilience4j (Hystrix не развивается с 2018 года). Fallback должен быть осознанным: закэшированные данные или явно деградированный ответ, но не пустой список вместо ошибки, который клиент примет за «данных нет».
Наблюдаемость. Сквозной идентификатор трассировки по W3C Trace Context, проброс в логи через MDC, структурированный JSON в централизованное хранилище (Elasticsearch или Loki; Grafana это визуализация, а не хранилище). Метрики в Prometheus, алерты по доле 5xx и сгоранию бюджета ошибок SLO, а не по каждому исключению.
Асинхронная часть. Неразобранные сообщения уходят в DLQ вместе с причиной, повтор с backoff, дедупликация на стороне потребителя.
Тестирование. WireMock и Toxiproxy для отказов и задержек зависимостей, отдельные тесты на то, какой код и тело отдаёт обработчик.
Где спотыкаются
Ограничиваются формулой «глобальный @ControllerAdvice и пишем в лог», и на уточняющий вопрос не могут объяснить, почему ретраи без таймаутов и предохранителя превращают одну медленную зависимость в отказ всей цепочки.