- PVSM.RU - https://www.pvsm.ru -
При любом взаимодействии клиента и сервера мы сталкиваемся с необходимостью повторять запросы. Сетевое соединение может быть ненадежно, могут быть проблемы на сервере или любые другие причины, из-за которых необходимо повторить запрос. То же самое касается и взаимодействия backend-сервера с базой данных или любым другим хранилищем данных (другим сервисом).
Мы сегодня поговорим об интервале повторов запроса. Через какой период времени после неудачного запроса можно его повторить? Давайте рассмотрим две стратегии: повтор через фиксированный интервал времени и экспоненциальное откладывание (exponential backoff). Мы увидим на симуляции, что при условии наличия большого числа клиентов повтор через фиксированный интервал может не дать серверу «подняться», а использование exponential backoff позволяет избежать этой проблемы.
Вопрос интервала повторов становится важным при проблемах на сервере. Очень часто сервер способен выдержать нагрузку от клиентов, которые отправляют запросы в некотором «текущем» режиме, распределяя свои запросы во времени случайным образом. Если на сервере происходит отказ, все клиенты обнаруживают его и начинают повторять запросы через некоторый интервал. Может оказаться, что частота таких запросов превышает тот предел, который сервер может обрабатывать.
Еще одним важным моментом является то, что клиент часто не может отличить проблемы на сервере от проблем с сетевым соединением на стороне клиента: если ответ на запрос не приходит в заданный интервал времени, клиент не может сделать заключение о том, в чем именно проблема. И поведение клиента (повтор запроса, интервал повтора) будут одинаковыми в обоих ситуациях.
Это самая простая стратегия (и самая часто используемая). В случае неуспешного выполнения запроса клиент повторяет выполнение запроса через интервал T. Здесь мы ищем компромисс между тем, чтобы не «завалить» сервер слишком частыми запросами в случае отказа, и между тем, чтобы не ухудшить user experience, слишком долго не повторяя запросы в случае единичных проблем в сети.
Типичный интервал повтора может составлять от сотен миллисекунд до нескольких секунд.
Алгоритм экспоненциального откладывания можно записать на псевдокоде следующим образом:
delay = MinDelay while True: error = perform_request() if not error: # ok, got response break # error happened, pause between requests sleep(delay) # calculate next delay delay = min(delay * Factor, MaxDelay) delay = delay + norm_variate(delay * Jitter)
В этом псевдокоде delay
— интервал между запросами, perform_request
выполняет сам запрос, а norm_variate
вычисляет нормальное распределение.
Есть следующие параметры алгоритма:
MinDelay
— минимальный (начальный) интервал повтора (например, 100 мс)MaxDelay
— ограничение на длительность интервала повтора (например, 15 мин)Factor
— коэффициент экспоненциального нарастания задержки (например, 2)Jitter
— «дрожание», определяет случайную составляющую (например, 0.1)Вычисление интервала задержки работает достаточно просто: первый интервал равняется MinDelay
, затем в случае ошибки выполнения запроса интервал увеличивается в Factor
раз, при этом добавляется случайная величина, пропорциональная Jitter
. Интервал повтора ограничен MaxDelay
(без учета Jitter
).
Чем данный способ вычисления повтора лучше повтора через фиксированный промежуток времени:
Можно задать резонный вопрос: а что если у клиента просто плохое сетевое соединение? Ведь алгоритм экспоненциального откладывания будет все время увеличивать интервал запроса, а пользователь не будет ждать бесконечно. Здесь можно привести три соображения:
Factor
, MaxDelay
и MinDelay
можно изменить, чтобы замедлить нарастание интервала повтора или ограничить его.Для иллюстрации графики интервала задержки при таком алгоритме с параметрами:
MinDelay
100 мс, Factor
2.7, Jitter
0.1, MaxDelay
10 мин;MinDelay
1 сек, Factor
1.5, Jitter
0.1, MaxDelay
5 мин.По горизонтальной оси — номер попытки.
Те же графики, шкала логарифмическая:
Так ли легко поверить, что сложность экспоненциального откладывания оправдана? Давайте попробуем провести симуляцию и выяснить эффект от разных алгоритмов. Симуляция состоит из двух независимых частей: клиента и сервера, я их опишу по очереди.
Сервер эмулирует типичную для базы данных задержку ответа на запрос: до некоторого предела количества одновременных запросов сервер отвечает с фиксированной задержкой, а затем время отклика начинает нарастать экспоненциально в зависимости от количества одновременных запросов. Если мы говорим о backend-сервере, чаще всего его время отклика также ограничено временем отклика базы данных.
Сервер отвечает за minDelay
(100 мс), пока количество одновременных соединений не превысит concurrencyLimit
(30), после этого время отклика нарастает экспоненциально: minDelay * factor ^ ((concurrency - concurrencyLimit) / K)
, где factor
, K
— константы, а concurrency
— текущее количество одновременных соединений. В действительности сервер не выполняет никакой работы, он отвечает на запрос фиксированным ответом, но задержка вычисляется по выше указанной формуле.
Клиент моделирует поток запросов от N
клиентов, каждый из которых выполняет запросы независимо. Каждый поток клиента выполняет запросы с интервалами, соответствующим экспоненциальному распределению с параметром lambda
(мат. ожидание интервала запросов равно lambda
). Экспоненциальное распределение хорошо описывает случайные процессы, происходящие в реальном мире (например, активность пользователей, которая приводит к запросам на сервер). При ошибке выполнения запроса (или при превышении времени ожидания ответа) клиент начинает повторять запрос либо с простым фиксированным интервалом, либо по алгоритму экспоненциального откладывания.
Сценарий симуляции следующий: мы запускаем клиент и сервер с некоторыми параметрами, через 10-15 секунд возникает установившийся режим, в котором запросы выполняются успешно с небольшими задержками. Затем мы останавливаем сервер (эмулируем «падение» сервера или сетевые проблемы) просто сигналом SIGSTOP
, поток клиентов это обнаруживает и начинает повторять запросы. Затем восстанавливаем работу сервера и смотрим, как быстро работа сервера придет в норму (или не придет).
Код для симуляции написан на Go [1] и выложен на GitHub [2]. Для сборки потребуется Go 1.1+, компилятор можно поставить из пакета ОС или скачать [3]:
$ git clone https://github.com/smira/exponential-backoff.git $ cd exponential-backoff $ go build -o client client.go $ go build -o server server.go
Простой сценарий: в первой консоли запускаем сервер:
$ ./server Jun 21 13:31:18.708: concurrency: 0, last delay: 0 ...
Сервер ожидает входящих HTTP-запросов на порту 8070 и каждую секунду печатает на экран текущее количество одновременных соединений и последнюю вычисленную задержку обслуживания клиентов.
В другой консоли запускаем клиент:
$ ./client OK: 98.00 req/sec, errors: 0.00 req/sec, timedout: 0.00 req/sec OK: 101.00 req/sec, errors: 0.00 req/sec, timedout: 0.00 req/sec OK: 100.00 req/sec, errors: 0.00 req/sec, timedout: 0.00 req/sec ...
Клиент каждые 5 секунд печатает на экран статистику по успешным запросам, запросам, завершившимся с ошибкой, а также запросам, для которых был превышен таймаут на ожидание ответа.
В первой консоли останавливаем сервер:
Jun 21 22:11:08.519: concurrency: 8, last delay: 100ms Jun 21 22:11:09.519: concurrency: 10, last delay: 100ms ^Z [1]+ Stopped ./server
Клиент обнаруживает, что сервер остановлен:
OK: 36.00 req/sec, errors: 0.00 req/sec, timedout: 22.60 req/sec OK: 0.00 req/sec, errors: 0.00 req/sec, timedout: 170.60 req/sec OK: 0.00 req/sec, errors: 0.00 req/sec, timedout: 298.80 req/sec OK: 0.00 req/sec, errors: 0.00 req/sec, timedout: 371.20 req/sec
Пробуем восстановить работу сервера:
$ fg ./server Jun 21 22:13:06.693: concurrency: 0, last delay: 100ms Jun 21 22:13:07.492: concurrency: 1040, last delay: 2.671444385s Jun 21 22:13:08.492: concurrency: 1599, last delay: 16.458895305s Jun 21 22:13:09.492: concurrency: 1925, last delay: 47.524196455s Jun 21 22:13:10.492: concurrency: 2231, last delay: 2m8.580906589s ...
Количество одновременных соединений нарастает, увеличивается задержка ответа сервера, для клиента это будет означать еще больше ситуаций таймаута, еще больше повторов, еще больше одновременных запросов, сервер будет увеличивать задержку еще больше, наш модельный сервис «упал» и больше не поднимется.
Отстановим клиент и перезапустим сервер, чтобы начать сначала с экспоненциальным откладыванием повтора:
$ ./client -exponential-backoff
Повторим ту же последовательность действий с приостановкой и возобновлением работы сервера — сервер не упал и успешно поднялся, в течение короткого промежутка времени восстановилась работа сервиса.
У тестовых клиента и сервера есть большое количество параметров, которыми можно управлять как нагрузкой, так и таймаутами, поведением под нагрузкой и т.п.:
$ ./client -h $ ./server -h
Даже в этой простой симуляции легко видно, что поведение группы клиентов (которых эмулирет client) отличается в случае «успешной» работы и в случае ситуации проблем (в сети или на стороне сервера):
Совет: используйте экспоненциальное откладывание при любом повторе запроса — в клиенте при обращении к серверу или в сервере при обращении к базе данных или другому сервису.
Этот пост был написан по материалам мастер-класса про высокие нагрузки и надежность.
Автор: smira
Источник [4]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/veb-razrabotka/63021
Ссылки в тексте:
[1] Go: http://golang.org/
[2] GitHub: https://github.com/smira/exponential-backoff
[3] скачать: http://golang.org/doc/install
[4] Источник: http://habrahabr.ru/post/227225/
Нажмите здесь для печати.