text-media/shm-cache

Shared Memory ReadOnly Cache

v1.2.1 2019-07-10 06:09 UTC

This package is auto-updated.

Last update: 2024-04-10 17:09:17 UTC


README

Packagist Packagist

Кэш типа "ключ -> значение" на основе разделяемой памяти.

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

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

Настройка

Для настройки кэша необходимо создать класс-потомок абстракции \TextMedia\ShmCache\Behavior. В нем необходимо обязательно переопределить следующие константы:

КонстантаОписание
PROJECT_IDИдентификатор проекта. Это должен быть один символ (см. http://php.net/manual/ru/function.ftok.php).
CACHE_SIZEРазмер блока разделяемой памяти. Может быть задан целым числом байт или строкой типа: 1m, 100k и т.п. Должен быть не менее 1 Кб.
VALUE_SIZEОпределяет, сколько нужно байт для хранения длины упакованных данных. Может меняться в пределах от 1 до 4.

VALUE_SIZE должен подбираться из расчета длины хранимых значений. Если это просто число или небольшой текст (не более 255 ASCII-символов) - достаточно указать 1 байт.

CACHE_SIZE следует выбирать исходя из суммы следующих значений:

  • число записей в кэше, умноженное на 1 - столько байт потребуется для хранения длин ключей;
  • суммарная длина строковых представлений всех ключей;
  • число записей, умноженное на VALUE_SIZE - столько нужно для хранения длин значений;
  • суммарная длина строковых представлений всех значений.

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

Например, при сохранении массива вида ['x' => число1, 'y' => число2] достаточно сохранить только числа в бинарном виде, а при чтении из памяти приводить их к нужному типу и формировать массив нужного вида. Для этого в классе необходимо переопределить следующие методы:

МетодОписание
string packData(string $key, $data)Упаковка элемента данных в строку для записи.
mixed unpackData(string $key, string $packed)Распаковка прочитанных из памяти данных в исходную структуру.

Значение $key передается в оба метода, т.к. оно может повлиять на алгоритм упаковки/распаковки.

Например, если хранимые данные имеют вид массива со следующими полями:

  • user_id: число, на запись которого нужно не более 2 байт;
  • name: строка, длиной не более 255 символов;
  • desc: строка.
use TextMedia\ShmCache\Behavior;

class MyCacheBehavior extends Behavior
{
    public function packData(string $key, $data): string
    {
        return (self::packNumber($data['user_id'], 2)
            . self::packNumber(strlen($data['name'], 1))
            . $data['name']
            . $data['desc']);
    }

    public function unpackData(string $key, string $packed)
    {
        $nameLength = self::unpackNumber(substr($packed, 2, 1));
        return [
            'user_id' => self::unpackNumber(substr($packed, 0, 2)),
            'name'    => substr($packed, 3, $nameLength),
            'desc'    => substr($packed, 3 + $nameLength),
        ];
    }
}

Если метод packData() должен прости привести к строке, лучше использовать parent::packData(), т.к. он проверяет, можно ли значение привести к строки, и выбрасывает исключение, если нельзя. Это позволит избежать фатальных ошибок.

Метод unpackData() при распаковке данных может проверятьо их валидность, соответствие каким-то своим шаблонам, и в случае ошибки - выбрасывать исключение типа \TextMedia\ShmCache\Exception (см. раздел "Ошибки"), которое будет перехвачено основным объектом, в следствии чего будут выполнены следующие действия:

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

Так же этот класс обязательно должен определять метод getData(), необходимый для "прогрева" кэша. Этот метод должен возвращать ArrayObject для сохранения в кэше в виде "ключ-значение". Например:

use ArrayObject;
use TextMedia\ShmCache\Behavior;

class MyCacheBehavior extends Behavior
{
    public function getData(): ArrayObject
    {
        return new ArrayObject($this->database->getQueryResult('SELECT user_id, name, desc FROM users', 'user_id'));
    }
}

Класс \TextMedia\ShmCache\Behavior содержит следующие статичные методы, которые можно использовать для упаковки/распаковки данных:

МетодОписание
string packNumber(int $number, int $size)Упаковка числа в последовательность символов ($size - число байт).
int unpackNumber(string $string)Распаковка последовательности символов в число.

Обработчики событий

Класс-потомок \TextMedia\ShmCache\Behavior может переопределять поведение при возникновении некоторых событий (все три метода необязательны для переопределения и по умолчанию ничего не делают):

МетодСобытие
onCorruptВ процессе обработки данных, прочитанных из кэша, произошла ошибка.
onEmptyПри попытке чтения данных из кэша выяснилось, что он (кэш) пуст.
onWarmedЗавершен "прогрев" кэша.
onIndexedЗавершено формирование таблицы индексов (смещений).

Первым аргументом для каждого метода является объект-кэш класса \TextMedia\ShmCache\Cache, при работе с которым произошло данное событие.

Для обработчика onCorrupt дополнительными аргументами являются:

АргументТипОписание
$keystringКлюч, данные по которому не удалось обработать.
$valuestringДанные, прочитанные из памяти.

Обработчики onWarmed и onIndexed имеет дополнительно два аргумента $onStart и $onReady; оба - являются обычными объектами со следующими полями:

ПолеТипОписание
timefloatКогда был запущен/завершен процесс (microtime(true)).
memoryintegerИспользуемая память на момент запуска/завершения (memory_get_peak_usage(true)).
recordsintegerЧисло записанных/прочитанных записей (на момент старта = 0).
sizeintegerСколько байт было записано/прочитано (на момент старта = 0); без учета размера заголовка кэша.

Обработчик onEmpty дополнительных аргументов не имеет и по умолчанию вызывается обработчиком onCorrupt (если он не переопределен никак иначе), т.к. очевидно, что итоговое поведение в обоих случаях должно быть одно - исправить содержимое кэша, "прогреть" его заново.

Использование

Для работы с кэшем необходимо создать экземпляр класса \TextMedia\ShmCache\Cache, передав в его конструктор объект-наследник \TextMedia\ShmCache\Behavior.

Для "прогрева" кэша используется метод warmup(), имеющий один необязательный параметр, указывающий на то, нужно ли сперва полностью вычистить все данные из памяти, т.е. забить их 0-ми. По умолчанию этот параметр имеет значение TRUE.

Для чтение данных из разделяемой памяти доступны следующие методы:

МетодОписание
mixed getItem(string $key)Чтение по одному ключу ключу.
array getItems(array $keys)Чтение по множеству ключей. На выходе массив вида "ключ --> значение".

Вторым аргументом в метод getItems() можно передать bool $ignoreMissing (по умолчанию = TRUE), который указывает, нужно ли игнорировать отсутствующие в таблице индексов ключи или все же выбрасывать исключения. Если этот аргумент равен TRUE и было выброшено исключение с кодом FAILED_SEARCH_KEY (см. раздел "Ошибки"), то значение не попадет в результирующий массив; в остальных случаях - исключение будет проброшено вверх.

Ошибки

Все вышеперечисленные классы в случае ошибок выбрасывают исключения типа \TextMedia\ShmCache\Exception. Каждому типу ошибок назначен свой код - константа данного класса:

КонстантаЗначениеОписание ошибки
INVALID_PROJECT_ID1Неправильное значение идентификатора проекта.
FAILED_GET_SHM_ID2Не удалось определить идентификатор блока разделяемой памяти.
INVALID_CACHE_SIZE3Неправильное значение размера блока разделяемой памяти.
INVALID_VALUE_SIZE4Неправильное значение размера длины строкового представления данных.
FAILED_OPEN_SHMOP5Не удалось открыть блок разделяемой памяти.
FAILED_DELETE_SHMOP6Не удалось удалить блок разделяемой памяти.
INVALID_STATUS_VALUE7Неправильное значение статуса.
FAILED_READ_SHMOP8Ошибка чтения из разделяемой памяти.
FAILED_WRITE_SHMOP9Ошибка записи в разделяемую память.
FAILED_SEARCH_KEY10Указанный ключ отсутствует в таблице.
CACHE_NOT_READY11Кэш не готов к чтению.
FAILED_PACK_VALUE12Не удалось привести значение к строке.
FAILED_UNPACK_VALUE13Не удалось распаковать значение из строки.

Отладка

Для включения режима отладки необходимо вызвать метод Cache::setDebugMode() с аргументом TRUE; для отключения - его же, но с аргументом FALSE.

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

НазваниеОписание
CACHE CLEANОчистка кэша.
CACHE WARMUP"Прогрев" кэша.
TABLE CREATIONФормирование таблицы индексов.
OFFSET SEARCHПоиск по таблице индексов.

Данные отладки могут быть получены методом Cache::getDebugData(), который возврашает массив, список которого являются массивы со следующими полями:

ПолеОписание
actionВыполненное действие (см. таблицу выше).
timeЗатраченное время (секунды, до 8 знаков после запятой).
memoryСколько в итоге было максимально использовано памяти (в байтах).

Тесты

cd /path/to/package
vendor/bin/phpunit

Кроме проверки записи/чтения в разделяюмую память, тесты включают в себя сравнение производительности по сравнению с Memcached:

  • запускается отдельный скрипт, который:
    • очищает и разделяемую память, и memcached;
    • запускает 300 процессов (поровну для всех типов кэша); запущенные процессу "висят" в памяти, ожидая "прогрева" кэша;
    • выполняет "прогрев" - все типы кэшей заполняются 200 одинаковыми элементами;
  • запущенные скрипты выполняют 10000 запросов к кэшу к рандомным элемнтам;
  • по завершении работы всех скриптов выводится:
    • среднеее время работы отдельного процесса;
    • максимальное;
    • минимальное;
    • время "прогрева" кэша.

Тест производительности активен по умолчанию, но может быть отключен следующим образом:

export PHP_UNIT='--no-performance' && vendor/bin/phpunit

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

pkill -f "TestPerformance.php"