/sites/default/files/2020-05/office-2717014_1920.jpg

Влияние скорости загрузки страниц сайта на отказы и конверсии

На какие только уловки не идут владельцы лендингов, интернет-каталогов, веб-порталов и интернет-магазинов для привлечения посетителей к своим сайтам. Заказывают оригинальный и красивый дизайн, стараются наполнить интересным и содержательным контентом, придумывают разнообразные способы взаимодействия с посетителями, проводят дорогие рекламные кампании. Делают всё возможное для достижения одной цели — привлечения как можно большего количества посетителей, рассчитывая увеличить таким образом количество сделок, покупок и прочих доходных транзакций.

В статьях, посвящённых проблеме повышения коммерческой эффективности веб-проектов, основными факторами, влияющими на достижение поставленной цели, указываются дизайн, качество контента, удобство пользования («юзабилити»), регулярное обновление и качество кода. Всё это, конечно же, справедливо.

Однако нередко происходит так, что очередной шедевр сайтостроения, в который было вложено совершенно немыслимое количество денег, сил и нервов, оформленный самыми лучшими дизайнерами и наполненный тщательно продуманным и оптимизированным для поиска контентом, почему-то не пользуется высокой популярностью, на которую был рассчитан и не производит того эффекта, ради получения которого был задуман.

Проблема в том, что при создании и дальнейшем функционировании сайта мало кто заботится о скорости, с которой открываются страницы сайта в браузерах посетителей. Но как показывают, проведённые на эту тему, исследования и доказывает практика, этому вопросу стоит уделить особое внимание.

Например, согласно результатам исследования компании Akamai, оказывающей услуги по ускорению работы сайтов, было установлено следующее:

  • 47% пользователей ожидают, что веб-страница загрузится в течение 2 секунд;
  • 40% посетителей могут уйти с сайта, который грузится более 3 секунд;
  • 52% утверждают, что скорость загрузки влияет на лояльность посетителей;
  • 3 секунды ожидания уменьшают лояльность клиентов примерно на 16%.

То же самое утверждает и Google: «Если сайт загружается дольше трех секунд, 53% пользователей покинут его».

Очень интересны результаты исследования, проведенные Пэтом Минаном (Pat Meenan) из Google и Тамми Эвертсом (Tammy Everts) из SOASTA, представленные на конференции Velocity 2016.

«...В этом исследовании использовался специальный счетчик (системы mPulse) для сбора данных на множестве живых сайтов в области электронной коммерции. Среди данных были следующие: операционная система, браузер, география, длительность сессии, заголовки запросов, времена загрузки страниц, скорость подключения, состав страниц (количество элементов, картинок и т.д.). Использовалось более 1100000 загрузок страниц...»

В результате исследования были выявлены главные шесть детерминантов, влияющих на конверсии и процент отказов:

  1. Количество элементов на странице.
  2. Количество изображений на странице.
  3. Количество скриптов (JavaScript).
  4. Время загрузки страницы (после получения документа).
  5. Полное время загрузки.
  6. Время генерации HTML-документа сервером.

Первые три пункта относятся к составу страницы, три следующих — к скорости загрузки страницы. Вывод очевиден: скорость, с которой грузятся страницы сайта, влияет на количество отказов со стороны посетителей и на процент конверсии.

Исходя из полученных результатов исследования, для всех владельцев сайтов, желающих повысить конверсию или вовлечённость посетителей, рекомендуется провести аудит скорости сайта и инвестировать в его ускорение.

С точки зрения пользователя все ещё проще: никто не любит долгой загрузки страниц сайта, всем нравится быстродействие и скорость. Чем быстрее грузятся страницы, тем комфортнее чувствует себя посетитель сайта, тем благоприятнее у него впечатление о компании — владельце сайта.

Ну, или наоборот: чем медленнее грузятся страницы сайта, тем хуже отношение посетителя к его владельцу, а значит — тем меньше вероятности конверсии визита в сделку.

Проверять скорость загрузки страниц вашего сайта просто загружая их на свой компьютер недостаточно — это не покажет вам реального положения дел. Для получения реальной картины необходимо использовать сторонние сервисы, специально предназначенные для тестирования скорости работы сайта. Например, PageSpeed Insights — Google Developers покажет не только скорость загрузки страниц, как для стационарных компьютеров, так и для мобильных устройств, но и выдаст список предполагаемых проблем.

 

Результат тестирования скорости работы сайта
Рис. 1. Результат тестирования скорости работы сайта

 

Причины медленной загрузки страниц, возникшие из-за содержимого самого сайта, найти и устранить относительно просто — для этого надо оптимизировать графический контент по количеству и объёму картинок, сократить и оптимизировать таблицы стилей, java-скрипты, HTML-код. Сайты, созданные профессионалами, как правило, множеством ошибок в коде или графическом контенте не страдают, но избежать их совсем, к сожалению, не получается. Впрочем, они легко устраняются в процессе «обкатки» сайта при последующей его оптимизации.

