non-functionalcachingreliabilityperformanceqa

«У меня показывает старое» — как тестировать кэши, самый тихий источник багов

Знакомый тикет: «У пользователя отображаются старые данные». Идёшь проверять — у тебя всё свежее. Пишешь «не воспроизводится», юзер отвечает «уже само прошло». Закрыли. Через неделю тот же тикет от другого юзера. Потом ещё один. И вот тут стоит остановиться, потому что «само прошло» — это почти всегда не мистика и не «глюк». Это кэш: данные обновились, а какая-то из копий — нет, и умерла она ровно по своему TTL, поэтому «само прошло».

Есть классическая шутка, что в компьютерных науках есть только две по-настоящему сложные вещи: инвалидация кэша и придумывание имён. Смешно, потому что правда. Но при этом кэш почти никогда не тестируют осознанно — он просто «есть где-то там», пока не начнёт показывать старое. Расскажу, как я тестирую кэши и почему считаю их самым тихим источником багов.

Почему кэш-баги особые

Обычный баг воспроизводится шагами. Кэш-баг воспроизводится шагами и временем: результат зависит от того, когда ты смотришь и кто дёргал этот эндпоинт до тебя. Первый запрос прогревает кэш, второй читает из него — это два разных пути в коде, и баг может жить только в одном из них. Плюс на чистом стенде, куда кроме тебя никто не ходит, половина кэш-эффектов просто не проявляется.

Отсюда главная причина, почему такие баги закрывают как «не воспроизводится»: тестировщик приходит позже TTL, кэш уже умер, всё свежее. Юзер был раньше — и видел старое.

Сначала карта: где вообще живёт кэш

Тестировать кэш вслепую нельзя — надо знать, где он есть. Слоёв обычно больше, чем кажется:

  • браузер — HTTP-кэш по заголовкам Cache-Control/ETag;
  • CDN / edge — Cloudflare, CloudFront и компания, кэшируют ответы близко к юзеру;
  • API-gateway / реверс-прокси — nginx, Varnish, кэш на уровне маршрута;
  • приложение — in-memory или Redis/Memcached: результаты запросов, сессии, фиче-флаги;
  • БД — кэш запросов, материализованные вьюхи;
  • мобильный клиент — свой слой поверх всего: HTTP-кэш SDK, кэш картинок, локальная база, синхронизируемая «когда-нибудь».

Тикет «показывает старое» может жить на любом из шести. Поэтому первый вопрос при тестировании фичи — не «есть ли кэш», а «на каких слоях и с каким TTL». Если команда не может ответить — это уже находка.

Инвалидация: главный кейс

Кэш «на чтение» тестировать скучно — он либо работает, либо нет. Всё интересное начинается на изменении данных: поменял → где обновилось, а где осталась старая копия?

Самый багоносный сценарий — изменение через один канал, чтение через другой. Обновили цену через админку — а мобильное приложение читает через API с кэшем, который про админку не знает. Юзер отредактировал профиль на сайте — приложение показывает старое имя ещё час. Такие связки надо выписывать явно: все пути записи × все пути чтения, и для каждой пары спросить «кто инвалидирует кэш и как».

И отдельно — граница TTL: что видит юзер в момент, когда кэш истёк, а новый ещё не прогрелся? Ошибку? Медленный ответ? Старые данные ещё раз?

Кэш и чужие данные — самый дорогой класс багов

Функциональные кэш-баги неприятны, но есть класс хуже: кэш отдаёт одному юзеру данные другого. Происходит банально: ответ закэшировали без учёта того, кому он принадлежит — ключ кэша не содержит user id, или CDN закэшировал персонализированный ответ как общий, или забыли заголовок Vary и юзеру с другим языком/валютой/правами прилетает чужая версия страницы.

Что я проверяю всегда: залогинься двумя разными юзерами с разными правами и данными — и посмотри, не «просачивается» ли ответ первого второму. И обратный кейс: разлогинился / у юзера отобрали права — а закэшированные данные ещё живы и доступны? Кэш, который переживает logout и смену прав, — это не производительность, это уже безопасность.

