Збільшення продуктивності Magento

… чи правильна робота з колекціями.

Хочу розповісти вам про помилки, які бачив практично на кожному проекті на Magento у якого були проблеми з продуктивністю. Працюючи з Magento, мені іноді доводиться проводити аудит чужого коду. Тому я б хотів поділитися з вами досвідом, який допоможе поліпшити продуктивність ваших сайтів і уникнути помилок в майбутньому.

У цій статті розказано про Magento 1.*, але описане так само підходить і для Magento 2.*.

Практично на кожному проекті, де є проблеми з продуктивністю, можна зустріти щось подібне до такого:

$temp = array();
$collection = Mage::getModel('catalog/product')->getCollection()->addAttributeToSelect('*');
foreach ($collection as $product) {
$product = $product->load($product->getId());
$temp[] = $product->getSku();
}
Неправильно

замість

$temp = array();
$collection = Mage::getModel('catalog/product')->getCollection()->addAttributeToSelect('sku');
foreach ($collection as $product) {
$temp[] = $product->getSku();
}
Правильно

Причини такого дуже прості:

  1. Після завантаження немає необхідних атрибутів
  2. Так роблять «програмісти» в інтернеті
  3. Завантаження зайвих атрибутів за принципом «гірше не буде»
Для розуміння, що ж тут не так і що ми можемо зробити з продуктивністю, я пропоную сконцентруватися на роботі з колекціями:

  1. Eav/Flat таблиці
  2. Cache
  3. Правильна робота з колекціями
І звичайно ж висновки.



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

У Magento до EAV сутностей відносяться: продукти, категорії, кастомеры і кастомер адреси. Самі ж атрибути зберігаються в eav_attribute таблиці.

Всього типів значень атрибутів в Magento 5: text, varchar, int, decimal та datetime. Є ще 1 тип – static, він відрізняється від інших 5ти тим, що знаходиться в таблиці з сутністю.

В таблиці атрибутів зазначено в якої таблиці або якого типу є той чи інший атрибут і Magento вже знає куди його писати і звідки читати.

Таке зберігання значень дозволяє мати досить просто реалізуються атрибут сети ( коли кожна сутність може мати свій атрибут або не мати її зовсім), додавання нового атрибута це всього лише ще 1 рядок в БД. Додав нове значення для 1 атрибута для іншого стора – нова рядок у таблиці значень атрибута.

Як це зберігається в БДПосилання:
Product – catalog_product_entity,
Category – catalog_category_entity,
Customer – customer_entity,
Customer address – customer_address_entity

Attribute:
eav_attribute
catalog_eav_attribute
customer_eav_attribute

Value:
*_text
*_varchar
*_int
*_decimal
*_datetime

Flat — це звичний нам усім підхід, де все лежить в 1 місці і ніякі додаткові таблиці нам не потрібні, щоб отримати товар і всі його атрибути без зайвої роботи – SELECT * FROM табличка WHERE id = якийсь ід і все.

З EAV сутностей, Flat уявлення можна використовувати лише для категорій і товарів.

Як це зберігається в БДProduct:
catalog_product_flat_1// *_N store_view
Category:
catalog_category_flat_1// *_N store_view

Для того, щоб включити атрибут під Flat таблицю і взагалі включити використання Flat таблиць необхідно виконати наступнеВ адмінці Catalog > Attributes > Manage attributes

Magento додасть атрибут під Flat таблицю якщо у атрибута виставлено 1 з нижче вказаних значень.



В адмінці System > Configuration > Catalog

Magento буде використовувати Flat таблиці для сутностей зазначених нижче.



Зверніть увагу на наступні факти:

  1. Flat таблиці використовуються ТІЛЬКИ на сторінках категорій, списку продуктів у складі product Group, та й взагалі скрізь, де використовується колекція. Вони не використовуються на сторінці товарів, в адмінці, при використанні методу load у моделі.
  2. Після включення Flat таблиць необхідно провести реиндексацию, інакше Magento буде і далі використовувати лише EAV таблиці
  3. Після включення Flat таблиць Magento все одно продовжує використовувати EAV, але так само починає копіювати зміни в Flat таблицю при збереженні змін
Навіщо ж все ось це треба і чому б не використовувати скрізь Flat підхід? Подивіться на зведену таблицю плюсів і мінусівEAV:
+ Більш гнучка система чим Flat
+ При додаванні нового атрибута немає необхідності реиндексировать дані
+ Практично не обмежена кількість атрибутів
+ Завжди доступні всі атрибути
+ Статік атрибути (sku, created_at, updated_at) завжди присутні у вибірці, навіть якщо їх не вказувати спеціально
— Fatal error: Call to a member function getBackend() при вибірці/фільтрування не існуючого атрибуту
— Продуктивність

