Pull to refresh

Блочное кеширование на стороне клиента

Reading time6 min
Views6.8K
В последнее время в высоконагруженных сайтах стали все чаще применять технику Partial Caching или блочного кеширования. Достигается это, как правило, за счет применения, казалось бы уже давно забытого, SSI или близких ему технологий (например, ESI). Например, в связках Nginx + Memcached + SSI или Varnish + ESI.

Недавно и на Хабре тоже появился топик в котором автор описывал данный метод кеширования.

В данном топике в 3м варианте решения автор предложил читателям топика привести свои варианты решения относительно данной задачи.

Этому, собственно, и посвящается этот топик.

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


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

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

Пример страницы, использующей SSI вставки:
<html>
<body>

<div class="header">
<!--# include virtual="/header.php" -->
</div>

<div class="main_content">
<!--# include virtual="/main.php" -->
</div>


<!--# include virtual="/footer.php" -->

</body>
</html>


Здесь-то, казалось бы и все хорошо, но есть несколько НО, на которых и хотелось бы задержаться.

Проблемы


  • Персонализированные блоки — это блоки, содержащие персональные данные какого-то пользователя, например, «Привет, %username%!». На самом деле таких данных может быть очень много, взять ту же анкету на вконтакте. Не путайте их с блоками для авторизированных пользователей! Экземпляров вторых у вас в кеше всего два (для залогиненых и нет), для первых представление прийдется хранить в кеше для каждого пользователя! Cохраняя в мемкеше ключи такого вида {%block_id%}_{%PHPSESSID|user_id%}. А так как у нас кеширование на уровне представления, т.е. помимо данных мы храним еще и кучу html кода, который будет повторяться у нас для каждого пользователя, следовательно, расход памяти под кеш (Memcached) в данном случае очень сильно растет. Я уже не говорю про то, что в большой ферме мемкеш серверов, некоторые сервера время от времени отваливаются и даже с алгоритмом Consistent hashing проблемы все равно остаются
  • На разогревание кеша (обычно после перезагрузок, релизов новых версий и пр.) уходит очень много времени


Что предлагается?


А предлагается, собственно следующий механизм кеширования:
  • Блоки, отвечающие за представление, обобщаем для всех пользователей, т.е. выносим из них все персонифицированные данные, чтобы хранить всего один экземпляр блока в кеше для всех пользователей сайта. Что же остается от этих блоков? Правильно, остаются обычные темплейты представления, которые мы и будем передавать пользователю, а каждый пользователь заполнит данный шаблон сам, на стороне клиента, с помощью, JavaScript. Т.е. клиент по запросу к странице получит страницу, состоящую из логических блоков, каждый блок, в свою очередь будет являтся шаблоном. Например
    <html>
    <body>
        <div id="head_block">
            Some {%personified%} data here
        </div>
        <div id="main_block">
            Hello {%username%}!
        </div>
    </body>
    </html>
    


    Ну или, например, так
    <html>
    <body>
        <div id="head_block">
            Some <div id="{%personified%}"></div> data here
        </div>
        <div id="main_block">
            Hello <div id="{%username%}"></div>!
        </div>
    </body>
    </html>
    


  • Для того чтобы заполнять данные джаваскриптом их нужно откуда-то получать. Получать данные мы будем с помощью ифрейма-контейнера ну или с помощью AJAX запроса. Кому как больше нравится. Т.е. страница, которая вернется будет содержать невидимый ифрейм или Input hidden, содержащий URL, обратясь к которому мы получим список УРЛов с данными для каждого блока.
    В итоге, пользователь получает такую страничку
    <html>
    <body>
        <div id="head_block">
            Some <div id="{%personified%}"></div> data here
        </div>
        <div id="main_block">
            Hello <div id="{%username%}"></div>!
        </div>
        <iframe src ="all_blocks_data_urls.php" style="display: none"></iframe>
        <!-- или так -->
        <input type="hidden" name="all_blocks_data_urls" value="all_blocks_data_urls.php" />
    </body>
    </html>
    


  • Скрипт all_blocks_data_urls.php представляет из себя простейший скрипт, который проверяет есть ли кеш для данного пользователя. Выглядит он примерно так:
    <?php
    .......
    $key = $memcached->get($user_id.'all_blocks_data_urls');
    if($key) {
        header("HTTP/1.1 304 Not Modified"); 
        exit; 
    } else {
       // Extracting URLs here and send to user
    }
    ?>
    


    Т.е. в данном случае мы не храним данных в кеше. Ключ по каждой сессии служит для нас семафором, обозначающим пригодная ли версия кеша, которая хранится у пользорвателя или нет. Если пригодная, то просто возвращаем 304 заголовок, и говорим, что данные не обновились, если нет — обновляем список урлов. Лб этом ниже.
  • Итак, как я писал выше. Каждый набор данных для блока у нас представлен URLом, который возвращается по средством запроса к IFRAME по адресу «all_blocks_data_urls.php». На серверной части мы имеем семафор для каждого пользователя, доступный по ключу
    $memcached->get($user_id.'all_blocks_data_urls')
    

    , а также список ключей в мемкеше с сгенерированными урлами для данных в блоках, например имеющих вид:
    $user_id.'_'.$block_id => 'hash_for_url'
    


    Теперь, самое интересное: каждый URL для получения данных специфичный для пользователя, например
    www.site.com?block_id=1&hash=hash_for_url и возвращается с заголовком Expires далеко-далеко в будущем, т.е. мы кешируем данные посредством http заголовков в браузере навсегда.
  • Когда пользователь обновляет данные, привязанные к какому-то логическому блоку, например, раздел образование в соц. сети, то мы сбрасываем ключ в мемкеше для общего блока кеша $memcached->delete($user_id.'all_blocks_data_urls') а также удаляем ключ, который хранит УРЛ с данными для этого блока $memcached->delete($user_id.'_'.$block_id). При следующем запросе к IFRAME-контейнеру скрипт all_blocks_data_urls.php уже не вернет 304 ответ, а заново сформирует УРЛ для недостающего блока и вернет пользователю список, предварительно установив два ключа в мемкеше ($user_id.'all_blocks_data_urls' и $user_id.'_'.$block_id). При этом сами данные, т.е. кеш данных мы нигде не храним! Все кеширование организовано на уровне браузерного кеша у пользователя.
  • После того как клиент получил все УРЛы, указывающие на данные для блоков. Он начинает их запрашивать, т.к. эти УРЛы были отданы сервером с заголовком Expires, то большинство, если не все данные, клиент получит из кеша браузера. Кстати, формат данных можно выбирать какой угодно: JSON, XML, HTML. Можно даже настроить, чтобы это было кастомно в разных форматах! Ну и собственно в конце, с помощью JavaScript мы обрабатываем наши шаблоны и заполняем их пользовательскими данными.

Итоги


  • Получили блочное кеширование, которое я называю СSI (client side includes), основанное на кешировании http заголовков на стороне клиента, при этом объем данных, которые мы храним в кеше на сервере гораздо сократился
  • В самом что ни на есть смысле отделили логику от представления. По большему счету, используя данный подход, очень легко делать различные layouts для сайтов или кастомные лэйауты. Просто нужно подключать другие JavaScript библиотеки. Опять же, различное представление для различных платформ, в частности мобильных.


К недостаткам данного метода кеширования могу отнести проблемы с индексированием, т.к краулеры некоторых поисковых систем не обрабатывают JavaScript

Будет интересно услышать Ваши мнения по поводу подхода и предложения.

Список ссылок


Tags:
Hubs:
+26
Comments105

Articles