Перейти к содержимому
Поиск
Разработка

Valkey: кеш PHP и Node.js — мастерская и 8 реальных опытов

Кеш, который можно объяснить: попадания и промахи, обновление данных, TTL, память и отказ подключения. Восемь реальных опытов, мастерская, PHP/Node и Linux/VPS.

Александр Некрасов
Valkey: кеш PHP и Node.js — мастерская и 8 реальных опытов — обложка

Valkey для PHP и Node.js: cache-aside, TTL, инвалидация, конкурентные промахи, память и отказ кеша. Восемь реальных опытов, мастерская и исходники Windows/Linux.

Valkey: зачем приложению ещё одно место для данных

Каталог услуг, карточка товара, список тарифов или подготовленный отчёт часто читаются значительно чаще, чем меняются. Каждый запрос может повторять один и тот же SQL, объединять несколько ответов API и собирать одинаковый JSON. Пользователь ждёт, база занята повторением работы, а добавление ещё одного веб-сервера не устраняет причину. Кеш позволяет некоторое время использовать уже полученный результат.

Valkey — сервер данных в памяти, к которому приложение обращается по сети. В этой статье используем его именно как восстанавливаемый кеш: основная информация остаётся в источнике, а копию можно удалить и построить снова. Разберём попадания и промахи, время жизни, обновление карточки, отрицательный результат, конкурентные запросы, память и отказ подключения.

На странице есть мастерская с восемью опытами. Ответы получены настоящими клиентами Node.js и PHP от Valkey, а интерфейс позволяет пройти выполнение по событиям. В архиве находятся исходники, команды Windows/Linux и инструкция для VPS. После изучения вы сможете выбрать одну операцию своего приложения, добавить кеш и проверить не только быстрый ответ, но и правильность его обновления.

Где применять: каталог, справочники, отчёты и внешний API

Хороший первый кандидат — данные, которые дорого получить, часто запрашивают и допустимо ненадолго повторно использовать. Например, список услуг с описаниями меняется редко, а загружается на каждой странице. Кеш на минуту может убрать повторные чтения. Другой пример — агрегаты административной панели: расчёт выполняется один раз на период, несколько сотрудников получают готовый результат.

Кеш внешнего API помогает соблюдать лимиты и переживать короткие задержки, но требует правил свежести. Если поставщик изменил остаток, ваша копия не узнает об этом сама. Для интерфейса допустимо показать приблизительную доступность, а при оформлении заказа нужно проверить актуальные данные. То же относится к цене: информационная карточка и финансовая операция имеют разные требования.

Сначала измерьте повторяемость и стоимость запроса. Быстрая таблица на десять строк может не выиграть от дополнительного сетевого обращения. Не кешируйте всё приложение по умолчанию. Выберите конкретный объект, срок допустимого отставания, способ инвалидировать копию и ожидаемое поведение, когда Valkey недоступен. Эти четыре решения важнее красивого графика hit rate.

Valkey, Redis и Symfony Cache: разные уровни решения

Valkey поддерживает знакомые команды GET, SET, TTL, коллекции и другие возможности сервера данных. В нашем проекте используются npm-клиент redis и PHP-клиент Predis: необходимые операции проверены именно с выбранным Valkey. Совместимость нужного приложения следует подтверждать своим набором команд, типами ответов и конфигурацией, особенно при использовании модулей и специфичных возможностей другой версии сервера.

Redis и Valkey развиваются отдельно. Здесь не предполагается, что любой существующий Redis-проект можно без проверки переключить на другой адрес. На сайте уже есть материал про Redis, кеш и TTL; текущая статья сосредоточена на полном жизненном цикле копии, связке PHP/Node и эксплуатации Valkey.

Symfony Cache — уровень приложения: он помогает организовать кеширование в PHP и выбрать адаптер. Valkey — отдельный сетевой сервис. Эти инструменты можно сочетать. Если нужна абстракция пулов, ключей и callback, начните также со статьи Symfony Cache на практике. Здесь явные команды оставлены для того, чтобы было видно, что происходит при каждом чтении.

Что входит в стенд и какие версии зафиксированы

В архиве выбран стабильный Valkey 9.0.7, опубликованный 27 октября 2026 года. Используются node-redis 6.3.1, Predis 3.7.1 и Bootstrap 5.3.9. Версии сверены 13 ноября 2026 года; это проверенный набор проекта, а не обещание, что перечисленные версии навсегда останутся последними. Официальная страница загрузки Valkey показывает поддерживаемые ветки и конкретные Docker-теги.

