Pull to refresh

Кропотливая оптимизация PHP-приложений (рассматриваю PHP5, но большинство справедливо и для 4-й ветки)

Website development *

Когда во сне снится «ой а если сервера не хватит...»


Для начала, Доброй Ночи. Пишу что-то полезное вроде впервые (если не считать разного рода полу-тестов в моём блоге). Человек я допытливый до жути, неожиданно в голову пришло, что могу помочь сэкономить кому-то много времени ;).




В общем когда на PHP создаются достаточно большие проекты (>100000 строк кода) желание сделать «правильно» то, что было сделано давно грозит повергнуть всё в хаос. По крайней мере для новых программистов, которые могут прийти в компанию через неделю, месяц, год… Решение — четкая систематизация с самого начала и установление жестких архитектурных правил. Для себя я решил — не используя фреймворки писать буду только «Hello World»-сайты. Не мудрствуя лукаво когда подумал о фреймворках полистал, почитал, но решил отдаться-таки зенду с его ZendFramework. Добротный он, хотя и изменений я в нём для себя сделал огромное количество.


В таком решении на ряду со всеми возможными плюсами и удобством неожиданно встаёт вопрос-стена: теперь у меня бизнес логика занимает, наверное, где-то вовсе 1-2% от времени исполнения всей программы. Плата за удобство и ООП (или «удобство ООП»? Наверное даже просто «удобство» или просто «ООП» — это почти одно и то же ;)) — огромное количество сопутствующего и управляющего кода.


В общем когда я делал новый проект — была цель — не менее 50 запросов в секунду на захудалом Celeron 2.6GHz. Т.е. около 0.02сек на запрос, включая mysql и так далее тому подобное. За время создания проекта я его умудрялся разгонять в несколько раз какими-то улучшениями. Какими? Налейте чашечку кофе — и добро пожаловать в мир мудрого девелопинга :) Сразу скажу — получилось.


Оптимизация от А до Я. Рецепт супчика от MockSoul :)



Этап 0. Готовимся



