Ускоряем web-сценарии на Perl, или ещё раз про Perl vs PHP
Date February 17th, 2010 Author Vitaly Agapov
— х/ф “Эквилибриум”
Идея этой статьи родилась совершенно случайно после пары бесед с адептами PHP, утверждавшими, что Perl для Web-программирования не подходит по причине своей тормознутости, сложности и отсутствию многих полезных фич. Про второй и третий фактор говорить не будем по причине их субъективности. А вот с “тормознутостью” можно поработать. Действительно, из коробки сценарии на PHP должны работать быстрее сценариев на Perl из-за принципиального отличия их базовых парадигм: интерпретатор PHP загружен как модуль в Apache и не требует постоянной загрузки в память на каждый новый HTTP-запрос, в то время как интерпретатор Perl как раз и запускается отдельным процессом, выполняет сценарий и выгружается из памяти. Однако же спор на этом заканчивать рано, потому как для Perl существуют такие замечательные вещи, как mod_perl и FastCGI, которые могут добавить очков в корзину этому языку. При этом mod_perl по аналогии с mod_php загружается в память при старте Apache и не тратит драгоценных мгновений на запуск и выгрузку из памяти (жертвуя, правда, ресурсами сервера при этом), а FastCGI позволяет запустить интерпретатор с зацикленным сценарием один раз и обращаться к нему при поступлении новых запросов (здесь стоит отметить необходимость внесения некоторых изменений в сам скрипт).
Так вот. Попробуем разобраться, как всё это заставить работать и насколько всё это улучшает производительность по сравнению с работой сценариев в стандартном режиме с модулем mod_cgi.
Для проведения нашего маленького “бенчмарка” сделаем испытательный полигон на тестовом сервере (к слову сказать, будем это делать на обычном десктопе в установленной Ubuntu 9.10). Здесь у нас стоит Apache 2.2.11, PHP 5.2.6, Perl 5.10.0, mod_perl 2.0.4. В качестве нагрузки я буду использовать утилиту ab (Apache Benchmarking tool), которая умеет создавать нагрузку на HTTP от любого количества виртуальных пользователей. Каждый пользователь эмулируется отдельным форком родительского процесса, а сам родительский процесс подсчитывает среднее время ответа от web-сервера при текущей нагрузке.
Всё это, само собой, не претендует на объективность, истину в последней инстанции и божественное откровение, но позволит скоротать время и узнать что-то новое.
Итак, для проверки сделаем несколько наборов идентичных сценариев, написанных на PHP и на Perl. Чтобы абстрагироваться от особенностей работы с регулярными выражениями, с базами данных и иже с ними, будем делать обычные циклы и вывод на страницу:
Вариант 1
<?php $x=1; for ($i = 1; $i<=1000000; $i++) { $x++; } print $x; ?>
#!/usr/bin/perl print "Content-type: text/html\n\n"; my $x=1; for ($i = 1; $i<=1000000; $i++) { $x++; } print $x;
Вариант 2
<?php $x=1; for ($i = 1; $i<=1000; $i++) { $x++; } print $x; ?>
#!/usr/bin/perl print "Content-type: text/html\n\n"; my $x=1; for ($i = 1; $i<=1000; $i++) { $x++; } print $x;
Вариант 3
<?php for ($i = 1; $i<=1000; $i++) { print $i."<br/>"; } ?>
#!/usr/bin/perl print "Content-type: text/html\n\n"; for ($i = 1; $i<=1000; $i++) { print $i."<br/>"; }
Вариант 4 (радикальный)
<?php ?>
#!/usr/bin/perl print "Content-type: text/html\n\n";
mod_perl
Ставим mod_perl. Это, конечно, отдельная история для разных дистрибутивов. Я, как несложно догадаться, делаю следующее:
sudo aptitude install libapache2-mod-perl2
И проверить, чтобы в конфиге была строка
LoadModule perl_module /usr/lib/apache2/modules/mod_perl.so
Проверить то, что модуль работает, можно многими способами. Один из них – глянуть в error.log после старта Apache. Там должно быть что-то такое:
[Tue Feb 16 12:49:31 2010] [notice] Apache/2.2.11 (Ubuntu) PHP/5.2.6-3ubuntu4.5 with Suhosin-Patch mod_perl/2.0.4 Perl/v5.10.0 configured — resuming normal operations
Далее сделаем для наших тестов директорию /var/www/perl и поднимем virtual host:
Listen 82 <VirtualHost *:82> ServerAdmin webmaster@localhost DocumentRoot /var/www/ <Directory /> Options FollowSymLinks AllowOverride None </Directory> <Directory "/var/www/perl/"> AllowOverride None Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch Order allow,deny Allow from all </Directory> <Location /perl/perl> SetHandler cgi-script </Location> <IfModule mod_perl.c> <Location /perl/perlreg> SetHandler perl-script PerlResponseHandler ModPerl::Registry PerlOptions +ParseHeaders Options +ExecCGI PerlSendHeader On </Location> <Location /perl/perlrun> SetHandler perl-script PerlResponseHandler ModPerl::PerlRun PerlOptions +ParseHeaders Options +ExecCGI PerlSendHeader On </Location> </IfModule> ErrorLog /var/log/apache2/error.log LogLevel warn CustomLog /var/log/apache2/access.log combined </VirtualHost>
Согласно нашему конфигу, скрипт perl будет выполняться как обычный cgi-скрипт модулем mod_cgi, скрипт perlreg будет выполняться модулем mod_perl с хэндлером ModPerl::Registry, а скрипт perlrun – модулем mod_perl с хэндлером ModPerl::PerlRun. Также в директорию perl подсунем наш PHP-сценарий test.php. Само собой, скрипты perl, perlreg и perlrun будут одинаковы и содержать перловый скрипт из соответствующего варианта.
Тут надо сделать небольшое отступление, чтобы коснуться различий между хэндлерами Registry и PerlRun.
Чем отличаются ModPerl::Registry и ModPerl::PerlRun
Apache::Registry приводит скрипт к некоторому виду, при котором вся программа помещается в функцию handler() обработчика событий, а функция затем уже вызывается для входящих сообщений. Функция exit(), завершающая процесс perl, в тексте заменяется на Apache::exit(). Главный подводный камень здесь – необходимость обнуления переменных или объявления их в локальном контексте, так как процесс не завершается, пока работает Apache.
Хэндлер ModPerl::PerlRun как раз позволяет использовать сценарии, написанные под mod_cgi, не задумываясь об их доработке, так как при завершении сценария все переменные удаляются.
В качестве примера можно как раз привести мой тестовый скрипт. При работе mod_perl с хэндлером Apache::Registry, если несколько раз обновить страницу браузера со скриптом из варианта 1 с убранной строкой my $x, то каждый раз будет выводиться значение на миллион больше предыдущего. То есть модуль не прощает таких неаккуратно написанных сценариев. В идеале скрипт должен выглядеть так:
#!/usr/bin/perl use strict; print "Content-type: text/plain\n\n"; my $x; for (my $i = 1; $i<=1000000; $i++) { $x++; } print $x;
Вообще, подобный стиль достаточно важен. Если, к примеру, в указанном скрипте убрать строку my $x; (естественно, и use strict тоже), то его быстродействие упадёт почти вдвое. Проверено одним из тестов.
FastCGI
FastCGI – это другая сторона вопроса об ускорении работы web-сценариев. И речь, конечно, не только о Perl, но и о любых языках с поддержкой сокетов. FastCGI, как и mod_perl, отменяет необходимость перезапуска CGI-программы для нового запроса. Он позволяет сохранить процесс запущенным и передавать ему запросы. Кроме этого FastCGI обладает набором других преимуществ. К ним можно отнести, например, возможность запускать FastCGI-процессы на любом другом сервере в сети, а с web-сервером взаимодействовать по TCP/IP или Unix Domain Sockets (вместо стандартного STDOUT в CGI). Ещё одним преимуществом можно назвать возможность обработки запросов несколькими FastCGI-процессами параллельно.
Итак, ставим перловый модуль и модуль для Apache:
cpan -i FCGI apt-get install libapache2-mod-fcgid
mod_fcgid – это модуль, совместимый с mod_fastcgi. Он распространяется через репозитории Ubuntu.
Добавляем в конфиг виртуального хоста в Apache:
<IfModule mod_fcgid.c> IdleTimeout 300 SocketPath /var/lib/аpache2/fcgid/sock <Location /perl/fastcgi> SetHandler fcgid-script </Location> </IfModule>
Теперь, как уже выше говорилось, придётся слегка подправить Perl-скрипты для работы с FastCGI. Например, скрипт из варианта 1 примет такой вид:
#!/usr/bin/perl use FCGI; while (FCGI::accept() >= 0) { print "Content-type: text/html\n\n"; my $x=1; for ($i = 1; $i<=1000000; $i++) { $x++; } print $x; }
Назовём этот скрипт fastcgi (именно это я прописал в конфиге Apache) и проверим на запуск. Работает.
Бенчмарк
Приступим к тестам. Будем использовать по 10000 посылок в 30 параллельных потоков, то есть будут выполняться команды:
ab -n 10000 -c 30 http://localhost:82/perl/test.php ab -n 10000 -c 30 http://localhost:82/perl/perl ab -n 10000 -c 30 http://localhost:82/perl/perlreg ab -n 10000 -c 30 http://localhost:82/perl/perlrun ab -n 10000 -c 30 http://localhost:82/perl/fastcgi
Смотреть будем на количество запросов в секунду, на среднее время ответа и на съедаемую память. Все замеры (серьёзные люди всё-таки и студенческая жизнь с лабораторными работами ещё свежа в памяти) будем производить по три раза, а результаты усреднять.
А результаты таковы:
Вариант 1 | Вариант 2 | Вариант 3 | Вариант 4 | |||||||||
Req per sec | Time per req (ms) | Mem (Mb) | Req per sec | Time per req (ms) | Mem (Mb) | Req per sec | Time per req (ms) | Mem (Mb) | Req per sec | Time per req (ms) | Mem (Mb) | |
PHP | 8.2 | 3670 | 6 | 1808 | 16.6 | 6 | 1054 | 28.4 | 5 | 2856 | 10.5 | 8 |
Perl (mod_cgi) | 11.6 | 2589 | 6 | 508 | 59.0 | 11 | 456 | 65.0 | 11 | 561 | 53.4 | 11 |
mod_perl::Registry | 8.9 | 3369 | 50 | 1785 | 16.8 | 22 | 1305 | 22.9 | 21 | 2186 | 13.7 | 21 |
mod_perl::PerlRun | 9.1 | 3285 | 40 | 1143 | 26.2 | 13 | 834 | 35.9 | 14 | 1358 | 22.0 | 13 |
FastCGI | 11.3 | 2652 | 26 | 2031 | 14.7 | 20 | 656 | 45.0 | 32 | 3082 | 9.7 | 3 |
Выводы
Довольно сложно здесь делать какие-то конкретные выводы (особенно поленившись сделать наглядные графики), но в целом тенденция ясна.
Если время работы скрипта заметно больше накладных расходов на обработку события, запуск интерпретатора и т.д., то есть на первое место выходит скорость выполнения операций, то наибольшей производительностью обладает Perl-сценарий под модулем mod_cgi. А все технологии его ускорения только замедляют его (неожиданно, да?). И если с mod_perl здесь всё ясно – обычный интерпретатор Perl явно гораздо лучше оптимизирован и проработан, чем сторонний апачевский модуль, то c FastCGI ясности нет. Впрочем, и отставание не особо большое. PHP здесь в аутсайдерах.
В скрипте, где происходит в цикле инкремент переменной, но время его работы мало, лидируют FastCGI и PHP. Perl с модулем mod_cgi почему-то по скорости выполнения отстаёт в разы (перепроверял значение множество раз). Вероятно, много времени тратится именно на выделение памяти под переменную. Непонятно. Главное, что здесь наконец-то начинает быть видна полезность от mod_perl и FastCGI.
В сценарии, где происходит вывод на страницу, на первое место выходит mod_perl с хэндлером ModPerl::Registry. PHP слегка отстаёт, а все остальные варианты разочаровывают. Без знания внутренней архитектуры модулей и поллитры в нагрузку не разобраться.
В совсем голом сценарии, где скрипт ничего не делает, и основную роль играют именно накладные расходы на обслуживание приложения, в лидеры опять выбрался FastCGI с дышащим ему в затылок PHP. mod_cgi вполне ожидаемо оказался медленнее в разы.
Судя по всему, придётся подвести нежеланный, но ожидаемый итог – никакой из вариантов не оказался круче других. Хотя формально по очкам должен, конечно, победить FastCGI. PHP везде держится молодцом, да к тому же и ест заметно меньше памяти. Видимо, при отсутствии тяжёлых вычислений на сервере, он окажется наиболее оптимальным вариантом.
Для себя же я решил, что лучшая основа – это Perl с mod_cgi, а лёгкие страницы, вроде ответов на Ajax, лучше реализовывать с FastCGI.
Впрочем, всегда можно на конкретном сайте провести конкретные исследвания. Метод известен.
Ссылки
http://httpd.apache.org/mod_fcgid/
http://perl.apache.org/
http://xpoint.ru/forums/programming/perl/mod_perl/faq.xhtml
Tags: Apache, mod_fcgid, mod_perl, Perl
Category:
Apache, Perl, Web-dev |
No comments »