Медленная загрузка страниц сайта может происходить также по причине низкой скорости сетевых интерфейсов аппаратного уровня и низкой пропускной способности физических каналов связи, через который веб-сервер подключается к Сети. Выбирайте оборудование и подключение, которое способно обеспечить необходимую пропускную способность.

Если ваш сайт является высоконагруженным интернет-каталогом или интернет-магазином, содержащим объёмную базу данных и требующим обработки большого количества транзакций, то замедление загрузки страниц также может происходить по причине высокой загруженности самого веб-сервера, не успевающего обработать все запросы своевременно. Такое происходит, как правило, когда обмен данными большого объёма происходит через жёсткие диски, скорость работы интерфейсов которых недостаточно высока для обработки больших объёмов информации.

Постановка задачи

В данной статье описывается реальная работа по оптимизации скорости обработки запросов веб-сервером, на котором работает сайт интернет-магазина одного из наших клиентов. Коммерчески проект оказался весьма успешным и стал довольно популярным. Со временем сервер перестал справляться с возросшей нагрузкой и в «часы пик» стал заметно тормозить, в результате чего ощутимо снизилась скорость его работы.

Проведённые тесты показали, что среднее время загрузки страниц составило более 350 мс, а иногда посетителям сайта даже не в «пиковое» время приходилось ждать полной загрузки страницы более одной секунды. Дальнейшее игнорирование проблемы становилось невозможным, так как рост посещаемости продолжался, из-за чего время загрузки «медленных» страниц превысило критические 3 секунды, о чём сообщалось в панели Яндекс.Вебмастер в списке критичных ошибок сайта.

 

Сообщение Яндекс.Вебмастер: «При обращении к страницам сайта среднее время ответа сайта превышает 3 секунды...»
Рис. 2. Сообщение Яндекс.Вебмастер: «При обращении к страницам сайта среднее время ответа сайта превышает 3 секунды...»

 

Поэтому было принято решение оптимизировать работу сервера так, чтобы уменьшить время обработки входящих запросов и ускорить выдачу данных.

Для повышения скорости обработки и обмена информацией используют кэширование данных: наиболее часто используемые данные перемещают в оперативную память компьютера. Для проектов на Drupal чаще всего используется утилита Memcached, реализующая сервис кэширования данных в оперативной памяти на основе хеш-таблицы. Описанию процесса оптимизации работы веб-сервера и ускорению работы сайта с помощью настройки кэширования данных посвящена данная статья.

Оптимизация работы форм интернет-магазина и кеширование страниц с формами

Одной из ключевых метрик скорости загрузки страниц сайта является время генерации страницы веб-сервером. В работе по оптимизации сайта и сервера для уменьшения времени генерации страниц участвуют наши системные администраторы и специалисты DevOps, разработчики, специалисты со стороны заказчика и хостинг-провайдера. Мы используем профилировку XDebug медленных страниц и анализ времени выполнения отдельных функций PHP для поиска и устранения узких мест в коде сайта.

Для сбора агрегированной статистики времени обработки запроса на загрузку страниц сервером, а также скорости выполнения запросов к MySQL и Solr, с целью последующего анализа, использовался сервис мониторинга NewRelic.

В публикуемой статье описана проблема, довольно распространённая для сайтов на Drupal, и показано решение, позволяющее на 32% уменьшить время обработки запросов на загрузку страниц интернет-магазина.

Проблема

Есть в Drupal 7 давняя особенность — форма автоматически кэшируется при использовании AJAX фреймворка. При каждом выводе такой формы в таблице cache_form создаётся две записи — одна для структуры формы и одна для её состояния. Кэширование необходимо для корректной работы AJAX обработчика, которому нужно знать структуру и последнее состояние формы.

В случае, когда таких форм на странице много, возникает проблема быстрого роста количества записей в таблице cache_form. Например, «Добавить в корзину» в Commerce: запрос на вывод 50 товаров приведёт к созданию 100 записей в таблице cache_form при каждом просмотре страницы каталога. На сайте с высокой нагрузкой это приводит к тому, что таблица cache_form может иметь размер в несколько десятков гигабайт.

Учитывая тот факт, что кэш форм в Drupal 7 тесно связан с кэшированием страниц с минимальным временем жизни памяти (задаётся на странице «Производительность»), если удалить кэш формы раньше, чем память страницы, возникнет ошибка «Некорректные POST-данные формы» при взаимодействии с формой. Если задать минимальное время жизни памяти, устаревший кэш форм не будет очищаться в течение установленного времени.