Окружение? Моя наилюбимейшая схема:


  1. LigHTTPd. Под линуксом. Со включенным sys-epoll;

  2. PHP5. Через FastCGI. PHP должен быть собран с поддержкой CGI, sharedmem (или threads, лучше sharedmem — а и то и другое сразу не скомпилится ;)). Дикий пример с чем я собираю пхп:


    ./configure' '--prefix=/usr/lib/php5' '--host=i686-pc-linux-gnu' '--mandir=/usr/lib/php5/man' '--infodir=/usr/lib/php5/info' '--sysconfdir=/etc' '--cache-file=./config.cache' '--disable-cli' '--enable-cgi' '--enable-fastcgi' '--disable-discard-path' '--disable-force-cgi-redirect' '--with-config-file-path=/etc/php/cgi-php5' '--with-config-file-scan-dir=/etc/php/cgi-php5/ext-active' '--without-pear' '--disable-bcmath' '--with-bz2' '--disable-calendar' '--disable-ctype' '--without-curl' '--without-curlwrappers' '--disable-dbase' '--disable-exif' '--without-fbsql' '--without-fdftk' '--disable-filter' '--disable-ftp' '--with-gettext' '--without-gmp' '--disable-hash' '--disable-ipv6' '--disable-json' '--without-kerberos' '--enable-mbstring' '--with-mcrypt' '--without-mhash' '--without-msql' '--without-mssql' '--with-ncurses' '--with-openssl' '--with-openssl-dir=/usr' '--disable-pcntl' '--without-pgsql' '--without-pspell' '--without-recode' '--disable-simplexml' '--enable-shmop' '--with-snmp' '--disable-soap' '--enable-sockets' '--without-sybase' '--without-sybase-ct' '--disable-sysvmsg' '--disable-sysvsem' '--disable-sysvshm' '--with-tidy' '--disable-tokenizer' '--disable-wddx' '--disable-xmlreader' '--disable-xmlwriter' '--without-xmlrpc' '--without-xsl' '--disable-zip' '--with-zlib' '--disable-debug' '--enable-dba' '--without-cdb' '--without-db4' '--without-flatfile' '--with-gdbm' '--without-inifile' '--without-qdbm' '--with-freetype-dir=/usr' '--with-t1lib=/usr' '--disable-gd-jis-conv' '--with-jpeg-dir=/usr' '--with-png-dir=/usr' '--without-xpm-dir' '--with-gd' '--with-ldap' '--without-ldap-sasl' '--with-mysql=/usr' '--with-mysql-sock=/var/run/mysqld/mysqld.sock' '--without-mysqli' '--without-pdo-dblib' '--with-pdo-mysql=/usr' '--without-pdo-odbc' '--without-pdo-pgsql' '--without-pdo-sqlite' '--with-readline' '--without-libedit' '--with-mm' '--without-sqlite'


    Грамотно прикручиваем к lighttpd, а не абы как:


    fastcgi.server = (
        ".php" => (
            "localhost" => (
                <b>"socket"          => "/tmp/php5-gmru-sandbox-mocksoul-lighttpd.sock" [#1]</b>,
                <b>"bin-path"        => "/usr/lib/php5/bin/php-cgi -c " + "/path/to/application/config/php_config_dir" [#2]</b>,
                <b>"min-procs"       => 1 [#3]</b>,
                <b>"max-procs"       => 1 [#3]</b>,
                "bin-environment" => (
                    <b>"PHP_FCGI_CHILDREN" => "32" [#4]</b>,
                    <b>"PHP_FCGI_MAX_REQUESTS" => "3200" [#5]</b>
                )
            )
        )
    )



    ([#1], [#2],… — так буду ссылаться на комментарии к коду. Если хотите взять код — такие пометки надо будет стереть. Ниже в коде буду придерживаться такой же схемы)


    • [#1] — unix-сокеты много шустрее чем tcp-сокеты. Так что используйте их только если в TCP нет серьёзной необходимости (или, хаха, под Windows :))
    • [#2] — тут я просто показал пример как можно конфиг пхп прикручивать к разному хосту (через -c указываем на папку с php.ini)
    • [#3] — min-procs и max-procs ДОЛЖНЫ БЫТЬ = 1!!! Почему? Потому что далее я скажу про кеширование байткода. Кеш будет нелогичен при кол-ве процессов пхп более 1
    • [#4] — магический танец. Просим php запустить 32 потока в одном процессе для обработки запросов от lighttpd. Важно: если поставить, например, 10 и все 10 будут заняты каким-то диким 10-секундно-выполняющимся-скриптом — lighttpd будет отдавать 500 ошибку! Т.е. количество потоков не увеличивается в реалтайме — ставьте 32, 64 или, даже, 128 (работает это как threadpool)
    • [#5] — просим убить поток и создать новый через энное количество запросов. На всякий случай, ведь php не идеален :).

  3. Opcode Cacher. Или кешер байткода. Или «что за дибилизм — парсить одни и те же файлы при каждом запросе?!». Очень (ОЧЕНЬ!) рекомендую APC (Alternative PHP Cache) который лежит в PECL. Можно так же eAccelerator или даже ZendOptimizer. Вкусы разные бывают… Но при выборе между eAccelerator и APC — я рекомендую APC. Почему? Да хотя бы за возможность положить что угодно в shmem сегмент :). Ниже расскажу.



Этап 1. Пишем



Сначала пишем. Пишем и крутим в голове мысли о том как что-то сделать более разумным и быстрым сразу. Чтобы потом не отвлекаться (вообще это наверное совершенно естественное желание любого уважающего себя программиста %))


Моменты на которые сразу нужно обращать внимание:


  1. Вам, наверное, почти не нужно будет использовать require и include. В основном — require_once и include_once.
  2. Для итерации по массивам, их изменению и фильтрации — учимся использовать array_* функции в пхп. Особенно лямбда-функции:
    <?php
            
    $arr = array('that', 'is', 'this');
    array_walk($arr, create_function('&$v,$k', '$v = $v . " yeah";');
    print_r($arr);
    
    // outputs:
    // Array
    // (
    //   [0] => that yeah
    //   [1] => is yeah
    //   [2] => this yeah
    // )
    
    // А вы бы сделали это циклом? Ай-ай-ай...
    
    ?>

  3. Передача переменной по ссылке (например $a=1; call_func(&$a)) — не влияет на быстродействие. Передача массивов по ссылке — влияет чуть-чуть. Передача классов — влияет очень. Я это к тому — что не передавайте ничего по ссылке надеясь ускорить программу. Передавайте по ссылкам только когда вам это _действительно_ нужно
  4. Делайте классы статическими если можно. Т.е. если для работы класса закрытая инстанция в общем-то и не нужна.
  5. Комментировать можно сколько хочется — кешер байткода все равно комментарии игнорирует. На быстродействие это влияет… хм… на 0.000001% :)
  6. Избегайте глубоких рекурсий. Стандартную задачу — взять список файлов включая поддирректории можно сделать и без рекурсии вовсе =)
  7. Прочитайте грамотные доки. Документацию того же ZendFramework — там много чего полезного даже тем кто фреймворк не использует и использовать не собирается
  8. Старайтесь делить код на логические блоки. Так, чтобы можно было взять 10-20 строчек подряд и сказать — вот тут я делаю ТОЛЬКО ЭТО. Взять другие 10-20 — и сказать а тут я делаю ТОЛЬКО ДРУГОЕ. Кол-во строчек которые надо брать, конечно, зависит от вас. Но лучше чтобы блоки были не более чем по 30-40 строк. Разбивайте программу и любой блог на инициализацию, настройку, работу, сохранение результата (в переменную скажем). При чём тут скорость? Через полгода поймёте ;).
  9. О том «Может сделать мне $a = „some $v inline“ или $a = „some“. $v. „var“ даже думать не стоит. Лично я (имхо) нахожу абсолютно дибильным вставку переменных прямо в строки. Лучшая читаемость:
    • $var = 'some'. $in. 'li'. $ne. ' variable';
    • $var = sprintf('some %sli%s variable', $in, $li);

  10. Используйте константы для того что никогда не меняется. Они парсятся в самом начале и лежат вообще в другом куске памяти чем обычные переменные. Конструкции вида $str = 'some'. STR_CONSTANT и выглядят к тому же лучше. Особо грамотно — перенос строки. Обзывают его по-разному, я же люблю NL (NewLine) или CRLF(CarretReturnLineFeed)
  11. Не забывайте что foreach может и не делать копию массива :)
    foreach ($arr as $key => <b>&$val</b>) { ... }

  12. Как это ни парадоксально но вот такой момент меня в пхп совсем убивает: is_null() — придумана идиотом. if (null === $var) или if ($var === null) быстрее чем if (is_null($var))… дибилизм. Не используйте is_null() :)
  13. Регулярные выражения, работу со строками с помощью str_* функций и прочее оставляю на вашей совести как выходящее за рамки этой и без того раздутой статьи :)

Этап 2. Размышляем о возможных тратах времени



Так… вот написали вы чего-нибудь. А теперь давайте посмотрим что обычно отнимает достаточно дофига времени без вашей бизнес-логики:

  1. Коннект к БД
  2. Обработка тонны require_once и include_once
  3. Сами запросы к БД
  4. Где-то храним конфиг и парсим его каждый раз? Используем модели БД и инициализируем их каждый раз? Вообще посмотрите как много одинакового мы делаем каждый запрос!!
  5. Что-то делаем с файловой системой? А зачем? Лично я думаю что можно чуть ли не любой проект написать с вообще отсутствующим IO (конечно, кроме того что будет использовать БД и тп). Не нужно ничего хранить в файловой системе. Мелкое. Большое (какой-нибудь гиговый проиндексированный файл) — нужно


Это я все отсортировал по важности. А теперь по порядку по каждому ненасытному моменту:

Коннект к БД


Всё просто — если владеете сервером — используйте постоянные подключения! PDO_MYSQL, MYSQL — все это умеют )

Обработка тонны require_once и include_once


Вот тут начинается веселье =). Для начала я взял посмотрел сколько файлов у меня включаются при ЛЮБОМ запросе в ZendFramework. Оказалось — чуть менее 300 (!!!!). Если не использовать байткод кешер — это будет вообще ненормально долгая процедура.

Решение „влоб“ нашлось само собой — запихать всё это в один файл. Встал вопрос — а как узнать что у нас всегда инклудится — а что иногда? Вообще размышлять в тот момент особо времени не было поэтому и этот аспект я решил „влоб“ )

Дикий результат — http://www.mocksoul.ru/pub/dev/mkzend.phps

Там:
  • Насколько часто обращение к файлу — смотрим через APC кеш по статистике
  • Рисуем табличку
  • Изменяем зенд автоматом :). Типа вырезаем все require_once, комментарии, открывающие и закрывающие пхп теги, лишние пробелы… издеваемся короче :) Смотрите исходник
  • Сохраняем получившеся гигантский скрипт в файлик… )


