Нестандартная оптимизация проектов на PHP

Содержание

В данной публикации хочу рассказать о том, как нетрадиционными методами мы смогли снизить нагрузку на серверы и ускорить время обработки страницы в несколько раз.

8e5aed55fd6d46cd9c1332000efdbf90

Традиционные методы, думаю, всем известны:

  • Оптимизация SQL-запросов;
  • Поиск и исправление узких мест;
  • Переход на Memcache для часто используемых данных;
  • Установка APC, XCache и подобных;
  • Клиентская оптимизация: CSS спрайты и т.п.

В нашем же проекте всё это было сделано, но при этом проблема скорости обработки страниц сохранялась. Средняя скорость обработки страницы была в районе 500мс. В один прекрасный момент пришла идея проанализировать, какие ресурсы есть, и на что они могут тратиться.

После анализа были выявлены следующие основные ресурсы, которые нужно мониторить:

  1. Процессорное время;
  2. Оперативная память;
  3. Время ожидания других ресурсов (MySQL, memcache);
  4. Время ожидания диска.

В нашем проекте мы почти не используем запись на диск, поэтому сразу исключили 4-й пункт.

Дальше начался поиск узких мест.

Этап 1

Первое, что было испробовано — профайлинг MySQL запросов, поиск медленных запросов. Подход не принёс особого успеха: несколько запросов было оптимизировано, но среднее время обработки страниц не сильно изменилось.

Этап 2

Дальше была попытка профайлинга кода с помощью XHProf. Это дало ускорение в узких местах и смогло снизить нагрузку примерно на 10-15%. Но основной проблемы это тоже не решило. (Если будет интересно, могу отдельно написать статью о том, как оптимизировать с помощью XHProf. Вы только дайте знать в комментариях.)

Этап 3

В третьем этапе пришла мысль посмотреть, сколько памяти уходит на обработку запроса. Оказалось, что в этом и есть проблема – простой запрос может требовать загрузить до 20мб кода в ОЗУ. Причём причина этого была непонятна, так как идёт простая загрузка страницы — без запросов в базу данных, или загрузки больших файлов.

Было принято решение написать анализатор и выяснить, сколько памяти требует каждый пхп файл при его включении.

Анализатор очень простой: в проекте уже был автозагрузчик файлов, который на основе имени класса сам подгружал нужный файл (autoload ). В нём просто было добавлено 2 строки: сколько памяти было до загрузки файла, сколько стало после.

Пример кода:

Profiler::startLoadFile($fileName);
Include $fileName;
Profiler::endLoadFile($fileName);

Первая строка сохраняет, сколько памяти было в начале, последняя строка – вычитает разницу и добавляет в список загрузок. Так же профайлер сохраняет очередь загрузки файла (backtrack).

В конце выполнения всего кода мы добавили вывод панели с собранной информацией.

echo Profiler:showPanel();

Анализ показал очень интересные причины того, почему может использоваться излишняя память. После того, как мы проанализировали все страницы и убрали загрузку лишних файлов страница стала требовать менее 8мб памяти. Обновление страницы стало быстрее, нагрузка на сервера уменьшилась и на тех же машинах появилась возможность обрабатывать больше клиентов.

Дальше идёт список вещей, которые мы изменили. Здесь стоит отметить, что сам функционал не изменился. Изменилась только структура кода.

Все примеры сделаны специально упрощёнными, так как предоставить оригинальный код проекта возможности нет.

Пример 1: Загрузка, но не использование больших родительских классов

Пример:

class SysPage extends Page{
    static function helloWorld(){
        echo "Hello World";
    }
}

Файл сам по себе очень маленький, но при этом, он тянет за собой по крайней мере один другой файл, который может быть достаточно большой.

Решений несколько:

  1. Убрать наследование где не нужно;
  2. Сделать чтобы все родительские классы были минимальные по размерам.

 

Пример 2: Использование длинных классов, хотя нужна только малая часть класса

Очень похоже на предыдущий пункт. Пример:

class Page{
    public static function isActive(){}
    // тут длинный код на 1000-2000 строк
}

Естественно, что PHP не знает, что вам нужна только 1 функция. Как только идёт обращение к данному классу, идёт подгрузка всего файла.

Решение:
Если есть подобные классы, то стоит выделить функционал который используется везде в отдельный класс. В текущем классе можно ссылаться на новый класс для совместимости.

Пример 3: Подгрузка большого класса только для констант

Пример:

$CONFIG['PAGES'] = array(
    'news' => Page::PAGE_TYPE1,
    'about' => Page::PAGE_TYPE2,
);

Или:

if(get('page')==Page::PAGE_TYPE1){}

Т.е. пример похож на предыдущий, только теперь константа. Решение такое же как и в прошлом случае.

Пример 4: Авто создание в конструкторе классов, которые возможно и не будут использованы

Пример:

class Action{
    public $handler;
    public function __construct(){
        $this->handler = new Handler();
    }

    public function handle1(){}
    public function handle2(){}
    public function handle3(){}
    public function handle4(){}
    public function handle5(){}
    public function handle6(){}
    public function handle7(){}
    public function handle8(){}
    public function handle9(){}
    public function handle10(){
        $info = $this->handler->getInfo();
    }
}

Если Handler используется часто, то проблемы нет. Совсем другой вопрос, если он используется только в 1 из функций из 20.

Решение:
Убираем из конструктора, переходим на ленивую загрузку через магический метод __get или же например так:

public function handle10(){
    $info = $this->handler()->getInfo();
}
public function handler(){
    if($this->handler===null)
        $this->handler = new Handler();
    return $this->handler;
}

 

Пример 5: Подгрузка ненужных языковых файлов / конфигов

Пример: У вас есть большой файл с настройками всех страниц всех пунктов меню. В файле может быть много массивов.

Если часть данных используется редко, а часть часто – то данный файл является кандидатом на разделение.

Т.е. стоит подгружать данные настройки только тогда, когда они нужны.

Пример использований памяти у нас:

Файл в 16 кб, просто массив данных – требует от 100 кб и выше.

Пример 6: Использование serialize/unserialize

Часть настроек, которые часто меняются, мы хранили в файле в формате serialize. Мы обнаружили, что это загружает как процессор, так и оперативную память, причём у нас получилось, что на PHP версии 5.3.х у функции unserialize очень сильная утечка памяти.

После оптимизации мы максимально избавились от этих функций где возможно. Данные в файлах мы решили хранить в виде массива, сохранённого через var_export и загружать обратно через с помощью include/require. Таким образом мы смогли задействовать APC режим.

К сожалению, для данных которые хранятся в memcache, данный подход не работает.

Итоги

Все данные примеры легко находятся и очень легко правятся не сильно меняя структуру проекта. Мы взяли правило: «любые файлы, которые требуют более 100кб, надо проверять на предмет можно ли оптимизировать их загрузку. Так же мы смотрели на ситуации, когда загружалось несколько файлов из одной ветки. В этой ситуации мы смотрели, можно ли вообще не загружать всю ветку файлов. Ключевая мысль была такой: «всё, что мы загружаем, должно иметь смысл. Если можно не загружать каким-либо способом – лучше не загружать.

Заключение

После того, как мы убрали всё, что выше, один запрос к серверу стал требовать примерно в 2 раза меньше оперативной памяти. Нам это дало уменьшение нагрузки процессора примерно в 2 раза без перенастройки серверов и уменьшение средней скорости загрузки одной страницы в несколько раз.

Дальнейшие планы

Если будет интересно, есть в планах идея описать способы профилирования кода и поиска узких мест с помощью XHprof.