Что происходит с внешним сервисом при повторных обращениях во время сбоя
Что проверяют
Понимаете ли, что наивные повторы не лечат сбой, а усиливают его. Это вопрос про устойчивость распределённой системы, а не про синтаксис цикла с попытками.
Как отвечать
Главная мысль: во время сбоя повторные обращения добивают сервис. Он деградировал и отвечает медленно, клиенты по таймауту шлют запрос заново, нагрузка растёт кратно, и сервис не поднимается даже после того, как исходная причина устранена. Такое состояние называют метастабильным сбоем.
Что предлагать:
- таймаут и срок на каждый вызов через
context.WithTimeout, причём срок протаскивается по всей цепочке вызовов и сверху вниз только уменьшается; - экспоненциальная задержка обязательно со случайной добавкой: без неё клиенты синхронизируются и бьют залпами;
- ограничение числа попыток и бюджет повторов, то есть доля повторов от общего трафика. Иначе три звена по три попытки дают девять обращений на один пользовательский запрос;
- размыкатель цепи: после серии ошибок он открывается и какое-то время отвечает отказом сразу, затем пропускает пробный запрос. Это бережёт и чужой сервис, и свои горутины с соединениями;
- повторять только идемпотентное. Для платежей и создания сущностей нужен ключ идемпотентности, иначе повтор после таймаута создаст дубль.
Где спотыкаются
Повторяют всё подряд, включая ответы вида 4xx. Ошибка клиента от повтора не исправится, а сервису прилетит лишняя нагрузка. Повторять имеет смысл только временные отказы: таймаут, обрыв соединения, ответ о перегрузке.
Второе: ходят через клиент по умолчанию, у которого нет таймаута. Запросы висят до закрытия соединения другой стороной, горутины и дескрипторы копятся, и приложение падает раньше, чем чужой сервис успевает подняться.
Третье: считают таймаут доказательством того, что запрос не выполнился. Ответ мог потеряться уже после записи на той стороне, поэтому повтор без ключа идемпотентности даёт двойное списание или дубль заказа. Об этом стоит сказать до того, как спросят.