Valkey работает в отдельном Linux-контейнере, а драйвер опыта запускается либо из Windows, либо из Linux-контейнера. Источник данных — небольшая вымышленная модель товара в коде. Она позволяет точно видеть количество чтений и смену ревизии. Мы не подключаем настоящую клиентскую базу и не выдаём задержку одного локального вызова за универсальный результат производительности.

Для повторения нужны Node.js 24 LTS не ниже 24.15, PHP 8.2+, Composer 2 и Docker с Compose. package-lock.json и composer.lock фиксируют зависимости. Готовая папка public уже находится в архиве: её можно открыть через локальный HTTP-сервер даже без запуска Valkey.

npm ci --ignore-scripts
composer install --no-interaction --no-plugins --prefer-dist
# PHP-зависимости устанавливаются из composer.lock.
# Версии Node-зависимостей фиксирует package-lock.json.

Мини-мастерская: восемь опытов с настоящим кешем

Выберите задачу в списке, прочитайте, где она пригодится, и нажимайте «Следующее событие». В первом опыте увидите miss, чтение источника, сохранение копии и hit. В опыте TTL источник уже меняет ревизию, а кеш ещё возвращает старое значение. В опыте конкурентного промаха сравниваются двенадцать запросов без координации и с ограничением пересчёта.

Можно перемещать ползунок, включать воспроизведение, раскрывать полный JSON, скачивать результат и исходный файл. Конструктор ключа показывает, как связать версию схемы, команду и ID карточки; TTL задаётся в секундах. Этот конструктор генерирует текст команд и не отправляет их на сервер.

Механизм обозначен прямо: мастерская воспроизводит запись настоящего выполнения. PHP и Valkey в браузере не запускаются. Чтобы изменить поведение кеша, скачайте проект, измените callback или настройки и повторите capture. Отдельная ссылка открывает мастерскую на всю ширину, если встроенная область мешает рассматривать исходники.

Cache-aside: приложение отвечает за чтение и заполнение

В модели cache-aside приложение сначала спрашивает Valkey. Если значение найдено, оно проверяет формат и возвращает результат. Если значения нет, приложение читает основной источник, сериализует ответ и сохраняет копию с ограниченным временем жизни. Следующий запрос может использовать эту копию. Сам Valkey не выполняет SQL вместо вашего приложения и не знает, как получить отсутствующую карточку.

Это удобно тем, что кеш можно внедрить рядом с существующим repository. Но ответственность за ошибки, срок жизни и инвалидацию остаётся в коде. В нашем readThrough callback load вызывается только после промаха или проблемы кеша. Если сам основной источник падает, ошибка распространяется вызывающему коду: её нельзя незаметно заменить пустой карточкой.

Возвращаемое поле source нужно для обучения и диагностики: origin, hit, negative-hit или fallback. В публичном API не обязательно отдавать внутреннюю топологию, но в метриках приложения полезно различать эти исходы. Иначе ухудшение hit rate и недоступность кеша будут выглядеть одинаково — как увеличение нагрузки на базу.

const result = await readThrough(client, key, async () => {
  return repository.findProduct(id); // объект или null; ошибка остаётся ошибкой
}, { ttlMs: 60_000, negativeTtlMs: 2_000 });

return result.value;

Ключ: окружение, версия схемы, арендатор и объект

Ключ — часть контракта приложения. Если две команды сохраняют разные данные под product:42, одна из них рано или поздно прочитает чужой формат. Если многопользовательский сервис забывает tenant, ошибка становится ещё неприятнее: значение одной организации может попасть в ответ другой. Имена должны однозначно описывать принадлежность данных.

Для учебной карточки используем demo:v1:service-a:product:42. В рабочем проекте можно выбрать myapp:prod:v2:tenant-17:product:42. Версия помогает спокойно сменить JSON-схему. Окружение отделяет production от staging, но отдельные экземпляры и ACL всё равно дают более понятную изоляцию, чем надежда на дисциплину строковых префиксов.

Не вставляйте произвольный пользовательский ввод без правил. В проекте имя команды ограничено строчными латинскими буквами, цифрами и дефисом, ID должен быть положительным целым. У длинных поисковых параметров удобнее хранить каноническое представление и стабильный хеш. Сортировка параметров важна: одинаковый запрос не должен получать разные ключи из-за порядка полей.

