Нагрузочное тестирование показало, что сервер может обработать 10 запросов в секунду. Значит ли это, что на практике сервер выдержит такую нагрузку в Интернете?
В большинстве случаев ответ на этот вопрос — отрицательный. Давайте подробно разберемся почему.
Дан сервер, синхронно обрабатывающий запросы из Интернет в одном потоке. Т.е. в каждый момент времени сервер может либо обрабатывать один запрос, либо ожидать новые запросы. Запросы становятся в очередь ожидания, если в данный момент сервер занят обработкой другого запроса. Время обработки одного запроса равно 100 мс.
Как же так? Ведь очевидно, что за одну секунду сервер может последовательно обработать 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
.
Однако, автора можно здесь справедливо пнуть в незнание основ, с которыми, поверьте, у него всё очень даже неплохо. Поэтому, кто хочет зайти к этой же проблеме с другой, более классической стороны — рекомендую ознакомиться с Теорией массового обслуживания, которая очень плотно используется в сфере больших нагрузок (вот вам готовая методичка по ней на русском), но главный минус в этой теории в том, что там нет формул-выводов для случая многоканальной СМО с бесконечной очередью.
Посему есть существенно усовершенствованная модель, которая может эмулировать несколько серверов (многоканальная СМО) с несколькими режимами отправки запросов из очереди к серверам — least loaded, random и round robin —смотрите код и результаты тута: ideone.com/DXB9n .
Для режима round robin также можно варьировать количество очередей (клиентов). Для остальных режимов количество клиентов не имеет значения.
К этому моменту вам должно быть понятно, что результаты типичного нагрузочного тестирования перед выходом в 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-очереди заблокированных потоков всегда известно максимальное время ожидания — оно пропорционально максимальному количеству потоков и времени удерживания блокировки.
Всегда следите за нагрузкой на системы, обрабатывающие запросы, которые распределены во времени по процессу Пуассона. К таким системам относятся все системы с потенциально большим количеством пользователей. Например, все сервера подключенные к Интернет.
Окончание этой серии — читайте здесь.
2 комментария
Добрый день!
Подскажите пожалуйста, мн нужно развернуть 2-3 интернет магазина на prestashop хостер интересуется:
"О какой нагрузке идёт речь? Сколько запросов в секунду? Какой уровень отказоустойчивости нужен?"
Комментатор 97,
телепаты в отпуске на Бали загорают. Мы-то откуда можем это знать?