Pull to refresh

Потоки или события

Reading time 3 min
Views 13K
Существует два способа обрабатывать параллельные запросы к серверу. Потоковые (threaded, синхронные) серверы используют множество одновременно выполняющихся потоков, каждый из которых обрабатывает один запрос. В это время событийные (evented, асинхронные) серверы выполняют единственной цикл событий, который обрабатывает все запросы.

Чтобы выбрать один из двух подходов, нужно определить профиль нагрузки на сервер.

Предположим, что каждый запрос требует c миллиекунд CPU и w миллисекунд реального времени для обработки. Время CPU расходуется на активные вычисления, а реальное время включает все запросы к внешним ресурсам. Например, запрос требует 5 мс времени c от CPU и 95 мс на ожидание ответа от базы данных. В итоге получается 100мс. Давайте также предположим, что потоковая версия сервера может поддерживать до t потоков, прежде чем начнутся проблемы с планированием и переключением контекста.

Если каждый запрос требует только время CPU для обработки, сервер в состоянии ответить на самое большее 1000/c запросов в секунду. Например, если каждый запрос занимает 2 миллисекунды времени CPU, тогда получится 1000/2=500 запросов в секунду.

В общем случае многопоточный сервер в состоянии обработать t*1000/w запросов в секунду.

Пропускная способность потокового сервера состовляет минимум от этих выражений (1000/c и t*1000/w). Событийний сервер ограничен лишь производительностью CPU (1000/c), поскольку использует всего один поток. Все описанное выше можно выразить следующим образом:


def max_request_rate(t, c, w):
  cpu_bound = 1000. / c
  thread_bound = t * 1000. / w
  print 'threaded: %d\nevented: %d' % (min(cpu_bound, thread_bound), cpu_bound)


Теперь рассмотрим различные типы серверов и посмотрим, как они себя покажут в различной реализации.

Для примеров я буду использовать t = 100.

Начнем с классического примера: HTTP прокси сервер. Этот тип серверов почти не требует времени CPU, поэтому предположим, что c = 0.1 мс. Предположим, что стоящие следом сервера получают задержку, скажем w = 50 мс. Тогда мы получим:


>>> max_request_rate(100, 0.1, 50)
threaded: 2000
evented: 10000

Наши расчеты показывают, что потоковый сервер будет в состоянии обработать 2 000 запросов в секунду, а событийный 10 000. Большая производительность событийного сервера говорит нам, что для потокового сервера узким местом стало количество потоков.

Другой пример — сервер веб-приложений. Сперва рассмотрим случай приложения, которое не требует никаких внешних ресурсов, но производит определенные вычисления. Время обработки будет, допустим 5 мс. Т.к. никакие блокирующие вызовы не совершаются, время w составит также 5 мс. Тогда:


>>> max_request_rate(100, 5, 5)
threaded: 200
evented: 200

В данном случае узким местом стала производительность CPU.

А теперь представим, что приложению нужно запросить данные из внешнего ресурса и время CPU составит 0.5 мс, а общее время w = 100 мс.


>>> max_request_rate(100, 0.5, 100)
threaded: 1000
evented: 2000


Как видно из простых расчетов, да и вообще из математики, синхронная потоковая реализация не будет иметь большую производительность, чем асинхронная событийная.

Однако стоит учитывать, что конечного пользователя интересует в первую очередь реальное время, которое он тратит на ожидание ответа. Проблемы начинаются, когда приложению приходится делать что-то долгое. Например, совершить большой и сложный запрос в базу данных, провести сложный расчет, обработать графику, произвести запрос на чужой сервер. Примеров можно привести много.

Как только однопоточной программе, обслуживающей в цикле десять тысяч клиентов, приходится ради одного из них задержаться, например, на секунду, эту секунду ждут все остальные. Если таких запросов много — ни один клиент не получит ответ раньше, чем общее время обработки запросов. Кооперативная многозадачность в действии. В случае потоковых решений, действует вытесняющая многозадачность. Система не позволит «тяжелому» запросу тратить время всех.

С одной стороны мы имеем прозводительные асинхронные решения, узким местом которых является сложность написания программ, поскольку большинство существующих библиотек написаны в блокирующем стиле. Программист берет на себя отвестственность за то, что ошибка в одном запросе не скажется на остальных.

С другой стороны мы имеем блокирующие решения, для которых мы привыкли писать программы. Проблему количества поддерживаемых потоков можно решить с помощью специализированных средств, например гринлетов или Erlang-процессов. Если потоковые сервера достингут планки, при которой узким местом перестанет быть количество потоков, они будут выглядеть более привлекательно за счет времени отклика и надежности.
Tags:
Hubs:
+72
Comments 111
Comments Comments 111

Articles