export function cacheKey(tenant, id) {
  if (!/^[a-z0-9-]{1,32}$/.test(tenant) ||
      !Number.isSafeInteger(id) || id < 1) {
    throw new Error('Invalid cache identity');
  }
  return `demo:v1:${tenant}:product:${id}`;
}

JSON-контракт: отличать отсутствие записи от повреждённых данных

Для общей работы PHP и Node.js выбираем JSON с небольшой оболочкой: schema, kind и value. kind=value означает найденный объект, kind=missing — подтверждённое отсутствие. Это позволяет различать пустую строку, число ноль, отсутствующий кеш и результат «такой записи нет». Не стоит строить логику на truthy/falsy, когда значением может быть законный ноль.

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

Денежные суммы в примере хранятся целыми копейками, а не строками с символом рубля. PHP serialize здесь не подходит как общий формат для Node.js. Чем проще переносимый контракт, тем легче проверить его другим языком. Один из тестов действительно читает в Node JSON, который PHP записал в Valkey.

{
  "schema": 1,
  "kind": "value",
  "value": {
    "id": 42,
    "name": "Монтажный комплект",
    "priceMinor": 129900,
    "revision": 1
  }
}

Опыт 1. Повторное чтение карточки на Node.js

Первый опыт делает два последовательных чтения одного ключа. В начале ключа нет, поэтому readThrough обращается к callback источника. После получения карточки выполняется SET с PX. Второе чтение получает сохранённый JSON, проверяет оболочку и возвращает то же значение. Счётчик показывает одно чтение источника при двух запросах приложения.

Такой сценарий можно перенести в каталог услуг или справочник материалов. Начните с небольшого срока жизни и наблюдайте количество промахов, прежде чем увеличивать TTL. Если приложение всегда формирует уникальный ключ, кеш останется пустой тратой памяти. Если TTL слишком мал по сравнению с интервалом повторов, почти каждый запрос снова пойдёт в источник.

В исходниках есть самостоятельный examples/product.mjs. Запустите его несколько раз с ID42 и сравните source. ID999 вернёт отсутствие с коротким отрицательным кешем. Callback намеренно использует вымышленные данные; для своего проекта замените его обращением к repository, сохранив договорённость «объект, null либо исключение».

node examples/product.mjs 42
node examples/product.mjs 42
node examples/product.mjs 999

Подключение Node.js: события ошибок и конечное ожидание

Сетевой кеш может быть недоступен при рестарте, изменении сети или ошибке конфигурации. Клиенту нужны обработчик error и понятная политика ожидания. Неограниченные повторные подключения и очередь команд способны удерживать HTTP-запросы значительно дольше, чем допустимо для вашего интерфейса. В лаборатории автоматическое переподключение отключено, connectTimeout равен 300мс, очередь команд при отсутствии связи отключена.

Для операций проект добавляет предел 500мс. При его превышении соединение уничтожается, чтобы незавершённая команда не продолжала незаметно обслуживать последующие запросы. Это простой учебный механизм, а не универсальная настройка производственного пула. В рабочем сервисе учитывайте бюджет всего HTTP-запроса, повторное создание соединения и влияние отмены на других пользователей общего клиента.

Пароль лаборатории читается из закрытого runtime-файла. В реальном приложении подойдут secret manager или файл окружения вне web-root. Не печатайте целый connection URL с паролем в журнал при любой ошибке. API и параметры клиента сверяйте с документацией node-redis, поскольку версии клиента могут менять типы и значения по умолчанию.

const client = createClient({
  url: 'redis://127.0.0.1:8540',
  username: 'app',
  password: secretFromPrivateStorage,
  disableOfflineQueue: true,
  socket: { connectTimeout: 300, reconnectStrategy: false }
});
client.on('error', error => metrics.increment('cache_error'));
await client.connect();

Опыт 2. PHP и Predis: тот же результат и формат

Predis устанавливается через Composer и не требует отдельного бинарного PHP-расширения для данного примера. Создаём клиент с адресом, пользователем, паролем и конечными сетевыми таймаутами. Класс Aen\Cache повторяет ту же последовательность: GET, проверка JSON, callback источника, SET PX. При повторе карточка приходит из Valkey, а число чтений источника остаётся равным одному.

Обратите внимание на JSON_THROW_ON_ERROR: ошибка сериализации не должна превращаться в ложный успех. PHP-целые и JSON-числа должны оставаться в согласованном диапазоне. Для денег в примере используются целые копейки; для больших идентификаторов, выходящих за безопасный диапазон JavaScript, понадобится другая явная договорённость, например строка.

