Нагрузочное тестирование показало, что сервер может обработать 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 очереди заблокированных потоков всегда известно максимальное время ожидания - оно пропорционально максимальному количеству потоков и времени удерживания блокировки.
Релоцировались? Теперь вы можете комментировать без верификации аккаунта.