Для исправления этой проблемы уже существует несколько решений:

  • Переменная form_cache_expiration. Данная переменная была добавлена в версии 7.61 и позволяет управлять временем хранения кэша форм, которое по умолчанию равно 6 часам. Основной недостаток заключается в сильной зависимости от механизма очистки устаревшей памяти, без своевременного вмешательства которого, размер cache_form будет продолжать расти.

  • Модуль OptimizeDB. Позволяет гибко настроить очистку таблицы cache_form по cron. Можно задать полную очистку таблицы или очистку только устаревших записей. Но тогда высока вероятность возникновения ошибки «Некорректные POST-данные формы», при этом размер таблицы cache_form всё равно останется слишком большим.

  • Модуль Safe cache_form Clear предоставляет Drush команду для очистки устаревших записей в таблице cache_form. Недостатки аналогичны недостаткам модуля OptimizeDB.

  • Модуль Commerce Fast Ajax Add to Cart. Решение от xandeadx, направленное на корень проблемы — кэширование стандартной формы Drupal Commerce добавления товара в корзину. Минус решения, как ни странно, в том, что AJAX фреймворк не используется, а для нашего проекта разработчиками уже был написан нестандартный диалог добавления в корзину с использованием AJAX-команд Drupal. Кроме того, это решение не универсально и работает только для формы добавления товара в корзину;

  • Патч #94 из этого issue. Кроме применения патча потребуется дописать обработчик для нужной формы. Это решение может работать нестабильно для страниц со множественным выводом форм. Не работает для страниц со случайным списком товаров. Ну и уже большой минус в том, что надо патчить ядро.

Решение

Для примера возьмём чистую установку Drupal 7.67 с модулем Commerce 1.15. Реализуем страницы каталога с помощью Views. На каждой странице выведем по 50 товаров, в каждом тизере товара выведем кнопку добавления в корзину. Для удобства генерируем товары с помощью модуля Commerce Devel. Для AJAX-ификации кнопки добавления товара в корзину используем модуль Commerce Ajax Add to Cart. Открываем страницу каталога и проверяем — в таблице cache_form появилось 100 новых записей, проблема воспроизведена.

Решение, предлагаемое в данной статье, частично основано на данном комментарии. Для его реализации потребуется создать небольшой кастомный модуль или добавить код в уже имеющийся. В нашем примере это будет модуль custom.

Первым делом определим путь для нового AJAX-обработчика формы. По своей структуре он похож на определение пути «system/ajax» в модуле system.

 

  
/**
 * Implements hook_menu().
 */
function custom_menu() {
  $items['custom/form/ajax'] = array(
    'title' => 'AJAX callback',
    'page callback' => 'custom_form_ajax_callback',
    'delivery callback' => 'ajax_deliver',
    'access arguments' => array('access content'),
    'theme callback' => 'ajax_base_page_theme',
    'type' => MENU_CALLBACK,
  );
  return $items;
}

  

 

Изменим путь AJAX-обработчика у кнопки добавления товара в корзину (свойство path). Здесь важно не путать свойства path и callback — первое определяет адрес, на который будет отправлен AJAX-запрос, а второе указывает функцию, которая при этом запросе будет вызвана для формирования ответа. Как правило, path не указывают и берётся значение по умолчанию «system/ajax», его и требуется поменять. Также принудительно отключим кэширование, интересующей нас, формы.

 

  
/**
 * Implements hook_form_alter().
 */
function custom_form_alter(&$form, &$form_state, $form_id) {
  if (strpos($form_id, 'commerce_cart_add_to_cart_form') !== FALSE) {
    // Указываем, что хотим самостоятельно обработать AJAX-запрос к форме.
    $form['submit']['#ajax']['path'] = 'custom/form/ajax';
 
    // Отключаем кэширование формы.
    $form_state['no_cache'] = TRUE;
  }
}

  

 

Наконец, реализуем функцию custom_form_ajax_callback(), которую ранее указали в определении пути «custom/form/ajax». Код функции частично повторяет код функций ajax_get_form() и ajax_form_callback(). Основная идея заключается в том, что нужно получить правильное состояние формы без использования кэша, так как мы его уже отключили. Важно отметить, что приведённый далее код универсален и может быть применен для отключения кэширования других AJAX-форм, за исключением блока, в котором выполняется формирование товарной позиции. Именно в данном блоке происходит построение состояния формы, необходимой для корректной валидации и сабмита. Для поддержки атрибутов товаров потребуется доработка этого кода. Для других форм потребуется написать аналогичный код.

 

  
/**
 * Menu callback; handles Ajax requests for forms without caching.
 *
 * @return array|null
 *   Array of ajax commands or NULL on failure.
 */
