Когда во сне снится «ой а если сервера не хватит...»
Для начала, Доброй Ночи. Пишу что-то полезное вроде впервые (если не считать разного рода полу-тестов в моём блоге). Человек я допытливый до жути, неожиданно в голову пришло, что могу помочь сэкономить кому-то много времени ;).
В общем когда на PHP создаются достаточно большие проекты (>100000 строк кода) желание сделать «правильно» то, что было сделано давно грозит повергнуть всё в хаос. По крайней мере для новых программистов, которые могут прийти в компанию через неделю, месяц, год… Решение — четкая систематизация с самого начала и установление жестких архитектурных правил. Для себя я решил — не используя фреймворки писать буду только «Hello World»-сайты. Не мудрствуя лукаво когда подумал о фреймворках полистал, почитал, но решил отдаться-таки зенду с его ZendFramework. Добротный он, хотя и изменений я в нём для себя сделал огромное количество.
В таком решении на ряду со всеми возможными плюсами и удобством неожиданно встаёт вопрос-стена: теперь у меня бизнес логика занимает, наверное, где-то вовсе 1-2% от времени исполнения всей программы. Плата за удобство и ООП (или «удобство ООП»? Наверное даже просто «удобство» или просто «ООП» — это почти одно и то же ;)) — огромное количество сопутствующего и управляющего кода.
В общем когда я делал новый проект — была цель — не менее 50 запросов в секунду на захудалом Celeron 2.6GHz. Т.е. около 0.02сек на запрос, включая mysql и так далее тому подобное. За время создания проекта я его умудрялся разгонять в несколько раз какими-то улучшениями. Какими? Налейте чашечку кофе — и добро пожаловать в мир мудрого девелопинга :) Сразу скажу — получилось.
Оптимизация от А до Я. Рецепт супчика от MockSoul :)
Этап 0. Готовимся
Окружение? Моя наилюбимейшая схема:
- LigHTTPd. Под линуксом. Со включенным sys-epoll;
- 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 не идеален :).
- Opcode Cacher. Или кешер байткода. Или «что за дибилизм — парсить одни и те же файлы при каждом запросе?!». Очень (ОЧЕНЬ!) рекомендую APC (Alternative PHP Cache) который лежит в PECL. Можно так же eAccelerator или даже ZendOptimizer. Вкусы разные бывают… Но при выборе между eAccelerator и APC — я рекомендую APC. Почему? Да хотя бы за возможность положить что угодно в shmem сегмент :). Ниже расскажу.
Этап 1. Пишем
Сначала пишем. Пишем и крутим в голове мысли о том как что-то сделать более разумным и быстрым сразу. Чтобы потом не отвлекаться (вообще это наверное совершенно естественное желание любого уважающего себя программиста %))
Моменты на которые сразу нужно обращать внимание:
- Вам, наверное, почти не нужно будет использовать require и include. В основном — require_once и include_once.
- Для итерации по массивам, их изменению и фильтрации — учимся использовать 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 // ) // А вы бы сделали это циклом? Ай-ай-ай... ?>
- Передача переменной по ссылке (например $a=1; call_func(&$a)) — не влияет на быстродействие. Передача массивов по ссылке — влияет чуть-чуть. Передача классов — влияет очень. Я это к тому — что не передавайте ничего по ссылке надеясь ускорить программу. Передавайте по ссылкам только когда вам это _действительно_ нужно
- Делайте классы статическими если можно. Т.е. если для работы класса закрытая инстанция в общем-то и не нужна.
- Комментировать можно сколько хочется — кешер байткода все равно комментарии игнорирует. На быстродействие это влияет… хм… на 0.000001% :)
- Избегайте глубоких рекурсий. Стандартную задачу — взять список файлов включая поддирректории можно сделать и без рекурсии вовсе =)
- Прочитайте грамотные доки. Документацию того же ZendFramework — там много чего полезного даже тем кто фреймворк не использует и использовать не собирается
- Старайтесь делить код на логические блоки. Так, чтобы можно было взять 10-20 строчек подряд и сказать — вот тут я делаю ТОЛЬКО ЭТО. Взять другие 10-20 — и сказать а тут я делаю ТОЛЬКО ДРУГОЕ. Кол-во строчек которые надо брать, конечно, зависит от вас. Но лучше чтобы блоки были не более чем по 30-40 строк. Разбивайте программу и любой блог на инициализацию, настройку, работу, сохранение результата (в переменную скажем). При чём тут скорость? Через полгода поймёте ;).
- О том «Может сделать мне $a = „some $v inline“ или $a = „some“. $v. „var“ даже думать не стоит. Лично я (имхо) нахожу абсолютно дибильным вставку переменных прямо в строки. Лучшая читаемость:
- $var = 'some'. $in. 'li'. $ne. ' variable';
- $var = sprintf('some %sli%s variable', $in, $li);
- Используйте константы для того что никогда не меняется. Они парсятся в самом начале и лежат вообще в другом куске памяти чем обычные переменные. Конструкции вида $str = 'some'. STR_CONSTANT и выглядят к тому же лучше. Особо грамотно — перенос строки. Обзывают его по-разному, я же люблю NL (NewLine) или CRLF(CarretReturnLineFeed)
- Не забывайте что foreach может и не делать копию массива :)
foreach ($arr as $key => <b>&$val</b>) { ... }
- Как это ни парадоксально но вот такой момент меня в пхп совсем убивает: is_null() — придумана идиотом. if (null === $var) или if ($var === null) быстрее чем if (is_null($var))… дибилизм. Не используйте is_null() :)
- Регулярные выражения, работу со строками с помощью str_* функций и прочее оставляю на вашей совести как выходящее за рамки этой и без того раздутой статьи :)
Этап 2. Размышляем о возможных тратах времени
Так… вот написали вы чего-нибудь. А теперь давайте посмотрим что обычно отнимает достаточно дофига времени без вашей бизнес-логики:
- Коннект к БД
- Обработка тонны require_once и include_once
- Сами запросы к БД
- Где-то храним конфиг и парсим его каждый раз? Используем модели БД и инициализируем их каждый раз? Вообще посмотрите как много одинакового мы делаем каждый запрос!!
- Что-то делаем с файловой системой? А зачем? Лично я думаю что можно чуть ли не любой проект написать с вообще отсутствующим 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. Устали размышлять о тратах времени. Хотим график перед глазами!
Это для меня оказалось весьма ценным открытием. В общем пошаговый гид:
- Нам нужен PECL APD (Advanced PHP Debugger)
- Конфигурим dumpdir для apd в конфиге. Что-то вроде:
zend_extension=/usr/lib/php5/lib/php/extensions/no-debug-non-zts-20060613/apd.so apd.dumpdir="/tmp/php-apd-dump"
- В самом главном файлике пишем в сааамом верху
apd_set_pprof_trace();
, тем самым включая дамп профилера - Делаем 1-100 запросов на сервер. Каждый раз будет сохранятся новый файлик в нашей /tmp/php-apd-dump
- Теперь мы можем смотреть результаты профилера либо прямо в консоли — вместе с apd идёт скриптик pprofp
- А ещё можем сделать супервещь — преобразовать в более унифицированный формат :). С 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