OSEN-НИЙ SAAALEСкидка 50% на виртуальный хостинг и VDS
до 30.11.2025 Подробнее
Выберите продукт

APCu для PHP: быстрый кэш данных, TTL-стратегии и безопасная инвалидация

APCu — сверхбыстрый кэш данных в общей памяти PHP-процессов. В статье — практические паттерны: выбор TTL и джиттера, soft/hard TTL, защита от stampede, namespaced-инвалидация, мониторинг и настройки. Разберём работу с PHP-FPM, synergy с OPcache и типичные ошибки продакшена.
APCu для PHP: быстрый кэш данных, TTL-стратегии и безопасная инвалидация

APCu — это встроенный в PHP модуль кэширования пользовательских данных в общей памяти. Он обеспечивает микросекундный доступ и идеально подходит для кэширования результатов вычислений, сериализованных структур (массивов, DTO), небольших фрагментов конфигурации и метаданных. В отличие от OPcache, который хранит байткод, APCu хранит данные приложения. Их synergy даёт существенный прирост производительности: OPcache экономит на компиляции, APCu — на повторных вычислениях и запросах к внешним ресурсам.

Когда APCu уместен, а когда лучше воздержаться

APCu — локальный кэш узла: память общая для PHP-процессов в пределах одной инсталляции (обычно один мастер php-fpm и его воркеры). Он не распределённый, не шарится между серверами и не переживает рестарт процесса. Это делает его идеальным для:

  • Кэширования «горячих» вычислений и небольших наборов справочных данных.
  • Кэширования редко меняющихся результатов запросов к БД или API.
  • Негативного кэширования (ошибки/пустые результаты с малым TTL).
  • Счётчиков и атомарных инкрементов (ограниченно и без жёстких SLA).

Избегайте APCu, если вам нужен:

  • Распределённый кэш между несколькими узлами (лучше смотреть в сторону Redis/Memcached).
  • Кэш больших объектов (десятки мегабайт) — это быстро фрагментирует память сегмента.
  • Жизненно важный персистентный storage — APCu волатилен, очистится при рестарте.

Если нужен распределённый кэш, посмотрите сравнение Redis и Memcached для PHP-кэша.

Базовое правило: APCu — сверхбыстрый кэш для одного узла. Если у вас несколько узлов за балансировщиком без sticky-сессий, используйте его для вычислений и метаданных, но не как источник истины.

Установка и базовая настройка

На большинстве дистрибутивов ставится через пакетный менеджер. После установки проверьте, что модуль загружен, и отрегулируйте объём общей памяти и TTL. На типичных тарифах виртуальный хостинг и на собственном VDS APCu и OPcache обычно доступны и легко настраиваются.

# Debian/Ubuntu
sudo apt-get update
sudo apt-get install php-apcu

# AlmaLinux/Rocky/Oracle (в зависимости от репозиториев)
sudo dnf install php-pecl-apcu

# Проверка
php -m | grep -i apcu
php -i | grep -i apcu

Минимальная настройка в php.ini (или в отдельном apcu.ini):

; Включить APCu для FPM/Apache
apc.enabled=1
; Объём сегмента общей памяти (подбирайте по нагрузке)
apc.shm_size=128M
; Глобальный TTL по умолчанию для ключей без индивидуального TTL
apc.ttl=0
; Время жизни удалённых объектов перед сборкой (смягчает фрагментацию)
apc.gc_ttl=3600
; Для CLI обычно выключено, чтобы не плодить отдельные кэши
apc.enable_cli=0
; Если используете igbinary, это может уменьшить размер сериализации
; apc.serializer=igbinary

Важно: включение APCu в CLI создаст отдельную область кэша и не поможет «прогреть» кэш для php-fpm. Для прогрева делайте HTTP-запросы к самому приложению либо вызывайте код через FPM-пул.

Таймлайн soft/hard TTL и джиттера для записей APCu

Быстрый старт: API APCu

Чаще всего хватает следующих функций: apcu_store, apcu_fetch, apcu_add, apcu_delete, apcu_exists, apcu_inc/apcu_dec, а также удобной обёртки apcu_entry.

<?php
// Кэш-асайд: сначала пробуем взять из кэша, иначе считаем и кладём
function cache_get_or_compute(string $key, callable $compute, int $ttl = 300) {
    $success = false;
    $value = apcu_fetch($key, $success);
    if ($success) {
        return $value;
    }
    $value = $compute();
    apcu_store($key, $value, $ttl);
    return $value;
}

