Опыт внедрения кэширования в небольшой проект с сильной социальной составляющей

в 7:59, , рубрики: memcached, mysql, php, Веб-разработка, высокая производительность, кэширование, метки: , , ,

Хочу поделиться опытом внедрения кэширования при помощи memcached на своем сайте. Текст будет полезен новичкам в веб-разработке, которые задаются вопросом «как же применять на практике те 100500 статей о кэшировании, которые легко находятся в поисковиках». На истину не претендую, просто рассказываю, как получилось у меня.

Исходные данные:
Сайт крутится на одном выделенном сервере, но из-за вероятности в будущем еще подрасти, для кэширования выбран memcached;
Суточная посещаемость: ~23 000 уникальных посетителей и ~300 000 просмотров страниц;
80% посетителей — авторизованные пользователи;
Основной контент: текст (книги, которые авторы пишут и публикуют на сайте по главам, наподобие самиздата).
Сервисы: персонализированные новости, чтение текстов, разбитых на главы, комментарии, профили, блоги, рейтинги, подписка, метки, закладки, личные сообщения, счетчики, почтовые уведомления…
Пользовательская активность: более 10 000 действий, приводящих к изменению контента, в сутки.

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

Введение и лирика

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

Изначально проект написан на голом PHP, без фреймворков, без ООП, без использования сторонних библиотек, без шаблонизаторов и вообще без шаблонов. База данных, само-собой, MySQL, таблички, само-собой, MyISAM, сервер, само-собой, Apache (позже перед ним встал nginx), на фронт-энде, само-собой, jQuery и довольно много AJAX'а.

Время шло, пользователи прибывали, я развлекался тем, что придумывал, как бы сделать сайт еще более удобным для авторов и читателей (две основные роли, на которые делятся пользователи сайта), авторам и читателям нравилось и они звали друзей. Со временем, на сайте накопилось очень много всевозможных настроек, галочек и функций. И вот, когда по вечерам сервер стал показывать подозрительные 3-4 la, а phpmyadmin рассказал мне о 89 запросах в секунду и 48 Гб/час сетевого трафика, я полез искать информацию по кэшированию.

Подготовка — анализируем, что кэшировать

Я полез в liveinternet, взял статистику по страницам за последний месяц и составил табличку с распределением просмотров по разделам сайта.

Опыт внедрения кэширования в небольшой проект с сильной социальной составляющей

Отлично видно, что в первую очередь надо кэшировать два раздела (3, read), которые в сумме дают почти 60% всех просмотров страниц и, на которых много запросов в базу и много форматирования данных. Затем стоит закешировать main&news, ну а все остальное уже постольку поскольку — для очистки совести и в надежде на многократный рост посещаемости.

Сцена первая — кэшируем тексты книг

Раздел — read.
Самая простая часть и самая важная по экономии ресурсов. Итак, тексты книг хранятся в БД в виде одной таблички, где записью является глава книги. Текст главы и мета-информация хранятся вместе, причем текст хранится в сыром виде — перед выводом его еще надо обработать. Почему хранится сырой текст? — Так сложилось исторически, из БД текст может быть выведен во множество мест: на страницу чтения, в простой редактор автору, в визуальный редактор автору, в скрипт для создания fb2 версии. По-хорошему, давно надо переделать БД и хранить рядом с сырым текстом готовый для вывода вариант, но это пока остается заделом для дальнейшей оптимизации.

На страницу может выводится отдельная глава или все главы сразу. Поэтому решено для экономии памяти кэшировать:
1. Массив со списком глав (одномерный массив, содержащий порядковые номера глав). Ключ «f123».
2. Для каждой главы — массив, содержащий мета-информацию о главе и подготовленный к выводу текст главы. Ключ «f123c1».
Почему не хранится html-ка сразу со всей главой — потому что при выводе всех глав и выводе одной главы разное форматирование названия главы.

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

Алгоритм скрипта прост:
1. Запрошена глава.

if($is_author || $is_admin) {
    // Вывод главы из БД, без создания кэша
}
else {
    $data = cache_get('f'.$id.'c'.$cid); // $id - id книги, $cid - порядковый номер главы в книге, эти параметры передаются скрипту в $_GET
    if(!$data) {
        // Вывод из БД с созданием кэша
    }
    else {
        // вывод главы
    }
}

2. Запрошены все главы.

if($is_author || $is_admin) {
    // Вывод всех глав из БД, без создания кэша
}
else {
    $data = cache_get('f'.$id.'conarr'); // $id - id книги передается скрипту в $_GET
    if(!$data) {
        // Выбираем из БД одним запросом все главы. Но проходя циклом по результатам выбора, ищем каждую главу в кэше, если находим, выводим из кэша, если не находим, тогда только обрабатываем выбранные из БД данные и создаем кэш главы. Создаем кэш списка глав
    }
    else {
        $data = unserialize($data);
        foreach($data as $tmp) {
            $tmp_data = cache_get('f'.$id.'c'.$tmp['cid']);
            if(!$tmp_data) {
                // Запрашиваем главу из БД, выводим и создаем кэш
            }
            else {
                // Выводим главу
            }
        }
    }
}