Подробный вводный материал про Composer пригодится для автозагрузки и lock-файла. После Node-примера можно выполнить PHP-пример: оба обращаются к одному demo-ключу и понимают одну оболочку. Производственные роли и адреса затем отделяются от учебного пользователя lab.

$cache = new Aen\Cache($client);
$result = $cache->get(
    'demo:v1:service-a:product:42',
    fn() => $repository->findProduct(42),
    60000 // миллисекунды в методе проекта
);

echo json_encode($result['value'], JSON_THROW_ON_ERROR);

Опыт 3. TTL ограничивает жизнь копии, но не обновляет её

В третьем опыте сначала сохраняется карточка revision1. Затем источник меняется на revision2, но следующий запрос ещё получает revision1 из кеша. Это нормальное поведение выбранной политики: Valkey не подписан на изменения вашей модели. Пока копия существует, приложение использует её, если не выполняет дополнительную проверку свежести.

В лаборатории TTL всего 220мс, чтобы опыт выполнялся быстро. Драйвер ждёт фактического исчезновения ключа с ограничением общего ожидания, а не предполагает, что ровно через заданное число миллисекунд все планировщики сработают одинаково. После истечения следующее чтение обращается к источнику и получает revision2. В интерфейсе можно просмотреть эти три ответа рядом в полном JSON.

В рабочем сервисе TTL выбирают по смыслу данных. Справочник стран и остаток конкретного товара требуют разной свежести. Допустимое отставание должно быть бизнес-решением, а не числом, скопированным из учебника. Для группы ключей полезен небольшой случайный разброс TTL, чтобы они не истекали одновременно после массового прогрева.

await client.set(key, JSON.stringify(envelope), { PX: 60_000 });
const remainingMs = await client.pTTL(key);
// remainingMs >= 0: оставшееся время
// -1: ключ существует без срока
// -2: ключа уже нет

EX, PX, TTL и PTTL: не перепутать единицы

Команда SET принимает EX в секундах и PX в миллисекундах. TTL возвращает секунды, PTTL — миллисекунды. Переменная с названием ttl без единицы легко приводит к ошибке в тысячу раз. В проекте методы принимают ttlMs, а конструктор на странице явно подписан «TTL, секунд» и генерирует EX.

Важна и семантика перезаписи. Обычный SET без срока способен заменить значение и убрать прежний TTL. Поэтому чтение и последующее обновление ключа должны следовать одной договорённости. Если намеренно сохраняете прежний срок, изучите KEEPTTL и проверьте его поведение для вашего случая. Не добавляйте отдельный EXPIRE после SET без понимания промежутка, в котором процесс может завершиться.

Значения -1 и -2 полезны при диагностике: бессрочный ключ отличается от отсутствующего. Но PTTL сам по себе не доказывает актуальность содержимого. Копия может иметь положительный срок и уже отставать от источника, как показывает опыт3. Официальные определения находятся в справочниках SET и TTL.

SET demo:v1:service-a:product:42 '{"schema":1,"kind":"missing","value":null}' EX 2
TTL demo:v1:service-a:product:42
PTTL demo:v1:service-a:product:42

Опыт 4. Сначала изменить источник, затем инвалидировать копию

Пользователь отредактировал карточку. Если сначала удалить кеш, а потом обновить базу, другой запрос может успеть снова положить старые данные. Базовый порядок проще объяснить: подтвердить изменение основного источника, затем удалить зависящий от него ключ. Следующее чтение заполнит копию новой ревизией. В опыте UNLINK возвращает1, а новая карточка имеет revision2.

UNLINK удаляет ключ из пространства имён, освобождая память объекта асинхронно. Это удобно для крупных значений, но не превращает несколько операций приложения в одну транзакцию. Не следует утверждать, что commit+UNLINK полностью устраняет все гонки. Старый читатель, начавшийся до изменения, ещё способен позже записать старую копию.

Для требовательных данных добавляют версии, событийную инвалидацию, outbox, проверку revision или другой согласованный механизм. Выбор зависит от допустимой свежести и архитектуры источника. Учебный опыт показывает повседневную последовательность без конкурентного writer; отдельное испытание гонки потребуется в вашем приложении.

await repository.updateProduct(id, patch); // commit успешно завершён
await client.unlink(cacheKey(tenant, id));
// После этого обычное чтение заполнит отсутствующую копию.
// Для конкурентного старого reader нужна отдельная защита версий.