// Пример использования
$userCount = cache_get_or_compute('stats:user_count', function () {
    // тяжёлый запрос
    return (int) db_query_scalar('SELECT COUNT(*) FROM users');
}, 600);

apcu_entry (если доступна) упрощает атомарное получение или вычисление значения:

<?php
$result = apcu_entry('catalog:popular:page1', function () {
    return build_popular_items_page1();
}, 120);

TTL-стратегии: абсолютный, «мягкий» и с джиттером

Абсолютный TTL

Самый простой вариант — задавать TTL для каждого ключа. Например, справочники на 1 час, статистики на 5 минут, негативные результаты на 10–30 секунд. Плюсы: предсказуемо. Минусы: в момент истечения сразу все запросы одновременно начнут перестраивать кэш (stampede).

«Мягкий» TTL и «твёрдый» TTL

APCu сам по себе не поддерживает «скользящий» TTL или хранение просроченного значения, но это легко сделать на уровне приложения, сохранив вместе с данными метаданные и проверяя их при чтении. Идея:

  • Хранить структуру вида: ['payload' => ... , 'ts' => time(), 'soft_ttl' => 30, 'hard_ttl' => 300].
  • Если не истёк hard_ttl, можно отдавать «устаревшее» значение до soft_ttl и параллельно инициировать асинхронное обновление.
<?php
function cache_soft_hard(string $key, callable $compute, int $softTtl, int $hardTtl) {
    $ok = false;
    $entry = apcu_fetch($key, $ok);
    $now = time();
    if ($ok && isset($entry['payload'], $entry['ts'])) {
        $age = $now - $entry['ts'];
        if ($age <= $softTtl) {
            return $entry['payload'];
        }
        if ($age <= $hardTtl) {
            // Отдаём старое, но запускаем обновление в фоне (по месту — без блокировок)
            // В проде используйте очередь/фоновый воркер; здесь синхронно с блокировкой
            if (apcu_add('lock:' . $key, 1, 5)) {
                try {
                    $payload = $compute();
                    apcu_store($key, ['payload' => $payload, 'ts' => $now], $hardTtl);
                } finally {
                    apcu_delete('lock:' . $key);
                }
            }
            return $entry['payload'];
        }
    }
    $payload = $compute();
    apcu_store($key, ['payload' => $payload, 'ts' => $now], $hardTtl);
    return $payload;
}

Так вы снижаете «ступеньку холодного старта» и защищаетесь от массовой регенерации при истечении TTL.

Джиттер (рандомизация TTL)

Чтобы не истекали тысячи ключей одновременно, используйте джиттер: прибавляйте к TTL случайное число в пределах 5–20%. Это распределяет инвалидации во времени.

<?php
function with_jitter(int $ttl, float $ratio = 0.1): int {
    $delta = (int) floor($ttl * $ratio);
    return $ttl + random_int(-$delta, $delta);
}

Безопасная инвалидация: ключи, namespace и atomics

Прямая инвалидация ключей

Самый очевидный способ — apcu_delete($key). Он подходит, когда вы точно знаете, какие ключи затронуты. Минус — сложно, если у вас много производных ключей и шаблонов.

Версионирование namespace (namespaced keys)

Паттерн для массовой инвалидации: держите «версию» пространства имен и включайте её в ключ. При изменении данных просто увеличивайте версию — все старые ключи станут «невидимы».

<?php
function ns_key(string $ns, string $key): string {
    $v = apcu_fetch('ns:' . $ns);
    if ($v === false) {
        $v = 1;
        apcu_add('ns:' . $ns, $v);
    }
    return 'v' . $v . ':' . $ns . ':' . $key;
}

function ns_bump(string $ns): int {
    // Атомарное увеличение версии namespace
    $ok = false;
    $v = apcu_fetch('ns:' . $ns, $ok);
    if (!$ok) {
        apcu_add('ns:' . $ns, 1);
        return 1;
    }
    return apcu_inc('ns:' . $ns);
}

// Использование
$key = ns_key('catalog', 'popular:page1');
$value = cache_get_or_compute($key, fn() => build_popular_items_page1(), 300);
// Массовая инвалидация каталога
ns_bump('catalog');

Плюсы: O(1) по времени, нет перебора ключей. Минусы: старые объекты висят в памяти до истечения их TTL или until eviction.

Замки и защита от cache stampede

