Хотите дальше читать devby? 📝
Support us

Планирование нагрузки на сервер

Оставить комментарий
Планирование нагрузки на сервер
Нагрузочное тестирование показало, что сервер может обработать 10 запросов в секунду. Значит ли это, что на практике сервер выдержит такую нагрузку в Интернет? В большинстве случаев ответ на этот вопрос отрицательный.

Простая на первый взгляд задачка

Дан сервер, синхронно обрабатывающий запросы из Интернет в одном потоке. Т.е. в каждый момент времени сервер может либо обрабатывать один запрос, либо ожидать новые запросы. Запросы становятся в очередь ожидания, если в данный момент сервер занят обработкой другого запроса. Время обработки одного запроса равно 100 мс. Каково будет среднее время ожидания запроса в очереди при средней нагрузке на сервер в 10 запросов в секунду? Правильный ответ - время ожидания будет стремиться к бесконечности. Как же так? Ведь очевидно, что за одну секунду сервер может последовательно обработать 10 запросов. Откуда тогда берется бесконечность? Дело в том, что средняя и равномерная нагрузка - это "две большие разницы". Средняя нагрузка на сервер может быть представлена в виде процесса Пуассона. Это означает, что в начале первой секунды может сразу прийти 100 запросов, а за последующие 9 секунд - ни одного. В итоге выходит 10 запросов в секунду за интервал в 10 секунд со средним временем ожидания sum(1..99)/100 * 100 мс = 4.95 секунды. С увеличением рассматриваемого интервала увеличивается и среднее время ожидания. Если рассматривать среднее время ожидания на бесконечном интервале времени, то оно тоже устремится в бесконечность. Рассмотрим другие варианты. Очевидно, что при средней нагрузке, превышающей 10 запросов в секунду, сервер не сможет обрабатывать лишние запросы, что приведет к бесконечному увеличению очереди ожидания запросов. А что будет, если нагрузка будет ниже максимально допустимой? Например, каково будет среднее время ожидания при 95% нагрузке, т.е. для нашего случая 9.5 запросов в секунду? Думаете, 0мс или что-то близкое к этому? Ведь при такой нагрузке сервер должен простаивать в ожидании запросов 5% своего времени. Спешу вас снова огорчить - среднее время ожидания в этом случае будет равно 950 мс, т.е. в 9.5 раз больше времени, необходимого на обработку самого запроса! Идем дальше. Давайте попробуем предположить время ожидания при 90% нагрузке. Нет, вовсе не 900 мс, а 450 мс. Интересно... А при какой нагрузке среднее время ожидания будет равно времени, необходимому для обработки запроса? Иными словами, при какой средней нагрузке среднее время ответа от сервера будет равно 200 мс (100мс ожидание + 100мс обработка)? Правильный ответ - 66.666...%! Даже при одном запросе в секунду среднее время ожидания будет равно не нулю, а где-то 5.56 мс.

Мы тебе не верим. Откуда такие странные числа?

Числа получены экспериментальным путем, после чего была выведена формула. Вот функция, написанная на python, которая была использована для моделирования нагрузки на сервер и вычисления среднего времени ожидания.
def NormalizedAvgWaitTime(load_factor, requests_count):
  """Calculates normalized average wait time for the given load factor.
 
  Models requests to a synchronous single-threaded server using Poisson process
  and calculates an average wait time per request for the given load factor.
 
  Args:
    load_factor: a floating point value in the range [0..1). The value
      0 means the server isn't loaded with requests at all, the value 1 means
      the average qps load on the server is equivalent to its' maximum capacity.
    requests_count: an integer representing the number of requests to model.
      Higher values mean higher precision for the returned result.
 
  Returns:
    a floating point number representing normalized average wait time per
    request for the given load factor. The value 0 means requests
    are processed immediately, while any number W means requests stay
    in the queue for an average W*T seconds, where T is a time required
    for processing a single request on the idle server.
  """
  if not load_factor:
    return 0
  points = sorted(random.random() for i in range(requests_count))
  request_time = load_factor / requests_count
  total_wait_time = 0
  x = points[0]
  for i in range(1, requests_count):
    y = points[i]
    wait_time = request_time - y + x
    if wait_time > 0:
      total_wait_time += wait_time
      y += wait_time
    x = y
  return total_wait_time / load_factor
А вот тут вы можете удостовериться в подлинности вышеприведенных чисел и посмотреть, как requests_count влияет на точность вычислений. Заодно в python попрактикуетесь. После того, как были получены экспериментальные данные, осталось подогнать под них формулу :) При load_factor = 1 наша функция стремится к бесконечности. Значит, у нас должна быть дробь, в знаменателе которой присутствует множитель, стремящийся к нулю при load_factor = 1. На эту роль хорошо подходит (1 - load_factor). Идем дальше. При load_factor = 0 наша функция равна нулю. Значит, в числителе дроби должен быть множитель, равный нулю при load_factor = 0. Очевидно, что это и есть load_factor. Дальше - при load_factor = 0.5 наша функция должна равняться тоже 0.5 . Значит, C*0.5/(1-0.5) = 0.5 . Откуда C = 0.5 . В итоге формула принимает вид:
NormalizedAvgWaitTime(load_factor) = 0.5*load_factor/(1-load_factor)
Где-то должен существовать аналитический вывод этой формулы посредством матаппарата процесса Пуассона, но там, скорее всего, слишком много матана, который не очень интересен и не совсем понятен основной аудитории этой статьи. Уверен, в Интернете полно материалов по этой теме, так что желающие могут погуглить самостоятельно. Может оказаться, что моя модель с формулой не совсем корректна.

