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

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

image

Традиційні методи, думаю, всім відомі:
  • Оптимізація 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.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.