Используйте лёгкий «лок» на ключ через apcu_add('lock:'.$key, 1, 5), чтобы единственный процесс делал тяжёлую регенерацию, остальные отдавали старое или ждали короткое время.

<?php
function compute_with_lock(string $key, callable $compute, int $ttl = 300, int $lockTtl = 5) {
    $ok = false;
    $val = apcu_fetch($key, $ok);
    if ($ok) return $val;

    if (!apcu_add('lock:' . $key, 1, $lockTtl)) {
        // Кто-то уже считает. Подождать чуть-чуть и попробовать взять снова
        usleep(200000);
        $val = apcu_fetch($key, $ok);
        if ($ok) return $val;
    }

    try {
        $val = $compute();
        apcu_store($key, $val, $ttl);
        return $val;
    } finally {
        apcu_delete('lock:' . $key);
    }
}

CAS и счётчики

Для условных обновлений пригодится apcu_cas (compare-and-swap). Он позволяет обновить значение, если оно равно ожидаемому, что полезно для примитивных очередей/флагов. Для счётчиков используйте apcu_inc/apcu_dec.

Версионирование namespace и поиск ключа в APCu: схема потока

Проектирование ключей и размер объектов

  • Используйте короткие предсказуемые префиксы: app:ns:key. Не увлекайтесь длинными строками.
  • Если ключ составной (много параметров), применяйте хэш: hash('xxh3', json_encode($params)) и храните короткий хэш в ключе.
  • Следите за размером значений: старайтесь держать один объект до 1–2 МБ. Большие значения повышают риск фрагментации и вытеснений.
  • Для PHP-объектов оцените переход к массивам или сериализаторам (например, igbinary) — часто это экономит память.

Настройка памяти и фрагментация

apc.shm_size — ключевой параметр. Начните со 128–256 МБ для среднего проекта и замерьте метрики. Если наблюдаются частые вытеснения и высокий miss rate, увеличивайте сегмент. apc.gc_ttl помогает сборщику реже трогать только что удалённые блоки, смягчая фрагментацию.

Следите за статистикой:

<?php
$cache = apcu_cache_info(true);
$sma   = apcu_sma_info(true);
// $cache['num_hits'], $cache['num_misses'], $cache['expunges']
// $sma['avail_mem'], $sma['num_seg'] и т.д.

Признаки проблем: растущий expunges, высокий miss rate при стабильном трафике, быстрый рост фрагментации и снижение avail_mem при умеренных объёмах данных.

Совместная работа с PHP-FPM и CLI

В пределах одного php-fpm мастера APCu кэш общий для воркеров. Если у вас несколько версий PHP, отдельных мастеров или строгая изоляция, кэш будет разделён по инсталляциям. Помните:

  • CLI-процессы обычно используют отдельный кэш или вообще отключённый APCu (apc.enable_cli=0). Не рассчитывайте на них для прогрева.
  • Не храните в APCu чувствительные данные — память общая для всех воркеров данной инстанции PHP, и ошибки изоляции могут привести к утечкам между пулами.
  • При деплое и рестарте php-fpm кэш обнуляется. Заложите это в стратегию прогрева и TTL.
Виртуальный хостинг FastFox
Виртуальный хостинг для сайтов
Универсальное решение для создания и размещения сайтов любой сложности в Интернете от 95₽ / мес

Synergy с OPcache

OPcache экономит на парсинге и компиляции PHP-кода, APCu — на данных. В связке это даёт кратный рост производительности при типовой нагрузке «много небольших PHP-запросов». Рекомендации:

  • Включите OPcache и настройте разумные лимиты, чтобы скрипты не вытеснялись.
  • Используйте APCu для объектов, зависящих от БД или внешних API, и для результатов тяжёлых функций.
  • Добавьте джиттер/soft TTL, чтобы избежать синхронного «обвала» кэша.
  • Если используете Composer, рассмотрите опцию кэша автозагрузчика (Composer умеет использовать APCu для map).

Для кэширования фрагментов на уровне веб-сервера посмотрите технику SSI и сабреквестов в материале кэширование через Nginx SSI и subrequest.

FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

Негативное кэширование и дросселирование ошибок

Если внешний API отвечает 404/empty, запишите «пустой» результат с коротким TTL (например, 15–60 секунд). Это снизит давление при массовых повторных запросах и защитит от лавинообразных ретраев. Важно отличать «пусто» от «ошибка»: на ошибки ставьте ещё более короткий TTL (например, 5–10 секунд) и логируйте событие.