Зависимые ключи: одна карточка может менять несколько экранов

Удалить product:42 часто недостаточно. Изменённое название может присутствовать в карточке, результатах поиска, списке избранного и готовом отчёте. Поэтому список зависимостей лучше описать рядом с операцией изменения. Иначе интерфейс покажет новое значение в одном месте и старое в другом, а пользователь решит, что сохранение не сработало.

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

Не используйте KEYS по всему production-серверу в каждом HTTP-запросе ради удаления неизвестных зависимостей. Если необходимо обслуживание пространства ключей, рассмотрите ограниченный SCAN и фоновые операции. Но лучший первый шаг — сделать зависимости конкретными: какие данные меняются, какие представления их используют и какие копии надо обновить.

Опыт 5. Negative cache: сохранить подтверждённое отсутствие

Запрос несуществующего ID тоже может быть дорогим. Повторные проверки одного и того же отсутствующего объекта заставляют базу выполнять одинаковую работу. Negative cache сохраняет небольшой маркер kind=missing на короткий срок. Второй запрос возвращает null из кеша, и источник читается один раз. В мастерской этот результат обозначен negative-hit.

Нельзя смешивать отсутствие объекта и ошибку источника. Если база недоступна, запись missing замаскирует проблему и будет отвечать «не найдено» даже после восстановления связи. Поэтому callback должен возвращать null только при достоверно пустом результате, а при сбое выбрасывать исключение. В коде ошибка основного источника не перехватывается как cache miss.

У отрицательных записей обычно более короткий TTL. Новый объект мог появиться сразу после первого промаха. После создания карточки также полезно инвалидировать её старый missing-ключ. В многопользовательской системе учитывайте права: отсутствие доступа конкретного пользователя нельзя бездумно кешировать как отсутствие объекта для всех.

const envelope = value === null
  ? { schema: 1, kind: 'missing', value: null }
  : { schema: 1, kind: 'value', value };

await client.set(key, JSON.stringify(envelope), {
  PX: value === null ? 2_000 : 60_000
});

Опыт 6. Одновременный промах: двенадцать запросов за одним результатом

Популярный ключ истёк, и несколько запросов одновременно увидели miss. Каждый отправился в источник. В опыте двенадцать конкурентных вызовов одного Node-процесса выполняют реальные GET и затем один и тот же callback с небольшой задержкой. Без координации счётчик достигает12. Кеш существует, но именно в момент заполнения он не снижает нагрузку.

Вторая часть использует отдельный ключ права пересчёта через SET NX PX. Один участник получает право, остальные ограниченно ждут появления значения. Перед вычислением владелец повторно читает кеш: другой участник мог заполнить его между первым GET и получением права. После завершения используется Lua-проверка токена перед удалением ключа права, чтобы старый владелец не удалил чужую аренду.

В записанном опыте все двенадцать запросов получают одну ревизию, а источник вызывается один раз. Это конкретный проверенный сценарий, а не обещание exactly-once при любых сбоях. Здесь нет нескольких серверов приложения, failover или долгой остановки владельца; такие ситуации требуют отдельного проектирования.

const token = randomUUID();
const acquired = await client.set(key + ':fill', token, {
  NX: true, PX: 3000
});
if (acquired === 'OK') {
  // Ещё раз GET; затем вычисление и заполнение при отсутствии.
  // В finally — атомарная проверка токена и освобождение.
}

Ожидание заполнения, срок аренды и запасной ответ

Ожидание должно иметь предел. В проекте дедлайн цикла ожидания — 1800 мс, отдельные команды имеют бюджет до 500 мс, а право пересчёта выдаётся на 3000 мс. Дедлайн проверяется перед очередным проходом: команды и пауза внутри последнего прохода могут выйти за него. Получивший аренду участник затем выполняет load. Поэтому 1800 мс — не жёсткий предел общей длительности операции. Если цикл ожидания завершился без результата, функция выдаёт CACHE_FILL_BUSY. Это позволяет вызывающему коду выбрать понятный ответ: предложить повтор, вернуть ранее допустимую stale-копию или поставить работу в очередь. Бесконечный цикл GET с паузой удерживал бы ресурсы приложения.

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

На сайте отдельно разобран Symfony Lock, TTL и владение. Здесь фокус на уменьшении нагрузки при промахе. В production также нужны лимит параллельных обращений к источнику, случайный разброс TTL и контроль того, как часто система возвращает busy.

