Прямой ответ
Кэширование сокращает число обращений к медленному источнику, поэтому выбор уровня и стратегии записи определяет и ускорение, и риск устаревших данных.
| Стратегия | Когда запись попадает в источник |
|---|---|
| Cache-aside | Приложение пишет в источник, затем обновляет кэш |
| Write-through | Запись идёт в кэш и источник синхронно |
| Write-back | Запись идёт в кэш, источник обновляется отложенно |
def get_user(user_id: int):
key = f"user:{user_id}"
cached = redis.get(key)
if cached is not None:
return json.loads(cached)
user = db.fetch_user(user_id)
redis.setex(key, 300, json.dumps(user))
return user
Cache-aside читает из кэша и при промахе обращается к источнику, поэтому источник остаётся единственным местом истины. О стратегиях инвалидации — в статье про кэш-инвалидацию.
Уровни кэша и стоимость промаха
Кэш размещается на нескольких уровнях, поэтому стоимость промаха растёт с удалением от места вычисления.
Кэш процесса. Данные хранятся в памяти процесса, поэтому чтение выполняется без сетевого обмена. Промах означает обращение к внешнему кэшу либо к источнику.
Общий кэш. Данные хранятся в отдельном сервисе, поэтому чтение требует сетевого обмена, но остаётся быстрее запроса к базе. Промах означает обращение к источнику.
Кэш источника. База данных хранит собственные буферы и результаты запросов, поэтому повторное выполнение того же запроса выполняется быстрее. Промах означает чтение с диска.
Практическое следствие: кэш процесса не разделяется между экземплярами, поэтому при масштабировании доля попаданий падает, а нагрузка на источник растёт.
Отдельный аспект — стоимость промаха при массовом истечении срока. Если у многих записей одинаковый срок жизни, они истекают одновременно, поэтому источник получает пик нагрузки. Разброс срока жизни устраняет синхронность истечения.
Стратегии записи и согласованность
Стратегия записи определяет, когда источник получает изменение, поэтому согласованность кэша и источника различается.
Cache-aside. Приложение само обновляет кэш после записи в источник, поэтому окно несогласованности определяется порядком операций.
Write-through. Кэш и источник обновляются в одной операции, поэтому расхождение мало, но каждая запись становится медленнее.
Write-back. Запись фиксируется в кэше и переносится в источник позже, поэтому запись быстрая, но при отказе кэша данные теряются.
Практическое следствие: write-back даёт наименьшую задержку записи, но требует устойчивого хранилища кэша, иначе потерянный кэш означает потерю данных.
Срок жизни и доля попаданий
Срок жизни записи определяет, как долго данные считаются актуальными, поэтому доля попаданий балансируется со свежестью.
Короткий срок. Данные свежие, но доля попаданий ниже, поэтому нагрузка на источник выше.
Длинный срок. Доля попаданий выше, но данные устаревают, поэтому пользователь видит прежнее значение.
Измерение доли. Доля попаданий измеряется по счётчикам попаданий и промахов, поэтому срок жизни подбирается по факту, а не по предположению.
Практическое следствие: срок жизни, выбранный без измерения, либо не даёт ускорения, либо показывает пользователю устаревшие данные.
О согласованности распределённого кэша — в статье про distributed cache; о стратегиях распределённого кэширования — в статье про distributed caching.
Практика: настройка кэширования данных
1. Измеряйте долю попаданий до выбора срока жизни. Счётчики попаданий и промахов показывают, даёт ли кэш ускорение.
2. Разбрасывайте срок жизни записей. Разброс предотвращает одновременное истечение и пик нагрузки на источник.
3. Используйте cache-aside как исходную стратегию. Источник остаётся единственным местом истины, поэтому восстановление кэша выполняется чтением.
4. Ограничивайте размер кэша. Ограничение предотвращает вытеснение полезных записей и исчерпание памяти.
5. Кэшируйте только повторяющиеся чтения. Однократное чтение не выигрывает от кэша, поэтому накладные расходы не окупаются.
6. Фиксируйте промахи в метриках. Рост промахов указывает на изменение структуры запросов либо на истечение срока.
7. Проверяйте поведение при недоступности кэша. Приложение продолжает работать медленнее, но не отказывает.
Рекомендации:
- Не кэшируйте записи, которые читаются один раз. Накладные расходы на запись в кэш не окупаются при однократном чтении.
- Учитывайте размер записи при выборе уровня. Крупные записи в кэше процесса увеличивают потребление памяти экземпляра.
- Сопоставляйте срок жизни с частотой изменения. Часто изменяемые данные требуют короткого срока либо явной инвалидации.
- Фиксируйте источник значения в записи кэша. Источник позволяет определить, откуда получено значение при диагностике.
- Пересматриваем срок жизни при изменении нагрузки. Рост числа обращений меняет долю попаданий.
Практический сценарий: кэш не дал ускорения
Контекст: кэш был добавлен перед чтением из базы, но время ответа не сократилось, поэтому эффект отсутствовал.
- Диагностика. Счётчики показали долю попаданий около десяти процентов. Ключ кэша включал метку времени запроса, поэтому каждый запрос формировал новый ключ и попадал в промах.
- Причина. Состав ключа менялся для каждого обращения, поэтому записи не переиспользовались и кэш не давал ускорения.
- Действия. (а) Убрали изменяемую часть из ключа, чтобы одинаковые запросы давали одинаковый ключ; (б) измерили долю попаданий после изменения и получили значение выше восьмидесяти процентов; (в) добавили разброс срока жизни, чтобы записи не истекали одновременно; (г) настроили метрику промахов, чтобы изменение структуры запросов обнаруживалось; (д) проверили поведение при остановке кэша и убедились, что приложение продолжает отвечать.
- Результат. Доля попаданий выросла, поэтому время ответа сократилось и нагрузка на базу уменьшилась.
Вывод: кэш даёт ускорение только при переиспользовании записей, поэтому состав ключа определяет долю попаданий. Измерение доли попаданий показывает, работает ли кэш, до того как срок жизни подбирается по факту.
FAQ
Какую стратегию записи выбрать по умолчанию? Cache-aside, поскольку источник остаётся единственным местом истины, а восстановление кэша выполняется обычным чтением.
Как определить срок жизни записи? По измеренной доле попаданий и частоте изменения данных. Срок, выбранный без измерения, либо не даёт ускорения, либо показывает устаревшие данные.
Почему записи кэша истекают одновременно? Одинаковый срок жизни, отсчитанный от близкого момента, приводит к синхронному истечению. Разброс срока устраняет пик нагрузки на источник.
Что происходит при недоступности кэша? Приложение обращается к источнику напрямую, поэтому работа продолжается медленнее. Отказ кэша не должен приводить к отказу приложения.
Источники: Redis eviction, PostgreSQL query tuning, AWS Caching Best Practices — ссылки в шапке.