function custom_form_ajax_callback() {
  // Проверяем, что обрабатываем AJAX-запрос к форме.
  if (isset($_POST['form_id']) && isset($_POST['form_build_id'])) {
    $form_build_id = $_POST['form_build_id'];
    $form_id = $_POST['form_id'];
    $commands = array();
 
    // Инициализируем состояние формы.
    $form_state = form_state_defaults();
    $form_state['build_info']['args'] = array();
 
    // Заполняем состояние формы. Данный код уникален в рамках обрабатываемой формы.
    // Проверяем, что форма является формой добавления товара в корзину.
    if (strpos($form_id, 'commerce_cart_add_to_cart_form_') === 0) {
      $product = commerce_product_load($_POST['product_id']);
 
      if (!empty($product)) {
        // Формируем сущность товарной позиции на основе данных отправленной формы.
        $line_item = commerce_product_line_item_new($product, $_POST['quantity'] ?? 1);
        $line_item->data['context']['product_ids'] = array($product->product_id);
        $line_item->data['context']['add_to_cart_combine'] = TRUE;
 
        // Добавляем товарную позицию в состояние формы.
        $form_state['build_info']['args'] = array($line_item);
      }
    }
 
    // Строим форму, будут вызваны билдеры и соответствующие хуки.
    $form = drupal_retrieve_form($form_id, $form_state);
    drupal_prepare_form($form_id, $form, $form_state);
    $form['#build_id_old'] = $form_build_id;
 
    // Обрабатываем форму аналогично тому, как это сделано в ajax_get_form().
    if ($form['#build_id_old'] != $form['#build_id']) {
      $commands[] = ajax_command_update_build_id($form);
    }
    $form_state['no_redirect'] = TRUE;
    $form_state['rebuild_info']['copy']['#build_id'] = TRUE;
    $form_state['rebuild_info']['copy']['#action'] = TRUE;
    $form_state['input'] = $_POST;
 
    // Обрабатываем форму аналогично тому, как это сделано в ajax_form_callback().
    drupal_process_form($form['#form_id'], $form, $form_state);
    if (!empty($form_state['triggering_element'])) {
      $callback = $form_state['triggering_element']['#ajax']['callback'];
    }
    if (!empty($callback) && is_callable($callback)) {
      $result = $callback($form, $form_state);
      if (!(is_array($result) && isset($result['#type']) && $result['#type'] == 'ajax')) {
        $result = array(
          '#type' => 'ajax',
          '#commands' => ajax_prepare_response($result),
        );
      }
      $result['#commands'] = array_merge($commands, $result['#commands']);
      return $result;
    }
  }
  return NULL;
}

  

Результаты

В нашем примере добавление вышеуказанного кода приводит к сохранению AJAX-функционала кнопки добавления товара в корзину при отключённом кэшировании формы. Обновление страницы каталога теперь не приводит к созданию 100 записей в таблице cache_form. Результат обработки AJAX-запроса аналогичен результату обработки при использовании кэша. Изменения формы, добавленные в рамках Drupal API (например, атрибуты товаров) либо потребуют небольших изменений в коде (построения состояния формы), либо не потребуют их вообще.

Кроме того, интересен результат применения решения на реальном проекте, для которого оно и было реализовано:

  • Количество SELECT запросов к таблице cache_form уменьшилось в 10 раз;
  • Количество INSERT запросов к таблице cache_form уменьшилось в 10 раз;
  • Среднее время обработки запроса сервером уменьшилось на 32% (с 352 до 241 миллисекунд).

Более подробно статистика отображена на скриншотах.

 

Количество SELECT и INSERT запросов в минуту к таблице cache_form
Рис. 3. Количество SELECT и INSERT запросов в минуту к таблице cache_form

 

Трудоёмкость INSERT запросов к таблице cache_form.

Рис. 4. Трудоёмкость INSERT запросов к таблице cache_form

 

Количество INSERT запросов в минуту

Рис. 5. Количество INSERT запросов в минуту

 

Количество SELECT и INSERT запросов в минуту к таблице cache_form

Рис. 6. Количество SELECT и INSERT запросов в минуту к таблице cache_form

 

Время обработки запроса сервером до изменений

Рис. 7. Время обработки запроса сервером до изменений

 

Время обработки запроса сервером после изменений.

Рис. 8. Время обработки запроса сервером после изменений

 

Среднее время от запроса до ответа сервера APP SERVER уменьшилось с 352 до 241 миллисекунд. Размер таблицы cache_form уменьшился с ~10Гб до 200Мб.

Таким образом получилось сократить время транзакции на 32%. Дальнейшие доработки других форм сайта аналогичным образом позволят улучшить эти показатели.

Тонкая настройка кэширования страниц в Memcached и мониторинг кеша страниц

Одним из самых важных факторов, влияющих на отказы и конверсии интернет-магазина, да и любого сайта, является скорость загрузки страниц.

