Ускоряем web-сценарии на Perl, или ещё раз про Perl vs PHP

Date February 17th, 2010 Author Vitaly Agapov

“Концепция единообразия жизни позволяет наслаждаться каждым моментом, не отвлекаясь на сопоставление и сравнение.”
— х/ф “Эквилибриум”

Two_fast_two_furious_ver5Идея этой статьи родилась совершенно случайно после пары бесед с адептами 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: , , ,
Category: Apache, Perl, Web-dev | No comments »

Comments

Leave a comment

 Comment Form