Продолжу тему http серверов. На этот раз про сервера приложений для ruby и python. С другими языками я практически не дружу (не считая c++, конечно же).
Их много, все они разные, и главное отличие - скорость обработки запросов. Еще, конечно же, важно назначение http сервера. К примеру назначение http сервера Mongrel и Thin - запуск приложений, написанных на языке ruby, а Tornado или Twisted для приложений, написанных на python. Конечно их можно использовать и для раздачи статических файлов... но я не советую :-)
Из-за отсутствия нормальной реализации многопоточности в этих языках (и наличия GIL), http сервера приложений способны обслуживать клиентов только в один поток. Поэтому, чтобы заставить их использовать несколько процессоров, запускают несколько экземпляров http серверов, перед ними выставляя балансировщик (apache, nginx, lighttpd).
Основная задача, которую я хочу выполнить: узнать максимальное количество запросов в секунду, на которые способны указанные сервера приложений. Они были выбраны как самые быстрые (не считая Mongrel, конечно же). Если вы знаете более быстрые сервера приложений для ruby или python, пожалуйста, сообщите мне об этом. Методика тестирования заключается в создании минимального приложения, которое будет отдавать строку "Hello World". Сначала я хотел делать второй этап теста и отдавать больший объем данных, но судя по результатам, это не значительно влияет на результаты, поэтому их не привожу. Тестовый стенд: AMD Phenom(tm) II X4 940, 8 Gb RAM, Debian Squeeze, сборка для AMD64. Все тексты запускались несколько раз, чтобы убедится в стабильности результатов. Для теста используется ab, первый тест выполняется с аргументом -c 1, второй -c 100 (это количество параллельных запросов). При запуске я привязываю приложение к определенному процессору, чтобы оно не скакало с одного на другой. Полный лог вывода к сожалению не помещается (видимо на Blogger имеется ограничение на размер поста), исходные тексты тестов можно найти в конце текста.
Для начала о сервере Mongrel: http://ru.wikipedia.org/wiki/Mongrel. Помимо написанного в википедии добавлю, что он практически полностью написан на ruby, за исключением парсера http запросов (эта часть написана на C). Как уже указано в википедии, до недавнего времени достаточно нагруженный проект Twitter использовал Mongrel. Вот из статейки об архитектуре Twitter http://highscalability.com/scaling-twitter-making-twitter-10000-percent-faster:
Mongrel - hybrid Ruby/C HTTP server designed to be small, fast, and secure
На тот момент twitter использовали примерно 350 000 пользователей, уже кучу серверов, и Mongrel справлялся со своей задачей. Надо отметить, что во время второго теста с 100 клиентов, время от времени ab жаловался на "apr_socket_recv: Connection timed out (110)", что не очень хорошо. Вот, на что он способен (напоминаю, что смотреть нужно значение "Requests per second"):
Concurrency Level: 1
Time taken for tests: 36.468 seconds
Requests per second: 2742.11 [#/sec] (mean)
Time per request: 0.365 [ms] (mean)
Time per request: 0.365 [ms] (mean, across all concurrent requests)
Transfer rate: 353.47 [Kbytes/sec] received
Concurrency Level: 100
Time taken for tests: 26.562 seconds
Requests per second: 3764.78 [#/sec] (mean)
Time per request: 26.562 [ms] (mean)
Time per request: 0.266 [ms] (mean, across all concurrent requests)
Transfer rate: 485.30 [Kbytes/sec] received
Теперь посмотрим на Thin. Он использует EventMachine, который частично написан на C++. Соответственно используется асинхронная обработка запросов. Парсер запросов также написан на C. Каких-либо историй успехов найти не смог (вероятно плохо искал). Вот его результаты:
Concurrency Level: 1
Time taken for tests: 34.807 seconds
Requests per second: 2872.98 [#/sec] (mean)
Time per request: 0.348 [ms] (mean)
Time per request: 0.348 [ms] (mean, across all concurrent requests)
Transfer rate: 412.43 [Kbytes/sec] received
Concurrency Level: 100
Time taken for tests: 16.325 seconds
Requests per second: 6125.52 [#/sec] (mean)
Time per request: 16.325 [ms] (mean)
Time per request: 0.163 [ms] (mean, across all concurrent requests)
Transfer rate: 879.35 [Kbytes/sec] received
Thin явно быстрее Mongrel при увеличении количества параллельных запросов. Основное преимущество Thin в том, что его можно использовать для реализации Comet сервера.
Теперь перейдем к python. Честно говоря, я и с этим языком практически не дружу. Поэтому даже не знаю, кто в стане python является самым быстрым. Наиболее часто я слышу про Tornado и Twisted. Для начала о Twisted. Большей частью написан на python, но местами, разумеется, на C. Является не сервером приложений, а событийно-ориентированным фреймворком для разработки серверов. Т.е. то, чем является EventMachine для ruby. Сразу замечу, что результаты не впечатляют. Возможно потому, что парсер запросов не написан на C :-) Во время тестирования ошибка "apr_socket_recv: Connection refused (111)" выскакивала при отсутствии параллельных запросов.
Concurrency Level: 1
Time taken for tests: 70.951 seconds
Requests per second: 1409.43 [#/sec] (mean)
Time per request: 0.710 [ms] (mean)
Time per request: 0.710 [ms] (mean, across all concurrent requests)
Transfer rate: 202.33 [Kbytes/sec] received
Concurrency Level: 100
Time taken for tests: 31.263 seconds
Requests per second: 3198.64 [#/sec] (mean)
Time per request: 31.263 [ms] (mean)
Time per request: 0.313 [ms] (mean, across all concurrent requests)
Transfer rate: 459.18 [Kbytes/sec] received
Теперь Tornado. Был создан в компании Friendfeed и предоставлен на суд общественности после приобретения последней компанией Facebook. Они написали Tornado как раз потому, что Twisted имеет некоторые проблемы с быстродействием, источник: http://bret.appspot.com/entry/tornado-web-server. Судя по результатам, у них это получилось.
Concurrency Level: 1
Time taken for tests: 50.786 seconds
Requests per second: 1969.03 [#/sec] (mean)
Time per request: 0.508 [ms] (mean)
Time per request: 0.508 [ms] (mean, across all concurrent requests)
Transfer rate: 334.58 [Kbytes/sec] received
Concurrency Level: 100
Time taken for tests: 19.708 seconds
Requests per second: 5074.07 [#/sec] (mean)
Time per request: 19.708 [ms] (mean)
Time per request: 0.197 [ms] (mean, across all concurrent requests)
Transfer rate: 862.20 [Kbytes/sec] received
Признаться, я приятно удивлен тем, что ruby сервера справляются лучше, чем python. Но возможно причина в том, что разбор запросов в случае ruby осуществляется на C. Я не ставил целью сравнивать между собой эти два прекрасных языка программирования. Моя цель в том, чтобы показать общую картину. Хотя для того, чтобы показать ее полностью, следовало сравнить фреймворки Ruby On Rails и Django. Там все будет значительно худе, в лучшем случае несколько сотен запросов в секунду. Вот презентация: http://www.scribd.com/doc/51076611/RubyConfUA-Ruby-on-Rails-1000-запросов-в-секунду-Макс-Лашин, где описывается ситуация, что для обработки 1000 запросов в секунду требуется 14 серверов. К сожалению нет деталей, сколько из этих серверов отданы для БД, а сколько для Ruby On Rails. Разумеется это уже не "Hello, world", и вероятно основная нагрузка будет идти на БД. Однако, если судить по архитектуре других проектов, серверов слишком много для такой нагрузки.
Мне кажется, что подобные языки не предназначены для разработки http серверов, где приходится обслуживать большое количество клиентов. Я пишу именно http серверов, а не http приложений. В любое приложение на C или C++ легко встраивается почти любой язык программирования. И явно такие сервера должны быть. Интересно, почему они не пользуются популярностью? Для сравнения, вот, на что способен nginx, работая одним воркером:
Concurrency Level: 1
Time taken for tests: 17.761 seconds
Requests per second: 5630.18 [#/sec] (mean)
Time per request: 0.178 [ms] (mean)
Time per request: 0.178 [ms] (mean, across all concurrent requests)
Transfer rate: 1264.59 [Kbytes/sec] received
Concurrency Level: 100
Time taken for tests: 3.889 seconds
Requests per second: 25715.85 [#/sec] (mean)
Time per request: 3.889 [ms] (mean)
Time per request: 0.039 [ms] (mean, across all concurrent requests)
Transfer rate: 5776.02 [Kbytes/sec] received
При этом у nginx задача чуть сложнее, я не стал писать к нему модуль, поэтому nginx отдает файл. Результаты для 100 клиентов впечатляют. Я несколько раз перепроверял, но ошибки нет. Хотя если вдуматься, что такое 100 000 раз открыть и отправить какой-то там файлик? Это займет пол секунды (специально проверил). Основной расход ресурсов уходит на коммуникацию. Раз уж перешел к отдаче файлов, в последнее время часто упоминается yaws, написанный на Erlang. Встретил даже вот такие восхищенные строки:
Yaws (Yet Another Web Server) - web сервер написанный на языке Erlang. На языке, который по праву считается крайне производительным. Тоже самое можно сказать и об самом web сервере: по сравнению с apache2, yaws - просто реактивный.
http://the-bosha.ru/2010/08/30/yaws-lyogkiy-web-server-na-erlang/
Ну и множество других восторженных отзывов. Пройти мимо просто нельзя. Ради этого даже поставил yaws из репозитария wheezy, чтобы тестировать последнюю версию. Вот, что я получил:
Server Software: Yaws
Concurrency Level: 1
Time taken for tests: 33.808 seconds
Requests per second: 2957.89 [#/sec] (mean)
Time per request: 0.338 [ms] (mean)
Time per request: 0.338 [ms] (mean, across all concurrent requests)
Transfer rate: 652.82 [Kbytes/sec] received
Concurrency Level: 100
Time taken for tests: 24.427 seconds
Failed requests: 171
(Connect: 0, Receive: 57, Length: 57, Exceptions: 57)
Write errors: 0
Total transferred: 22587118 bytes
HTML transferred: 1998860 bytes
Requests per second: 4093.88 [#/sec] (mean)
Time per request: 24.427 [ms] (mean)
Time per request: 0.244 [ms] (mean, across all concurrent requests)
Transfer rate: 903.02 [Kbytes/sec] received
В глаза бросается строка (Connect: 0, Receive: 57, Length: 57, Exceptions: 57). И сколько бы я не запускал ab, ошибки вылезают. Видимо 100 запросов на один сервер, многовато. На реактивный он явно не похож.
UPDATE
По совету Viacheslav Biriukov повторил тест на gunicorn для python. Разбираться, как он реализован, пока нет времени. Вот его результаты:
Concurrency Level: 1
Time taken for tests: 37.928 seconds
Requests per second: 2636.57 [#/sec] (mean)
Time per request: 0.379 [ms] (mean)
Time per request: 0.379 [ms] (mean, across all concurrent requests)
Transfer rate: 424.84 [Kbytes/sec] received
Concurrency Level: 100
Time taken for tests: 19.764 seconds
Requests per second: 5059.64 [#/sec] (mean)
Time per request: 19.764 [ms] (mean)
Time per request: 0.198 [ms] (mean, across all concurrent requests)
Transfer rate: 815.27 [Kbytes/sec] received
При отсутствии конкуренции он показал себя значительно лучше Tornado и Twisted.
Итоговый результат в виде картинки:
Ну а теперь исходные тексты.
Mongrel:
require 'mongrel'
class HelloHandler < Mongrel::HttpHandler
def process(request, response)
response.start(200) do |head,out|
head['content-type'] = 'text/html'
out << '<p>Hello World!</p>'
end
end
end
h = Mongrel::HttpServer.new("0.0.0.0", "3000")
h.register("/hello", HelloHandler.new)
Thin
require 'thin'
app = proc do |env|
# Response body has to respond to each and yield strings
# See Rack specs for more info: http://rack.rubyforge.org/doc/files/SPEC.html
body = ['<p><p>Hello World!</p>']
[
200, # Status code
{ 'Content-Type' => 'text/html' }, # Reponse headers
body # Body of the response
]
end
run app
Twisted
# -*- coding: utf-8 -*-
from twisted.web import server, resource
from twisted.internet import reactor
class Simple(resource.Resource):
isLeaf = True
def render_GET(self, request):
return "<p>Hello world!</p>"
site = server.Site(Simple())
reactor.listenTCP(3000, site)
reactor.run()
Tornado
# -*- coding: utf-8 -*-
import tornado.httpserver
import tornado.ioloop
import tornado.web
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write("<p>Hello, world<p>")
if __name__ == "__main__":
application = tornado.web.Application([
(r"/hello", MainHandler),
])
http_server = tornado.httpserver.HTTPServer(application)
http_server.listen(3000)
tornado.ioloop.IOLoop.instance().start()
gunicorn
# -*- coding: utf-8 -
from wsgiref.validate import validator
#@validator
def app(environ, start_response):
"""Simplest possible application object"""
data = '<p>Hello, World</p>'
status = '200 OK'
response_headers = [
('Content-type','text/plain'),
('Content-Length', str(len(data)))
]
start_response(status, response_headers)
return iter([data])