Опыт 7. Maxmemory и allkeys-lru: копии могут исчезать раньше TTL

TTL — не единственная причина исчезновения ключа. Когда Valkey достигает настроенного бюджета памяти, выбранная политика может вытеснить часть значений. В отдельном учебном экземпляре драйвер устанавливает небольшой лимит, записывает160 значений по8192байта и проверяет увеличение evicted_keys. Сохраняется меньше160 ключей, хотя для этих записей не задавался TTL.

Политика allkeys-lru допускает вытеснение любых ключей и использует приближённый алгоритм выбора. Не стоит обещать, что удалится строго один определённый ключ в каждом запуске. Мы проверяем наличие реального вытеснения и уменьшение числа сохранившихся ключей. Точное количество может различаться между системами и повторениями опыта.

Такое поведение нормально для восстанавливаемого кеша: miss должен быть обычным исходом. Если приложение хранит там единственную копию документа или критическое состояние очереди, потеря значения уже имеет другой смысл. Эти нагрузки нужно разделять по требованиям и, часто, по экземплярам. Детали политик разобраны в документации key eviction.

# Пример бюджета отдельного восстанавливаемого кеша:
maxmemory 256mb
maxmemory-policy allkeys-lru
# Значение256mb выбирают по ресурсам и измерениям своего хоста.

Noeviction, RSS и наблюдение за памятью

При noeviction Valkey не удаляет старые значения ради новой записи и может вернуть OOM. Вторая часть опыта действительно получает такой отказ. Приложение, уже прочитавшее правильный результат из источника, обычно может вернуть его пользователю даже если сохранение копии не удалось. При этом ошибка кеш-записи должна попасть в метрики, иначе система незаметно перестанет получать пользу от кеша.

Maxmemory ограничивает учитываемый набор данных, но не является жёсткой гарантией общего RSS процесса. Есть служебные структуры, allocator, буферы и дополнительные расходы. На VPS RAM делят ОС, PHP, Node, база и другие сервисы. Выделить весь объём хоста под maxmemory — плохой способ избежать OOM на уровне операционной системы.

Наблюдайте used_memory, used_memory_rss, evicted_keys, hits/misses и ошибки записей. Если вытеснение идёт постоянно, сначала оцените размер значений, число уникальных ключей и полезность их повторного чтения. Увеличение памяти не исправит бесконечное создание уникальных ключей по случайному параметру пользователя.

INFO memory
INFO stats
# Смотрите used_memory, used_memory_rss, evicted_keys,
# keyspace_hits и keyspace_misses в контексте нагрузки.

Опыт 8. Недоступный кеш и чтение основного источника

В последнем опыте создаётся настоящий отказ подключения: драйвер получает свободный временный loopback-порт, закрывает его и пытается подключить клиент. Соединение не устанавливается. Последующий readThrough фиксирует проблему кеша и получает карточку из основного источника. В интерфейсе этот результат имеет source=fallback.

Такое поведение подходит для необязательного слоя ускорения, но требует запаса мощности. Если кеш обслуживал большую долю запросов, после его отказа база получит резкий рост нагрузки. Поэтому кроме fallback нужны таймауты, предел параллельных запросов, circuit breaker или временное ограничение части операций. «Есть запасной путь» не означает, что он выдержит любое число пользователей.

Не скрывайте ошибку источника за красивой пустой карточкой. Если недоступны и кеш, и база, приложение должно вернуть предусмотренный статус ошибки либо явно разрешённый устаревший результат. Для интерфейса полезно объяснить возможность повторить операцию, а для разработчика — различать cache_error и origin_error в журнале.

try {
  return await readThrough(client, key, loadFromRepository);
} catch (error) {
  // Сюда дойдёт ошибка основного источника.
  // Верните предусмотренный вашим API ответ, например503.
  throw error;
}

Windows: повторить все опыты из PowerShell

Распакуйте архив в отдельный каталог и установите зависимости из lock-файлов. Docker Desktop должен работать в режиме Linux containers. Команда prepare создаёт уникальный пароль и ACL в .runtime; секрет не выводится в консоль и не входит в скачиваемый архив. После этого Compose поднимает только собственный aen-valkey-b19 на loopback-порту8540.

PHP capture выполняется первым, потому что общий Node capture включает результат PHP и проверяет записанный им JSON. Затем запускаются тесты, запись и сверка хешей. Каждая команда должна успешно завершиться до следующей. Если порт занят, изменяйте настройки только своей лаборатории и согласуйте адрес в клиенте; не останавливайте чужой сервис ради примера.

