Статьи

HTTP-заголовок Retry-After на практике

Retry-After — это менее известный заголовок ответа HTTP. Позвольте мне процитировать соответствующую часть RFC 2616 (спецификация HTTP 1.1) :

14.37 Повторная попытка

Поле заголовка ответа Retry-After может использоваться с ответом 503 ( Service Unavailable ), чтобы указать, как долго ожидается, что служба будет недоступна для запрашивающего клиента. Это поле МОЖЕТ также использоваться с любым ответом 3xx (Перенаправление), чтобы указать минимальное время, в течение которого пользовательский агент должен ждать ожидания перед отправкой перенаправленного запроса. Значение этого поля может быть либо HTTP-датой, либо целым числом секунд (в десятичной дроби) после времени ответа.

1
Retry-After  = "Retry-After" ":" ( HTTP-date | delta-seconds )

Два примера его использования:

1
2
Retry-After: Fri, 31 Dec 1999 23:59:59 GMT
Retry-After: 120

В последнем примере задержка составляет 2 минуты.

Хотя вариант использования с ответом 3xx интересен, особенно в случае непротиворечивых систем (« ваш ресурс будет доступен по этой ссылке в течение 2 секунд ), мы сосредоточимся на обработке ошибок. Добавив Retry-After к серверу ответов, он может дать подсказку клиенту, когда он снова станет доступным. Кто-то может утверждать, что сервер едва ли когда-либо знает, когда он вернется в оперативный режим, но есть несколько допустимых случаев использования, когда такие знания могут быть каким-то образом получены:

  • Плановое обслуживание — это очевидно, если ваш сервер не работает в пределах окна запланированного обслуживания, вы можете отправить Retry-After с прокси-сервера с точной информацией, когда нужно перезвонить. Клиенты не будут пытаться повторить попытку раньше, конечно, если они понимают и уважают этот заголовок
  • Очередь / пул потоков заполнен — ​​если ваш запрос должен быть обработан пулом потоков, и он полон, вы можете оценить, когда следующий запрос может быть обработан. Это требует связанной очереди (см. ExecutorService — 10 советов и рекомендаций , пункт 6) и грубой оценки того, сколько времени потребуется для выполнения одной задачи. Обладая этими знаниями, вы можете оценить, когда следующий клиент может быть обслужен без очереди.
  • Автоматический выключатель разомкнут — в Hystrix вы можете запросить
  • Следующий доступный токен / ресурс / что угодно

Давайте сосредоточимся на одном нетривиальном случае использования. Представьте, что ваш веб-сервис поддерживается командой Hystrix:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
private static final HystrixCommand.Setter CMD_KEY = HystrixCommand.Setter
    .withGroupKey(HystrixCommandGroupKey.Factory.asKey("REST"))
    .andCommandKey(HystrixCommandKey.Factory.asKey("fetch"));
  
@RequestMapping(value = "/", method = GET)
public String fetch() {
    return fetchCommand().execute();
}
  
private HystrixCommand<String> fetchCommand() {
    return new HystrixCommand<String>(CMD_KEY) {
        @Override
        protected String run() throws Exception {
            //...
        }
    };
}

Это работает, как и ожидалось, если команда не выполнена, время ожидания истекло или автоматический выключатель разомкнут, клиент получит 503. Однако в случае автоматического выключателя мы можем по крайней мере оценить, сколько времени потребуется для повторного замыкания цепи. К сожалению, нет общедоступного API, сообщающего, как долго цепь остается открытой в случае катастрофических сбоев. Но мы знаем, как долго выключатель по умолчанию остается разомкнутым, что является хорошей максимальной оценкой. Конечно, цепь может оставаться разомкнутой, если основная команда продолжает давать сбой. Но Retry-After не гарантирует, что сервер будет работать в указанное время, это всего лишь подсказка для клиента прекратить попытки заранее. Следующая реализация проста, но не работает:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
@RequestMapping(value = "/", method = GET)
public ResponseEntity<String> fetch() {
    final HystrixCommand<String> command = fetchCommand();
    if (command.isCircuitBreakerOpen()) {
        return handleOpenCircuit(command);
    }
    return new ResponseEntity<>(command.execute(), HttpStatus.OK);
}
  
