惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
Vulnerabilities – Threatpost
D
Docker
C
Check Point Blog
P
Proofpoint News Feed
H
Help Net Security
A
About on SuperTechFans
GbyAI
GbyAI
MyScale Blog
MyScale Blog
F
Fortinet All Blogs
U
Unit 42
Y
Y Combinator Blog
The GitHub Blog
The GitHub Blog
云风的 BLOG
云风的 BLOG
I
InfoQ
Recent Announcements
Recent Announcements
Stack Overflow Blog
Stack Overflow Blog
博客园 - 三生石上(FineUI控件)
Google DeepMind News
Google DeepMind News
Apple Machine Learning Research
Apple Machine Learning Research
Simon Willison's Weblog
Simon Willison's Weblog
WordPress大学
WordPress大学
Attack and Defense Labs
Attack and Defense Labs
D
DataBreaches.Net
C
CXSECURITY Database RSS Feed - CXSecurity.com
人人都是产品经理
人人都是产品经理
J
Java Code Geeks
Help Net Security
Help Net Security
P
Proofpoint News Feed
Latest news
Latest news
L
LINUX DO - 最新话题
K
Kaspersky official blog
B
Blog
C
Cybersecurity and Infrastructure Security Agency CISA
S
SegmentFault 最新的问题
C
Cyber Attacks, Cyber Crime and Cyber Security
Project Zero
Project Zero
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
H
Heimdal Security Blog
阮一峰的网络日志
阮一峰的网络日志
小众软件
小众软件
Jina AI
Jina AI
Vercel News
Vercel News
AWS News Blog
AWS News Blog
L
Lohrmann on Cybersecurity
aimingoo的专栏
aimingoo的专栏
H
Hackread – Cybersecurity News, Data Breaches, AI and More
博客园 - 【当耐特】
Hacker News - Newest:
Hacker News - Newest: "LLM"
W
WeLiveSecurity
Martin Fowler
Martin Fowler

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Как я построил кеш страниц для многодоменного проекта с помощью PVC и кастомного подхода
NikFourNik · 2026-04-27 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение5 мин

Охват и читатели260

Кейс

У меня был проект, где один Next.js сайт обслуживал несколько доменов, и возникла задача - эффективно кешировать страницы, чтобы не пересоздавать их каждый раз. Сначала я попробовал внедрить кеширование через Redis: я написал хендлер, подключил его, но вскоре обнаружил, что Redis потребляет колоссальный объём оперативной памяти - порядка 100 ГБ, и это при том, что ещё не все запросы были закешированы. Тогда я решил поискать другой подход и обратил внимание на PVC - общее хранилище, которое могли бы использовать все поды. Я начал изучать варианты работы с PVC и довольно быстро пришёл к идее общего кеш-хранилища для всех подов. Я попробовал просто писать данные в PVC, но столкнулся с проблемой: каждый раз, когда под поднимался, он перезаписывал кеш. До тех пор, пока не подняты все поды, данные постоянно перезаписывались, а мне нужно было, чтобы первый под записал данные, а последующие только читали их. Я начал искать, как сделать кастомный кеш-хендлер, но готовых решений не нашёл.

Я нашёл в документации Next.js упоминание о кастомных кеш-хендлерах (вот ссылка), но там показан только минимальный интерфейс кеш-хендлера, без деталей про shared storage, инвалидацию и конкуренцию чтения/записи и не было примера, как это использовать с PVC. Разбираясь, я понял, что у кеш-хендлера есть ключевые методы. set - отвечает за сохранение данных, get - за получение, а revalidateTag - обновляет данные по тегам. Для работы с общим хранилищем мне также пришлось отдельно продумать сериализацию и десериализацию записей на стороне Node.js. Я создал хранилище на PVC, где записывал страницы, а поды затем читали данные оттуда. Так я получил централизованное кеширование. Вот реализация кеш-хендлера из документации:

const cacheHandler = {
  async get(cacheKey, softTags) {
    const entry = cache.get(cacheKey)
    if (!entry) return undefined
 
    // Check if expired
    const now = Date.now()
    if (now > entry.timestamp + entry.revalidate * 1000) {
      return undefined
    }
 
    return entry
  },
    async set(cacheKey, pendingEntry) {
    // Wait for the entry to be ready
    const entry = await pendingEntry
 
    // Store in your cache system
    cache.set(cacheKey, entry)
  },
}

Используя Node.js, я реализовал метод set, который подготавливает запись, вычисляет срок её актуальности и сохраняет данные в общем внешнем хранилище:

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

async set(key, data, ctx) {
  if (this.isBuildPhase || !this.storageUrl) {
    return;
  }

  await this.initPromise;

  const existingRequest = this.pendingWrites.get(key);
  if (existingRequest) {
    return existingRequest;
  }

  const entryKey = getEntryKey(key);

  const request = (async () => {
    try {
      if (data == null) {
        await storage.remove(entryKey);
        return;
      }

      const revalidateSec = extractRevalidateSeconds(data, ctx);
      const tags = parseTagsFrom(data, ctx);
      const ttl = revalidateSec > 0 ? revalidateSec : DEFAULT_TTL_SEC;
      const now = Date.now();

      const entry = {
        value: data,
        modifiedAt: now,
        tags,
        revalidateSec: ttl,
        expiresAt: now + ttl * 1000,
      };

      await storage.write(entryKey, encodeEntry(entry), ttl);
      await this.touchEntry(entryKey);
      await this.evictIfNeeded();
    } catch (error) {
      console.error('[CacheHandler] Failed to write cache entry', error);
    } finally {
      this.pendingWrites.delete(key);
    }
  })();

  this.pendingWrites.set(key, request);
  return request;
}