Слишком медленная работа сайта способна свести на нет любые старания владельца по повышению конверсии веб-проекта. Причина слишком медленной загрузки веб-страниц очень часто содержится в медленной обработке запросов и низкой скорости процессов обмена данными на самом веб-сервере.

Как известно, самыми медленными в компьютере являются операции ввода и вывода данных, хранящихся на жёстком диске. Даже самые быстрые современные устройства хранения зачастую не могут обеспечить требуемую скорость чтения и записи данных через интерфейс жёсткого диска. Чтобы ускорить процессы обмена и обработки информации применяется кэширование — т.е. хранение часто используемых данных в оперативной памяти, для чего в проектах на Drupal чаще всего используется утилита Memcached, настройке которой и посвящен данный раздел статьи.

Проблема

Работая над задачей по увеличению скорости загрузки страниц сайта интернет-магазина, мы обнаружили неожиданное поведение сервиса кэширования. Несмотря на то, что утилита кэширования данных Memcached была настроена и запущена, страницы отдавались по запросу не из памяти, как ожидалось, а генерировались всякий раз заново, загружая данные с жёсткого диска даже при повторном запросе от пользователя.

Погрузившись в проблему, мы поняли, что невозможно определить объем памяти, используемый именно под хранение страниц в кэше, ведь кэш Drupal хранит не только их. Было принято решение тщательно исследовать работу Memcached и перенастроить его так, чтобы страницы выдавались из памяти сайта.

Анализ

Для того, чтобы понять масштаб проблемы и оценить потенциальную эффективность ее решения мы использовали следующие инструменты:

  • memcached-tool — perl-скрипт, входящий в стандартную поставку Memcached. Позволяет просматривать статистику запущенного процесса Memcached. Его удобно использовать в bash скриптах для организации автоматического мониторинга.
  • PHPMemcachedAdmin — панель управления для отображения статистики и работы Memcached, с помощью которой удобно осуществлять мониторинг работы Memcached.
  • логи WEB сервера. Мы включили в действующий формат логов nginx логирование заголовков кэша Drupal. Для этого в директиве log_format добавили еще одну переменную $sent_http_x_drupal_cache.
  • Zabbix-сервер — центральный узел мониторинга.

Далее рассмотрим подробнее, как работает Memcached и как мы использовали, перечисленные выше, инструменты.

Администраторы сайтов и веб-серверов зачастую просто устанавливают Memcached, выделяя ему, например, 1-2 Гб оперативной памяти, и благополучно забывают про него, не заморачиваясь долгой и трудоёмкой настройкой, полагая, что он прекрасно работает и без этого. Но так ли это на самом деле? С помощью панели PHPMemcachedAdmin можно детально изучить работу службы. Вот пример главного экрана мониторинга:

 

Главный экран мониторинга PHPMemcachedAdmin

Рис. 9. Главный экран мониторинга PHPMemcachedAdmin

 

Первое, что обращает на себя внимание — используемая память. Если вы используете 100% выделенной памяти, значит новые запросы не будут кэшироваться, так как под них не остаётся ресурсов.

Второй важный параметр — wasted. Если процент wasted высокий, значит используемая память расходуется не рационально. Чтобы понять, что такое wasted, нужно разобраться, как Memcached использует выделенную ему память.

Вся выделяемая память в Memcached делится на слабы (Slab). Слаб, в свою очередь, содержит чанки (chank), а в чанках уже хранятся непосредственно данные. Каждый слаб хранит в себе чанки одинакового размера. Такой подход к хранению данных позволяет Memcached легко и быстро находить область в памяти для записи новых данных.

Когда в Memcached приходят данные для записи, он находит слаб, который содержит чанки размером, максимально приближенном к размеру поступивших данных, и записывает эти данные в него, при условии, что он свободен. Более в этот чанк ничего записать нельзя. Если размер чанка превышает размер записанных в него данных, в чанке остается немного свободной памяти, которую уже нельзя использовать. Суммарный объем этой неиспользуемой памяти и составляет wasted.

Таким образом, только посмотрев на главный экран панели PHPMemcachedAdmin, можно сразу определить, насколько рационально используется память в Memcached и нужно ли вообще что-то менять. Но если используемая память расходуется на 100%, а процент Wasted относительно невысок, то достаточно увеличить выделяемую для Memcached память. Если же процент Wasted высокий необходимо оптимизировать работу Memcached. Хорошим показателем работы является 15-25% Wasted и 20-25% свободной памяти. Это означает, что ваш экземпляр Memcached рационально использует выделенную ему память и у него есть небольшой запас для записи новых запросов.

Решение

В зависимости от того, что хранится в кэше, а точнее — от объема данных, Memcached по-разному настраивается. Поэтому первое, что мы сделали — отделили кэш страниц от всего остального, т.к. размер страницы сильно отличается от остального кэша, который хранится в памяти.