Шаблон «мягкий фоллбек» при инвалидации

Когда меняете данные (например, админ обновил запись), используйте версионирование namespace и механизм «мягкого фоллбека»: сразу после инвалидации допускайте кратковременную отдачу старого значения (если оно закешировано под старой версией и у вас есть доступ к нему на уровне объекта), пока генератор не обновит кэш. Это снижает вероятность резких скачков latency.

Тестирование на проде без боли

  • Включите детальные метрики APCu на админском endpoint (защищённом доступе).
  • Добавьте логирование причин пересборки: «miss», «expired», «lock-timeout», «ns-bump».
  • Внесите фиче-флаги TTL в конфиг, чтобы временно увеличивать/уменьшать TTL для отдельных namespaces без релиза.

Чек-лист внедрения APCu

  • Оцените, где кэш даёт максимальную экономию CPU/IO: сложные джоины, агрегации, внешние API.
  • Спроектируйте ключи: короткие, детерминированные, с namespace и версией схемы.
  • Выберите стратегию TTL: абсолютный + джиттер, либо soft/hard TTL с фоновым обновлением.
  • Реализуйте защиту от stampede: замки на ключ, apcu_entry, «раннее обновление» перед истечением.
  • Настройте apc.shm_size, следите за hits/misses/expunges, тестируйте под нагрузкой.
  • Продумайте инвалидацию: точечная и массовая через ns_bump. Логируйте события.
  • Не храните секреты, не кладите в кэш большие объекты без крайней необходимости.
  • Закройте админские инструменты статистики ACL-ами и не светите ключи наружу.

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

  • Сериализация стоит денег. Если данные читаются чаще, чем записываются, APCu окупается. Если наоборот — подумайте о другом слое (например, кэшировать уже отрендеренные блоки).
  • Инкременты и счётчики в APCu быстры, но не предназначены для строгих лимитов с длительным хранением — лучше держать их в персистентном хранилище, а APCu использовать как слой ускорения.
  • Старайтесь минимизировать контенцию на «горячих» ключах: используйте шардирование (несколько ключей с разными суффиксами) и агрегацию при чтении.

Отладка и типичные ошибки

  • «В CLI всё работает, а в вебе пусто» — вы включили APCu в CLI и считаете, что прогрели кэш. На самом деле это отдельный кэш. Прогревайте через веб-приложение.
  • «После деплоя всплеск латентности» — пересобирается кэш. Используйте soft TTL, джиттер и/или последовательный прогрев.
  • «Ключи пропадают» — сегмент мал, вытеснение/фрагментация. Увеличьте apc.shm_size, уменьшите размер объектов, настройте TTL.
  • «Утечки между пулами» — оцените модель изоляции php-fpm. Для многопуловых инсталляций разных проектов используйте отдельные мастера/версии PHP.

Итоги

APCu — один из самых недооценённых инструментов оптимизации PHP. При правильном проектировании ключей, продуманной стратегии TTL и безопасной инвалидации он обеспечивает значимый прирост производительности без внешних зависимостей. В связке с OPcache это превращает ваш PHP-стек в эффективную систему для высоконагруженных сайтов и API. Начните с небольших участков кода, добавьте защиту от stampede, настройте мониторинг — и вы быстро увидите результат по метрикам CPU, латентности и пропускной способности.

Поделиться статьей

Вам будет интересно

PHP и Node.js на одном VDS: аккуратный запуск под systemd и cgroup v2 OpenAI Статья написана AI (GPT 5)

PHP и Node.js на одном VDS: аккуратный запуск под systemd и cgroup v2

Разбираем, как на одном VDS безопасно собрать PHP-бэкенд, Node.js-воркеры и WebSocket-сервисы, очереди на RabbitMQ или Redis, наст ...
Docker Registry proxy cache на VDS: ускоряем CI и экономим трафик OpenAI Статья написана AI (GPT 5)

Docker Registry proxy cache на VDS: ускоряем CI и экономим трафик

Разбираем практическую схему: свой docker registry proxy cache на отдельном VDS, который прозрачно проксирует Docker Hub и другие ...
Composer на VDS: быстрый install через packagist mirror и shm-подход к autoloader OpenAI Статья написана AI (GPT 5)

Composer на VDS: быстрый install через packagist mirror и shm-подход к autoloader

В реальных проектах Composer крутится в каждом CI job и на каждом деплое, а vendor раздувается до сотен мегабайт. На PHP VDS с SSD ...