Flat:
+ Продуктивність
+ У вибірку/фільтрацію можуть бути застосовані лише наявні атрибути, які додані у Flat таблицю
— Обмеження на розмір рядка (до 65,535 байт, тобто 85 varchar 255) і кількістю стовпців (InnoDB до 1000, деякі до 4096)
— Використовується тільки при роботі з колекцій (при завантаженні завжди використовується EAV)
— Результат відрізняється від видачі запиту при EAV (відсутні статік атрибути)
— Після включення потрібно реиндексация, в іншому випадку будуть використані EAV таблиці
— При додаванні нового атрибута необхідно реиндексировать Flat таблиці



Cache
Звичайно кожен з вас може мені сказати, що навіщо нам розбиратися як прискорити запити в БД і взагалі як працюють колекції якщо кеш нас врятує і все буде закэшировано. Відповім коротко – кеш вас не врятує. Ні 1 з кешей представлених в Magento або не кешує колекції автоматично або не працює у ваших кастомних контролерах і моделях, які ви використовуєте, скажімо, при імпорті даних або підрахунку чогось. Та й до того ж до того, як воно потрапить в кеш, адже треба це якось туди покласти і швиденько показати користувачеві.

Типи кешей в Magento 1.*:



  • Configuration – кешує конфігураційні файли
  • Layout – кешує layout файли
  • Block HTML output – кешує phtml шаблони. За замовчуванням використовується на фронтенде тільки в top menu і footer.
  • Translations – кешує csv транслейт файли
  • Collections data – кешує колекції, які використовують ->initCache(...) метод. За замовчуванням кешує core_store, core_store_group, core_website колекції при ініціалізації.
  • EAV types and attributes – повинен кешувати eav атрибути, А не кешує. Використовується в 1 методі, який ніколи не викликається починаючи з Magneto CE 1.4
  • Web services cache – кешує api.xml файли
  • Page Cache (FPC) – кешує весь HTML, кешує CMS, Category, Product сторінки. Ігнорується, якщо протокол https, гет параметр ?no_cache=1, куки NO_CACHE
  • DDL Cache (Прихований) – кешує DESCRIBE виклики до БД, використовується в операціях запису
...і ні 1 не кешує колекції автоматично.


Правильна робота з колекціями
Для того, щоб показати більш наочно чому щось треба робити інакше ніж багато хто звикли, я вирішив навести деякі тести продуктивності різних підходів. Почнемо мабуть з тестового стенда. Для тестування я використовував:

Тестовий стенд:
OS X 10.10
3.1 GHz Intel Core i5 (4 cores)
8GB

Magento конфігурація:
Magento EE 1.14.0
MySQL 5.5.38
PHP 5.6.2

Контент:
3 Categories
2000 Products
2000 CMS pages

Процес:
Для тестів був створений экстеншен з 1 контролером і 1 екшеном, кожен тест проводився 5 разів, потім вважалося середнє час. Всі результати вказані в секундах.

class Test_Test_IndexController extends Mage_Core_Controller_Front_Action
{
public function indexAction()
{
$temp = array();
$start = microtime(true);
Init values

Loop start
$temp[] = $product->getSku();
End Loop
Or
Some code snippet

$stop = microtime(true);
echo $stop - $start;
}
}

Псевдо-код
Тести
  1. EAV/Flat з перезавантаженням моделей і без
  2. Кешування колекцій
  3. Правильне використання count() і getSize()
  4. Правильне використання getFirstItem і setPage(1,1)
EAV/Flat з перезавантаженням моделей і без
Цикл з колекції. З load (перезавантаження) моделей всередині циклу:

$temp = array();
$collection = Mage::getModel('catalog/product')->getCollection()->addAttributeToSelect(...);
foreach ($collection as $product) {
$product = $product->load($product->getId());
$temp[] = $product->getSku();
}

Цикл з колекції. Без load моделей всередині:

$temp = array();
$collection = Mage::getModel('catalog/product')->getCollection()->addAttributeToSelect(...);
foreach ($collection as $product) {
$temp[] = $product->getSku();
}

3 види вибірки даних:

  1. addAttributeToSelect('*');// все атрибути
  2. addAttributeToSelect('sku');// 1 статік атрибут
  3. addAttributeToSelect('name');// 1 стандартний атрибут
Результати

Як ви мабуть помітили, що час без перезавантаження моделей В РАЗИ менше, ніж коли ви перезавантажуємо модельки. Так само час ще менше, коли Flat таблиці включені (тобто немає зайвих джойнов і юніонів) і ми вибираємо тільки необхідні атрибути.

В 1ом випадку ми виконуємо завантаження з купою джойнов… а потім робимо це знову, але для модельки і так 2000 разів.

2ий раз ми робимо це для статік атрибута (він знаходиться в тій же табличці, що і сам продукт) і Magento не треба робити джойны. Тому час менше.

3ий раз Magento потрібно приджойнить ще табличку де зберігається цей атрибут.

З Flat таблицями все аналогічно, а в 2-ох випадках все ідентично – це тому, що обидва атрибути знаходяться в таблиці 1, звідси і час ідентичне.