Для этого на сервере клиента мы развернули второй инстанис Memcached. Сделать это было не сложно — мы скопировали существующий юнит systemd и вписали в него новый путь к конфигурационному файлу.

 

  
cat /etc/systemd/system/memcached1.service 
[Unit]
Description=memcached daemon
After=network.target
Documentation=man:memcached(1)

[Service]
ExecStart=/usr/share/memcached/scripts/systemd-memcached-wrapper /etc/memcached1.conf

[Install]
WantedBy=multi-user.target
  

 

Затем скопировали файл /etc/memcached.conf в /etc/memcached1.conf и указали в нем новый файл сокета.

 

  
systemctl daemon-reload
systemctl start memcached1 && systemctl enable memcached1
  

 

Готово. Теперь у нас есть два экземпляра Memcached (memcached и memcached1), с которыми «научим» работать Drupal. Мы используем модуль Memcached Storage. Для его настройки изменим блок описания кэша в файле settings.php:

 

  
$conf['cache_backends'][] = 'sites/all/modules/memcache_storage/memcache_storage.inc';
$conf['cache_default_class'] = 'MemcacheStorage';
$conf['cache_class_cache_update'] = 'DrupalDatabaseCache';
$conf['cache_class_cache_form'] = 'DrupalDatabaseCache';
# Configure Memcache Storage module.
$conf['memcache_storage_wildcard_invalidate'] = 60 * 60 * 24;
# Set current extension.
$conf['memcache_extension'] = 'Memcached';
# Configure memcached extenstion.
$conf['memcache_options'] = array(
  Memcached::OPT_TCP_NODELAY => TRUE,
  Memcached::OPT_BUFFER_WRITES => TRUE,
  Memcached::OPT_NO_BLOCK => TRUE
);
$conf['memcache_servers'] = array(
  'unix:///tmp/memcached.sock' => 'default',
  'unix:///tmp/memcached1.sock' => 'page',
);
$conf['memcache_bins'] = array(
  'cache'           => 'default',
  'cache_page'      => 'page',
);
# Set custom expiration for cached pages.
$conf['memcache_storage_page_cache_custom_expiration'] = TRUE;
$conf['memcache_storage_page_cache_expire'] = 60 * 60 * 24;
$conf['memcache_storage_key_prefix'] = 'XXXXXXXXXXXX';
$conf['page_compression'] = 1;
$conf['page_cache_maximum_age'] = 60 * 60 * 24;
  

 

Отлично! Теперь страницы попадают в кэш, созданный memcached1, а все остальное — в сформированный memcached. Теперь осталось оптимизировать работу обоих экземпляров memcached. При этом следует помнить, что у каждого — свой конфигурационный файл.

Наверняка вы столкнетесь с высоким процентом Wasted. Рассмотрим методику его уменьшения.

У нас есть возможность регулировать размеры чанка. В параметрах Memcached мы выставляем размер начального чанка, т.е. размер чанка, который создается в первом слабе. И выставляем так называемый фактор роста — это соотношение размера большего чанка к меньшему. Т.е. если размер начального чанка составляет 1 кб, а фактор роста — 1.15, то его размер во втором слабе составит 1 кб х 1.15 = 1.15 кб, размер чанка в следующем слабе 1.15 кб х 1.15 = 1,3225 кб и так далее.

Подбор подходящих значений начального чанка и фактора роста является занятием довольно кропотливым. Выставляя сначала одни значения, которые кажутся вам подходящими, через какое-то время следует проверять полученный результат и при необходимости корректировать параметры, выставляя новые значения. У нас ушло около недели, чтобы выбрать оптимальные параметры для интернет-магазина нашего клиента.

Для этого можно воспользоваться тем же PHPMemcachedAdmin. Перейдя по ссылке See this Server Slabs Stats в панели управления, вы увидите текущие состояния слабов, процент Wasted в каждом слабе, количество и размеры используемых чанков, которые хранятся в каждом слабе.

 

Экран Server Slabs Stats утилиты PHPMemcachedAdmin
Рис. 10. Экран Server Slabs Stats утилиты PHPMemcachedAdmin

 

Учтите, что максимальное количество слабов — 63. Поэтому не стоит выставлять слишком маленький фактор роста, это может привести к тому, что у вас будет много слабов с маленьким размером чанков, а под данные большего размера у вас не останется слабов. Для настройки параметров Memcached используются следующие ключи: -m — задает количество оперативной памяти для Memcached, -n — определяет размер начального чанка в байтах, -f — назначает фактор роста. Вот небольшой пример настройки Memcached:

 

  
cat memcached1.conf

-m 1536M
-u www-data
-s /tmp/memcached1.sock
-a 0777
-n 40960
-f 1.015
  

 

