Shared Memory ReadOnly Cache
, (*1)
Кэш типа "ключ -> значение" на основе разделяемой памяти., (*2)
Предназначен для работы большого количества одновременно запущенных скриптов
с большим блоком статичных неизменяемых данных:, (*3)
- один скрипт по расписанию или какому-то событию "прогревает" кэш,
т.е. читает откуда-то данные, приводит их к нужному виду и сохраняет в памяти;
- множество других скриптов читают данные из памяти,
т.е. не тратят ни ресурсы процессора на чтение и приведение к нужному виду,
ни память на хранение одних и тех же общих для всех них данных.
Настройка
Для настройки кэша необходимо создать класс-потомок абстракции \TextMedia\ShmCache\Behavior
.
В нем необходимо обязательно переопределить следующие константы:, (*4)
Константа |
Описание |
PROJECT_ID |
Идентификатор проекта. Это должен быть один символ (см. http://php.net/manual/ru/function.ftok.php). |
CACHE_SIZE |
Размер блока разделяемой памяти. Может быть задан целым числом байт или строкой типа: 1m, 100k и т.п. Должен быть не менее 1 Кб. |
VALUE_SIZE |
Определяет, сколько нужно байт для хранения длины упакованных данных. Может меняться в пределах от 1 до 4. |
VALUE_SIZE
должен подбираться из расчета длины хранимых значений. Если это просто число или небольшой текст
(не более 255 ASCII
-символов) - достаточно указать 1 байт., (*5)
CACHE_SIZE
следует выбирать исходя из суммы следующих значений:, (*6)
- число записей в кэше, умноженное на 1 - столько байт потребуется для хранения длин ключей;
- суммарная длина строковых представлений всех ключей;
- число записей, умноженное на
VALUE_SIZE
- столько нужно для хранения длин значений;
- суммарная длина строковых представлений всех значений.
Строковые представления значений не должны быть представлены в виде результата выполнения var_export
и т.п.,
т.к. такие данные имеют избыточную информацию, в частности - имена ключей., (*7)
Например, при сохранении массива вида ['x' => число1, 'y' => число2]
достаточно сохранить только числа в бинарном виде,
а при чтении из памяти приводить их к нужному типу и формировать массив нужного вида.
Для этого в классе необходимо переопределить следующие методы:, (*8)
Метод |
Описание |
string packData(string $key, $data) |
Упаковка элемента данных в строку для записи. |
mixed unpackData(string $key, string $packed) |
Распаковка прочитанных из памяти данных в исходную структуру. |
Значение $key
передается в оба метода, т.к. оно может повлиять на алгоритм упаковки/распаковки., (*9)
Например, если хранимые данные имеют вид массива со следующими полями:, (*10)
-
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()
,
т.к. он проверяет, можно ли значение привести к строки, и выбрасывает исключение, если нельзя.
Это позволит избежать фатальных ошибок., (*11)
Метод unpackData()
при распаковке данных может проверятьо их валидность, соответствие каким-то своим шаблонам,
и в случае ошибки - выбрасывать исключение типа \TextMedia\ShmCache\Exception
(см. раздел "Ошибки"),
которое будет перехвачено основным объектом, в следствии чего будут выполнены следующие действия:, (*12)
- кэшу будет выставлен статус "поврежден";
- вызовется метод
onCorrupt
- по умолчанию он ничего не делает, его можно переопределить;
- исключение будет проброшено выше, т.е. работа скрипта будет прекращена.
Так же этот класс обязательно должен определять метод getData()
, необходимый для "прогрева" кэша.
Этот метод должен возвращать ArrayObject
для сохранения в кэше в виде "ключ-значение". Например:, (*13)
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
содержит следующие статичные методы,
которые можно использовать для упаковки/распаковки данных:, (*14)
Метод |
Описание |
string packNumber(int $number, int $size) |
Упаковка числа в последовательность символов ($size - число байт). |
int unpackNumber(string $string) |
Распаковка последовательности символов в число. |
Обработчики событий
Класс-потомок \TextMedia\ShmCache\Behavior
может переопределять поведение при возникновении некоторых событий
(все три метода необязательны для переопределения и по умолчанию ничего не делают):, (*15)
Метод |
Событие |
onCorrupt |
В процессе обработки данных, прочитанных из кэша, произошла ошибка. |
onEmpty |
При попытке чтения данных из кэша выяснилось, что он (кэш) пуст. |
onWarmed |
Завершен "прогрев" кэша. |
onIndexed |
Завершено формирование таблицы индексов (смещений). |
Первым аргументом для каждого метода является объект-кэш класса \TextMedia\ShmCache\Cache
,
при работе с которым произошло данное событие., (*16)
Для обработчика onCorrupt
дополнительными аргументами являются:, (*17)
Аргумент |
Тип |
Описание |
$key |
string |
Ключ, данные по которому не удалось обработать. |
$value |
string |
Данные, прочитанные из памяти. |
Обработчики onWarmed
и onIndexed
имеет дополнительно два аргумента $onStart
и $onReady
;
оба - являются обычными объектами со следующими полями:, (*18)
Поле |
Тип |
Описание |
time |
float |
Когда был запущен/завершен процесс (microtime(true)). |
memory |
integer |
Используемая память на момент запуска/завершения (memory_get_peak_usage(true)). |
records |
integer |
Число записанных/прочитанных записей (на момент старта = 0). |
size |
integer |
Сколько байт было записано/прочитано (на момент старта = 0); без учета размера заголовка кэша. |
Обработчик onEmpty
дополнительных аргументов не имеет и по умолчанию вызывается обработчиком onCorrupt
(если он не переопределен никак иначе),
т.к. очевидно, что итоговое поведение в обоих случаях должно быть одно - исправить содержимое кэша, "прогреть" его заново., (*19)
Использование
Для работы с кэшем необходимо создать экземпляр класса \TextMedia\ShmCache\Cache
,
передав в его конструктор объект-наследник \TextMedia\ShmCache\Behavior
., (*20)
Для "прогрева" кэша используется метод warmup()
, имеющий один необязательный параметр,
указывающий на то, нужно ли сперва полностью вычистить все данные из памяти, т.е. забить их 0-ми.
По умолчанию этот параметр имеет значение TRUE
., (*21)
Для чтение данных из разделяемой памяти доступны следующие методы:, (*22)
Метод |
Описание |
mixed getItem(string $key) |
Чтение по одному ключу ключу. |
array getItems(array $keys) |
Чтение по множеству ключей. На выходе массив вида "ключ --> значение". |
Вторым аргументом в метод getItems()
можно передать bool $ignoreMissing
(по умолчанию = TRUE
),
который указывает, нужно ли игнорировать отсутствующие в таблице индексов ключи или все же выбрасывать исключения.
Если этот аргумент равен TRUE
и было выброшено исключение с кодом FAILED_SEARCH_KEY
(см. раздел "Ошибки"),
то значение не попадет в результирующий массив; в остальных случаях - исключение будет проброшено вверх., (*23)
Ошибки
Все вышеперечисленные классы в случае ошибок выбрасывают исключения типа \TextMedia\ShmCache\Exception
.
Каждому типу ошибок назначен свой код - константа данного класса:, (*24)
Константа |
Значение |
Описание ошибки |
INVALID_PROJECT_ID |
1 |
Неправильное значение идентификатора проекта. |
FAILED_GET_SHM_ID |
2 |
Не удалось определить идентификатор блока разделяемой памяти. |
INVALID_CACHE_SIZE |
3 |
Неправильное значение размера блока разделяемой памяти. |
INVALID_VALUE_SIZE |
4 |
Неправильное значение размера длины строкового представления данных. |
FAILED_OPEN_SHMOP |
5 |
Не удалось открыть блок разделяемой памяти. |
FAILED_DELETE_SHMOP |
6 |
Не удалось удалить блок разделяемой памяти. |
INVALID_STATUS_VALUE |
7 |
Неправильное значение статуса. |
FAILED_READ_SHMOP |
8 |
Ошибка чтения из разделяемой памяти. |
FAILED_WRITE_SHMOP |
9 |
Ошибка записи в разделяемую память. |
FAILED_SEARCH_KEY |
10 |
Указанный ключ отсутствует в таблице. |
CACHE_NOT_READY |
11 |
Кэш не готов к чтению. |
FAILED_PACK_VALUE |
12 |
Не удалось привести значение к строке. |
FAILED_UNPACK_VALUE |
13 |
Не удалось распаковать значение из строки. |
Отладка
Для включения режима отладки необходимо вызвать метод Cache::setDebugMode()
с аргументом TRUE
;
для отключения - его же, но с аргументом FALSE
., (*25)
Режим отладки включает сохранение данных о времени выполнения операций и используемой для этого памяти.
Отслеживаются следующие операции:, (*26)
Название |
Описание |
CACHE CLEAN |
Очистка кэша. |
CACHE WARMUP |
"Прогрев" кэша. |
TABLE CREATION |
Формирование таблицы индексов. |
OFFSET SEARCH |
Поиск по таблице индексов. |
Данные отладки могут быть получены методом Cache::getDebugData()
, который возврашает массив,
список которого являются массивы со следующими полями:, (*27)
Поле |
Описание |
action |
Выполненное действие (см. таблицу выше). |
time |
Затраченное время (секунды, до 8 знаков после запятой). |
memory |
Сколько в итоге было максимально использовано памяти (в байтах). |
Тесты
cd /path/to/package
vendor/bin/phpunit
Кроме проверки записи/чтения в разделяюмую память, тесты включают в себя сравнение производительности по сравнению с Memcached
:, (*28)
- запускается отдельный скрипт, который:
- очищает и разделяемую память, и
memcached
;
- запускает 300 процессов (поровну для всех типов кэша);
запущенные процессу "висят" в памяти, ожидая "прогрева" кэша;
- выполняет "прогрев" - все типы кэшей заполняются 200 одинаковыми элементами;
- запущенные скрипты выполняют 10000 запросов к кэшу к рандомным элемнтам;
- по завершении работы всех скриптов выводится:
- среднеее время работы отдельного процесса;
- максимальное;
- минимальное;
- время "прогрева" кэша.
Тест производительности активен по умолчанию, но может быть отключен следующим образом:, (*29)
export PHP_UNIT='--no-performance' && vendor/bin/phpunit
Если тест производительности не был отключен и либо "завис", либо прерван,
перед запуском следующего теста необоходимо "убить" запущенные ранее процессы:, (*30)
pkill -f "TestPerformance.php"