Метод get читает запись из внешнего хранилища, декодирует её и дополнительно проверяет, не устарела ли она по времени или по тегам:

async get(key) {
  if (this.isBuildPhase || !this.storageUrl) {
    return null;
  }

  await this.initPromise;

  const existingRequest = this.pendingReads.get(key);
  if (existingRequest) {
    return existingRequest;
  }

  const entryKey = getEntryKey(key);

  const request = (async () => {
    try {
      const storage = await this.initPromise;
      const rawValue = await storage.read(entryKey);

      if (!rawValue) {
        return null;
      }

      let entry;
      try {
        entry = decodeEntry(rawValue);
      } catch (error) {
        console.error('[CacheHandler] Failed to decode cache entry', error);
        await storage.remove(entryKey);
        return null;
      }

      if (
        typeof entry?.expiresAt === 'number' &&
        entry.expiresAt > 0 &&
        entry.expiresAt <= Date.now()
      ) {
        await storage.remove(entryKey);
        return null;
      }

      if (
        entry?.tags?.length &&
        await this.isEntryStaleByTags(entry.tags, entry.modifiedAt)
      ) {
        await storage.remove(entryKey);
        return null;
      }

      await this.touchEntry(entryKey);
      return entry;
    } catch (error) {
      console.error('[CacheHandler] Failed to read cache entry', error);
      return null;
    } finally {
      this.pendingReads.delete(key);
    }
  })();

  this.pendingReads.set(key, request);
  return request;
}

Метод revalidateTag не удаляет запись напрямую, а помечает связанные теги как инвалидированные. Благодаря этому при следующем чтении устаревшая запись будет отброшена и пересобрана заново:

async revalidateTag(tags) {
  if (this.isBuildPhase || !this.storageUrl) {
    return;
  }

  await this.initPromise;

  const tagsList = Array.isArray(tags) ? tags : [tags].filter(Boolean);
  if (!tagsList.length) {
    return;
  }

  const invalidatedAt = Date.now();
  await storage.markTagsInvalid(tagsList, invalidatedAt);
}

Так шаг за шагом я построил кастомный подход, интегрированный с PVC.

Когда я реализовывал кастомный кеш-хендлер на PVC, у меня возникли проблемы. PVC медленнее памяти, и нужно было избегать гонки. Во-первых, я решал проблему устаревших данных с помощью функции isEntryStaleByTags:

async isEntryStaleByTags(tags, modifiedAt) {
  if (!tags.length) {
    return false;
  }

  const invalidatedAt = await storage.getTagsInvalidationTime(tags);
  return invalidatedAt > modifiedAt;
}

Во-вторых, я ограничивал количество записей функцией evictIfNeeded :

async evictIfNeeded() {
  const overflow = await storage.getOverflowCount();
  if (overflow <= 0) {
    return;
  }

  const staleEntries = await storage.getLeastRecentlyUsed(overflow);
  await storage.removeEntries(staleEntries);
}

В-третьих, я гарантировал, что при записи мы аккуратно обновляем индекс (при каждом чтении или записи я обновляю индекс активности записи, чтобы можно было ограничивать размер кеша) через touchEntry:

  async touchEntry(entryKey) {
    await storage.touchIndex(entryKey, Date.now());
  }

Эти функции вместе обеспечили актуальность, ограничение и последовательность кеша.

Отдельной задачей оказался сброс кеша. В какой-то момент я попробовал схему с L1-кешем на каждом поде: каждый экземпляр приложения держал локальный горячий кеш, а для синхронизации инвалидации я использовал внешний сигнал через Redis. На практике это оказалось слишком сложным. Каждый под должен был отслеживать изменения во внешнем слое, вовремя понимать, что данные устарели, и отдельно сбрасывать свой локальный L1-кеш. Чем больше подов становилось в системе, тем менее удобной и предсказуемой выглядела такая схема.

В итоге мы отказались от идеи использовать Redis как основное или координирующее хранилище для всего кеша. Вместо этого Redis остался только небольшим быстрым L1-слоем для самых горячих записей - например, для последней тысячи наиболее посещаемых страниц. Всё остальное продолжило храниться в PVC как в основном общем хранилище. Такой подход позволил не раздувать потребление оперативной памяти, но при этом сохранить быстрый доступ к самой актуальной части кеша.

Кроме того, при подключении кастомного кеш-хендлера я добавил его в финальный runtime-образ на этапе сборки.

COPY --from=builder /workspace/custom-cache-handler.js ./custom-cache-handler.js

В next.config.ts я добавил конфигурацию, которая указывает путь к кастомному кеш-хендлеру через переменную окружения. Заодно я отключил встроенный in-memory-кеш, чтобы приложение не расходовало лишнюю память поверх внешнего хранилища.

  cacheHandler: process.env.HANDLER_PATH
    ? path.resolve(process.env.HANDLER_PATH)
    : undefined,
  cacheMaxMemorySize: 0

Так я подключил кастомный кеш‑хендлер к Next.js, отключил встроенный in‑memory‑кэш и получил предсказуемую схему кэширования, которая работает в многоподовом окружении с общим PVC.