После оптимизации настроек Memcached не стоит думать, что об этом можно забыть. Приложение развивается, объем данных, поступающих на хранение в Memcached может измениться. Мы рекомендуем время от времени проверять состояние Memcached и при необходимости корректировать параметры.

Мы оптимизировали работу Memcached и надеемся, что страницы будут всегда отдаваться из кэша. Чтобы убедиться, что все работает так как должно быть, можно воспользоваться тем же PHPMemcachedAdmin. На главной панели есть секция Hit & Miss Rate с гистограммой, отображающей количество попаданий в кэш. Удобная визуализация, но гораздо полезнее эти данные хранить в системе мониторинга, чтобы отслеживать работу кэша во времени и среагировать в случае изменения работы приложения. Чтобы получить те же данные, можно воспользоваться консольной утилитой netcat (nc), например, так:

 

  
echo "stats" | nc -U /tmp/memcached.sock -w 1 | grep get_hit | awk '{print $3}'
4330
echo "stats" | nc -U /tmp/memcached.sock -w 1 | grep get_miss | awk '{print $3}'
41
  

 

Такие несложные ванлайнеры можно запускать с помощью zabbix-агента и хранить результаты на zabbix-сервере.

Тюнинг производительности базы данных MySQL

На производительность сайта существенно влияет скорость работы СУБД MySQL. Для оптимизации работы MySQL мы используем разработанный нами сервис MySQLConfigurer: https://habr.com/ru/post/499410/

Он позволяет ускорить работу MySQL на 30% по сравнению с параметрами MySQL по умолчанию: https://docs.google.com/spreadsheets/d/1J9FDgBGbvNA356d74WKYBaEzSwK7H-wgjHEQgYh8CMI/edit?usp=sharing

При заходе на закэшированную страницу Drupal делает не более двух запросов к БД. Однако при заходе на незакэшированную страницу или при заходе на сайт авторизованным пользователем количество запросов может быть велико. Нами была проведена оптимизация кэширования на уровне блоков и оптимизация кэша сущностей Drupal в memcached таким образом, чтобы при загрузке страницы карточки товара интернет-магазина не из кэша делалось не более 300 запросов к БД.

Прогрев и мониторинг кэша страниц

Картина, полученная в мониторинге, может вам не понравиться, т.к. количество не попаданий в кэш (miss) может быть довольно велико. Причина в методе работы Memcached. Чтобы страница попала в кэш, ее должен кто-то запросить. Очевидно, что главная страница сайта или страницы с популярными товарами быстро попадут в кэш. Но представьте себе, что посетитель интернет-магазина ищет товар не из самой популярной категории. Он будет переходить от страницы к странице, которые генерируются не из кэша, скорость работы сайта конкретно для этого человека окажется слишком низкой, что может оттолкнуть потенциального покупателя. Чтобы максимально увеличить количество попаданий страниц в кэш наша команда реализовала так называемый «прогрев кэша». Он представляет собой текстовый файл со списком url-адресов страниц, которые нужно добавить в кэш, и bash-скрипт, который с помощью утилиты curl обращается к каждой строке, содержащей url-ссылку из этого файла.

Чтобы реализовать этот инструмент, был написан следующий bash-сценарий:

 

  
#!/bin/bash
# файл со списком url
$SOURCEFILE="urls.txt"
# функция, которая запрашивает страницу
function cached_url() {
time=$(date +"%T")
curl -LI -w "$time;%{time_total};$1\n" -o /dev/null -s $1
}
# запускаем перебор файла и вызываем функцию cached_url для каждого url
flag=0
cat $SOURCEFILE | while read url; do
cached_url $url &
let "flag = flag + 1"
if [[ $flag == "4" ]];
then
    # мы делаем периодические паузы, чтобы сильно не нагружать сервер
    wait
    sleep 1
    flag=0
fi
done  
  

 

Нужно поместить этот скрипт в крон, который нужно запускать каждую ночь после импорта и обновления данных сайта, чтобы обеспечить попадание страниц сайта в кэш еще до того как к ним обратится первый реальный посетитель. При этом необходимо учитывать, что выполняться такой скрипт может достаточно долго, генерируя довольно чувствительную нагрузку на сервер. Поэтому необходимо продумать время его запуска и количество загружаемых url-адресов. В нашем случае мы добавляли только те страницы, которые были либо слишком объёмными, либо слишком редкими для загрузки пользователями.

Еще одним инструментом мониторинга правильной работы кэша являются логи Web-сервера. Дело в том, что Drupal к каждой странице добавляет http заголовок под названием x-drupal-cache, который может иметь только два значения — HIT или MISS. Мы добавили в формат лога nginx переменную $sent_http_x_drupal_cache, чтобы отслеживать эти заголовки и точно понимать, какой запрос был отдан из кэша и была возможность анализировать общую картину.