private ResponseEntity<String> handleOpenCircuit(HystrixCommand<String> command) {
    final HttpHeaders headers = new HttpHeaders();
    final Integer retryAfterMillis = command.getProperties()
            .circuitBreakerSleepWindowInMilliseconds().get();
    headers.set(HttpHeaders.RETRY_AFTER, Integer.toString(retryAfterMillis / 1000));
    return new ResponseEntity<>(headers, HttpStatus.SERVICE_UNAVAILABLE);
}

Как видите, мы можем спросить любую команду, разомкнут выключатель или нет. Если он открыт, мы устанавливаем заголовок Retry-After со значением circuitBreakerSleepWindowInMilliseconds . В этом решении есть тонкая, но катастрофическая ошибка: если однажды цепь открывается, мы никогда не запускаем команду снова, потому что мы с нетерпением возвращаем 503. Это означает, что Hystrix никогда не будет пытаться выполнить ее снова, и схема останется открытой навсегда. Мы должны пытаться вызывать команду каждый раз и перехватывать соответствующее исключение:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
@RequestMapping(value = "/", method = GET)
public ResponseEntity<String> fetch() {
    final HystrixCommand<String> command = fetchCommand();
    try {
        return new ResponseEntity<>(command.execute(), OK);
    } catch (HystrixRuntimeException e) {
        log.warn("Error", e);
        return handleHystrixException(command);
    }
}
  
private ResponseEntity<String> handleHystrixException(HystrixCommand<String> command) {
    final HttpHeaders headers = new HttpHeaders();
    if (command.isCircuitBreakerOpen()) {
        final Integer retryAfterMillis = command.getProperties()
            .circuitBreakerSleepWindowInMilliseconds().get();
        headers.set(HttpHeaders.RETRY_AFTER, Integer.toString(retryAfterMillis / 1000));
    }
    return new ResponseEntity<>(headers, SERVICE_UNAVAILABLE);
}

Этот работает хорошо. Если команда выдает исключение и связанная цепь открыта, мы устанавливаем соответствующий заголовок. Во всех примерах мы берем миллисекунды и нормализуем до секунд. Я бы не рекомендовал этого, но если по какой-то причине вы предпочитаете абсолютные даты, а не относительные таймауты в заголовке Retry-After , форматирование даты HTTP, наконец, является частью Java (начиная с JDK 8):

1
2
3
4
5
6
import java.time.format.DateTimeFormatter;
  
//...
  
final ZonedDateTime after5seconds = ZonedDateTime.now().plusSeconds(5);
final String httpDate = DateTimeFormatter.RFC_1123_DATE_TIME.format(after5seconds);

Примечание об авто-DDoS

Вы должны быть осторожны с заголовком Retry-After если отправляете одну и ту же метку времени множеству уникальных клиентов. Представьте себе, что сейчас 15:30, и вы отправляете Retry-After: Thu, 10 Feb 2015 15:40:00 GMT всем вокруг — просто потому, что вы каким-то образом рассчитали, что услуга будет работать в 15:40. Чем дольше вы продолжаете отправлять одну и ту же метку времени, тем большую «атаку» DDoS вы можете ожидать от клиентов, уважающих Retry-After . Обычно все планируют повторные попытки точно в 15:40 (очевидно, что часы не выровнены идеально, а сетевая задержка меняется, но все же), заполняя вашу систему запросами. Если ваша система правильно спроектирована, вы можете пережить ее. Однако есть вероятность, что вы уменьшите эту «атаку», отправив еще один фиксированный заголовок Retry-After , по существу перепланировав атаку позже.

При этом следует избегать фиксированных абсолютных временных отметок, отправляемых нескольким уникальным клиентам. Даже если вы точно знаете, когда ваша система станет доступной, распределите значения Retry-After некоторый период времени. На самом деле вы должны постепенно впускать все больше и больше клиентов, так что экспериментируйте с различными распределениями вероятностей.

Резюме

Заголовок ответа HTTP Retry-After не является ни общеизвестным, ни часто применимым. Но в довольно редких случаях, когда можно ожидать простоев, рассмотрите возможность его реализации на стороне сервера. Если клиенты также знают об этом, вы можете значительно сократить сетевой трафик, одновременно улучшая пропускную способность системы и время отклика.

Ссылка: HTTP-заголовок Retry-After на практике от нашего партнера по JCG Томаша Нуркевича из блога Java и соседей .