Cache stampede: кэш как нагрузочный кейс

Пока кэш тёплый, он скрывает реальную стоимость запросов. А теперь деплой — и кэш сбросился. Все запросы, которые месяцами обслуживались из кэша, одновременно полетели в БД. Если БД к этому не готова — прод складывается ровно в момент релиза, и в логах это выглядит как «после деплоя всё умерло», хотя код не виноват.

Это называется cache stampede, и это тестируемый сценарий: как система ведёт себя с холодным кэшем под обычной нагрузкой? Прогоните нагрузочный тест дважды — с прогретым кэшем и со сброшенным — и сравните. Разница и есть ваш риск на каждый деплой. Туда же смежный вопрос: если источник данных упал, кэш отдаёт stale-копию или ошибку? Паттерн stale-while-revalidate описан ещё в RFC 5861 — но работает он только если его поведение кто-то проверил.

Кэш и релизы

Кэш живёт дольше, чем код. После деплоя в кэше лежат объекты, сериализованные старой версией: если формат поменялся — новый код читает старый кэш и падает (или тихо отдаёт кривые данные, что хуже). Обратная сторона: клиент с закэшированным старым бандлом ходит в новый API. Вопросы к каждому релизу: что из кэша переживёт деплой, умеет ли новый код это читать, и надо ли чистить кэш руками (и написано ли это в чек-листе релиза).

Чем смотреть

Инструментов немного, и все простые. Для HTTP-слоёв — DevTools (вкладка Network: откуда пришёл ответ, from disk cache / from memory cache), curl с заголовками и Proxyman/Charles для мобильного клиента: видно Cache-Control, ETag, Age и заголовки CDN типа cf-cache-status: HIT/MISS — по ним понятно, какой слой ответил. Для серверных кэшей — Redis CLI: посмотреть ключ, его TTL (TTL key), удалить и проверить, что система переживает промах. Если в ключах нет user id у персонализированных данных — вот и баг из раздела про чужие данные, найденный за минуту.

Антипаттерны

Тестировать с выключенным кэшем, «чтобы не мешал». Понимаю желание — с кэшем результаты нестабильные. Но прод работает с кэшем, значит, стенд без кэша тестирует систему, которой не существует. Кэш — не помеха тестированию, а его объект.

Чинить «почисти кэш» без вопроса «почему». Если баг лечится очисткой кэша — это не решение, это диагноз: инвалидация не работает. У следующего юзера тот же баг, а кэш ему никто не почистит.

Стенд без кэша при проде с кэшем. Разновидность первого, но на уровне инфраструктуры: на стейдже нет CDN/Redis «для простоты» — и весь класс кэш-багов уезжает в прод непроверенным.

Чек-лист тестирования кэша

  • Карта слоёв: где кэш, какой TTL, кто инвалидирует
  • Каждый кейс дважды: холодный кэш и тёплый — это разные пути кода
  • Изменение через каждый канал записи → проверка через каждый канал чтения
  • Два юзера с разными правами/данными: ничего не просачивается
  • Logout и смена прав: закэшированное не переживает их
  • Vary/ключи кэша учитывают язык, валюту, платформу, юзера
  • Холодный кэш под нагрузкой: деплой не кладёт БД
  • Источник упал: stale или ошибка — поведение осознанное, не случайное
  • Релиз: старый кэш читается новым кодом, очистка в чек-листе релиза
  • «Не воспроизводится» + «само прошло» = проверь TTL, прежде чем закрывать

Кэш — это осознанное решение показывать юзеру не совсем свежие данные в обмен на скорость. Решение нормальное. Ненормально — когда никто не может сказать, насколько несвежие, кому и что будет, если копия и оригинал разойдутся. Вот это и есть работа тестировщика: сделать компромисс кэша видимым и проверенным, а не случайным.