Думаю цифри говорять самі за себе.


Кешування колекцій
Без кеша:

$collection = Mage::getModel('catalog/product')->getCollection()
->addAttributeToSelect('*');

Використовуючи метод initCache:

$collection = Mage::getModel('catalog/product')->getCollection()
->addAttributeToSelect('*')
->initCache(Mage::app()->getCache(),'our_data',array('SOME_TAGS'));

Кастомний реалізація кешування:

$cache = Mage::app()->getCache();
$collection = $cache->load('our_data');
if(!collection) {
$collection = Mage::getModel('collection/product')->getCollection()->addAttributeToSelect('*')->getItems();
$cache->save(serialize($collection),'our_data',array(Mage_Core_Model_Resource_Db_Collection_abstract::CACHE_TAG));
} else {
$collection = unserialize($collection);
}

Розглянемо вибірку без використання кешу, з використанням методу, який нам пропонує Magento і з милицею, який я ніде не бачив… сам сваял, заснований на методах модельки кеша. Зверніть увагу, що для всіх тестів після складення запиту я виробляв завантаження даних і перетворення колекції до масиву об'єктів.

Результати

Без кешу власне нічого дивного… все як завжди.

А от використовуючи маджентовский кеш особисто я здивувався, коли побачив, що час стало більше. А про EAV кешування взагалі дурною витівкою, тому що EAV колекція вантажить спочатку сутності з таблиці продуктів (саме ось це і кешується), а потім окремим запитом вибирає значення атрибутів і заповнює об'єкти. Під Flat там всі з 1 таблиці женеться. Але тим не менш час більше уходится на роботу з кешем ніж з БД (тестував я причому як з файловою системою, так і з redis – відмінності 4а цифра після коми… тобто на 2к сутності її немає). Суть InitCache методу полягає в тому, що він спочатку збере всі дані в колекцію сам (пагинация, фільтри, events і так далі), створить хеш з sql запиту і його буде шукати в кеші, а якщо там щось є, то він це ансерелизует, а потім відбувається запуск всіх events і наступних методів. Це найповільніша процедура у всьому процесі, саме ось тут виходить що кеш повільніше ніж простий запит до БД. Але зате не надсилає запит до БД… що не так і страшно.

Окремо варто приклад з кешем, написаним мною на коліні, там ми кешуємо кінцевий результат колекції, причому минаючи всі events і дозагрузку атрибутів. Це працює для EAV і для Flat колекцій.

Правильне використання count() і getSize()
getSize()

$size = Mage::getModel('catalog/product')->getCollection()
->addAttributeToSelect('*')
->getSize();

count()

$size = Mage::getModel('catalog/product')->getCollection()
->addAttributeToSelect('*')
->count();

Результати

Різниця методів полягає в тому, що count() виробляє завантаження всіх об'єктів колекції, а потім звичайним пхпшным count'ом підраховує кількість об'єктів і повертає нам число. getSize ж не виробляє завантаження колекції, а генерує ще 1 запит до БД, де немає лімітів, ордерів та списку вибраних атрибутів, є лише COUNT(*).

Приклад використання обох методів такий:

Якщо вам треба знати, чи взагалі значення в БД чи скільки їх – використовуйте getSize, якщо ж вам у будь-якому випадку колекція потрібна завантажена, або вже завантажилася то використовуйте count() – він поверне вам кількість елементів, завантажених в колекцію.

Правильне використання getFirstItem і setPage(1,1)
getFirstItem()

$product = Mage::getModel('catalog/product')->getCollection()
->getFirstItem();

setPage(1,1)

$product = Mage::getModel('catalog/product')->getCollection()
->setPage(1,1)
->getFirstItem();

load()

$product = Mage::getModel('catalog/product')->load(22);

Результати

Проблема getFirstItem в тому, що він завантажує всю колекцію цілком, а потім просто в foreach повертає перший елемент, а якщо його немає то повертає порожній об'єкт.

setPage ($this->setCurPage($pageNum)->setPageSize($pageSize)) ж обмежує вибірку рівне 1 записом, що як ви бачите значно прискорює завантаження результату.

Навіть load швидше getFirstItem, але зверніть увагу, що load виявився повільніше ніж вибірка з колекції 1 елемента. Це пов'язано з тим, що load завжди працює з EAV таблицями.



Висновки
Підводячи підсумок всьому вище написаному, хочу порадити всім людям, що працюють з Magento:

  • Ніколи не викликайте повторно load метод у об'єктів, отриманих з колекції
  • Завантажуйте тільки необхідні атрибути
  • Якщо застосовно до проекту, використовуйте Flat таблиці
  • Використовуйте count для підрахунку результатів завантаженої колекції і getSize для отримання числа всіх записів
  • Не використовуйте getFirstItem метод без setPage(1,1) або аналогічних методів

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

0 коментарів

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