Опыт памяти намеренно требует пустую базу и ограничивает адреса лабораторией. Он меняет maxmemory и policy только отдельного экземпляра, затем восстанавливает их. Не перенаправляйте этот capture на production. Для обычного использования приложения нужны функции кеша и минимальные права, а не тестовый сценарий изменения конфигурации.

npm ci --ignore-scripts
composer install --no-interaction --no-plugins --prefer-dist
node tools/prepare.mjs
docker compose up -d --wait valkey
php php/capture.php
npm test
npm run capture
npm run verify
npm run build
node tools/serve.mjs
# Открыть http://127.0.0.1:8523

Linux: те же исходники и контейнерный повтор

На Linux можно выполнить тот же набор команд установленными Node.js, PHP и Composer. Проверьте версии перед началом и используйте подходящий Docker Engine с Compose plugin. Установка системных пакетов зависит от дистрибутива: наличие конкретного имени пакета не гарантирует, что он предоставляет нужную версию PHP или Valkey.

В архиве есть дополнительные одноразовые linux-php и linux-node сервисы профиля tools. Первый выполняет PHP-опыт, второй устанавливает зафиксированные npm-зависимости, запускает тесты и повторяет все сетевые сценарии. Результат сохраняется отдельно в recording-linux.json. Основная мастерская продолжает читать recording.json, поэтому можно сравнить результаты без незаметной смены показанного запуска.

Node runner использует тот же рабочий каталог и выполняет npm ci. Не запускайте одновременно npm-операции из Windows в этом каталоге. В данном проекте зависимости не требуют переноса нативных бинарных модулей между ОС. Проверки сравнивают смысл результатов и хеши исходников; число вытеснений и остаток TTL не обязаны совпадать побайтно.

node --version
php --version
composer --version
docker compose version

# После prepare и запуска отдельного Valkey:
docker compose run --rm linux-php
docker compose run --rm linux-node
# В README есть также вариант с установленными PHP/Node на Linux.

Linux/VPS: где слушает Valkey и кто к нему подключается

Веб-браузеру пользователя не нужен прямой доступ к Valkey. Браузер вызывает HTTP API, приложение проверяет пользователя и права, а затем обращается к кешу. На одном сервере разумно начать с loopback. В нашем Compose публикация имеет вид 127.0.0.1:8540:6379; внутри приватной сети сервиса используется имя valkey:6379.

Если приложение и кеш находятся на разных серверах, потребуется приватная сеть, правила доступа и защита канала согласно среде. ACL проверяет пользователя и команды, но не заменяет TLS. Проверяйте фактические слушающие адреса и доступ с запрещённой машины; особенно учитывайте взаимодействие Docker и firewall, а не только текст одного правила UFW.

Официальная модель безопасности Valkey исходит из доверенных клиентов и контролируемой сети. В архиве docs/LINUX-VPS.md объясняет перенос, диагностику и ограничения учебного пользователя. Открытый6379 в интернете не нужен для того, чтобы ваш сайт пользовался кешем.

ss -ltnp | grep -E ':(6379|8540)\b'
docker compose ps
docker compose logs --tail 60 valkey
# Внешнюю доступность проверяйте с другой машины по своей политике сети.

ACL и секреты: приложению не нужен полный административный доступ

Лабораторный пользователь lab имеет расширенные права, потому что опыт памяти вызывает CONFIG. Это осознанная роль отдельного одноразового стенда. Производственному PHP/Node API обычно достаточно GET, SET, DEL/UNLINK и нескольких диагностических команд в своём префиксе. Полный административный доступ усложняет последствия обычной ошибки приложения.

Создайте отдельного пользователя app и ограничьте ключи, например myapp:prod:v1:*. Для схемы с Lua дополнительно оцените права EVAL и команд сценария. Секрет храните вне web-root и репозитория; пароль не должен появляться в исходниках статьи, скриншотах и ошибках подключения. Подготовка лаборатории хранит только локальные runtime-файлы, которые исключаются из архива.

ACL-файл ниже показывает форму настройки, а SHA256_OF_YOUR_RANDOM_PASSWORD — явный placeholder. Его необходимо заменить хешем собственного случайного пароля, а клиенту передать исходный пароль через закрытое хранилище. Порядок и смысл правил описаны в справочнике ACL. Проверяйте разрешённые и запрещённые команды под реальной ролью приложения.

