Уровни кэширования: от L1 до CDN, и где не тот уровень кусается
Каждый уровень — от кэша CPU до CDN — меняет latency на расстояние и стоимость. Кэширование не на том уровне, двойное кэширование или кэш перед быстрым источником делают систему хуже, а не быстрее.
Команда ставит Redis перед запросом в Postgres, чтобы «ускорить». Latency становится хуже. Запрос был выборкой по primary key из тёплого buffer pool — Postgres отвечал за ~0.3 мс из RAM. Новый путь сериализует ключ, делает сетевой round trip в Redis (~1 мс в том же регионе), десериализует и только потом, может быть, попадает. Они обернули источник со скоростью RAM в кэш со скоростью сети. Кэш стал тем самым узким местом, которое должен был убрать.
За десять минут ты поймёшь, как читать числа latency, ловить несоответствие уровня до того, как оно уйдёт в прод, и запомнишь три вопроса, которые решают любое кэшинговое решение.
Иерархия — это лестница latency
Кэширование — это не одна вещь, а стек уровней, каждый из которых больше, дешевле и медленнее предыдущего. Стек существует ровно потому, что быстрая память маленькая и дорогая, а большая — медленная и дешёвая. Каждый уровень держит горячее подмножество уровня ниже, чтобы большинство обращений никогда не доходило до медленного слоя. Числа — это и есть вся суть: сеньор держит их в голове, потому что они решают, помогает ли кэш вообще.
Порядки величины важнее точных цифр. L1 примерно в 200 раз быстрее RAM; RAM примерно в 1000 раз быстрее SSD; SSD примерно в 100 раз быстрее сетевого хопа через континент. Добавляя уровень кэша, ты делаешь ставку, что фронтящийся слой медленнее кэша с большим запасом. Зафронтишь то, что и так быстрее, — и ты добавил latency, а не убрал.
| Уровень | Типичный доступ | Размер / охват | Кому принадлежит |
|---|---|---|---|
| L1 кэш CPU | ~1 нс | ~32–64 КБ / ядро | Железо |
| L2 кэш CPU | ~4 нс | ~256 КБ–1 МБ / ядро | Железо |
| L3 кэш CPU | ~10–40 нс | ~8–64 МБ / сокет | Железо |
| RAM | ~100 нс | ГБ / машина | ОС + твой процесс |
| Page cache ОС | ~100 нс (попадание в RAM) | свободная RAM | Ядро |
| Кэш приложения (Redis/memcached) | ~0.2–1 мс (в регионе) | ГБ–ТБ, общий | Ты |
| SSD | ~100 мкс | ТБ / машина | ОС |
| Reverse proxy / edge CDN | ~мс (edge), 100+ мс (далеко) | глобально, на POP | Ты / провайдер |
Для чего на самом деле каждый уровень
Аппаратные кэши (L1/L2/L3) и RAM — не твои в управлении: CPU и ОС крутят их автоматически. Но они всё равно формируют твой код: кэш-дружелюбный паттерн доступа (последовательный, с локальностью) может работать на порядок быстрее, чем погоня по указателям, потому что нужная cache line уже загружена. Поэтому плоский массив обходит связный список при итерации даже при одинаковом big-O.
Page cache ОС — самый недооценённый уровень. Когда ты читаешь файл, ядро держит его страницы в свободной RAM; второе чтение — это попадание в память, а не в диск. Поэтому твоя база быстра на горячих данных без всяких усилий с твоей стороны — buffer pool Postgres плюс page cache означают, что «дисковое» чтение обычно оказывается чтением из RAM. Сеньор знает это до того, как тянуться к Redis: если рабочий набор влезает в RAM, база уже является кэшем в RAM.
Кэши приложения (Redis, memcached) — первый уровень, которым ты управляешь сам. Это общее, доступное по сети хранилище в RAM: любой инстанс приложения может попасть в один и тот же кэш, чего локальная in-process память не умеет. Цена — сетевой round trip: обычно меньше миллисекунды в том же регионе, но 100+ мс, если клиент и кэш на разных континентах. Reverse proxy и CDN выталкивают закэшированные ответы ближе к пользователю, так что попадание вообще не доходит до твоего origin; вся ценность CDN географическая — отдать французскому пользователю из POP в Париже за миллисекунды вместо пересечения Атлантики.
▸Почему это работает
«Просто добавь Redis» — рефлекс, который пропускает главный вопрос: какова реальная latency источника? Если данные уже лежат в памяти твоего процесса, или в page cache ОС, или в тёплом buffer pool базы, источник работает со скоростью RAM, и сетевой кэш может только замедлить. Redis заслуживает место, когда источник по-настоящему дорогой — агрегат с кучей джойнов, внешний API, скан холодного диска, — а не когда он фронтит выборку по горячему ключу.
Hit ratio — единственное число, оправдывающее кэш
Прежде чем согласиться на «просто добавим кэш» — спроси: какой реальный hit ratio и что говорит математика? Ответ часто убивает идею на месте.
Кэш, который промахивается, платит обе цены: и lookup в кэше, и выборку из источника. Поэтому средняя latency кэшируемого пути — это hit_rate × cache_latency + miss_rate × (cache_latency + origin_latency). Посчитай — и низкий hit ratio строго хуже, чем без кэша, потому что на каждом промахе ты платишь налог кэша впустую. Поэтому «повысить hit ratio» не всегда цель: кэширование холодных, редко переиспользуемых данных может поднять твою общую latency, вытеснив по-настоящему горячие ключи ради одноразовых.
Сеньорская формулировка: кэш оправдан только когда origin_latency велика, hit ratio высокий, и данные терпят небольшую несвежесть. Промахнёшься хоть по одному из трёх — и ты добавляешь движущиеся части, новый домен отказа и проблему инвалидации в обмен на ничто. Когда аудитируешь кэш с hit ratio 40%, который все боятся убирать — ты нашёл самый дорогой его вид: тот, что стоит сложности, но почти ничего не зарабатывает.
Read-эндпоинт делает джойн по 3 таблицам за 80 мс, его зовут 50 раз/сек, а данные меняются пару раз в день. Куда ставить кэш?
Сеньорский провал: не тот уровень и двойное кэширование
Опасные баги — не промахи кэша, а кэши, которые врут. Двойное кэширование — классика: значение кэшируется в Redis и ответ кэшируется на reverse proxy/CDN, у каждого свой TTL и своя инвалидация. Запись чистит Redis, но CDN ещё 5 минут отдаёт старый HTML по своему TTL — или наоборот, CDN очистили, а proxy перед ним продолжает отдавать свою устаревшую копию. Получается stale-on-stale: два независимых уровня рассинхронизированы, а cf-cache-status в DevTools показывает тебе только один из них. Дебаг — это обход запроса слой за слоем (X-Cache, Cache-Status, логи proxy, попадания в Redis), чтобы найти, какая копия врёт.
Провал не того уровня — это Hook: фронтить быстрый источник медленным кэшем. Он же проявляется как кэширование на уровне, который не видит событие инвалидации, — кэширование персонализированных данных на CDN (ключ по URL, пользователей не различает, поэтому течёт данные одного пользователя другому) или кэширование на edge данных, меняющихся на каждый запрос. Правило, которое сеньор усваивает: кэшируй настолько близко к потребителю, насколько позволяет волатильность данных, и никогда не давай двум уровням кэшировать одно и то же с независимыми сроками жизни. Один владелец на каждый закэшированный факт, один путь инвалидации.
Ты оборачиваешь выборку Postgres по primary key (тёплая, ~0.3 мс из buffer pool) в Redis. Что будет с latency?
Уровень кэша с hit ratio 35% фронтит источник на 80 мс, lookup в кэше — 1 мс. По сравнению с «без кэша» средняя latency:
Расставь уровни кэша от самого быстрого доступа к самому медленному:
- 1 L1 кэш CPU (~1 нс)
- 2 L3 кэш CPU (~10–40 нс)
- 3 RAM / page cache ОС (~100 нс)
- 4 SSD (~100 мкс)
- 5 Кэш приложения по сети — Redis в регионе (~1 мс)
- 6 Edge CDN для далёкого пользователя (100+ мс)
- 01Объясни, почему добавление кэша может ухудшить latency, опираясь на числа access-latency.
- 02Что такое двойное кэширование и почему оно порождает баги stale-on-stale, которые трудно дебажить?
Кэширование — это лестница уровней: L1/L2/L3, RAM, page cache ОС, кэши приложения вроде Redis, reverse proxy и CDN, каждый больше, дешевле и медленнее предыдущего. Числа latency — это вся суть: примерно 1 нс на L1, 100 нс на RAM, 100 мкс на SSD, ~1 мс на round trip в Redis в регионе, 100+ мс на далёкий хоп CDN, с порядками величины между ступенями. Кэш окупается только когда фронтит по-настоящему более медленный источник, hit ratio высокий, и данные терпят несвежесть — потому что промах платит и lookup в кэше, и выборку источника, так что низкий hit ratio хуже, чем без кэша. Сеньорские провалы — кэширование не на том уровне (фронтить источник со скоростью RAM кэшем со скоростью сети или кэшировать персонализированные данные на CDN с ключом по URL) и двойное кэширование, где два уровня держат один факт с независимыми TTL и расходятся в баги stale-on-stale, всплывающие, только когда обходишь запрос слой за слоем. Теперь, когда услышишь «просто добавим кэш» — сначала измеришь реальную latency источника, проверишь три ворот и дашь каждому закэшированному факту одного владельца и один путь инвалидации.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.