Обнуление кэша

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

Сцена вторая — кэшируем содержание книги

Содержание книги выводится в двух разделах (3 и read), но выводятся они по-разному, придется делать две версии кэша.
Сложности кэширования содержания:
1. В разделе read в случае просмотра конкретной главы, в содержании помечается на которой главе мы находимся в данный момент.
2. В разделе 3 могут выводится ссылки на подгрузку аудио версии конкретной главы, если эта аудио версия существует.
3. В разделе 3, если конкретная глава опубликована сегодня, после нее выводится картинка NEW.
4. В обоих разделах в содержании могут выводится закладки, если читатель уже ранее читал эту книгу и отметил какие-то места. Закладок в одной книге и у одного пользователя может быть максимум пять, но на каждой главе может быть несколько закладок (хоть все пять в одной главе).

Решение:
В кэш будет сохраняться шаблон содержания с якорями под дополнительную информацию.
1. Пометкой о нахождении на конкретной главе является спецсимвол, который выводится перед названием главы (в будущем сделаю специальным классом для li, но для описываемого алгоритма ничего не изменится). Якорь имеет вид {Nar}, где N — порядковый номер главы. Соответственно, при выводе:

// Заменяем якорь текущей главы на отметку. $cid - порядковый номер текущей главы, передается скрипту в $_GET
$contents_list = str_replace('{'.$cid.'ar}', '<span class="red">></span>', $contents_list);
// И удаляем все остальные якоря
$contents_list = preg_replace('/{d+ar}/', '', $contents_list);

2. С аудио версиями все просто. В той версии кэша, что предназначена для раздела 3, есть ссылки на аудио версии глав, а в версии для раздела read, их нет.
3. Для вывода картинки NEW, после названия каждой главы в шаблоне вставлены якоря вида {d.m.Y} с датами публикации каждой главы, а при выводе:

// Заменяем якорь текущей даты на картинку
$tmp = date('d.m.Y');
$contents_list = str_replace('{'.$tmp.'}', ' <img src="/images/new.png">', $contents_list);
// И удаляем все остальные якоря
$contents_list = preg_replace('/{d+.d+.d+}/', '', $contents_list);

4. Здесь сначала пришлось реализовать кэширование закладок каждого пользователя. Кратко: создается кэш с массивом закладок конкретного пользователя в конкретной книге, ключ вида «u90000f123bm», где 90000 — id пользователя, а 123 — id книги. Если у пользователя нет закладок в этой книге, кэш все равно создается, значение указывается «no» (это нужно чтобы не запрашивать каждый раз БД, если в кэше не нашлось данных по нужному ключу). Кэш создается бессрочным, при каждом добавлении или удалении закладки производится попытка удаления кэша для книги, которой принадлежит создаваемая/удаляемая закладка. В случае существования закладок, в кэш сохраняется массив, в каждой записи которого находится мета-информация закладки и порядковый номер главы в книге, которой принадлежит закладка.
В шаблоне содержания после названия каждой главы сохраняется якорь вида {Nbm}, где N — порядковый номер главы. При выводе:

$bm = LibMember_bm($id); // функция получает кэш закладок текущего пользователя для книги с $id, если кэша не существует, создает его
if($bm != 'no') {
    $bm = unserialize($bm);
    foreach($bm as $tmp) {
        $contents_list = str_replace('{'.$tmp['cid'].'bm}', $tmp['код закладки'].'{'.$tmp['cid'].'bm}', $contents_list); // Заметьте, что якорь не удаляется, это позволяет вставить несколько закладок в одной главе
    }
}
// И удаляем все якоря
$contents_list = preg_replace('/{d+nm}/', '', $contents_list);
Обнуление кэша

Кэш содержания создается бессрочный. Управлять надо вручную.
Кэш содержания удаляется при публикации автором новых глав или при изменении старых (может измениться название главы).

Пока все

Эти два пункта и еще кое-какие мелочи — это пока и все мои шаги на пути внедрения кэширования в работающий проект. Дальше будет больше, если статья вызовет интерес, опишу новые приемы, которые будут придуманы для кэширования данных в оставшихся разделах сайта.
Уже сейчас удалось снизить вечерний la сервера до 0.8-1.5 и показания phpmyadmin до 59 запросов в секунду и 4.6 Гб/час сетевого трафика (правда, это показания через сутки после перезагрузки сервера — показатель трафика в час еще упадет из-за заполнения кэша).
Напомню, что изначальные показатели были таковы:

3-4 la, а phpmyadmin рассказал мне о 89 запросах в секунду и 48 Гб/час сетевого трафика

Автор: ReFeRy

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js