Как мы ускоряли Drupal Commerce
Disclamer: если все, о чем написано далее, покажется для вас «детским лепетом» и совсем уж очевидными вещами, будем рады поработать с вами :)Предыстория: около года назад наша небольшая, но гордая веб-студия получила заказ на разработку интернет-магазина printer38.ru. А так как мы специализируемся на CMS Drupal, в качестве модуля интернет-магазина решили использовать Drupal Commerce.
Тех, кому интересно, почему загрузка одной страницы каталога занимала у нас 5 минут, и как нам удалось это побороть, прошу пожаловать под кат.
Если вы когда-нибудь подбирали принтер через Яндекс Маркет, вы должны представлять количество полей у подобных товаров. У нас для каждого товара в базе хранится 184 поля с характеристиками — от скорости печати до наличия возможности работы от аккумулятора.
Здесь надо сказать об одной особенности CMS Drupal — на каждое поле создается отдельная таблица в базе данных. Плата за универсальность, чего хотите…Другая особенность, уже конкретно нашего проекта — в том, что многие поля используются в фильтре, из за чего кэшировать всю страницу каталога не удается. Таким образом, при отображении страницы каждый раз происходит запрос к базе данных.
Первый раз, когда мы создали views, в который честно вывели все поля фильтров, нам так и не удалось дождаться загрузки главной страницы сайта. И это при том, что сайт работает на отдельном сервере с весьма неплохими характеристиками.
Начались классические «танцы с бубном» — оптимизация MySQL, кэширование запросов, жесткий дебаггинг и профайлинг :)Попытаюсь в этом посте восстановить последовательность действий по оптимизации, в результате которых нам удалось добиться приемлемой скорости загрузки страниц сайта.
1. Подключение модуля memcachedНа любой «пинок» в сторону Drupal — мол, медленно работает, — его евангелисты отвечают «Use cache». Собственно, этим и занялись.Стандартное кэширование Drupal, как известно, сохраняет кэш в ту же базу данных, что в нашем случае изначально было бесполезно.Поэтому решено было держать кэш в оперативной памяти — благо, на нашем сервере ее было достаточно. Для этой цели использовали memcached на сервере и memcache_storage в Drupal (спасибо Евгению Spleshka за замечательный модуль).После переноса данных в кэш все стало шевелится заметно быстрее, но все же не так, как хотелось. Разбираемся дальше…
2. Вынос кэша форм в memcached В один из пасмурных дней мы обратили внимание на какой-то нечеловеческий размер таблицы cache_form — более 7Gb! Таблицу почистили, однако она снова стала расти молниеносными темпами.Причина оказалась простой: каждая кнопка «Купить» в нашем каталоге товаров — это мини-форма, которую Drupal считает необходимым положить в кэш, попутно утягивая туда «все, что попадется под руку». А при использовании AJAX-кнопок (как в нашем случае) кэш начинает расти на порядок быстрее. Вдобавок к этому, по какой-то ведомой только разработчикам Drupal таблица cache_form не очищается автоматически, как это происходит с другими таблицами кэша.
Понять проблему — значит на 90% решить ее:)cache_form был так же вынесен в memcached (вопреки рекомендациям хранить ее в базе данных).Для периодической очистки таблицы использовали модуль optimizedb (теперь ставим его по умолчанию на все сайты с Drupal Commerce).Проблему с AJAX-кнопками решил xandeadx, но его решение появилось только спустя пару месяцев после описанных мной действий, поэтому в то время мы не могли использовать его в нашем проекте. ;)
Главная страница сайта стала «летать». Однако с каталогом все еще беда — страница открывается 2–3 минуты. :(
3. Отказ от полей во views «Друзья» — как то сказал наш самый опытный разработчик, — «Мы все знаем, что Drupal кэширует ноды. Так почему мы не пользуемся этим, и заставляем каждый раз views дергать все данные из базы данных?».Сказано — сделано. Каких-то 1–2 часа работы команды на перепиливание вьюсов и переверстывание страниц, и — о чудо — удалось снизить загрузку страницы каталога до «каких-то» 40–50 секунд. Пользователи подождут, верно ведь? Им торопиться некуда…
4. Очередная попытка оптимизации — отказ от стандартного пагинатора, подключение кэширования во «вьюсе», кэширование сущностей (entity) Дальше программисты опять достали бубен со шкафа (благо, далеко убрать не успели).У умных людей прочитали, что проблемой «тормозов» может быть стандартный пэйджер (он же пагинатор, он же paging). Вылечили установкой модуля views_litepager.
Заодно поставили модуль commerce_entitycache, который должен кэшировать entity (сущности) объекта product.
Однако все эти «пляски» дали лишь небольшой прирост в скорости.
Самый существенный результат дало подключение кэша для views, однако и здесь оказалось не все гладко. Во-первых, при кэшировании запроса наш фильтр товаров стал выдавать один и тот же результат, пришлось отключить. А во-вторых, ускорение наблюдалось только при загрузке «чистой» страницы, когда ни один фильтр не выбран. Стоило выбрать хотя бы один чекбокс, и можно было опять идти пить кофе в ожидании загрузки страницы.
Page execution time was 69728.43 msМ-да…
6. Почти победа. Ручная оптимизация запроса views В определенный момент мы поняли, что пора действовать жесткими методами. А именно — детально изучить, что же такое views запрашивает в базе данных, что на формирование результата уходит не менее 30 секунд.И увидели мы примерно такое:
… INNER JOIN {commerce_product} commerce_product_field_data_field_product_reference ON field_data_field_product_reference.field_product_reference_product_id = commerce_product_field_data_field_product_reference.product_id INNER JOIN {field_data_commerce_price} commerce_product_field_data_field_product_reference__field_data_commerce_price ON commerce_product_field_data_field_product_reference.product_id = commerce_product_field_data_field_product_reference__field_data_commerce_price.entity_id AND ( commerce_product_field_data_field_product_reference__field_data_commerce_price.entity_type = 'commerce_product' AND commerce_product_field_data_field_product_reference__field_data_commerce_price.deleted = '0' ) INNER JOIN {field_data_field_printer_a4_speed_2} commerce_product_field_data_field_product_reference__field_data_field_printer_a4_speed_2 ON commerce_product_field_data_field_product_reference.product_id = commerce_product_field_data_field_product_reference__field_data_field_printer_a4_speed_2.entity_id AND ( commerce_product_field_data_field_product_reference__field_data_field_printer_a4_speed_2.entity_type = 'commerce_product' AND commerce_product_field_data_field_product_reference__field_data_field_printer_a4_speed_2.deleted = '0' ) … и так — для каждого поля, участвующего в фильтрации.Да, JOIN’ов много. Но не могут они отрабатывать ТАК долго! «Постойте, а зачем нам проверка на тип? У нас же все сущности с id товара должны быть 'commerce_product'?»
Взяв в руки IDE, пишем небольшой хук в своем модуле:
/** * Implementation of hook_views_query_alter * @param type $view * @param type $query */ function mymodule_views_query_alter (&$view, &$query) { if ($view→name == 'catalog_v_2') { foreach ($query→table_queue as $key=>$item) { $query→table_queue[$key]['join']→extra=array (); } } } То есть просто «выкидываем» все дополнительные условия из JOIN (в том числе непонятный deleted, который, по нашему опыту, всегда равен нулю).Можно было совсем избавиться как минимум от одного JOIN’а, но была уже поздняя ночь, и всем хотелось спать :)
Сравним:
Before: 34665.211 msAfter: 0.13 ms
Да, «нет предела совершенству», поэтому мы продолжаем эксперименты по оптимизации. Надеемся, наш опыт будет кому-то полезен, и на свет появится много удобных и быстрых интернет-магазинов на Drupal Commerce:)