user default off resetpass -@all
user app on #SHA256_OF_YOUR_RANDOM_PASSWORD ~myapp:prod:v1:* +ping +get +set +del +unlink +ttl +pttl

Persistence, рестарт и холодный кеш

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

Valkey поддерживает RDB, AOF и их сочетание; официальное описание находится в разделе persistence. Сохранение на диск может ускорить прогрев и уменьшить потерю состояния, но само по себе не делает данные актуальными относительно внешней базы. После восстановления всё равно нужны TTL и правила согласования.

Отдельно проверьте холодный старт. После рестарта популярные ключи одновременно промахиваются, и база может получить нагрузку, которой не было в обычный день. Помогают ограниченный прогрев, случайный разброс сроков, координация заполнения и ограничение конкуренции. Не запускайте массовый прогрев без оценки источника только ради немедленного высокого hit rate.

# Только для отдельного восстанавливаемого кеша:
save ""
appendonly no
# Для сессий, очередей или важных данных решение принимается отдельно.

Системная служба, мониторинг и обновление на VPS

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

После изменения конфигурации планируйте перезапуск как изменение сервиса. Проверьте доступность приложения, ошибки кеша, нагрузку на источник и восстановление hit rate. Одного зелёного systemctl status недостаточно: клиент может подключаться с неправильным пользователем или использовать префикс, который не разрешён ACL.

Для наблюдения полезны серверные показатели памяти и команд, а также метрики приложения: количество hit/miss/fallback, время ожидания кеша и время чтения источника. Структурированные журналы помогают связать всплеск ошибок с конкретной выкладкой. На сайте есть материал про Loki и Alloy, который можно использовать для такой диагностики.

apt-cache policy valkey
systemctl list-unit-files 'valkey*'
systemctl cat valkey
# После проверки имени службы и подготовки изменения:
sudo systemctl restart valkey
sudo systemctl status valkey --no-pager
sudo journalctl -u valkey -n 80 --no-pager

Что проверено и как повторить проверку после изменения

В каждом из двух окружений выполнены восемь сетевых опытов и28 проверок результатов. PHP отдельно подтвердил первое чтение из источника, повторный hit, равенство значений, один вызов источника и положительный TTL. Node проверил, что понимает JSON, реально записанный PHP. Дополнительные node:test проверки покрывают разделение ключей, валидацию схемы и сохранение ошибки основного источника.

Конкурентный опыт дал12 чтений без координации и1 с ней. Проверены настоящее истечение TTL, смена ревизии после инвалидации, negative-hit, реальные вытеснения, OOM при noeviction и отказ сетевого подключения. Запись содержит SHA-256 шести файлов реализации. После изменения этих файлов verify потребует получить новую запись, чтобы старые результаты не выдавались за проверку нового кода.

Linux-повтор выполнялся в изолированных локальных контейнерах. Это не проверка вашего внешнего VPS, его firewall или отказоустойчивого кластера. Публичная мастерская показывает результаты Windows-драйвера и реальные PHP/Valkey-операции; время и количество вытеснений остаются наблюдениями конкретного запуска.

npm test
php php/capture.php
npm run capture
npm run verify
npm run build

Скачать проект, остановить стенд и внедрить одну полезную операцию

В архиве находятся Node/PHP-реализации, Compose, конфигурация, подготовка локальных ACL, тесты, реальные записи, исходники интерфейса и инструкции Windows/Linux/VPS. Авторский код распространяется по MIT. Зависимости устанавливаются из lock-файлов, сгенерированные runtime-секреты не распространяются. Готовую мастерскую можно обслуживать как статическую папку public.

После опытов остановите только собственный Compose-проект. Команда down в его каталоге не требует глобальной очистки Docker. Если меняли порт или конфигурацию, сохраните свои изменения рядом с проектом и проверьте, что не затронуты другие сервисы. Удаление пользовательских контейнеров и сетей не является частью установки кеша.

Первый практический шаг — выбрать одно чтение карточки в тестовом приложении. Определите ключ, допустимую свежесть, операцию инвалидации и поведение при отказе. Проверьте hit, miss, изменение, истечение и недоступность. Когда сможете объяснить каждый исход, масштабируйте подход на другие данные, сохраняя источник истины и понятные пользователю ответы.

docker compose down
# Только текущая лаборатория aen-valkey-b19.
# Не нужен docker system prune.