Скрипт абсолютно нестабилен и заточен под один проект. Запускать надо через браузер, чтобы APC отработал. Просто как пример. У вас работать не будет со 100% вероятностью =).

Как оказалось — 300 файлов парсились 2 сек, из байткешера вытаскивались за 0.3 сек, а сгенерированый суперфайл большой парсится 0.7сек а из кеша вытягивается за 0.003сек. Проект сразу разогнался почти в 3 раза :). Маньячная оптимизация, однако. Метод подходит для production-сервера, т.к. девелопить библиотеки которые из другого файла грузятся — невозможно.

Запросы к БД


Пройдите экскурс в ДБА и начните, наконец, использовать MYSQL_QUERY_CACHE. В my.cnf пишем query_cache_size = 100M. За кешем следим путём show status like 'qcache%'. Ещё очень плотно читаем доки MySQL относительно Query Cache


Хватит делать одно и тоже — кешируйте!



Прочитали конфиг? Распарсили? Получили готовенький массив? Ну и зачем парсить его снова? ) У вас же есть — shared memory под рукой в виде APC! :) Невероятно быстрая скорость работы… Храните в нём все что только можно — конфигурацию, собранные объекты, результаты запросов а-ля „describe table“ (это прерогатива Zend_Db_Table_*). Из кеша данные берутся с невообразимой скоростью — 0.000001с где-то. В памяти, если не дублировать ничего, можно сохранить просто дофига данных. Помните, что 1 гиг — это огромная куча возможной информации. Не используйте IO в файловую систему для этого — лучше память. В зависимости от вашей квалификации — от 10 до 100% прироста скорости. Смотрите ниже про APD ;)

