| ||
Содержание:
Достижение высокой производительности
Узкие места
Сеть
Центральный процессор
Shared Memory (общедоступная память)
Файловая система
Управление процессами
Соединение с другими серверами
Когда начинать оптимизацию?
Настройка вашего сервера для PHP
Apache 1.3/2.0
Настройка IIS
Высока производительность в Windows: IIS и FastCGI
PHP 4 и Zend Engine
HTML кэширование с использованием PEAR Cache
Использование тестов производительности
Этапы тестирования производительности
Перегрузка при 40 подключениях
Исправляем ситуацию
Заключение
Оптимизация кода
Высокие степени оптимизации
Оптимизации объектно-ориентированной программы
Дополнительные детали
Резюме
Бесполезная оптимизация
Ускоряют ли ссылки ваш код?
PHP очень быстрый язык программирования, но есть еще множество способов оптимизации, помимо оптимизации кода.
В этом материале мы объясним, почему оптимизация PHP захватывает собой гораздо больше факторов, нежели простая оптимизация кода, и почему настройка PHP требует понимания, каким образом работает PHP относительно других компонентов вашего сервера. Также мы займемся выявлением узких мест, связанных с этими компонентами и устранением их. Также мы затронем вопросы оптимизации ваших PHP скриптов, чтобы они работали еще быстрее.
Когда мы говорим о хорошей производительности, мы не говорим о том, насколько быстро ваши скрипты будут запускаться. Производительность это набор компромисов между масштабируемостью и скоростью. Скрипты, настроенные, чтобы использовать минимум ресурсов, могут быть медленнее, чем скрипты, использующие кэширование, потомучто больше копий одного и того же скрипта может быть запущено одновременно на одном сервере.
В примере ниже, A.php спринтер настроен для быстрого запуска, а B.php бегун марафона может бегать трусцой, почти с той же самой скоростью. При низкой загрузке, A.php существенно быстрее, но как только трафик начинает увеличиваться, работа B.php снижается лишь немного, тогда как A.php выдыхается.
Давайте возьмем более реалистичный пример для дальнейших рассуждений. Предположим, нам необходимо написать скрипт, который должен прочитать файл размером 250 кб и сгенерировать HTML, резюмирующий информацию из файла. Мы пишем два сценария, которые делают одно и тоже: hare.php, который читает файл в память целиком и обрабатывает его за один проход, и tortoise.php, который читает файл построчно не сохраняя в памяти информации, больше чем одна строка. Tortoise.php будет медленнее, потомучто использует многократное чтение, требует большего количества системных вызовов.
Hare.php требует 0.04 секунды центрального процессора, а также 10 мб памяти. Tortoise.php требует 0.06 секунд процессорного времени и 5 мб памяти. Сервер располагает 100 мб свободной памяти и процессор на 99% свободен. Предположим, что никакой фрагментации памяти не происходит (для упрощения).
При 10 одновременных запусках скрипта, hare.php исчерпает память (10 х от 10 до 100). В этой точке, tortoise.php все еще будет иметь в своем распоряжении 50 мб свободной памяти. 11 запуск hare.php приведет к уменьшению производительности, поскольку система начнет использовать виртуальную память. При этом, его первоначальная скорость может упасть более чем в половину. Каждый запуск hare.php на данный момент занимает 0.08 секунд процессорного времени. Тем временем tortoise.php все также будет занимать свое процессорное время 0.06 секунд.
В таблице ниже, самый быстрый сценарий выделен полужирным:
Соединения | CPU секунды при запуске 1 скрипта | CPU секунды при запуске 10 скриптов | CPU секунды при запуске 11 скриптов |
Hare.php | 0.04 | 0.40 | 0.88 (свободная память закончилась) |
Tortoise.php | 0.06 | 0.60 | 0.66 |
Вышеприведенные примеры говорят нам о том, что тот, кто хочет получить высокую производительность не пишет просто быстрые скрипты. Для достижения высокой производительности PHP требуется хорошее понимание работы основных аппаратных средств, операционной системы и другого программного обеспечения, типа сервера и базы данных.
Пример hare и tortoise (зайца и черепахи) показал нам причины узких мест. С бесконечной оперативной памятью hare будет быстрее всегда, чем tortoise. К сожалению, вышеупомянутая модель слишком упрощена, и есть еще много других узких мест, препятствующих работе, помимо оперативной памяти:
Ваша сеть, вероятно, самое узкое место. Например, вы говорите, что у вас 10 Мбит связь с интернетом, по которой вы можете скачать 1 мб данных в секунду. Если каждая страница у вас занимает 33 кб, то обычные 33 страницы в секунду будут занимать весь ваш канал.
Более тонкие узкие места в сети это скорость отклика, например, сервера имен (DNS), или адресация нужного количества памяти для программного обеспечения сети.
Если вы контролируете загрузку центрального процессора, то увидите, что пересылка обычных HTML страниц вообще не будет обременять процессор, и, в данном случае, самым узким местом будет сеть. Однако, для сложных динамических web-страниц, производимых PHP, скорость вашего процессора будет ограничивающим фактором. Наличие нескольких процессоров или фермы серверов может значительно снизить подобное ограничение.
Общедоступная память используется для связи между процессами, и хранит ресурсы, которые делятся между несколькими процессами, типа кэшированных данных и кода. Если общей памяти недостаточно, то любая попытка обратиться к ресурсам, использующим общую память, например, соединение с базой данных, или запуск кода, вызовет ошибку.
Доступ к жесткому диску может быть в 50-100 раз медленнее, чем доступ к данным в оперативной памяти. Кэширование файлов с использованием оперативной памяти могут снизить данное ограничение. Однако, в условиях небольшого количества памяти для системного кэша, работа будет замедляться. Системные файлы так же могут быть сильно фрагментированными, замедляя дисковый доступ. Частое использование симлинков на UNIX системах также приводят к замедлению.
Также печально известны установки поумолчанию для доступа к диску в Linux системах, которые настроены для совместимости, а не для скорости. Используйте команду hdperm, чтобы перенастроить параметры диска вашей Linux системы.
В некоторых операционных системах, например, в Windows, создание нового процесса это медленная операция. Это означает, что CGI процесс, вызываемый для каждой операции, будет работать значительно медленнее. Запуск PHP в multi-threaded (в виде модуля) режиме должно увеличить скорость ответа (примечание: старые версии PHP неустойчивы в данном режиме).
Избегайте переполнения вашего сервера множеством ненужных процессов. Например, если ваш сервер предназначен только для обслуживания сети, избегайте запуска (или даже вообще установки) X-Windows. На Windows-системах избегайте запуска Microsoft Find Fast (компонент офиса), а также всякого рода screensaver'ов. Потомучто все это заканчивается 100% использованием процессора.
Некоторые программы вы можете рассмотреть к удалению, включая неиспользуемые сетевые протоколы, серверы почты, сканеры антивирусов, аппаратные драйверы для мыши, инфракрасного порта и так далее. На Unix: я подразумеваю, что вы общаетесь со своим сервером посредствам SSH. В этом случае, вы можете рассмотреть к удалению следующее:
Также вы можете отключить запуск некоторых программ при загрузке, изменяя файлы запуска, которые обычно хранятся в /etc/init* или /etc/rc*/init* директории.
Также рассмотрите задачи, которые запускаются по крону. Посмотрите, можно ли их удалить или перенести на непиковые периоды.
Если ваш сервер требует услуг, выполняющихся на других серверах, это тоже может стать узким местом. Самый общий пример это медленный сервер базы данных, который обслуживает много сложных SQL запросов от разных серверов сети.
Многие говорят, что оптимизацию лучше отложить до тех пор, пока кодирование не будет закончено. Этот совет имеет смысл, только если вша группа программистов выдает код очень высокого качества и вы уже имеете хорошее чувство рабочих параметров вашего приложения. Иначе вы подвергаете себя риску необходимости переписывать существенные части приложения после проведения испытаний.
Мой совет преже чем начинать проектирование приложения, сделайте некоторые основные эталонные тесты аппаратных средств и программного обеспечения, чтобы получить чувство максимальной работоспособности, которую вы могли бы достигнуть. Тогда, когда вы проектируете и кодируете приложение, держите желательные рабочие параметры в памяти, потому что на каждом этапе пути вам придется идти на компромиссы между производительностью, функциональностью, защитой и гибкостью.
Также выберите хорошие испытательные данные. Если ваша база данных, как ожидается, будет хранить только 100 000 записей, избегайте тестирования ее только со 100 записями, вы будете потом об этом сожалеть. Это когда-то случилось с одним из программистов в моей компании: мы обнаружили медленный код значительно позже, потратив много времени впустую, поскольку пришлось переписать большую часть кода, который работал, но не масштабировался.
Далее, мы рассмотрим с вами, как оптимизировать работу с PHP двух самых распространенных web-серверов, Apache 1.3 и IIS.
Разработчики PHP заявляют, что нет никакой разницы в скорости и преимуществах масштабирования при использовании Apache 2.0 над Apache 1.3, особенно, если PHP установлен в виде модуля.
Этот сервер доступен на Unix и Windows платформах, это самый популярный сервер в мире. Apache 1.3 использует pre-forking (предразветвления) модель для обслуживания сети. После старта, сервер создает дочерние процессы для обслуживания каждого HTTP запроса. Родительский процесс действует как ангел-хранитель, проверяя, что все дочерние процессы работают правильно и координирует их действия. Чем больше HTTP запросов, тем больше дочерних процессов будет порождено, чтобы их обработать. Поскольку HTTP запросы начнут замедляться, родительский процесс уничтожает неактивные дочерние процессы, освобождая ресурсы для новых процессов. Красота данного подхода в том, что это делает Apache чрезвычайно устойчивым. Даже если дочерний процесс рушится, родительский и другие дочерние процессы будут изолированы от сбойного дочернего процесса.
Модель предразветвления не настолько быстра, как другие решения. Но, что касается меня, то это является суматохой ни о чем, на сервере, обслуживающем PHP сценарии, потомучто другие узкие места дадут сбой раньше, чем проблемы Apache станут существенными для сервера. Ошибкоустойчивость и надежность Apache более важный фактор.
Apache 2.0 может работать в модульном режиме (multi-threaded). Мои эталонные тесты показали, что преимуществ этого режима не так уж много. Также будте готовы к тому, PHP расширения несовместимы, например GD и IMAP. Проверено на Apache 2.0.47 (23 октября 2003).
Apcahe сконфигурирован при помощи файла httpd.conf. Следующие параметры особенно важны при настройке дочерних процессов:
Параметр | Поумолчанию | Описание |
MaxClients | 256 |
Максимальное число дочерних процессов, которые может создать сервер. Значение поумолчанию 256 означает, что одновременно может быть обработано 256 HTTP запросов. Любые дальнейшие запросы будут поставлены в очередь. |
StartServers | 5 |
Количество дочерних процессов, которые будут созданы сразу после старта сервера. |
MinSpareServers | 5 |
Число неактивных дочерних процессов, которые должны быть созданы. Если число неактивных процессов падает ниже этого числа, то 1 ребенок создается первоначально, 2 на следующую секунду, 4 еще через секунду, и так далее, пока не будет создано 32 дочерних процесса с интервалом в секунду. |
MaxSpareServers | 10 |
Если создано больше, чем данное число дочерних процессов, то эти дополнительные процессы будут остановлены. |
MaxRequestsPerChild | 0 |
Устанавливает какое число HTTP запросов ребенок должен обработать перед завершением. Если вы устанавливаете ее в 0, то дочерний процесс никогда не умирает. Установите его в пределах от 100 до 10000, если вы подозреваете утечки памяти или неправильно использование ресурсов. |
Для больших сайтов наилучшими значениями будут близкие к этим:
Apache под Windows ведет себя иначе. Вместо того, чтобы использовать дочерние процессы, Apache использует треды. Вышеупомянутые параметры не используются. Вместо этого мы имеем один параметр: ThreadsPerChild который имеет значение поумолчанию 50. эта переменная указывает число тредов, которые могут быть порождены Apache. Поскольку в Winodws есть только один дочерний процесс, то количество HTTP запросов, которые он может обработать, равняется 50. Для серверов сети, которые испытывают более серьезные нагрузки, увеличьте этот параметр от 256 до 1024.
Другие полезные параметры, которые вы можете изменить приведены ниже:
Параметр | Поумолчанию | Описание |
SendBufferSize | Определяется операционной системой. |
Определяет объем буфера вывода (в байтах), используемого в TCP/IP соединениях. Этот параметр прежде всего полезен для переполненных или медленных сетей, когда пакеты необходимо буферизировать. В этом случае, установите этот параметр близким к размеру самого большого пересылаемого файла. Один TCP/IP буфер будет создан при соединении. |
KeepAlive [on|off] | On |
В оригинале HTTP спецификации, каждый HTTP запрос должен создавать новое соединение с сервером. Keep-alive заголовок был создан, чтобы уменьшить нагрузку на сервер. Параметр keep-alive говорит серверу, чтобы он использовал тоже самое соединение через socket (сокет) для нескольких HTTP запросов. Если у вас есть отдельные сервер, который обслуживает все изображения (images), то вы можете выключить этот параметр. Подобная техника поможет значительно сэкономить ресурсы сервера. |
KeepAliveTimeout | 15 |
Количество секунд для удержания сокет-соединения. Это время включает в себя генерацию контента и ответ клиента. Если клиент не реагирует в течение этого времени будет создано новое соединение. Это значение не должно быть большим, иначе сокет будет простаивать в этот период. |
MaxKeepAliveRequests | 100 |
Сокет-соединения будут закончены, как только их количество достигнет этой цифры. Установите большое значение, но меньше MaxClients или ThreadsPerChild. |
TimeOut | 300 |
Отсоединиться, если время простоя превышает это число. Вы можете установить меньшее значение, если ваши клиенты имеют небольшую задержку. |
LimitRequestBody | 0 |
Максимальный размер PUT или POST. 0 нет лимита. |
Если вы не требуете DNS поиска и не используете htaccess для настройки отдельных директорий в Apache, вы можете задать:
# выключить DNS поиск: PHP скрипты получают только IP адрес HostnameLookups off # отключить проверку htaccess <Directory /> AllowOverride none </Directory> |
Если вас не волнует безопасность папок при вызове симлинков, включите FollowSymLinks и выключите SymLinksIfOwnerMatch, чтобы предотвратить дополнительный системный вызов lstat():
Options FollowSymLinks #Options SymLinksIfOwnerMatch |
Настройка производительности исходя из обращений к серверу в день. |
Определяет, какое количество памяти отвести для IIS. (Performance Tab). |
Управление полосой пропуска |
Управляет полосой пропускав секунду, разделенную по серверам. (Performance Tab). |
Управление процессом |
Управляет процентом процессорного времени, доступного для процесса по серверам. (Performance Tab). |
Таймаут |
По умолчанию 900 секунд. Установите более низкое значение, если у вас локальная сеть. (Web Site Tab) |
HTTP компрессия |
В IIS 5 вы можете сжимать динамические страницы, HTML и картинки. Вы можете настроить эти параметры. Значение по умолчанию выключено. HTTP сжатие нужно включать для всего сервера. Чтобы включить этот параметр нажмите правую кнопку мыши на консоли сервера (не на одном из подсайтов), выберите Свойства (Properties). Нажмите на Вкладке Service, затем выберите Compress application files для компрессии динамических данных, и Compress static files для компрессии статического контента. |
Также вы можете изменить, настроенный поумолчанию, уровень изоляции web-сервера. Во вкладке Home Directory в Application Protection вы можете определить уровень изоляции. На высшем уровне изоляции ваш сайт будет работать медленнее, потомучто это будет отдельный процесс от IIS, вто время как запуск сайта в IIS процессе самая высока скорость, но сервер может лечь, если в вашем скрипте есть серьезные ошибки. На данный момент я рекомендую запускать PHP сайты как CGI или с использованием ISAPI совместно с Application Protection установленной в hight (высокая).
Вы также можете использовать regedit.exe для изменения параметров IIS 5, сохраненных в ветке HKEY_LOCAL_MACHINESYSTEMCurrentControlSetServicesInetinfoParameters.
MemCacheSize |
Устанавливает количество памяти, которую IIS будет использовать для кэширования своих файлов. Поумолчанию, ISS может использовать 50% установленной в компьютере памяти. Вы можете увеличить этот параметр, если на машине работает только IIS. Значение задается в мегабайтах. |
MaxCachedFileSize |
Определяет максимальный размер файла, который может быть кэширован. Размер указывается в байтах. Значение поумолчанию 262,144 (256 кб). |
ObjectCacheTTL |
Устанавливает время (в миллисекундах), в течение которого кэшированный объект сохраняется в памяти. Значение поумолчанию 30,000 миллисекунд (30 секунд). |
MaxPoolThreads |
Устанавливает число нитей на процессор. Определяет, сколько CGI приложений может быть запущено одновременно. Значение поумолчанию 4. увеличьте это значение, если вы используете PHP как CGI. |
ListenBackLog |
Максимальное количество активных живых соединений (Keep alive), которые ISS поддерживает в очереди соединений. Значение поумолчанию 15, должно быть увеличено к числу одновременных соединений, поддерживаемых вашим сервером. Максимально 255. |
Если эти параметры отсутствуют в данной ветке реестра будут использоваться параметры поумолчанию.
После большого тестирования, я считаю, что самая высока производительность PHP в Windows достигается использованием IIS с FastCGI. CGI это протокол для вызова внешней программы из сервера. Это дает не самую высоку скорость, потомучто CGI программа удаляется каждый раз после создания страницы. FastCGI изменяет этот протокол для достижения более высокой производительности, заставляя CGI программу оставаться запущенной после обработки запроса, и многократно использоваться для обработки последующих запросов.
Поскольку установка FastCGI на IIS достаточно сложна, вы должны использовать EasyWindows PHP Installer (http://phplens.com/phpeverywhere/easywindows). Он установит PHP, FastCGI и Turck MMCache для достижения лучшей производительности. Этот инсталлер также может установить PHP для Apache 1.3/2.0.
Zend Engine это внутренний компилятор и движек, использованный для создания PHP 4. Созданный Zeev Suranski и Andi Gutmn, Zend Engine это сокращение от их имен. На начальном этапе PHP работал следующим образом:
PHP скрипт загружается в Zend Engine и компилируется в opcode. Opcode это набор низкоуровневых бинарных инструкций. После запуска opcode происходит генерация HTML и передача его клиенту. Opcode удаляется из памяти после выполнения.
Сегодня есть множество продуктов и методов, чтобы ускорить этот процесс. На следующей диаграмме вы увидите, каким образом работают современные PHP скрипты. Все затемненные элементы являются необязательными.
PHP скрипт загружается в Zend Engine и компилируется в opcode. Opcode может быть оптимизирован с использованием необязательного оптимизатора, названного Zend Optimizer. В зависимости от скрипта, он может увеличить скорость выполнения PHP скрипта до 50%.
Раньше, после выполнения, opcode уничтожался. Теперь вы можете организовать его кэширование, используя несколько альтернативных вариантов: продукты с открытым кодом и Zend Accelerator (раньше известен как Zend Cache), который является закрытым коммерческим продуктом. Только кэшированный opcode совместим с Zend Optimizer и Zend Accelerator. Opcode кэш ускоряет выполнение, удаляя из цепочки загрузку исходного скрипта и его компиляцию. Время выполнения, с использованием кэширования, может улучшиться от 10 до 200%.
ДОПОЛНЕНИЕ:
Где искать инструменты для кэширования opcode?
Zend Accelerator. Это коммерческий продукт, развитый командой Zend Engine. Очень надежный. Искать здесь: http://zend.com.
Вы обязательно должны протестировать программы с открытыми кодами, прежде чем начинать их использовать, поскольку их работа и надежность очень зависит от запускаемых PHP скриптов.
Turck MMCache (http://turck-mmcache.sourceforge.net/) больше не поддерживается. Смотрите eAccelerator, который является развитием mmcache и активно развивается.
Alternative PHP Cache http://apc.communityconnect.com/.
PHP Accelerator http://www.php-accelerator.co.uk/.
AfterBurner Cache http://www.bwcache.bware.it/.
Один из секретов высокой производительности состоит не в том, чтобы написать быстрый PHP код, а в том, чтобы кэшировать результаты выполнения скрипта в файл или общую память. Скрипт запускается однажды, генерируемый HTML захватывается и все последующие обращения к скрипту приводят к показу уже готового HTML. Если требуется регулярное обновление данных, то необходимо указать срок жизни для кэшированных HTML. Кэширование HTML это не часть PHP языка или Zend Engine но осуществляется при помощи PHP кода. Существует много классов и библиотек для организации подобного. Одна из них PEAR Cache, о которой мы поговорим в следующей части. Другой распространенный способ библиотека Smarty.
И, наконец, HTML, отдаваемый браузеру клиента, может быть сжат. Это включается добавлением следующего кода вначале вашего скрипта:
ob_start("ob_gzhandler"); ::: ::: |
Если ваш HTML очень сжимаем, то это может уменьшить размер на 50-80%, сокращая тем самым требования пропускного канала. Другая сторона это необходимость в более мощном процессоре, чтобы эффективно и быстро сжимать страницы.
PEAR Cache это набор классов для кэширования разнообразных типов данных, включая HTML и картинки.
Самое распространенное использование PEAR Cache это кэширование HTML текста. Чтобы использовать кэширование задействуем Output buffering class, с кэшированием всего выдаваемого текста между функциями start() и end():
require_once("Cache/Output.php"); $cache = new Cache_Output("file", array("cache_dir" => "cache/") ); if ($contents = $cache->start(md5("это уникальный ключ!"))) { # # Возвращаем кэшированные данные # print $contents; print "<p>Кэшированные данные</p>"; } else { # # Кэшированных данных нет, либо срок их жизни истек # print "<p>Не покидайте дом без этого:</p>"; # помещаем в кэш print "<p>Stand and deliver</p>"; # помещаем в кэш print $cache->end(10); } |
Примечание: С тех пор, как я написал эти строки, была создана более продвинутая система кэширования PEAR: Cache Lite (http://pear.php.net/package/Cache_Lite); чтобы узнать об этом подробнее, смотрите memcached (http://www.danga.com/memcached/).
Cache конструктор принимает первым параметром тип драйвера для сохранения кэша. Доступны следующие драйверы: файл, база данных, общая память (смотрите папку: pear/Cache/Container). Тесты Ulf Wendel показывают, что драйвер файл дает наилучшую производительность. Второй параметр это параметры для используемого драйвера. Варианты: cache_dir это папка для сохранения кэшированных данных, filename_prefix уникальная приставка к названию файлов для сохранения кэшированных данных. Что странно, время жизни кэша не является параметром для конструктора.
Для кэширования каких-то данных, вам необходимо сгенерировать уникальный ключ (id) для этих данных. В примере выше, мы использовали md5("это уникальный ключ!").
Функция start() использует ключ для поиска кэшированной копии данных. Если контент не был кэширован, функция вернет пустую строку. И все последующие echo и print будут буферизироваться в кэш, до тех пор, пока не встретится функция end().
Функция end() возвращает содержимое буфера и заканчивает буферизацию вывода. Функция end() принимает в качестве первого параметра время жизни кэша. Этим параметром могут быть секунды или Unix таймштамп, указывающий точное время окончания жизни кэша, или 0 установит интервал по умолчанию 24 часа.
Другой способ использования PEAR Cache это сохранение значения переменных или других данных. Для реализации этого используйте основной Cache класс:
require_once("Cache.php"); $cache = new Cache("file", array("cache_dir" => "cache/") ); $id = $cache->generateID("'это уникальный ключ"); if ($data = $cache->get($id)) { print "Кэшированные данные.<br>Дата: $data"; } else { $data = "Количество памяти достаточное..."; $cache->save($id, $data, $expires = 60); print "Кэш потерян.<br>"; } |
Для сохранения используемых данных применяйте функцию save(). Если выбранный вами уникальный ключ уже существует, вы можете использовать функцию generateID(). Объекты и массивы могут быть сохранены благодаря использованию функции serialize() внутри функции save(). Последний параметр это время жизни кэша. Этим параметром могут быть секунды или Unix таймштамп, указывающий точное время окончания жизни кэша, или 0 установит интервал по умолчанию 24 часа. Для получения кэшированных данных используйте функцию get().
Выше мы свами затронули много проблем и моментов касающихся производительности. Теперь мы прибавляем мяса и костей, и смотрим на то, как проверять и тестировать производительность вашего кода, и как получить информацию относительно того, что и как работает и настраивается.
Если вы хотите получить наиболее реалистичные тесты производительности вебсервера, вам необходим инструмент, умеющий посылать разнообразные HTTP запросы. На Unix обычные инструментальные средства тестирования включают ab (сокращение от apachebench), который являются частью пакета Apache. И более нового flood (http://httpd.apache.org/test/flood). На Windows NT/2000 вы можете использовать Microsoft's free Web Application Stress Tool (http://webtool.rte.microsoft.com).
Эти утилиты могут делать параллельные множественные HTTP запросы моделируя тем самым множество клиентов сети, и выдавать вам детальную статистику по завершению теста.
Вы можете контролировать, каким образом ведет себя ваш сервер, поскольку тесты проводятся на основе Unix с использованием "vmstat1". Он печатает информацию о статусе вашего диска, виртуальной памяти, и загрузки процессора каждую секунду. Альтернативно вы можете использовать "top d 1" который дает вам полноэкранную индикацию всех процессов запущенных и сортированных по степени загрузки процессора каждую секунду.
На Windows 2000 вы можете использовать Performance Monitor или Task Manager для просмотра статистики своей системы.
Если вы хотите проверить специфический аспект вашего кода без необходимости использования HTTP запросов, вы можете написать тест с использованием функции microtime(), которая возвращает текущее время в микросекундах в виде строки. Следующая функции конвертирует это значение в число, подходящее для статистики:
function getmicrotime() { list($usec, $sec) = explode(" ",microtime()); return ((float)$usec + (float)$sec); } $time = getmicrotime(); # # Здесь тестируемый код # echo " |
Альтернативно, вы можете использовать специальные инструменты для профелирования: APD (http://www.linuxjournal.com/article.php?sid=7213) или Xdebug (http://xdebug.derickrethans.nl/). Еще вы можете прочитать специальную статью на эту тему: http://phplens.com/phpeverywhere/node/view/52.
Далее, я привожу детали реального теста производительности, созданного для одного из клиентов. В данном случае, клиент хотел гарантированное время отклика 5 секунд для любой PHP страницы, которая не делала сложных SQL запросов. Конфигурация сервера: Apache 1.3.20, PHP 4.0.6 на Red Hat 7.2 Linux. Аппаратное обеспечение: два Pentium III 933 Mhz, 1Gb RAM. HTTP запросы будут обращаться к скрипту testmysql.php. Этот скрипт читает и обрабатывает примерно 20 записей из MySql базы данных, расположенной на другом сервере. Для простоты мы предполагаем, что вся графика грузится с другого сервера.
Как инструмент для тестирования производительности мы использовали ab. Мы задали ab делать 1000 запросов (-n1000), используя 10 одновременных соединений (-c10). Вот результаты:
# ab -n1000 -c10 http://192.168.0.99/php/testmysql.php This is ApacheBench, Version 1.3 Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Copyright (c) 1998-1999 The Apache Group, http://www.apache.org/ Server Software: Apache/1.3.20 Server Hostname: 192.168.0.99 Server Port: 80 Document Path: /php/testmysql.php Document Length: 25970 bytes Concurrency Level: 10 Time taken for tests: 128.672 seconds Complete requests: 1000 Failed requests: 0 Total transferred: 26382000 bytes HTML transferred: 25970000 bytes Requests per second: 7.77 Transfer rate: 205.03 kb/s received Connnection Times (ms) min avg max Connect: 0 9 114 Processing: 698 1274 2071 Total: 698 1283 2185 |
После запуска теста производительности, он начал контролировать на стороне сервера использование ресурсов с использованием команды "top d 1". Параметр "d 1" указывает, что необходимо делать задержку в 1 секунду перед обновлением данных. Вывод показан ниже.
10:58pm up 3:36, 2 users, load average: 9.07, 3.29, 1.79 74 processes: 63 sleeping, 11 running, 0 zombie, 0 stopped CPU0 states: 92.0% user, 7.0% system, 0.0% nice, 0.0% idle CPU1 states: 95.0% user, 4.0% system, 0.0% nice, 0.0% idle Mem: 1028484K av, 230324K used, 798160K free, 64K shrd, 27196K buff Swap: 2040244K av, 0K used, 2040244K free 30360K cached PID USER PRI NI SIZE RSS SHARE STAT %CPU %MEM TIME COMMAND 1142 apache 20 0 7280 7280 3780 R 21.2 0.7 0:20 httpd 1154 apache 17 0 8044 8044 3788 S 19.3 0.7 0:20 httpd 1155 apache 20 0 8052 8052 3796 R 19.3 0.7 0:20 httpd 1141 apache 15 0 6764 6764 3780 S 14.7 0.6 0:20 httpd 1174 apache 14 0 6848 6848 3788 S 12.9 0.6 0:20 httpd 1178 apache 13 0 6864 6864 3804 S 12.9 0.6 0:19 httpd 1157 apache 15 0 7536 7536 3788 R 11.0 0.7 0:19 httpd 1159 apache 15 0 7540 7540 3788 R 11.0 0.7 0:19 httpd 1148 apache 11 0 6672 6672 3784 S 10.1 0.6 0:20 httpd 1158 apache 14 0 7400 7400 3788 R 10.1 0.7 0:19 httpd 1163 apache 20 0 7540 7540 3788 R 10.1 0.7 0:19 httpd 1169 apache 12 0 6856 6856 3796 S 10.1 0.6 0:20 httpd 1176 apache 16 0 8052 8052 3796 R 10.1 0.7 0:19 httpd 1171 apache 15 0 7984 7984 3780 S 9.2 0.7 0:18 httpd 1170 apache 16 0 7204 7204 3796 R 6.4 0.7 0:20 httpd 1168 apache 10 0 6856 6856 3796 S 4.6 0.6 0:20 httpd 1377 natsoft 11 0 1104 1104 856 R 2.7 0.1 0:02 top 1152 apache 9 0 6752 6752 3788 S 1.8 0.6 0:20 httpd 1167 apache 9 0 6848 6848 3788 S 0.9 0.6 0:19 httpd 1 root 8 0 520 520 452 S 0.0 0.0 0:04 init 2 root 9 0 0 0 0 SW 0.0 0.0 0:00 keventd |
Посмотрите на шапку вывода. Результаты показывают, что Apache, на машине с двумя процессорами, отработал с 0% простоя. Это очень плохо. Средняя загрузка составила 9.07 за последнюю минуту (3.29 за последние 5 минут, 1.79 за последние 15 минут). Средняя загрузка это среднее число процессов, готовых быть запущенными. Для двухпроцессорного сервера любая загрузка больше 2, означает, что система будет перегружена процессами. Здесь вы можете видеть тесную связь между загруженностью 9.07 и количеством запущенных процессов (10), которые были определены в тесте ab.
К счастью, мы располагаем большим объемом свободной оперативной памяти, приблизительно 798 160 Мб, никакой виртуальной памяти не используется.
Далее, внизу, мы можем видет процессы, упорядоченные по количеству использования центрального процессора. Самые активные это процессы Apache (httpd). Первая задача httpd использует 7280 Кб памяти и отнимет в среднем 21.2% процессора, и 0.7% оперативной памяти. Колонка STAT показывает статус: R работает, S бездействует, W означает, что процесс выгружен из памяти.
Вышеприведенные цифры показывают нам типичную пиковую нагрузку, мы можем сделать некоторое планирование. Если среднее число загрузки для сервера с двумя процессорами 9.0, и задача отнимает для исполнения примерно одно и тоже время, то слегка загруженный сервер должен быть 9.0/2 процессора = в 4.5 раз более быстрым. Так что HTTP запрос, который обыкновенно занимал 1.283 секунды, при пиковой нагрузке займет примерно 1.283/4.5 = 0.285 секунд.
Для проверки этого, мы делаем тест производительности с 2 одновременными процессами (вместо 10 выше). Получаем 0.281 секунды, что очень близко к предсказанному значению 0.285!
# ab -n100 -c2 http://192.168.0.99/php/testmysql.php [ some lines omitted for brevity ] Requests per second: 7.10 Transfer rate: 187.37 kb/s received Connnection Times (ms) min avg max Connect: 0 2 40 Processing: 255 279 292 Total: 255 281 332 |
Наоборот, удваивая количество подключений, мы можем предсказать, что среднее время выполнения должно удвоиться с 1.283 до 2.566 секунд. В тестах производительности мы, фактически, получили 2.570 секунд.
После того, как мы запустили тест производительности с 40 запросами, сервер был перегружен с 35% неудачных запросов. При дальнейшем расследовании было обнаружено, что MySql сервер отказывал в запросах с ошибкой Слишком много подключений.
Тест производительности также указал на поведение дочерних процессов Apache. Каждый скрипт PHP использовал 2 постоянных соединения, так, на 40 запросах мы имели 80 постоянных подключений, что значительно ниже значения по умолчанию MySql (max_connections 100). Однако, дочерние процессы Apache, которым не переданы немедленно новые запросы, всеравно удерживают постоянное соединение. Эти неактивные дочерние процессы породили еще более 20 постоянных соединений, которые оказались соломкой, которая сломала спину верблюду.
Переведя скрипты на непостоянные соединения, мы устранили эту проблему и получили результат 5.340 секунд. Альтернативное решение состояло в том, чтобы увеличить MySql max_connections выше, чем значение по умолчанию.
Вышеупомянутое исследование в очередной раз показывает нам, что оптимизация вашей работы является чрезвычайно сложной. Это требует понимания множества программных систем, включая маршрутизацию сети, стек TCP/IP, количество физической и виртуальной памяти, количество процессоров, поведение дочерних процессов Apache, ваших скриптов PHP и конфигурации базы данных.
В данном случае, код PHP был очень хорошо отлажен и самым узким местом оказался центральный процессор, который вызывал замедление в момент ответа. Поскольку нагрузка возросла, система значительно замедлилась, но находилась у требуемого порога (что является хорошим показателем), пока мы не столкнулись с более серьезным узки местом недостаточным количеством возможных подключений к MySql. Это вызвало многократные ошибки наших скриптов, пока мы не устранили их переходом на непостоянные соединения.
Из вышеприведенных значений, мы можем вычислить какое количество соединений мы можем обработать не выходя за пределы желаемого времени ответа. Предполагая наличе двусторонней сети с временем доступа 0.5 секунд на Internet (0.25 секунд одна сторона), мы можем предсказать:
Поскольку наш клиент хотел время ответа 5 секунд, сервер сможет обрабатывать до 34 одновременных подключений в секунду. Это дает на пиковой нагрузке 34/5 = 6.8 страниц в секунду.
Для получения максимального количества страниц для просмотра в день, мы должны умножить пиковую способность в секунду на 50.000 (эта методика предложена веб-мастерами pair.com, большой хостинговой компанией), что даст нам 340 000 страниц в день.
Терпеливому читателю, который все еще задается вопросом, почему мы так много акцентируем на обсуждении не PHP проблем, напоминаю, что PHP это быстрый язык, и многие из вероятных узких мест, которые замедляют скорость, находятся вне PHP.
Большинство PHP сценариев просты. Они получают небольшое количество информации о сессии, загружают некоторое количество информации из системы управления контентом или базы данных, форматируют соответствующий HTML и отдают результат своей работы HTTP клиенту. Предположим, что типичный PHP сценарий исполняется 0.1 секунду, время ожидания Internet 0.2 секунды, только 33% времени из этих 0.3 секунд будут использоваться для генерации PHP сценарием ответа. Так, если вы улучшите скорость своего скрипта на 20%, клиент будет видеть, что время ответа сократилось до 0.28 секунд, что является незначительным усовершенствованием. Конечно сервер сможет обработать на 20% больше запросов к одной и той же странице, что увеличивает масштабируемость.
Вышеприведенный пример вовсе не означает, что мы должны опустить руки и все бросить. Это означает, что мы не должны чувствовать гордость после отвоевания очередного 1% скорости в своих скриптах. Мы должны тратить свое время на оптимизацию заслуживающих внимание областей нашего кода, чтобы получить более высокую отдачу.
Места, где высокая степень оптимизации достижима, находятся внутри циклов while и for, где каждое замедление кода увеличивается в несколько раз из-за их повторения. Лучший способ для понимания этого, это рассмотреть несколько примеров:
Вот вам один из простых примеров, печатающих массив:
for ($j = 0;$j < sizeof($arr);$j++) echo $arr[$j] . '<br>'; |
Этот код может быть существенно ускорен, если переписать его следующим образом:
for ($j=0, $max = sizeof($arr), $s = '';$j < $max;$j++) $s .= $arr[$j] . '<br>'; echo $s; |
Для начала необходимо понять, что выражение $j < sizeof($arr) будет вычисляться каждый раз, пока цикл будет выполнятся. Поскольку результат этого выражения всегда постоянен, мы переносим его в $max. Если говорить техническим языком, то это называется оптимизацией инварианта цикла.
Вторая проблема заключается в том, что в PHP 4 многократный вывод на экран командой echo работает гораздо медленнее, нежели сохранения всех данных в строке и однократный вывод результата на экран. Это связано с тем, что echo дорогая операция, которая может повлечь за собой посылку пакетов TCP/IP HTTP клиенту. Конечно, накопление данных в строке $s имеет некоторые проблемы масштабируемости, поскольку расходует больше памяти.
Дополнительный способ ускорить вышеприведенный код это использование буферизации вывода. Это позволит накапливать информацию внутренне и потом вывести ее целиком на экран в конце выполнения скрипта. Это существенно уменьшит нагрузку на сеть за счет использования большего количества памяти и увеличения времени ожидания. В части моего кода, состоящего полностью из команд echo, улучшение работы достигло 15%.
ob_start(); for ($j=0, $max = sizeof($arr), $s = '';$j < $max;$j++) echo $arr[$j] . '<br>'; |
Обратите внимание, на то, что буферизация при помощи функции ob_start() может использоваться как глобальная оптимизация для всех ваших скриптов. В долго выполняющихся сценариях вы, возможно, захотите периодически выводить содержимое буфера, чтобы иметь определенную обратную связь с HTTP клиентом. Это может быть сделано при помощи функции ob_end_flush(). Эта функция также выключает буферизацию вывода, так что вам необходимо будет вызвать функцию ob_start() для ее возобновления.
Резюме: этот пример показал нам, как оптимизировать варианты циклов и как использовать буферизацию вывода для ускорения вашего кода.
В следующем примере мы циклом проходим по PEAR DB recordset'у, используя специальную форматирующую функцию для форматирования результата, а затем выводим его командой echo. На сей раз я протестировал производительность, время выполнения составило 10.2 микросекунды (я не учитывал время подключения к базе и исполнение SQL запроса):
function FormatRow(&$recordSet) { $arr = $recordSet->fetchRow(); return '<storng>' . $arr[0] . '</storng><em>' . $arr[1] . '</em>'; } for ($j = 0;$j < $rs->numRows();$j++) { print FormatRow($rs); } |
Из первого примера мы с вами узнали, каким образом можно оптимизировать цикл. Изменим код следующим образом (получим 8.7 микросекунд):
function FormatRow(&$recordSet) { $arr = $recordSet->fetchRow(); return '<strong>' . $arr[0] . '</strong><em>' . $arr[1] . '</em>'; } ob_start(); for ($j = 0, $max = $rs->numRows();$j < $max;$j++) { print FormatRow($rs); } |
Мои тесты производительности показали, что использование переменной $max сэкономило 0.5 микросекунды, а ob_start() - 1.0 микросекунду. В общей сложности 1.5 микросекунды.
Однако, изменяя алгоритм цикла мы можем упростить и ускорить код. В нашем случае, мы увеличили скорость выполнения до 8.5 микросекунд:
function FormatRow($arr) { return '<strong>' . $arr[0] . '</strong><em>' . $arr[1] . '</em>'; } ob_start(); while ($arr = $rs->fetchRow()) { print FormatRow($arr); } |
Однако, в данном случае возможна еще одна оптимизация. Мы можем убрать функцию форматирования (потенциально жертвуя удобством эксплуатации ради скорости), чтобы сэкономить еще 0.1 микросекунду (в результате получим 8.4 микросекунды):
ob_start(); while ($arr = $rs->fetchRow()) { print '<strong>' . $arr[0] . '</strong><em>' . $arr[1] . '</em>'; } |
Переключаясь на использование PEAR Cache, время выполнения повысилось до 3.5 микросекунд для кэшированных данных:
require_once("Cache/Output.php"); ob_start(); $cache = new Cache_Output("file", array("cache_dir" => "cache/")); $t = getmicrotime(); if ($contents = $cache->start(md5("this is a unique kexy!"))) { print "<p>Cache Hit</p>"; print $contents; } else { print "<p>Cache Miss</p>"; ## ## Code to connect and query database omitted ## while ($arr = $rs->fetchRow()) { print '<strong>' . $arr[0] . '</strong><em>' . $arr[1] . '</em>'; } print $cache->end(100); } print (getmicrotime()-$t); |
Ниже я суммирую методы оптимизации:
Время выполнения (микросекунды) | Время выполнения (микросекунды) |
9.9 |
Первоначальный вариант без оптимизации, не учитываем время соединения с базой и выполнение SQL запроса. |
9.2 |
Используем ob_start() |
8.7 |
Оптимизируем цикл при помощи $max и ob_start() |
8.5 |
Меняем тип цикла с for на while, парсим как массив в FormatRow() с использованием ob_start() |
8.4 |
Удаляем FormatRow() и используем ob_start() |
3.5 |
Используем PEAR Cache совместно с ob_start() |
Из вышеприведенных данных вы видите, что самая существенная оптимизация была достигнута не простым щипанием кода, а глобальной оптимизацией при помощи функции ob_start(), а также радикально отличающимся алгоритмом - кэшированием HTML.
В марте 2001 года я провел некоторые неофициальные тесты производительности классов на PHP 4.0.4 pl1. Дам несколько советов, исходящих из этих результатов. Три главных пункта:
Внимание: поскольку PHP постоянно совершенствуется, могли произойти усовершенствования в лучшую сторону.
Также я пришел к выводу, что вызов метода объекта (функция определена в классе) примерно в два раза медленнее, нежели обычный вызов функции. Что касается меня, то я считаю эту ситуацию приемлемой если сопоставлять с другими ООП языками.
Внутри метода (следующие отношения весьма приблизительны):
Дополнение от 11 июля 2004: эти испытания были проведены почти 3 года назад. Я проверил эти данные вновь на версии 4.3.3. Вызов функции теперь занимает 20 $localvar++, а вызов метода 30 $localvar++. Это может быть потому, что $localvar++ стало выполняться быстрее или вызов функция стал медленнее.
Некоторые виды оптимизации полезны. Другие напрасная трата времени. Часто полезность усовершенствования пренебрежимо мала. Часто, внутренние изменения в PHP делают щипковые изменения кода устаревшими.
Вот некоторые общие легенды:
Ссылки не обеспечивают никакого преимущества для работы со строками, целыми числами и другими базовыми типами данных. Например, рассмотрим следующий код:
function TestRef(&$a) { $b = $a; $c = $a; } $one = 1; ProcessArrayRef($one); |
И тот же самый код без ссылок:
function TestNoRef($a) { $b = $a; $c = $a; } $one = 1; ProcessArrayNoRef($one); |
PHP фактически не создает копии переменных, когда обрабатывает значение, но использует вместо этого очень быструю ссылку, рассчитывая все внутри себя. Так в TestRef(), $b и $c будут устанавливаться дольше, так как ссылки должны отслеживаться, в то время как TestNoRef() $b и $c только указывают на первоначальное значение $a и PHP всего лишь увеличивает число ссылок. Поэтому TestNoRef() будет выполнена быстрее, чем TestRef().
Напротив, функции, которые принимают массивы и объекты в качестве параметров, имеют преимущества в работе, если использовать ссылки. Это происходит потому, что массивы и объекты не используют счетчик ссылок. Поэтому при использовании массивов и объектов без ссылок будет создано множество копий, когда они будут обрабатываться. Так следующий код:
function ObjRef(&$o) { $a =$o->name; } |
$function ObjRef($o) { $a = $o->name; } |
Замечание: в PHP 5 все объекты передаются по ссылке автоматически, без требования явного указания &. Объектная модель PHP 5 должна быть значительно быстрее.
Закладки на сайте Проследить за страницей |
Created 1996-2024 by Maxim Chirkov Добавить, Поддержать, Вебмастеру |