Нагрузочное тестирование показало, что сервер может обработать 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 . В итоге формула принимает вид:
Где-то должен существовать аналитический вывод этой формулы посредством матаппарата процесса Пуассона, но там, скорее всего, слишком много матана, который не очень интересен и не совсем понятен основной аудитории этой статьи. Уверен, в Интернете полно материалов по этой теме, так что желающие могут погуглить самостоятельно. Может оказаться, что моя модель с формулой не совсем корректна.
Многопоточные сервера, серверные кластеры и асинхронная обработка запросов
До сих пор мы рассматривали сферического коня в вакууме 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% от максимально допустимой, т.к. это ведет к молниеносному увеличению среднего времени ожидания входящих запросов. Грамотно интерпретируйте результаты нагрузочного тестирования.
10+ сертификаций Coursera, которые могут изменить вашу карьеру
Бюджетный способ прокачать навыки и повысить зарплату — это профессиональный сертификат от Google, IBM или крупного зарубежного университета. На Coursera как раз можно найти десятки полезных обучающих программ по машинному обучению, проджект-менеджменту и не только. Собрали 10+ сертификаций, которые будут выигрышно смотреться в резюме как новичка, так и опытного специалиста.
Релоцировались? Теперь вы можете комментировать без верификации аккаунта.