3.15 Кэширование и доставка контента
Обзор и мотивация
Кэш это копия данных, хранимая где-то быстрее или ближе оригинала, чтобы отвечать на запрос, не выполняя снова полную дорогую работу. Почти каждая система, кажущаяся быстрой, быстра благодаря кэшированию. Запрос к базе данных, который занял бы 40 миллисекунд, возвращается быстрее одной, когда его результат уже лежит в памяти. Изображение, которое пересекло бы океан, отдаётся с машины в том же городе. Кэширование самая рычажная техника повышения производительности, какая у вас есть, и оно же чаще всего подкидывает тонкую, сводящую с ума ошибку.
Эта глава глубоко разбирает стратегию кэширования. Глава 3.4 (архитектура данных и хранение) вводит кэши и сети доставки содержимого как одну из многих забот хранения, а глава 3.13 (сети и связность) охватывает сетевой путь, по которому они едут. Здесь вы получаете решения: где разместить кэш, как задавать ключи, когда инвалидировать, как защищать под нагрузкой и как рассуждать об устаревании, которое вы меняете на скорость. Кэширование касается инженерии производительности (глава 2.16), масштабируемости и устойчивости (глава 3.5), реалий частичных отказов распределённых систем (глава 3.3) и, поскольку отравленный кэш может подать атаку тысячам пользователей, безопасности приложений (глава 4.2).
Мотивация сводится к трём рычагам. Кэширование сокращает задержку, и пользователи ждут меньше. Оно сокращает нагрузку, и ваш исходный сервер обслуживает больше трафика на том же оборудовании. И оно сокращает стоимость, потому что запрос, отвеченный на краю, никогда не касается вашей базы данных, вычислений или счёта за исходящий трафик. Для больших команд общая стратегия кэширования это разница между платформой, предсказуемо масштабирующейся, и той, где каждый сервис изобретает инвалидацию заново и делает её неверно. В корпоративных и государственных системах, где трафик всплескивает на сроках подачи деклараций и в дни запуска, хорошо спроектированный кэш нередко то, что стоит между работающим порталом и публичным провалом.
Ключевые принципы
- Кэшируйте, чтобы сократить задержку, нагрузку и стоимость, и знайте, что именно вы покупаете.
- Размещайте кэши на правильном уровне иерархии, ближе всего к тому, где они помогают больше всего.
- Воспринимайте инвалидацию как трудную часть; проектируйте ключи и сроки жизни до кэширования.
- Защищайте кэш под нагрузкой слиянием запросов, джиттером и защитой от лавины.
- Выбирайте шаблон записи намеренно: согласованность и скорость тянут в разные стороны.
- Измеряйте долю попаданий, устаревание и нагрузку на исходный сервер; неизмеряемый кэш это обязательство.
- Воспринимайте кэшированное содержимое как поверхность атаки; отравленный кэш обслуживает всех.
Рекомендации
Понимайте иерархию кэшей
Кэширование не одна вещь в одном месте. Это иерархия копий, каждая ближе к пользователю, чем предыдущая, и вы проектируете сразу по всей иерархии. Ближе всего к пользователю кэш клиента: HTTP-кэш браузера, локальное хранилище мобильного приложения, кэш в памяти процесса. Далее сеть доставки содержимого (CDN), парк серверов, распределённых по миру и хранящих копии вашего содержимого на краю сети, рядом с пользователями. За ней стоит кэш обратного прокси или шлюза, общий кэш перед вашими серверами. Затем кэш приложения: быстрое хранилище «ключ-значение», например сетка данных в памяти, хранящая вычисленные результаты, сессии и отрисованные фрагменты. Наконец, собственный кэш запросов и буферов базы данных, держащий горячие страницы в памяти, чтобы реже касаться диска.
Каждый уровень выполняет свою работу: кэш клиента полностью устраняет запрос, CDN поглощает глобальный трафик чтения, обратный прокси защищает ваш исходный сервер от повторяющейся одинаковой работы, кэш приложения экономит пересчёт, а кэш базы данных сохраняет отзывчивость хранилища. Запрос, промахнувшийся мимо каждого уровня и дошедший до базы данных, самый медленный и дорогой путь, какой у вас есть, поэтому смысл иерархии в том, чтобы отвечать как можно выше и дальше наружу, насколько безопасно. Проектируйте её как систему, потому что кэшированный фрагмент на уровне приложения и устаревшая копия CDN над ним могут расходиться способами, сбивающими пользователей с толку.
Воспринимайте инвалидацию как трудную проблему
Есть старая шутка, что две труднейшие проблемы информатики это именование вещей, инвалидация кэша и ошибки на единицу. Шутка живёт, потому что инвалидация действительно трудна: кэш это копия, и в миг, когда оригинал меняется, каждая копия потенциальная ложь. У вас три широкие стратегии. Истечение по времени с временем жизни (TTL), длительностью, в течение которой запись остаётся действительной, пока не считается устаревшей, проще всего: вы принимаете ограниченное устаревание и даёте записям состариться. Явная инвалидация очищает или обновляет записи при изменении исходных данных, что точно, но требует знать каждое место, где живёт копия. Событийно-ориентированная инвалидация подписывает кэши на события изменений, чтобы они обновлялись сами, что лучше масштабируется на много кэшей, но добавляет зависимость от обмена сообщениями.
Большинство реальных систем смешивают их: короткие TTL для часто меняющихся данных, терпящих секунды устаревания, более длинные TTL плюс явная очистка для данных, меняющихся редко, но обязанных быть верными, когда меняются, и версионированные ключи для содержимого, неизменяемого после публикации. Приём с версионированным ключом стоит усвоить: вместо инвалидации вы меняете ключ. Таблица стилей, отдаваемая как app.v187.css, никогда не требует очистки, потому что новая версия это новый ключ, а старый просто перестаёт запрашиваться. Всякий раз, когда можете превратить проблему инвалидации в проблему именования, делайте это.
Проектируйте ключи кэша и TTL намеренно
Кэш хорош настолько, насколько хорош его ключ. Ключ кэша это идентификатор, под которым значение хранится и ищется, и ошибка в нём вызывает два противоположных отказа. Слишком грубый, и вы отдаёте данные одного пользователя другому: персонализированная страница, кэшированная под URL, игнорирующим идентичность пользователя, утечка данных. Слишком тонкий, и доля попаданий рушится, потому что никакие два запроса не делят ключ. Решите намеренно, что входит в ключ: идентичность ресурса плюс всё, что законно меняет ответ (язык, валюта, класс устройства) и ничто сверх. Нормализуйте ключи, чтобы тривиальные различия вроде порядка параметров запроса не дробили кэш.
TTL заслуживают такого же обдумывания. TTL это обещание о максимальном устаревании, которое вы отдадите, поэтому задавайте его по реальной терпимости данных, а не по круглому числу, которое кто-то угадал: биржевой тикер терпит секунды, каталог товаров минуты, опубликованное постановление часы, или версионированный ключ вовсе без истечения. Добавьте небольшой случайный разброс, называемый джиттером, чтобы партия записей, записанных вместе, не истекла в один миг и не обрушилась лавиной на исходный сервер. Записывайте эти выборы, потому что TTL без обоснования это число, которое следующий инженер побоится менять.
Защищайтесь от лавины и сливайте запросы
Когда популярная кэшированная запись истекает, каждый запрос, который её хотел, одновременно промахивается и бросается на исходный сервер вместе. Это лавина кэша (cache stampede), также называемая «громовым стадом», и она может обрушить ту самую базу данных, которую кэш защищал. Постройте защиту один раз и используйте её везде. Слияние запросов (single-flight) позволяет только первому запросу на отсутствующий ключ пересчитать значение, пока остальные ждут его результата, так что тысяча одновременных промахов вызывает один вызов исходного сервера. Вероятностный ранний пересчёт случайно обновляет горячую запись чуть раньше её истечения, так что один фоновый запрос обновляет её до того, как толпа увидит промах. Политика «устаревшее, пока обновляется» (stale-while-revalidate) немедленно отдаёт слегка устаревшую копию и обновляет её асинхронно, так что пользователи вообще не ждут на промахе.
Эти шаблоны важнее всего именно тогда, когда кэш нужен больше всего, при пиковой нагрузке, поэтому проверяйте их в реалистичном масштабе: защита, работающая для десяти пользователей, всё ещё может отказать на десяти тысячах. Сочетайте их с шаблонами устойчивости главы 3.5, особенно тайм-аутами и предохранителями, чтобы при подлинной медленности исходного сервера ваш слой кэша защищал его, а не нагромождал нагрузку. Цель кэш, ведущий себя лучше всего под давлением, а не усиливающий всплеск до отказа.
Выбирайте шаблон записи намеренно
То, как вы обрабатываете записи, определяет свежесть кэша и риск при отказе. Есть четыре распространённых шаблона. В cache-aside (ленивая загрузка) приложение проверяет кэш, а при промахе читает исходный сервер, заполняет кэш и возвращает значение; записи идут на исходный сервер и инвалидируют запись. Это вариант по умолчанию не зря: просто, а кэш хранит только запрошенное. В сквозной записи (write-through) каждая запись идёт в кэш и на исходный сервер вместе, так что кэш всегда актуален ценой задержки записи и кэширования данных, которые, возможно, никогда не прочтут. В отложенной записи (write-back, write-behind) записи сначала попадают в кэш и сбрасываются на исходный сервер асинхронно, что ускоряет записи, но рискует потерей, если кэш умрёт до сброса. В записи в обход (write-around) записи идут прямо на исходный сервер и пропускают кэш, избегая оттока от часто записываемых, но редко читаемых данных ценой гарантированного промаха при первом чтении.
Выбирайте по нагрузке, а не раз для всей системы. Каталог с преобладанием чтения подходит для cache-aside или сквозной записи. Журнал или поток метрик с преобладанием записи подходит для записи в обход, чтобы кэш не перемалывался данными, которые никто не перечитывает. Отложенная запись подходит для записи высокой пропускной способности, где приемлем небольшой понятный риск потери, а долговечность обеспечена в другом месте. Называйте шаблон для каждого кэша явно, потому что читатель, предполагающий cache-aside, когда код делает отложенную запись, неверно оценит и свежесть, и поведение при отказе.
Подбирайте политику вытеснения под шаблон доступа
У кэша фиксированный размер, поэтому, когда он заполняется, что-то должно уйти. Политика вытеснения решает, что именно. Наименее недавно использованное (LRU) вытесняет запись, нетронутую дольше всех, ставя на то, что недавнее использование предсказывает будущее, и это разумное значение по умолчанию. Наименее часто используемое (LFU) вытесняет запись с наименьшим числом попаданий, что подходит для стабильных горячих наборов, где несколько элементов вечно популярны, но может цепляться за записи, бывшие горячими когда-то, и не адаптируется. Варианты вроде сегментированного LRU и адаптивных политик смешивают недавность и частоту; «первым вошёл, первым вышел» и простое истечение по времени дешевле, но грубее.
Подбирайте политику под то, как обращаются к данным: LFU или политика с учётом частоты для небольшого горячего набора, редко смещающегося, LRU там, где популярность движется со временем, как у новостей или трендового содержимого. Что бы ни выбрали, подбирайте размер кэша так, чтобы горячий набор помещался, потому что кэш, слишком малый для рабочего набора, пробуксовывает, вытесняя записи прямо перед тем, как они снова понадобятся. Следите за долей вытеснений как за первоклассной метрикой, поскольку резкий рост обычно означает, что кэш недоразмерен или взрыв ключей его дробит.
Правильно используйте семантику HTTP-кэширования
В HTTP встроена зрелая стандартизированная модель кэширования, и её грамотное использование даёт вам кэширование клиента и CDN даром. Заголовок Cache-Control пульт управления: max-age задаёт срок свежести, public и private говорят, могут ли общие кэши хранить ответ, no-store запрещает кэширование, а stale-while-revalidate разрешает отдавать устаревшую копию во время обновления. Валидация позволяет кэшу дёшево проверить свежесть без повторной загрузки тела. ETag (метка сущности) это непрозрачный идентификатор версии, прикрепляемый сервером к ответу; клиент отправляет его обратно в заголовке If-None-Match, а сервер отвечает 304 Not Modified без тела, если ничего не изменилось. Last-Modified с If-Modified-Since делает то же самое по временным меткам.
Практическая дисциплина в явности. Задавайте Cache-Control в каждом ответе, а не оставляйте кэшам гадать эвристиками. Помечайте частные ответы на пользователя как private или no-store, чтобы общий прокси никогда не хранил их, частая и опасная ошибка. Используйте версионированные URL с длинным max-age и директивой immutable для статических ресурсов, а валидацию с ETag для содержимого, меняющегося непредсказуемо. Правильные заголовки превращают весь уровень клиента и CDN в корректный стандартный кэш, который вам не пришлось строить.
Переносите работу на край с помощью CDN и пограничных вычислений
CDN начинался как способ кэшировать статические файлы рядом с пользователями и всё ещё делает это превосходно: изображения, скрипты, видео и загрузки, отдаваемые из пограничной точки в миллисекундах, а не из далёкого исходного сервера. Современные CDN идут дальше. Они кэшируют динамическое и персонализированное содержимое с тонкими ключами, завершают TLS на краю, поглощают всплески трафика и распределённые атаки отказа в обслуживании и всё чаще запускают ваш код. Пограничные вычисления выполняют логику в самих пограничных точках, так что вы можете персонализировать ответ, проверить авторизацию или собрать фрагмент страницы без кругового обхода до центрального региона.
Опирайтесь на это для чтений, преобладающих в большинстве систем. Ставьте статические ресурсы за CDN с долгоживущими версионированными URL, кэшируйте ответы API на краю там, где свежесть позволяет (с аккуратным ключом, чтобы персонализация не утекала), и используйте пограничные вычисления для чувствительной к задержке лёгкой логики рядом с пользователями. Размен это охват против контроля: край быстр и близок, но далёк от ваших данных и труднее отлаживается, поэтому держите всё, требующее сильной согласованности или свежего авторитетного состояния, на исходном сервере и позволяйте краю обрабатывать огромный кэшируемый трафик чтения.
Воспринимайте кэш как поверхность атаки
Кэш отдаёт один и тот же сохранённый ответ многим пользователям, что делает его мишенью. Отравление кэша это атака, где запрос составлен так, что кэш сохраняет вредоносный или контролируемый атакующим ответ и затем отдаёт его всем последующим. Обычно она эксплуатирует незаключённый в ключ ввод: заголовок, который приложение отражает в ответ, но кэш игнорирует при построении ключа. Родственная атака обмана веб-кэша обманом заставляет кэш сохранить частный ответ жертвы под публичным URL. Обе это отказы формирования ключа и доверия вводу, рассматриваемые шире в главе 4.2.
Защищайтесь намеренно. Включайте в ключ кэша каждый ввод, способный менять ответ, и отказывайтесь отражать не включённые в ключ заголовки в кэшируемые тела. Никогда не позволяйте общему кэшу хранить аутентифицированные ответы на пользователя под общим ключом. Нормализуйте и проверяйте пути и параметры запросов перед кэшированием. Правильно задавайте Vary, чтобы кэши разделяли ответы по заголовкам, которые действительно важны, таким как кодирование содержимого или язык. Поскольку одна отравленная запись вредит каждому нижестоящему пользователю, воспринимайте конфигурацию кэша как чувствительный для безопасности код и рецензируйте её соответственно.
Делайте поведение кэша наблюдаемым
Нельзя управлять кэшем, которого не видишь. Главная метрика доля попаданий: доля запросов, обслуженных из кэша, а не с исходного сервера. Доля попаданий, тихо упавшая с 95 до 70 процентов, может во много раз умножить нагрузку на исходный сервер и предвещать отказ, а вы поймаете это рано, только если смотрите. Инструментируйте каждый уровень отдельно, поскольку здоровая доля попаданий CDN может скрывать рушащуюся долю попаданий кэша приложения под ним. Это кэш-специфичное лицо практик наблюдаемости из главы 9.2.
Отслеживайте больше, чем попадания: долю вытеснений и давление на память, чтобы поймать недоразмеренность, задержку на каждом уровне, чтобы подтвердить, что кэш действительно быстрее, частоту запросов к исходному серверу, чтобы видеть, сколько нагрузки кэш поглощает, и устаревание (насколько стары отдаваемые записи), чтобы подтвердить соблюдение обещаний свежести. Оповещайте по соотношениям, предсказывающим беду, особенно падающей доле попаданий или растущей доле вытеснений, чтобы узнавать о деградирующем кэше с панели, а не от пользователей. Наблюдаемый кэш актив, который можно настраивать; ненаблюдаемый скрытая зависимость, ждущая момента удивить.
Компромиссы: плюсы и минусы
Кэширование покупает скорость и масштаб валютой свежести и сложности. Каждый кэш ставка на то, что устаревшее, но быстрое лучше свежего, но медленного для этих конкретных данных, и искусство в том, чтобы делать эту ставку осознанно, а не по умолчанию. Таблица ниже резюмирует основные выборы.
| Выбор | Плюсы | Минусы |
|---|---|---|
| Cache-aside | Просто; кэширует только читаемое | Первое чтение всегда промахивается; риск краткого устаревания после записей |
| Сквозная запись | Кэш всегда актуален при записи | Медленнее записи; кэширует данные, которые, возможно, никогда не прочтут |
| Отложенная запись | Очень быстрые записи; поглощает всплески | Риск потери данных, если кэш откажет до сброса |
| Запись в обход | Избегает перемалывания кэша часто записываемыми данными | Гарантированный промах при первом чтении |
| Короткий TTL | Ограниченное малое устаревание | Ниже доля попаданий; больше нагрузки на исходный сервер |
| Длинный TTL / версионированные ключи | Высокая доля попаданий; малая нагрузка на исходный сервер | Устаревание без инвалидации; нужны дисциплинированные ключи |
| CDN и край | Глобально низкая задержка; поглощает всплески | Далеко от данных; труднее отлаживать и инвалидировать |
| Вытеснение LRU | Адаптируется к смещающейся популярности | Может вытеснить стабильный горячий набор при нагрузке сканированием |
| Вытеснение LFU | Защищает стабильный горячий набор | Медленно адаптируется; цепляется за прежде горячие записи |
Повторяющееся напряжение это согласованность против производительности. Кэш с длинным TTL и высокой долей попаданий быстр и дёшев и может отдавать устаревшие данные; кэш с коротким TTL и агрессивной инвалидацией свеж и корректен и сильнее нагружает исходный сервер. Универсально правильного ответа нет, только правильный ответ для каждого фрагмента данных, определяемый его реальной терпимостью к устареванию. Второе напряжение это простота против охвата: кэш приложения близок к данным и легко осмысляется, тогда как край далёк, быстр и труднее инвалидируется. Разрешайте оба классификацией данных по потребности в свежести и объёму чтения, затем размещением и настройкой каждого класса намеренно.
Вопросы для обсуждения с командой
Какова реальная терпимость к устареванию каждого вида кэшируемых данных и задали ли мы TTL и инвалидацию из этой терпимости, а не по привычке? Большинство команд кэширует с TTL, который кто-то выбрал однажды и никогда не пересматривал, так что одни данные отдаются более устаревшими, чем бизнес может принять, а другие истекают так агрессивно, что кэш почти не помогает. Принесите ваши десять главных кэшируемых ресурсов и по каждому спросите владельцев этих данных, насколько безопасно им быть устаревшими: секунды, минуты, часы или никогда после публикации. Вы обычно обнаружите, что ответы сильно различаются, а ваши текущие TTL им не соответствуют. Желаемый результат короткая классификация свежести, где каждый класс отображён на подход (короткий TTL, длинный TTL плюс очистка или версионированные неизменяемые ключи), так что решения о кэшировании следуют из семантики данных, а не из догадки.
Если бы наша самая популярная запись кэша истекла прямо сейчас при пиковом трафике, что случилось бы с исходным сервером? Этот вопрос показывает, есть ли у вас настоящая защита от лавины или только надежда. Многие системы работают прекрасно, пока горячий ключ не истечёт в пик трафика и каждый запрос не обрушится на базу данных одновременно, превращая кэш из щита в спусковой крючок. Пройдите по пути конкретно для вашей самой загруженной конечной точки: есть ли слияние запросов, так что на исходный сервер доходит один промах, есть ли джиттер, чтобы записи не истекали в унисон, есть ли политика stale-while-revalidate, чтобы пользователи не ждали пополнения? Принесите свидетельства нагрузочного тестирования, а не интуицию, потому что защита от лавины, держащаяся на десяти пользователях, всё ещё может рухнуть на десяти тысячах. Если вы не можете ответить уверенно, ваше следующее вложение в устойчивость только что нашло вас.
Уверены ли мы, что ни один общий кэш никогда не хранит частные данные одного пользователя под ключом, который может поразить другой пользователь? Это ошибка кэширования, становящаяся инцидентом безопасности и заголовком новостей. Она случается, когда персонализированный или аутентифицированный ответ кэшируется под ключом, опускающим идентичность пользователя, или когда заголовок
Cache-Control, призванный держать ответ частным, отсутствует, так что общий прокси или CDN сохраняет его и отдаёт следующему человеку. Проведите аудит того, какие ответы кэшируемы на общих уровнях, подтвердите, что каждый ответ на пользователя помеченprivateилиno-store, и подтвердите, что каждый ключ кэша включает каждый ввод, меняющий ответ. Воспринимайте это как рецензию безопасности, потому что радиус поражения каждый нижестоящий пользователь, и свяжите это с практиками главы 4.2.Какой шаблон записи на самом деле использует каждый из наших кэшей и выбирал ли его кто-нибудь намеренно? Cache-aside, сквозная запись, отложенная запись и запись в обход дают противоположные обещания о свежести и о том, что вы теряете при отказе кэша, однако в большинстве кодовых баз шаблон тот, что скопировал первый автор. Для большой команды это важно, потому что один сервис, предполагающий свежесть cache-aside, пока другой тихо делает отложенную запись, может породить данные, выглядящие испорченными, но лишь устаревшие, и дежурный инженер тратит часы, гоняясь за призраком. Принесите опись по кэшам: шаблон записи, гарантируемая свежесть и что происходит с несброшенными записями, если процесс умрёт. Там, где какой-либо кэш использует отложенную запись, принесите историю долговечности, которая за ней стоит. В корпоративных и государственных системах, работающих с финансовыми данными или записями, кэш с отложенной записью без поддерживающей гарантии это находка аудита, ждущая своего часа, поэтому обсуждение должно закончиться тем, что шаблон каждого кэша назван, обоснован и записан.
Когда мы развёртываем или меняем данные, инвалидирует ли каждый значимый уровень кэша правильно или мы полагаемся на то, что кто-то вспомнит очистить? Инвалидация трудная часть, а режим отказа молчаливый: исправленное значение остаётся неверным часами, потому что один уровень иерархии, CDN, обратный прокси или кэш приложения, так и не получил сообщения. Большая организация умножает этот риск, потому что одно логическое изменение может потребовать распространения по многим кэшам во многих регионах, которыми владеют разные команды. Принесите конкретную трассу одного недавнего изменения данных и проследите её через каждый уровень кэша, спрашивая на каждом: что вызвало инвалидацию здесь и сколько это заняло? Отдавайте предпочтение проектам, превращающим инвалидацию в именование (версионированные ключи) или в события (изменение публикует очистку), а не ручным ранбукам. В государственных системах, где неверная опубликованная цифра, налоговая ставка или сумма пособия, может иметь юридический вес, разрыв инвалидации не досада, а экспозиция соответствия, поэтому результатом должен быть нанесённый на карту путь инвалидации для каждого класса кэшируемых данных.
Воспринимаем ли мы кэширование как общую платформенную инфраструктуру или каждая команда изобретает ключи, инвалидацию и защиту от лавины заново сама? Хорошо сделанное кэширование это небольшой набор трудных проблем, решённых один раз: нормализованные ключи, событийно-ориентированная инвалидация, слияние запросов, правильная семантика HTTP и наблюдаемость по уровням. Когда каждая команда импровизирует это, большая организация платит за одни и те же ошибки раз за разом, а ошибка отравления или утечка частных данных, исправленная в одном сервисе, молча сохраняется в десяти других. Принесите честную карту того, кто владеет соглашениями о кэшировании сегодня и сколько дублированного кода кэширования существует по сервисам. Конкурирующее соображение автономия: команды сопротивляются навязанной общей библиотеке, поэтому взвесьте «проторённую дорогу» по умолчанию, лёгкую для принятия, против жёсткого стандарта, который принуждается. Для платформенной группы корпорации или государства общая хорошо протестированная возможность кэширования также дешевейший способ добиться единообразного выполнения требований безопасности и аудита, поэтому обсуждение должно решить, что становится общей инфраструктурой и кто её финансирует.
Взгляд по секторам
Стартап. Кэширование ваш самый дешёвый путь пережить всплеск трафика, под который вы ещё не можете позволить себе масштабироваться, поэтому потратьте немногое время на несколько рычажных размещений: CDN с версионированными URL для статических ресурсов и единый слой cache-aside с короткими TTL и джиттером перед вашим самым горячим запросом. Опирайтесь на управляемые сервисы CDN и кэша, а не запускайте свои, и добавьте слияние запросов рано, потому что лавина в день запуска против маленькой базы данных это отказ, который скорее всего плохо закончит хороший день. Пропустите изощрённые схемы инвалидации, пока данные не скажут вам, что они важны.
Малый бизнес. Без специалиста по кэшированию и при ограниченном бюджете предпочитайте кэширование, которое вы получаете бесплатно внутри уже используемых инструментов: CDN, входящий в ваш хостинг, HTTP-заголовки Cache-Control на ответах вашего веб-фреймворка и встроенный кэш запросов базы данных. Решение «купить или построить» здесь почти всегда в пользу покупки, поскольку неверно заданный ключ, отдающий данные одного клиента другому, стоит гораздо больше управляемого сервиса, которого вы избежали. Сделайте верно две дешёвые победы, правильные HTTP-заголовки и никогда не кэшировать аутентифицированные страницы на общих уровнях, и оставьте экзотические шаблоны в покое.
Крупная организация. В масштабе по многим командам риск смещается с любого одного кэша на несогласованность между ними: расходящиеся схемы ключей, неравномерная инвалидация и утечки частных данных, возникающие в одном сервисе и не в другом. Предоставляйте кэширование как общую платформенную инфраструктуру с «проторёнными дорогами» по умолчанию для ключей, инвалидации, защиты от лавины и наблюдаемости по уровням, чтобы доля попаданий, вытеснения и устаревание были видны в одном месте и управлялись единообразно. Делайте конфигурацию кэша подлежащей рецензии как чувствительный для безопасности код и воспринимайте инвалидацию между регионами как первоклассную проектную проблему, а не ранбук отдельной команды.
Государственный сектор. Ограничения закупок и прозрачности определяют, что вы можете кэшировать и как доказываете безопасность. Кэшируйте публичное содержимое агрессивно, руководства, формы и таблицы ставок за CDN с длинными TTL, чтобы всплеск в день срока подачи поглощался далеко от исходного сервера, и документируйте эту конфигурацию для аудита. Аутентифицированные страницы, показывающие собственные записи гражданина, никогда не должны касаться общего кэша, и это правило должно быть проверяемым, а не просто заявленным. Где CDN или сервис кэширования закупается у поставщика, требуйте, чтобы контракт открывал нужные вам средства управления (ключи, очистка и журналирование), и избегайте привязки, запирающей общественные данные за проприетарными форматами кэша.
Примеры
Стартап. Небольшое потребительское приложение пропускает каталог продуктов через слой cache-aside на хранилище в памяти с TTL в 60 секунд и джиттером, чтобы записи не истекали вместе. Статические ресурсы идут в CDN с версионированными именами файлов и max-age в один год, так что развёртывание, меняющее таблицу стилей, отдаёт новый URL и никогда не требует очистки. Когда запуск в популярном подкасте даёт всплеск трафика, слияние single-flight означает, что тысячи одновременных промахов главной страницы вызывают одно чтение базы данных, а не тысячи. Основатели тратят на кэширование почти ничего, но справляются со всплеском, который расплавил бы их маленькую базу данных, потому что разместили несколько хорошо выбранных кэшей намеренно.
Крупная организация. Международный ритейлер обслуживает миллионы покупателей слоистым кэшем: CDN для изображений и кэшируемых ответов API, общий кэш обратного прокси в каждом регионе и кэш приложения для вычисленных фрагментов цен и склада. Ключи кэша нормализованы и включают валюту, язык и класс устройства, так что персонализация не утекает, а доля попаданий остаётся высокой. Данные продуктов используют короткие TTL с событийно-ориентированной инвалидацией, так что изменение цены публикуется в шину сообщений, очищающую затронутые ключи по регионам за секунды. Каждый уровень сообщает долю попаданий, долю вытеснений и устаревание платформе наблюдаемости главы 9.2, и оповещение о падающей доле попаданий однажды поймало недоразмеренный кэш, прежде чем он стал отказом на оформлении заказа.
Государственный сектор. Национальная налоговая служба ведёт портал подачи деклараций, тихий большую часть года и перегруженный вблизи срока. Команда кэширует агрессивно там, где безопасно, и никогда там, где нет. Публичное содержимое (страницы руководств, формы, таблицы ставок) отдаётся из CDN с длинными TTL и версионированными URL, поглощая всплеск чтения в день срока далеко от исходного сервера. Аутентифицированные страницы, показывающие собственную декларацию гражданина, помечены no-store и никогда не касаются общего кэша, так что ни одному налогоплательщику никогда не отдают чужие данные. Конфигурация кэша рецензируется как чувствительный для безопасности код по практикам главы 4.2, а защита от лавины нагрузочно тестируется в масштабе дня срока за месяцы вперёд, так что портал, раньше прогибавшийся в самый загруженный день года, теперь держится.
Бизнес-обоснование: мотивы, ROI и TCO
Отдача от кэширования необычно прямая и легко поддаётся количественной оценке. Кэш, поднимающий долю попаданий с 80 до 95 процентов, сокращает трафик к исходному серверу на три четверти, что может означать отсрочку обновления базы данных, запуск меньшего числа серверов приложений или переживание всплеска трафика, который иначе потребовал бы аварийного масштабирования. Улучшения задержки превращаются в выручку в коммерции и в удовлетворённость и доли завершения в государственных услугах, где исследования давно связывают более быстрые страницы с более высокой конверсией и меньшим отказом. Затраты на исходящий трафик и вычисления падают, потому что запрос, обслуженный с края, не платит за пропускную способность или обработку исходного сервера. Для систем с преобладанием чтения, а это большинство систем, кэширование нередко самая дешёвая производительность, какую можно купить.
Взвешивайте совокупную стоимость владения честно. Прямые затраты скромны: инфраструктура CDN и кэша дешева относительно мощности исходного сервера, которую она экономит. Реальная цена инженерная дисциплина, потому что неверный кэш хуже отсутствия кэша. Ошибки устаревания, ошибки инвалидации и уязвимости отравления кэша несут реальную цену и растут, когда кэширование импровизируется командами, а не предоставляется как общая хорошо протестированная возможность. Сильнейшее бизнес-обоснование финансирует небольшое количество общей инфраструктуры и соглашений кэширования (стандартные ключи, инвалидация, защита от лавины и наблюдаемость), чтобы каждая команда получала выгоду, не повторяя ошибок. В формулировке для руководства кэширование связано с метриками, которые оно уже отслеживает: стоимость инфраструктуры, задержка страниц, доли конверсии и завершения и частота инцидентов во время пиковых событий.
Антипаттерны и ловушки
- Кэширование без инвалидации: задание длинного TTL без возможности очистки, так что исправленное значение остаётся неверным часами.
- Слишком грубые ключи: кэширование персонализированных ответов под общим ключом, отдающее данные одного пользователя другому.
- Слишком тонкие ключи: включение изменчивых вводов в ключ, так что никакие два запроса никогда не совпадают, а доля попаданий рушится.
- Нет защиты от лавины: горячий ключ истекает под нагрузкой, и каждый запрос бросается на исходный сервер сразу.
- Синхронное истечение: партия записей, записанных вместе, истекает в один миг без джиттера, вызывая периодическое «громовое стадо».
- Кэширование частных данных на общих уровнях: отсутствие
Cache-Control: privateилиno-store, так что прокси или CDN хранит аутентифицированные ответы. - Игнорирование не включённых в ключ вводов: отражение заголовка в тело ответа при его отсутствии в ключе, открывающее дверь отравлению кэша.
- Недоразмеренный кэш: кэш, слишком малый для рабочего набора, пробуксовывает и вытесняет записи прямо перед тем, как они нужны.
- Неизмеряемый кэш: нет метрик доли попаданий или вытеснений, так что деградирующий кэш невидим, пока не станет отказом.
- Отложенная запись без долговечности: быстрые записи, исчезающие, когда кэш умирает до сброса, без поддерживающей гарантии.
Модель зрелости
- Уровень 1, Начальный: Кэширование стихийное и на разработчика, добавляется реактивно, когда что-то кажется медленным. TTL угаданы, ключи непоследовательны, инвалидация ручная или отсутствует, а устаревшие данные и загадочные ошибки обычны. Никто не отслеживает долю попаданий, а всплеск трафика, который кэш должен был поглотить, вместо этого вызывает отказ.
- Уровень 2, Развитие: Команды кэшируют в очевидных местах и используют CDN для статических ресурсов. Появляются базовые TTL и cache-aside, но соглашения различаются от сервиса к сервису, инвалидация непоследовательна, защиты от лавины нет, правила частного против общего кэширования неформальны, а наблюдаемость ограничена случайными проверками.
- Уровень 3, Стандартизация: Стратегия кэширования документирована и соблюдается по организации. Ключи кэша нормализованы, TTL следуют общей классификации свежести, инвалидация событийно-ориентирована там, где это важно, защита от лавины и правильная семантика HTTP стандартны, частные данные никогда не кэшируются на общих уровнях, а каждый уровень сообщает долю попаданий и вытеснений в общий конвейер наблюдаемости.
- Уровень 4, Управление: Кэширование измеряется и контролируется относительно базовых значений. У каждого уровня есть целевые доли попаданий, бюджеты устаревания и пороги вытеснений, а панели оповещают, когда доля попаданий падает или доля вытеснений растёт сверх базовой. Защита от лавины нагрузочно тестируется в пиковом масштабе, снижение нагрузки на исходный сервер количественно оценивается по каждому кэшу, TTL и политики вытеснения настраиваются по измеренным шаблонам доступа, а конфигурация кэша рецензируется как чувствительный для безопасности код до выпуска.
- Уровень 5, Оркестрация: Кэширование непрерывно улучшается и интегрировано по организации. Размещение, ключи и TTL адаптируются к смещающемуся трафику, пограничные вычисления используются там, где они оправдывают себя, планирование мощности и модели стоимости опираются на метрики кэша, а уроки инцидентов одной команды питают общие соглашения. Организация воспринимает кэширование как спроектированную, измеряемую, адаптивную возможность, а не набор локальных хаков.
Идеи для обсуждения
- Какой единственный кэш в вашей системе, если бы он прямо сейчас остыл, больше всего угрожал бы вашему исходному серверу и что его защищает?
- Можете ли вы назвать по памяти текущую долю попаданий для каждого уровня вашей иерархии кэшей, а если нет, что это говорит вам?
- Где вы превратили проблему инвалидации в проблему именования с помощью версионированных ключей и где ещё могли бы?
- Какие из ваших путей записи используют cache-aside, сквозную запись, отложенную запись или запись в обход и был ли каждый выбран намеренно?
- Если бы атакующий контролировал один заголовок запроса, мог ли бы он отравить любой кэшированный ответ, общий для ваших пользователей?
- Как бы вы узнали за минуты, что ваша доля попаданий тихо упала на двадцать пунктов?
Главные выводы
- Кэширование сокращает задержку, нагрузку и стоимость, а иерархия кэшей (клиент, CDN и край, обратный прокси, приложение, база данных) позволяет отвечать как можно выше и дальше наружу, насколько безопасно.
- Инвалидация трудная часть; проектируйте ключи кэша и TTL намеренно и превращайте проблемы инвалидации в проблемы именования с версионированными ключами везде, где можете.
- Защищайте кэш под нагрузкой слиянием запросов, джиттером и stale-while-revalidate, потому что кэш нужнее всего именно тогда, когда лавина может его сломать.
- Выбирайте шаблоны записи и политики вытеснения по нагрузке и используйте семантику HTTP-кэширования (
Cache-Control, ETag, валидацию) явно, а не оставляйте кэшам гадать. - Воспринимайте кэшированное содержимое как поверхность атаки и измеряйте долю попаданий, вытеснения и устаревание, потому что ненаблюдаемый или неверно заданный кэш это скрытое обязательство, а не актив.
Источники и дополнительное чтение
- Martin Kleppmann, Designing Data-Intensive Applications
- Andrew S. Tanenbaum and Herbert Bos, Modern Operating Systems
- John L. Hennessy and David A. Patterson, Computer Architecture: A Quantitative Approach
- Roy T. Fielding and Julian Reschke, “Hypertext Transfer Protocol (HTTP/1.1): Caching,” RFC 7234, IETF
- Mark Nottingham, “Caching Tutorial for Web Authors and Webmasters”
- James Kettle, “Practical Web Cache Poisoning,” PortSwigger Research
- Betsy Beyer, Chris Jones, Jennifer Petoff, and Niall Richard Murphy (eds.), Site Reliability Engineering: How Google Runs Production Systems
- Michael T. Nygard, Release It! Design and Deploy Production-Ready Software