Скачать CV

Frontend

Слабы Memcached и SSR-страницы разного размера

Процесс Vue SSR кэшировал целый HTML в Memcached. Большие страницы статей заняли память толстыми слабами. Мелким фрагментам плиток негде было жить, hit rate падал при высоком RSS.

Vue SSR на этом сайте это процесс Node за nginx, не SPA. Я кэшировал готовый HTML в Memcached, ключ из локали и пути, чтобы на следующем запросе не трогать Node. Вечером это работало. К утру RSS стоял на 64 МБ при инстансе 64 МБ, hit rate был 31 процент.

Посетители всё равно ждали Node на сетке заметок. Хост выглядел забитым. Утечки в Memcached не было. RAM заперлась в толстых классах слабов. Как только туда попала страница статьи на 80 КБ, этот кусок RAM принимал только объекты того же класса. Фрагмент плитки на 3 КБ туда не вставал, даже если страницу на 80 КБ больше никто не читал, пока этот объект не вытеснят.

У Memcached нет кучи в смысле Redis. Память нарезана на классы слабов. Каждый класс держит объекты узкого диапазона размера. Смешать HTML статей и мелкие фрагменты в одном инстансе это как hit rate падает, а RSS стоит у потолка.

Толстые слабы держат HTML статей, мелкие ключи не входят
Толстые слабы держат HTML статей, мелкие ключи не входят

Что мы реально клали

Сетка заметок использует пять размеров плиток: sm, md, lg, tall, wide. SSR-оболочка списка занимала около 18 КБ. Страница заметки с длинным телом занимала от 70 до 95 КБ, в зависимости от локали. Немецкий чуть длиннее английского. Русский посередине.

Я снял stats slabs и stats items. Класс около 82 КБ держал большую часть RAM. Классы ниже 8 КБ были почти пустые. Вытеснения шли в мелких классах, не в толстом. В этом ловушка. Память как будто занята. Ключи, которые нужны на каждом запросе списка, приземлиться не могут.

Коэффициент роста был дефолтный 1.25. Для значений похожего размера это нормально. Плохо, когда в одной куче лежат JSON-фрагмент, оболочка списка и целая статья. Страницы статей ещё тащили шапку, подвал и вшитые i18n-сообщения оболочки. Одна лишняя строка локали в bootstrap-состоянии хватала, чтобы страница перескочила в следующий класс слаба.

Два пула, потом ни одного

Сначала разнесём на два инстанса Memcached в Docker: memcached-html на 48 МБ для страниц больше 16 КБ, memcached-frag на 16 МБ для остального. Hit rate фрагментов вернулся. RSS у HTML-инстанса всё равно стоял у потолка: страницы статей читают один раз, и они висят, пока LRU не заметит.

Страницы уходят в микрокэш nginx, фрагменты остаются в Memcached
Страницы уходят в микрокэш nginx, фрагменты остаются в Memcached

Правильный ход был перестать кэшировать целый SSR HTML в Memcached. У nginx уже есть микрокэш в shared memory для GET. Я поставил 10 секунд на публичные пути заметок и новостей. Это закрывает пачку запросов после деплоя и не прибивает строки по 90 КБ в слабы на час.

В Memcached осталось мелкое и скучное специально:

  • отрендеренные каталоги локалей для оболочки, примерно по 4 КБ
  • упакованный payload плиток сетки заметок, один ключ на локаль, меньше 6 КБ
  • флаг, что процесс Node тёплый, чтобы nginx не ретраил мёртвый upstream на каждый miss

HTML статьи рендерится снова, когда истекает микрокэш. CPU Node чуть вырос. RAM Memcached упал примерно вдвое, hit rate фрагментов ушёл за 90 процентов. На машине, где ещё крутятся Redis, MySQL и php-fpm, этот обмен правильный.

Куча Node это другая история

Когда слабы успокоились, процесс Vue всё равно рос. Это был не Memcached. Рендерер SSR держал кэш компонентов и дерево скомпилированных i18n-сообщений на локаль запроса. Три локали на тёплом рендерере это нормально. Утечка появляется, если запрос ставит локаль, а дерево кладётся в глобальную map с новым ключом каждый раз, например en-US вместо en.

Я зафиксировал локаль как en, ru, de на слое nginx, тем же префиксом, что и API Laravel. В map у Node теперь три записи. Куча после нескольких тысяч запросов достаточно ровная, чтобы еженедельный рестарт в Docker был привычкой, а не лечением.

Если страничный кэш всё же нужен, кладите его в Redis сжатой строкой с TTL 45 секунд, рядом с кэшем API. Redis вытесняет LRU по всем размерам. Memcached так не умеет.

Какой опыт из этого

Меряю stats slabs и hit rate фрагментов, не RSS отдельно. Полный инстанс с вытеснениями в мелких классах это смесь размеров, не недостающий кэш.

Одному пулу Memcached для HTML на 90 КБ и плиток на 3 КБ я не верю. Микрокэш nginx на 10 секунд для публичного GET это хранилище страниц. Memcached держит каталоги, payload плиток и флаг прогрева меньше 6 КБ.

Правило, которое оставляю: локаль фиксировать как en, ru, de на nginx до Node. Если нужен кэш страницы, gzip в Redis. HTML статей в слабы не прибивать.

К заметкам