Комментарии 36
Да, в идеальном мире NRE не должно быть, но мы — живые люди, и мы ошибаемся. Для себя мы пока решили оставить ретраи для кода 500, но вопрос всё ещё дискуссионный.Очень философски
Отличная статья, это нужно прочитать каждому, кто собирается делать автоматические повторы запросов. Но возникает вопрос — а зачем это все нужно? Есть очень простая политика — сервис вернул ошибку, ретранслируй ее пользователю, не пытайся повторять запросы. Не вижу смысла использовать что-то ещё в сервисе заказа пиццы или интернет магазине. Какие проблемы пользователя или бизнеса вся эта машинерия решает?
Есть очень простая политика — сервис вернул ошибку, ретранслируй ее пользователю, не пытайся повторять запросы.
А что пользователь будет с этой ошибкой делать?
Какие проблемы пользователя или бизнеса вся эта машинерия решает?
Если мы знаем, что сервис иногда возвращает проходящие ошибки, повтор запроса с такой ошибкой позволит пользователю достичь своей цели, даже не зная, что внутри была ошибка.
Я комментарий свой написал когда поймал себя на мысли, что тяжело придумать такие трансиентные ошибки, которые бы а) возникали достаточно часто, чтобы это слало проблемой и б) решались сами собой в пределах 10 секунд.
Мы же говорим о системе которая живет в облаке, скорее всего в одном дата центре. При этом обсуждаются синхронные сценарии, когда запрос пользователя в очередь не сложить (как с отправкой почты, например). То есть вырисовывается некий критичный для выполнения транзакции сервис, который часто падает и тут-же поднимается. Я с таким не сталкивался никогда. Если где-то это норма, то хочу знать где и почему так, для общего развития.
Простейший пример — сеть моргнула.
Ниже вот подсказывают кейс с пулом сервисов и 429. Это понятный сценарий и очень даже жизненный. Я о таких сценариях, в контексте Dodo и спрашиваю.
У нас такое случается постоянно, по разным причинам.
Так может это у клиента сеть моргнула, а не в дата-центре.
Мы же говорим о системе которая живет в облаке, скорее всего в одном дата центре.
Почему бы?
При этом обсуждаются синхронные сценарии
Почему бы?
Ну то есть вы взяли какие-то допущения, я понимаю, но почему вы считаете, что эти допущения верны?
Скажем, у меня вот мобильное приложение говорит с бэкендом (тут, в принципе, уже все это применимо, но на мобилке, все-таки, маловероятен .net, так что пойдем дальше), и запускает там некую условно долгоиграющую (15-30-60 секунд) задачу. Это, понятное дело, уже асинхрония (в моем случае — через поллинг, но не важно), поэтому второе ваше допущение неверно. А бэкенд говорит с сервисом, предоставляемым другой компанией, и они в разных облаках, если не на разных континентах (что уже передает привет всем сетевым проблемам), и в документации на этот сервис явно написано: бывает вот такая ошибка, в этом случае сделайте ретрай через время, указанное в заголовке.
Если у меня есть политики повторов в бэкенде, все будет работать более-менее само. Если у меня бэкенд будет просто пробрасывать ошибку пользователю, пользватель будет сам нажимать кнопку "повторить" (если мы ее ему дали) и расстраиваться. А зачем?
Сделать последствия таких сбоев менее заметными и не усугубить ситуацию — для этого и используются все эти политики.
Ну и, в дополнение, существует еще ряд несистемных транзиентных ошибок, которые могут быть заложены бизнес-логикой приложения. Например, eventually consistency или optimistic concurrency в принципе используют такой дизайн, что ошибки (а с ними и ретраи) будут неизбежны.
Сколько можно делать retry? Где-то несколько секунд, ну секунд 20 максимум, после этого пользователю запрос уже не интересен. Из ошибок, которые сами быстро исправляются я могу придумать только рестарт сервиса. Какие конкретно ошибки в вашей системе вы обходите с помощью повторов запроса?
Я привёл выше ошибки с eventually consistency, когда информация, нужная для запроса, еще не успела записаться в БД. Ошибки блокировки записей в БД.
И не забывайте, что речь идёт не только о взаимодействии пользователя с системой, а о распределенной системе, где сервисы взаимодействуют между собой в том числе через http-запросы. Если Вы думаете, что в интернет-магазине весь процесс заканчивается, после того, как пользователь нажал на кнопку «Заказать», то Вы сильно ошибаетесь.
Про кейсы.
Ну например, мы послали запрос в пулл серверов и получили ответ 429. Что это означает? Это означает, что конкретный инстанс сейчас перегружен, ему тяжело. Это не значит, что соседний инстанс не сможет обработать запрос.
Таймаут мы можем получить опять же по причине утилизации коннекшенов.
Кейс с рестартом сервиса тоже валидный, так как по разным причинам такое может случиться.
Про количество и время.
Опять же, вопрос что с чем взаимодействует. Например, у нас есть платежный шлюз и есть эквайреры, у которых заложено очень большое время на ответ. И ты вынужден, во-первых, долго ждать, во-вторых, очень хочется прийти в консистентное состояние в результате взаимодействия.
За пример с пулом серверов спасибо. Пожалуй пока это единственный сценарий, когда retry оправдан в рамках обработки пользовательского запроса, а не какой-то background job.
Кстати, а почему 429, а не 503?
429 означает же, что конкретный клиент сделал слишком много запросов, а перегрузка чаще возникает когда клиентов много.
429 это же клиентский код, это значит, что сервер может его использовать пока он еще в состоянии обрабатывать запросы, но ему уже сложно. Например, когда вы используете bulkhead на стороне сервера, и намеренно обрубаете запросы, которые не влезают в очередь.
А 503 – это уже как бы последняя стадия. Не смогла, так не смогла.
Потому что «синхронность» запроса она очень условная и внутри запрос разойдется на кучу потоков, могуть создаться деад-локи, может заблочится доступ к файлам, может какой-нибудь сервис начать отбивать ошибку перегрузки, а может тупо отваливаться по таймауту. Очередь может забиться и перестать принимать сообщений и т.д.
Из своих вариантов: 404 (балансировщик выкинул все ноды сервиса, нет ни одной доступной), таймаут соединения, 502 (в балансировщике сервис есть — по факту не в состоянии ответить ), etc
Такие политики работают как обёртка над стандартным HttpClient’ом.
Скорее "плагин", а не обертка. Под оберткой обычно имеют ввиду агрегирование одного класса другим
Есть сервис, который выполняет какие-то операции и вызывает другой сервис. Этот сервис тоже выполняет операции и вызывает сервис. Наконец, последний сервис просто что-то делает. У каждой операции в пайплайне есть свой таймаут, например 10 секунд. Если собственные операции каждого сервиса будут работать по 4 секунды, то на гейтвее может случиться ошибка таймаута, хотя каждый сервис отработал за корректное время. Вы как-то решаете подобные проблемы?
Но вообще несмотря на то, что Polly — это прекрасная библиотека, вам наверное стоит пересмотреть идею использовать ее для CircuitBreaker'а. Раз у вас SOA, то вы наверное используете горизонтальное масштабирование, и вряд ли вы сможете шарить состояние CB между двумя инстансами одного сервиса. Возможно, целый ряд ваших проблем с распространением ошибки решат Hystrix или Istio, но CB-то уж точно стоит туда отправить.
У нас сейчас действительно CB на уровне отдельных инстансов и, как правило, в логах можно наблюдать, как они практически одновременно открываются на каждом из инстансов. Да, я чуть-чуть смотрел на Istio, согласен, что в случае горизонтального масштабирования это выглядит лучше. Спасибо за ваш совет.
и я поклонник вашей пиццы, не только статей и кода!
спасибо вам вы крутые
такой вопрос - где взять IHttpClientBuilder или IHttpClientFactory вне asp.net?
нужно выполнить запрос из произвольной DLL.
Ну или - как прикрутить эти Policy к HttpClient без этих инструментов.
Повышаем надёжность HttpClient’а в .NET Core или как ошибиться в 3 строках кода 4 раза