Многопоточные сервера, серверные кластеры и асинхронная обработка запросов

До сих пор мы рассматривали сферического коня в вакууме aka "однопоточный сервер, последовательно обрабатывающий независимые запросы в синхронном режиме". На практике такие сервера встречаются достаточно редко (если не считать Python runtime в Google AppEngine). Как же быть с многопоточными серверами, серверными кластерами и прочими асинхронными node.js'ами? Очень просто - для них тоже действует вышеописанная формула. Нужно лишь забыть о деталях реализации и представить наш сервер или кластер в виде black box'а, после чего определить максимальную нагрузку, которую способен выдержать этот black box, чтобы правильно откалибровать load_factor в формуле. Максимальную нагрузку, выраженную в запросах в секунду, можно определить следующим способом: отправляем нашему black box'у в максимально короткое время (чем меньше, тем точнее результат) фиксированное число real-world запросов (чем больше, тем точнее результат) и засекаем время, когда все они будут корректно обработаны. Затем делим количество обработанных запросов на время. Это и есть искомая максимальная нагрузка на нашу систему, относительно которой калибруется load_factor.

Как правильно планировать нагрузку на сервер?

К этому моменту вам должно быть понятно, что результаты типичного нагрузочного тестирования перед выходом в production не совсем корректно отражают объективную реальность. Главная проблема в том, что поток тестовых запросов не является Пуассоновским процессом, с которым суждено встретиться серверу в production. Но это легко поправимо. Достаточно найти максимальную нагрузку, выдерживаемую системой по схеме, описанной в предыдущем параграфе. После этого по формуле вычисляете допустимую нагрузку в production для заданного среднего времени ожидания запроса. Например, для максимальной нагрузки в 1000 qps и среднего времени ожидания запроса, равного 100% времени обработки запроса: 1 = 0.5*load_factor/(1-load_factor) => load_factor = 1/1.5 = 2/3. Тогда допустимая нагрузка равна 1000*2/3 = 666.666... qps.

Многопоточные программы

Можно ли применить только что полученные знания для вычисления среднего времени ожидания доступа к разделяемому ресурсу, защищенного с помощью блокировки в многопоточной программе? Ответ - нет, т.к. процесс доступа к разделяемому ресурсу из небольшого количества потоков (например, меньше тысячи) не может быть аппроксимирован Пуассоновским процессом. В многопоточных программах всегда известна максимальная нагрузка на доступ к разделяемому ресурсу - она пропорциональна максимальному количеству потоков, которые могут захватить блокировку. Более того, для простейшей реализации блокировок в виде FIFO очереди заблокированных потоков всегда известно максимальное время ожидания - оно пропорционально максимальному количеству потоков и времени удерживания блокировки.

Заключение

Всегда следите за нагрузкой на системы, обрабатывающие запросы, которые распределены во времени по процессу Пуассона. К таким системам относятся все системы с потенциально большим количеством пользователей. Например, все сервера подключенные к Интернет. Не допускайте, чтобы средняя нагрузка повышалась более 75% от максимально допустимой, т.к. это ведет к молниеносному увеличению среднего времени ожидания входящих запросов. Грамотно интерпретируйте результаты нагрузочного тестирования.
Помогаете devby = помогаете ИТ-комьюнити.

Засапортить сейчас.

Читайте также
10+ сертификаций Coursera, которые могут изменить вашу карьеру
10+ сертификаций Coursera, которые могут изменить вашу карьеру
10+ сертификаций Coursera, которые могут изменить вашу карьеру
Бюджетный способ прокачать навыки и повысить зарплату — это профессиональный сертификат от Google, IBM или крупного зарубежного университета. На Coursera как раз можно найти десятки полезных обучающих программ по машинному обучению, проджект-менеджменту и не только. Собрали 10+ сертификаций, которые будут выигрышно смотреться в резюме как новичка, так и опытного специалиста.
Как оплачиваются самые популярные языки GitHub и какой прогноз
Как оплачиваются самые популярные языки GitHub и какой прогноз
Как оплачиваются самые популярные языки GitHub и какой прогноз
Не только Python: 3 алгоритма выбора первого языка программирования
Не только Python: 3 алгоритма выбора первого языка программирования
Bubble
Не только Python: 3 алгоритма выбора первого языка программирования
Rust стал самым быстрорастущим языком по числу разработчиков
Rust стал самым быстрорастущим языком по числу разработчиков
Rust стал самым быстрорастущим языком по числу разработчиков

Хотите сообщить важную новость? Пишите в Telegram-бот

Главные события и полезные ссылки в нашем Telegram-канале

Обсуждение
Комментируйте без ограничений

Релоцировались? Теперь вы можете комментировать без верификации аккаунта.

Комментариев пока нет.