Мы храним логи nginx в Elasticsearch. Kibana — фронтенд для визуализации логов в Elasticsearch, предоставляет отличные возможности для визуализации, сортировки, выборки и анализа логов. Немного поработав с дашбордами можно получить исчерпывающие графики и гистограммы попадания запросов в кэш.

 

Drupal cache headers proportion

Рис. 11. Drupal cache headers proportion

 

Drupal cache header graph

Рис. 12. Drupal cache header graph

 

Другой возможностью Kibana является поиск и сортировка логов. Например, вы можете найти все MISS запросы и отсортировать их по количеству вхождений в лог за день или неделю, т.е. составить своеобразный ТОП не кэшируемых запросов — первых претендентов на добавление в прогрев кэша.

Результаты

Оптимизация кэширования AJAX-форм позволила нам ускорить обработку запросов на 32%. Благодаря тонкой настройке Memcached и использованию прогрева кэша мы смогли добиться того, что все входные страницы сайта хранятся в оперативной памяти. Проведённые тесты показали среднее время загрузки страниц 48 ms — это пятикратное ускорение! Но для выполнения других операций очень важна общая производительность сервера.

Добившись ускорения работы сайта с помощью оптимизации кода, работы баз данных и прочего сопутствующего ПО, наблюдая за процессом роста числа посетителей и загрузок страниц сайта интернет-магазина, мы подумали, что можем столкнуться с проблемами недостаточной производительности сервера во время пиковых нагрузок. Кроме того, запас производительности сервера будет очень кстати в случае добавления в интернет-магазин новых функций или, например, для добавления новых веб-проектов, расширяющих коммерческие возможности клиента. Поэтому предложили клиенту перевести сайт на более мощное и производительное аппаратное обеспечение.

Перед началом работ наш клиент арендовал виртуальную машину с 4 ядрами cpu 3200 Mhz и 16 Gb оперативной памяти. После анализа работы интернет магазина было принято решение о переносе сайта на выделенный физический сервер со следующей конфигурацией: процессором Intel® Xeon® E-2236 CPU @ 3.40GHz 12 core и 32 Gb оперативной памяти.

Среднее время кэшируемых страниц сайта сократилось до 11.9 ms, а не кэшируемых — снизилось в среднем на 76 %, с 616 мс до 102 мс.

В итоге мы получили следующие результаты мониторинга скорости загрузки страниц сервисом Newrelic:

До начала работ

 

Время обработки запроса сервером до настройки кэширования

Рис. 13. Время обработки запроса сервером до настройки кэширования

 

После окончания работ и перехода на «новый» сервер

 

Время обработки запроса сервером после настройки кэширования и переноса сервера на «новое» аппаратное обеспечение уменьшилось с 352 мс до 11.9 мс

Рис. 14. Время обработки запроса сервером после настройки кэширования и переноса сервера на «новое» аппаратное обеспечение уменьшилось с 352 мс до 11.9 мс

Заключение

Разрабатывая веб-проект, необходимо очень внимательно отнестись к качеству кода и стилю программирования. Ошибки, допускаемые при программировании, накапливаются, что весьма негативно сказывается на скорости загрузки страниц у пользователя. А неаккуратный и запутанный стиль программирования не способствует быстрому поиску и устранению этих ошибок.

Кэширование в оперативной памяти сервера страниц сайта, особенно крупного интернет-магазина, заметно повышает скорость обработки запросов и загрузки страниц у пользователя. Но использование программы, обеспечивающей сервис кэширования данных, требует тщательной настройки и хорошего понимания самого процесса. В противном случае это не даст никакого эффекта и окажется совершенно бесполезным, а обработка запросов и выдача результатов пользователю так и будет происходить через жёсткие диски.

Разумеется, требуется тщательный подход к выбору аппаратной конфигурации сервера, на котором базируется веб-хост. Использование более мощного аппаратного обеспечения позволяет быстрее обрабатывать запросы пользователя, имея при этом хороший запас прочности при возникновении пиковых нагрузок, а также резервы для модернизации коммерческого веб-проекта.

Высокая скорость обработки запросов и быстрая загрузка страниц в браузерах пользователя делает пребывание посетителя на сайте наиболее комфортным, увеличивает вероятность совершения сделки, а значит повышает коммерческую эффективность сайта в целом.

 

Результат тестирования сайта клиента инструментом Google PageSpeed Insights после оптимизации. Идеальный результат, полученный специалистами компании ИНИТЛАБ.

Рис. 15. Результат тестирования сайта клиента инструментом Google PageSpeed Insights после оптимизации. Идеальный результат, полученный специалистами компании ИНИТЛАБ.

Добавить комментарий