Як ми прискорювали Drupal Commerce

    photo's author: Corrie...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 ms
 After: 0.13 ms
 
Так, «немає межі досконалості», тому ми продовжуємо експерименти з оптимізації. Сподіваємося, наш досвід буде комусь корисний, і на світ з'явиться багато зручних і швидких інтернет-магазинів на Drupal Commerce :)
    
Джерело: Хабрахабр

0 коментарів

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