Генеральный спонсор: Хостинг «Джино»

Система Orphus
Russian version
Добавить на Del.icio.us
English version
Добавить на Digg.com

 dkLab | Конструктор | Dklab_Cache: правильное кэширование — тэги в memcached, namespaces, статистика 

Карта сайта :: Форум «Лаборатории» :: Проект «Денвер»
Проект «Orphus» :: Куроводство: наблы :: Конструктор


2008-04-19
Обсудить на форуме

Dklab_Cache — это (в основном) библиотека поддержки тэгирования ключей для memcached, использующая интерфейсы Zend Framework. Вот полный список возможностей библиотеки:

  • Backend_TagEmuWrapper: тэги для memcached и любых других backend-систем кэширования Zend Framework;
  • Backend_NamespaceWrapper: поддержка пространств имен для memcached и др.;
  • Backend_Profiler: подсчет статистики по использованию memcached и др. backend-ов;
  • Backend_MemcachedTag: поддержка еще весьма "сырого" патча memcached-tag;

    Лирическое отступление 
    Описание теории тэгирования кэша читайте в статье
    Кэширование: memcached, тэги, пространства имен.

  • Frontend_Slot, Frontent_Tag: каркас для высокоуровневого построения систем кэшиирования в сложных проектах.

    Лирическое отступление 
    Рассуждения насчет того, как писать неизбыточный код кэширования,
    читайте в статье Правильный способ кэширования данных.

Поддержка со стороны backend: Dklab_Cache_Backend

Memcached — отличная система кэширования данных, особенно удобная при разработке высоконагруженных сайтов. Она используется в таких проектах, как Мой Круг, Facebook, LiveJournal и др. К сожалению, memcached не поддеживает тэгирование ключей, а потому код, следящий за валидностью кэша, подчас становится очень сложным.

Чайник 

Если в вашем проекте Zend Framework не используется (в том числе по принциальным соображениям), не расстраивайтесь. Совершенно не обязательно подключать весь Zend Framework, чтобы работать с его подсистемой кэширования. Вы можете взять только несколько необходимых файлов; они, в частности, содержатся в дистрибутиве данной статьи. Конечно, вам следует вначале прочитать документацию по кэшированию в Zend Framework на русском языке.

Нужно заметить, что memcached community предприняло немало попыток написать "родные" патчи для кода memcached, добавляющие в него поддержку тэгов. Наиболее известный из таких патчей — проект memcached-tag. К сожалению, memcached-tag все еще очень далек от стабильной версии: нетрудно написать скрипт, приводящий к зависанию пропатченного memcached-сервера. Увы, на момент написания данной статьи не существует ни одного надежного решения проблемы тэгирования на уровне самого memcached-сервера.

Поддержка тэгов: Dklab_Cache_Backend_TagEmuWrapper

Класс TagEmuWrapper представляет собой декоратор ("обертку") для backend-классов кэширования Zend Framework. Другими словами, вы можете с ее помощью "прозрачно" добавить поддержку тэгов в любую подсистему кэширования Zend Framework. Мы будем рассматривать backend для работы с memcached: Zend_Cache_Backend_Memcached, но, если в вашем проекте используется какой-то другой backend-класс, вы можете подключить тэгирование и к нему без каких-либо особенностей.

TagEmuWrapper реализует стандартный backend-интерфейс Zend_Cache_Backend_Interface, поэтому с точки зрения вызывающей системы он сам является кэш-backend'ом.

Чайник 

Zend Framework хорош тем, что на уровне интерфейса он поддерживает тэги с самого начала! Например, в методе save() уже имеется параметр, позволяющий снабдить ключ тэгами. Однако ни один из backend-ов в составе Zend Framework тэги не поддерживает: попытка добавить тэг к некоторому ключу вызывает исключение (в частности, для Zend_Cache_Backend_Memcached).

Пример использования библиотеки приведен ниже.

Листинг 1: Использование обертки Dklab_Cache_Backend_TagEmuWrapper
// Initialize the standard memcached backend.
$memcached = new Zend_Cache_Backend_Memcached(
  array("servers" => array("host" => "localhost"))
);

// Create the wrapper object with tags support for $memcached.
$backend = new Dklab_Cache_Backend_TagEmuWrapper($memcached);

// Save two keys with a number of tags.
$backend->save("Ivan", "firstname", array("tag1", "tag2", "tag3"));
$backend->save("Ivanov", "lastname", array("tag3", "tag4"));