Зачем вам ФС?



Используйте ФС как хранитель чего угодно, только если это не влазит в память. Даже если пишите лог или статистику запросов — ложите в APC! И сохраняйте, скажем, каждые 5 минут на винт.

Этап 3. Устали размышлять о тратах времени. Хотим график перед глазами!



Это для меня оказалось весьма ценным открытием. В общем пошаговый гид:

  1. Нам нужен PECL APD (Advanced PHP Debugger)
  2. Конфигурим dumpdir для apd в конфиге. Что-то вроде:
    zend_extension=/usr/lib/php5/lib/php/extensions/no-debug-non-zts-20060613/apd.so
    apd.dumpdir="/tmp/php-apd-dump"
  3. В самом главном файлике пишем в сааамом верху apd_set_pprof_trace();, тем самым включая дамп профилера
  4. Делаем 1-100 запросов на сервер. Каждый раз будет сохранятся новый файлик в нашей /tmp/php-apd-dump
  5. Теперь мы можем смотреть результаты профилера либо прямо в консоли — вместе с apd идёт скриптик pprofp
  6. А ещё можем сделать супервещь — преобразовать в более унифицированный формат :). С APD кроме pprofp есть ещё pprof2calltree. Она преобразует дампы профилера в формат, понимаемый cachegrind и KCacheGrind в частности. Полученный файлик открываем в kcachegrind — и рукоплещем от удовольствия.


В целом — обычный такой профилер получается. Вот только для PHP я раньше такого не делал ;)

Этап 4. Проверяем



Проверять скорость простыми запросами на 1 урл при помощи ab или ab2 — глупо.

Более логичный вариант — сделать список всех (или не всех ;)) урлов, положить в текстовый файлик, взять Siege и тестить. Во время теста следить за TPS (TransactionsPerSecond) на винты (например при помощи iostat из пакета sysstat), следить за загрузкой процессоров, смотреть чтобы в конце не было ответов сервера отличных от 2хх.

Зачем это всё


Так сильно пытаться все ускорить нужно когда проект разрастается. Увеличение быстродействия на 10% на 1 сервере даёт прирост в скорости равный 10%. А если у вас уже 10 серверов — то 10%-ое увеличение быстродействие будет равно добавлению ещё одного 11-го сервера. Т.е. +100% в пересчете на 1 сервер. Это много. Это деньги. И это более высокий порог входа для конкурентов ;).

Ээээ


2 дня назад сломал ключицу. И писал всё это одной рукой. Памятник мне!!! :))

Kind Regards, Vadim Burmakin aka MockSoul © 2007
Tags:
Hubs:
Total votes 99: ↑95 and ↓4 +91
Views 8.4K
Comments Comments 147