Русский
Кеш страниц и неожиданное узкое место
Профилировщик показал: обычная страница документации в 30 раз дороже кастомного лендинга. Мы думали наоборот.
Под нагрузкой стояли два варианта. Jet-главная: кастомный лейаут с mesh-сеткой, фирменным дизайном, ручной вёрсткой. И страница документации в default-темплейте, которую получаешь без всякой настройки. Казалось бы: дорогой рендер у того, что сложнее. Профилировщик сказал: docs-страница — ~28 мс/запрос, Jet-главная — ~0,9 мс/запрос. В тридцать раз медленнее. И всё это в одном месте: синхронный поиск по косинусному сходству по всем эмбеддингам заметок на каждый запрос, подписанный «похожие заметки».
Дорогое место было невидимым
На каждый запрос trip2g загружает страницу, собирает лейаут и до отправки ответа перебирает все векторы заметок, считает косинусное сходство для каждого и отбирает топ-совпадения как «похожие материалы». Без кеша, на каждый запрос, для каждого читателя. На небольшом хранилище (сотни заметок) — ~28 мс чистых вычислений на пути чтения, около 94% CPU-бюджета одного запроса.
У Jet-главной ничего этого не было. Парсинг шаблона уже кеширован, страница собирается быстро. Визуально сложнее — вычислительно дешевле больше чем на порядок.
Вывод: мерить, а не предполагать. Узкое место редко там, где кажется.
Что делает кеш анонимных страниц
Можно было ускорить поиск. Эффективнее оказалось не запускать его на каждый запрос. Кеш анонимных страниц хранит готовый gzip-ованный HTML-ответ по ключу (путь, хост, версия заметки, конфиг, язык). При попадании в кеш сервер копирует готовые байты из памяти: ни косинусный поиск, ни шаблонизатор, ни gzip не запускаются.
Версионирование держит всё корректным: при обновлении заметки её запись в кеше инвалидируется. Первый запрос после изменения контента платит 28 мс и кеширует результат. Все последующие читатели не тратят на это ни миллисекунды.
Измерено на 12-ядерной dev-машине, одна страница под непрерывной нагрузкой: docs-страница выдавала 421 зап/с до кеша и 8 504 зап/с после прогрева — в 20 раз. CPU на попаданиях в кеш: около 0%.
Jet-главная и так была быстрой. После кеша — тоже около 9 000 зап/с. Кеш уравнивает дорогие и дешёвые страницы на одном потолке пропускной способности. Этот потолок оказался сетью и системными вызовами, а не прикладной логикой.
Почему кеш только для анонимных читателей. Авторизованный пользователь может быть администратором (видит элементы управления редактирования), подписчиком (у него другой доступ к материалам) или иметь персональный контекст, который отличает его страницу от анонимной. Отдать такому читателю кешированную анонимную версию — значит рискнуть показать не то.
Здесь есть нюанс, специфичный для default-темплейта: разница между видом анонима и авторизованного (кнопка входа, значок редактирования) рендерится клиентским скриптом в браузере. HTML с сервера для обоих одинаков. Технически кеш можно было бы распространить на авторизованных пользователей в default-темплейте без риска. Но не для кастомных лейаутов и не для заметок за пейволлом. Текущий дизайн выбирает простое и надёжное решение: любой авторизованный запрос обходит кеш.
Как мерили: три ловушки
Бенчмарк запущен 2026-06-29 на двух реальных Hetzner shared-vCPU VM:
- cx23 (2 vCPU / 3,8 ГБ, €6,49/мес) — тестируемый сервер
- cx33 (4 vCPU / 7,7 ГБ, €8,99/мес) — генератор нагрузки
Атака с cx33 по сети, разные машины. vegeta на неограниченном темпе: 12 секунд (всплеск), потом 10 минут (устойчивая нагрузка).
Результат: cx23 выдал около 9 000 зап/с во всплеске и 8 557 зап/с в среднем за 10 минут. Ноль ошибок. CPU steal — 0% весь прогон.
Три альтернативных стенда, каждый со своей ловушкой.
Ловушка 1: атака с той же машины. Запустили vegeta на том же cx23. Получили около 5 400 зап/с. Генератор делил два ядра с сервером. CPU сервера не упирался в 100%; упирался атакующий. Мы мерили потолок генератора, а не сервера. Межмашинная цифра — реальный потолок.
Ловушка 2: слабый атакующий, сильный сервер. Trip2g на cx33, атака с cx23. CPU cx33 — 43–45%. Cx23 не смог насытить cx33 нагрузкой. Ограничение атакующего, не измерение сервера.
Ловушка 3: steal как индикатор дросселирования. Shared vCPU подвержен fair-use-ограничениям: провайдер может забрать процессорное время при перегрузке физического хоста. Сигнал — метрика steal. За весь 10-минутный прогон steal держался на 0%. Throttling не было. Полученные 8 557 зап/с — то, что сервер реально выдал.
Почему важен именно 10-минутный прогон: провайдер может пропускать короткие всплески без ограничений и дросселировать длительную нагрузку. Двенадцать секунд с нулевым steal — недостаточная проверка. Прошло. Но оговорка остаётся: максимальная нагрузка 24/7 на shared vCPU в конечном счёте может задействовать fair-use-ограничения. Для такого режима подходит dedicated-vCPU.
Что дальше
Кеш анонимных страниц скрывает косинусный поиск для попаданий в кеш, но не устраняет его. Два случая по-прежнему платят полную цену:
- Cache miss: первый запрос после изменения контента.
- Авторизованные пользователи: обходят кеш.
Следующий шаг — структурный: вычислять «похожие заметки» один раз при перезагрузке заметки, хранить рядом с отрендеренным HTML и отдавать из памяти при каждом чтении. O(N)-поиск уходит с пути чтения и заменяется одним обращением к памяти. Когда хранилища вырастут до размеров, при которых и разовое сканирование станет медленным, следующий шаг — ANN-индекс.
Бенчмарк показал, где искать. До измерения мы считали, что дорогой кейс — кастомный Jet-лейаут. Оказалось наоборот. Это открытие указало на нужную оптимизацию, после которой кеш для большей части скрытых им вычислений станет ненужным.