Увеличение производительности 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 атрибута для другого стора – новая строчка в таблице значений этого атрибута.

Как это хранится в БД

Entity:
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 таблицы используются ТОЛЬКО на страницах категорий, списке продуктов в составе Group product, да и вообще везде, где используется коллекция. Они не используются на странице товаров, в админке, при использовании метода 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();         Loop end         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ух случаях вcе идентично – это потому что оба атрибута находятся в 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) или аналогичных методов

ссылка на оригинал статьи https://habrahabr.ru/post/282025/