// Clear all keys marked by the tag "tag3".
$backend->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, array("tag3"));

// Load a key: returns false, because the key was cleared by tag.
var_dump($backend->load("firstname"));

Вы также можете использовать TagEmuWrapper наравне с другими backend-классами Zend Framework в его frontend-классах кэширования:

Листинг 2: Совместное использование с frontend-классом Output
$backend = new Dklab_Cache_Backend_TagEmuWrapper($memcached);
$frontend = new Zend_Cache_Frontend_Output();
$frontend->setBackend($backend);

if (!$frontend->start()) {
    echo "Some page content!";
    $frontend->end(array("tag1", "tag2"));
}

Аспекты производительности

Т.к. memcached не работает с тэгами на встроенном уровне, библиотека TagEmuWrapper эмулирует их поддержку, заводя для каждого тэга специальный ключ в оборачиваемом backend-е. Это ведет к увеличению накладных расходов на кэширование. К примеру, если вы записываете в кэш ключ и помечаете его 10 разными тэгами, библиотека фактически произведет 11 обращений к memcached-серверу на запись. Более того, если вы затем захотите считать значение этого ключа, последуют те же самые 11 запросов для проверки "валидности" всех связанных с ним тэгов. Таким образом, каждый доболнительный тэг добавляет одну операцию при чтении и одну операцию при записи ключа.

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

Пространства имен: Dklab_Cache_Backend_NamespaceWrapper

Класс-обертка NamespaceWrapper позволяет разбить единственный backend-сервер на несколько непересекающихся пространств имен, содержащих гарантированно непересекающиеся наборы ключей. Каждое пространство имен для системы выглядит, как отдельный backend-объект.

Зачем нужны пространства имен? Иногда разные части проекта используют один и тот же кэш-backend совершенно различным образом. Например, какие-то ключи в кэше хранятся "вечно", а какие-то — "стираются" при любом изменении таблицы message в БД.

Листинг 3: Использование пространств имен
// Initialize the standard memcached backend.
$memcached = new Zend_Cache_Backend_Memcached(
  array("servers" => array("host" => "localhost"))
);

// Create the global namespace.
$globalBackend = new Dklab_Cache_Backend_NamespaceWrapper($memcached, "global");

// Get last-modified time of "message" table.
$lastModified = getLastModifiedTimeOfTable("message");

// Create the "per-table" namespace based on the table time.
$messageBackend = new Dklab_Cache_Backend_NamespaceWrapper($memcached, "message_{$lastModified}");

// Read keys.
$val1 = $globalBackend->load("key1");
$string = $messageBackend->load("hello");

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

Чайник 

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

Измерение накладных расходов: Dklab_Cache_Backend_Profiler

Класс-обертка Profiler позволяет измерить время, которое потратили скрипты на "общение" с memcached-сервером. С его помощью вы легко сможете оценить, является ли memcached "узким местом" в вашем проекте или нет.

Класс Profiler вызывает некоторую callback-функцию или метод всякий раз, когда производится обращение к "обернутому" в него backend-у. Функции передается время, потраченное в этом backend-е, а также имя вызванного метода (load, save, test и т. д.).

Листинг 4: Измерение накладных расходов
// Initialize the standard memcached backend.
$memcached = new Zend_Cache_Backend_Memcached(
  array("servers" => array("host" => "localhost"))
);

// Create the wrapped backend with statistics accounting.
$backend = new Dklab_Cache_Backend_Profiler($memcached, "profilerCallback");

// Do some operations with $backend.
$backend->save("Some data", "key");
echo $backend->load("key");

// Print usage statistics.
printf("Took %.3f ms; performed %d queries.\n", $mmcTime * 1000, $mmcCount);

// Define the profiler accounting callback.
function profilerCallback($time, $method)
{
    global $mmcTime, $mmcCount;
    $mmcTime += $time;
    $mmcCount += 1;
}

Конечно, вместо callback-функции можно передавать и ссылку на метод некоторого объекта; это делается стандартным для PHP способом:

Листинг 5: Передача ссылки на метод
// Create the wrapped backend with statistics accounting.
$backend = new Dklab_Cache_Backend_Profiler($memcached, array($obj, "callbackMethod"));

Использование всех трех "оберток" разом

Паттерн декоратор ("обартка"), используемый в классах Dklab_Cache_Backend, позволяет делать занятную штуку: использовать одновременно все три обертки. Вот как это делается:

Листинг 6
// Create a "super-wrapped" backend.
$backend = new Dklab_Cache_Backend_TagEmuWrapper(
    new Dklab_Cache_Backend_NamespaceWrapper(
        new Dklab_Cache_Backend_Profiler(
            new Zend_Cache_Backend_Memcached(
                array("servers" => array("host" => "localhost"))
            ),
            "profilerCallback"
        ),
        "namespaceName"
    )
);

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

"Родные" тэги: Dklab_Cache_Backend_MemcachedTag

К сожалению, "родной" патч для сервера memcached, memcached-tag, все еще далек от совершенства. Тем не менее, если вдруг в будущем он станет более стабильным, вы можете использовать класс Dklab_Cache_Backend_MemcachedTag, входящий в библиотеку Dklab_Cache. Если бы не плохая стабильность memcached-tag, то использование этого класса дало бы программе значительно лучшую производительность, чем TagEmuWrapper, т.к. в memcached-tag поддержка тэгов встроена в сам сервер memcached.

Поддержка со стороны frontend: Dklab_Cache_Frontend

Использовать только backend-классы библиотеки не очень удобно, потому что они предоставляют слишком низкоуровневый интерфейс к кэшу, а также провоцируют программиста на введение лишних зависимостей в коде. Все это подробно рассмотрено в статье Правильный способ кэширования данных (рекомендуем прочитать ее прямо сейчас, т.к. там детально разъясняются примеры). Решение предоставляет frontend-часть библиотеки.

Классы-слоты: Dklab_Cache_Frontend_Slot

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

Листинг 7: Определение классов-слотов для будущего использования
// Базовый класс для всех будущих пользовательских классов-слотов.
// Определяет, с каким backend-ом будет идти работа.
class Cache_Slot_Abstract extends Dklab_Cache_Frontend_Slot {
    protected function _getBackend() {
        // Код может быть любым, лишь бы он возвращал один и тот же backend.
        return Zend_Registry::get('memcachedBackend');
    }
}

// Пользовательский класс 1: кэширование профиля пользователя.
class Cache_Slot_UserProfile extends Cache_Slot_Abstract {
    public function __construct(User $user) {
        parent::__construct("profile_{$user->id}", 3600 * 24);
    }
}

// Пользовательский класс 2: кэширование пользовательского фотоальбома.
class Cache_Slot_UserPhotos extends Cache_Slot_Abstract {
    public function __construct(User $user) {
        parent::__construct("photos_{$user->id}", 3600 * 24);
    }
}

// ...

Классов-слотов должно быть столько, сколько "различных видов кэширования" существует в вашей системе. Каждый из слотов должен иметь метод _getBackend(), который определяет, в каком кэш-хранилище хранятся данные этого слота. Чтобы не повторять код этого метода в каждом классе, мы вынесли его в базовый класс Cache_Slot_Abstract и унаследовали все слоты от него.

Вот как теперь мы можем использовать наши новые классы-слоты:

Листинг 8
// Код использования кэша.
$slot = new Cache_Slot_UserProfile($user);
if (false === ($data = $slot->load())) {
    $data = $user->loadProfile();
    $slot->addTag(new Cache_Tag_User($loggedUser);
    $slot->addTag(new Cache_Tag_Language($currentLanguage);
    $slot->save($data);
}

// Еще вариант ("сквозной" вызов метода loadProfile()).
$slot = new Cache_Slot_UserProfile($user);
$slot->addTag(new Cache_Tag_User($loggedUser);
$slot->addTag(new Cache_Tag_Language($currentLanguage);
$data = $slot->thru($user)->loadProfile();
display($data);

// Код очистки кэша.
$tag = new Cache_Tag_Language($currentLanguage);
$tag->clean();

Чайник 

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

Классы-тэги: Dklab_Cache_Frontend_Tag

Наверное, вы заметили, что в примере выше используются не только классы-слоты, но также и классы-тэги Cache_Tag_User и Cache_Tag_Language. Их, конечно, тоже нужно определить в вашей программе. Классов-тэгов должно существовать столько, сколько имеется зависимостей между данными в приложении:

Листинг 9: Определение классов-тэгов для будущего использования
// Базовый класс для всех будущих пользовательских классов-тэгов.
// Определяет, с каким backend-ом будет идти работа.
class Cache_Tag_Abstract extends Dklab_Cache_Frontend_Tag {
    protected function getBackend() {
        // Использовать Zend_Registry совсем не обязательно; код может быть любым, 
        // лишь бы он всегда возвращал один и тот же backend.
        return Zend_Registry::get('memcachedBackend');
    }
}

// Пользовательский класс 1: кэширование профиля пользователя.
class Cache_Tag_User extends Cache_Tag_Abstract {
    public function __construct(User $user) {
        parent::__construct("user_{$user->id}");
    }
}

// Пользовательский класс 2: кэширование пользовательского фотоальбома.
class Cache_Tag_Language extends Cache_Tag_Abstract {
    public function __construct(Language $language) {
        parent::__construct("language_{$language>id}");
    }
}

// ...

Если вы все сделали правильно и определили каждый класс-слот в отдельном файле, то в директории Cache/Slot у вас окажется аккуратный набор файлов-слотов, используемых в системе. Вам будет довольно легко в нем ориентироваться. Аналогично произойдет и для тэгов в директории Cache/Tag.

Полная картина классов

Неужели вы еще здесь? Отлично! Тогда вот полный список файлов с классами, затронутых в этой статье.

Листинг 10: Все файлы, затронутые в статье
lib/                           - директория со всеми сайтонезависимыми библиотеками
  Zend/                          - стандартный Zend Framework или его часть Zend_Cache
    ...
  Dklab/
    Cache/                       - дистрибутив библиотеки Dklab_Cache
      Backend/                     - backend-часть
        TagEmuWrapper.php 
        NamespaceWrapper.php
        Profiler.php 
      Frontend/                    - frontend-часть
        Slot.php
        Tag.php

classes/                         - директория классов, специфичных для конкретного проекта
  Cache/                           - спекифичные для проекта классы кэширования
    Slot/                            - слоты кэша
      Abstract.php                     - абстрактный класс; все слоты унаследованы от него
      UserProfile.php                  - слот кэширования профиля пользователя
      UserPhotos.php                   - слот кэширования фотоальбома
      ...                              - другие слоты (может быть и сотня файлов)
    Tag/                             - тэги кэша
      Abstract.php                     - абстрактный класс; все тэги унаследованы от него
      User.php                         - зависимость от данных пользователя
      Language.php                     - зависимость от текущего национального языка
      ...                              - другие тэги (может быть много)

Резюме

Для полного понимания материала данной статьи, возможно, придется прочитать еще две:

Популярная система кэширования memcached не имеет "на борту" встроенной поддержки тэгов, хотя необходимость в ней остро ощущается в современных высоконагруженных проектах. Представленное в этой статье семейство PHP5-библиотек Dklab_Cache:

  • добавляет поддержку тэгов к memcached или любому другому backend-у кэширования;
  • поддерживает разбиение backend-кэша на несколько пространств имен;
  • позволяет измерять время, затраченное на общение с memcached-сервером (либо любым другим backend-ом).

Также дистрибутив включает в себя и frontend-часть, облегчающую работу с кэшем в конечных приложениях:

  • класс поддержки типизированный слотов кэша;
  • центрадизованная поддержка типизированных тэгов.

Ну и последнее. Если примеры в этой статье покажутся вам недостаточно прозрачными, загляните в директорию t/ дистрибутива библиотеки. Там вы найдете более 20 regression-тестов, хранящихся в phpt-файлах. Изучение этих тесов — хороший способ окончательно разобраться с функциональностью Dklab_Cache на примерах.


Обсудить статью в форуме
Скачать библиотеку и примеры


На странице:
Поддержка со стороны backend: Dklab_Cache_Backend
     Поддержка тэгов: Dklab_Cache_Backend_TagEmuWrapper
          Аспекты производительности
     Пространства имен: Dklab_Cache_Backend_NamespaceWrapper
     Измерение накладных расходов: Dklab_Cache_Backend_Profiler
     Использование всех трех "оберток" разом
     "Родные" тэги: Dklab_Cache_Backend_MemcachedTag
Поддержка со стороны frontend: Dklab_Cache_Frontend
     Классы-слоты: Dklab_Cache_Frontend_Slot
     Классы-тэги: Dklab_Cache_Frontend_Tag
Полная картина классов
Резюме





Дмитрий Котеров, Лаборатория dk. ©1999-2014
GZip
Добавить на Del.icio.us   Добавить на Digg.com   Добавить на reddit.com