← Все статьи

layercache: как убрать лавину запросов к кэшу в Node.js

L1 + Redis, дедупликация запросов и плавная деградация — бенчмарки layercache против «стада» обращений к базе.

Содержание

Коротко

Классический путь в Node.js: Map в памяти → Redis при масштабировании → гибрид, который ломается в 2:00. На Dev.to автор протестировал layercache — многослойный кэш с защитой от лавины запросов при истечении ключа, плавной деградацией при сбоях Redis и координацией между экземплярами. На HTTP-бенчмарках пропускная способность выросла до ~100× по сравнению с маршрутом без кэша.

Что произошло

layercache складывает уровни: L1 в памяти процесса (0,01 ms), L2 Redis (0,5 ms), опционально L3 на диске. При промахе нижний слой обратно наполняет верхний — попадание в L2 автоматически заполняет L1.

Главная боль — «стадо»: ключ истёк, 75 одновременных запросов бьют в базу 75 раз. Дедупликация через координационную блокировку в Redis (или памяти) оставляет одного загрузчика; остальные ждут. В тесте 375 запросов к «холодному» ключу источник данных вызывался ровно один раз на раунд истечения, а не 75.

На Express + autocannon (40 соединений, 8 с): /nocache ~161 зап/с, /memory и /layered ~17k зап/с при 97-м перцентиле около 4 ms — разница между «только память» и многослойным кэшем на горячем пути почти исчезает, зато Redis даёт общий L2 для нескольких экземпляров.

При искусственной задержке Redis 500 ms горячие L1-ключи оставались быстрыми; промах по холодному ключу с недоступным Redis всё равно упирался в таймаут (~2 с) — плавная деградация спасает прогретые ключи, но не подменяет источник для новых.

При maxSize: 25 и 180 объектах по 256 KB L1 вытеснял старые записи, но при повторном чтении данные подтягивались из L2, а не из базы. Два экземпляра Node.js с общим Redis: сброс кэша через Pub/Sub и 60 параллельных запросов на отсутствующий ключ → одно обращение к источнику.

Почему это важно

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

layercache не отменяет операционную дисциплину: критичные ключи нужно прогревать до инцидента с Redis, а крупные объекты держать в щедром L1 — сериализация 1 MB в Redis в бенчмарке была ~200× медленнее, чем попадание в память.

На практике

  1. Для нескольких экземпляров — многослойный стек (L1 + Redis) с дедупликацией, а не только локальный Map.
  2. Прогревайте горячие ключи перед пиками; промах по холодному ключу при медленном Redis может стоить секунды.
  3. Не рассчитывайте на плавную деградацию для новых ключей при полном падении Redis — только для уже прогретого L1.
  4. Задайте maxSize L1 с запасом под крупные объекты.
  5. Подключите RedisInvalidationBus и RedisSingleFlightCoordinator для кластера.
const cache = new CacheStack([
  new MemoryLayer({ ttl: 60, maxSize: 10_000 }),
  new RedisLayer({ client: redis, ttl: 3600 })
])
const user = await cache.get('user:123', () => db.findUser(123))

Итог

layercache закрывает три продакшен-проблемы — лавину запросов, распределённый сброс кэша и частичную деградацию — без потери скорости L1 на горячем пути. Для сервисов за пределами одного процесса это практичнее, чем очередная самодельная обёртка над ioredis.