Как часто нужно обновлять кэш с учетом времени отклика внешнего сервиса
Что проверяют
Понимаете ли вы, что частоту обновления задаёт не время отклика, а допустимая устарелость данных и лимиты внешнего сервиса. Время отклика отвечает на другой вопрос: когда начинать обновление и можно ли делать его прямо в запросе пользователя. Следом обычно спрашивают, что будет, если сервис лёг или сотня горутин одновременно увидела протухший ключ.
Как отвечать
Начните с трёх вводных: как часто данные реально меняются, насколько старые данные терпит бизнес и сколько запросов разрешает сервис. Из первых двух получается TTL, из третьей нижняя граница интервала. Дальше механика:
- запас на отклик: если p99 сервиса 800 мс, фоновое обновление стартует заранее, раньше истечения ключа как минимум на это время плюс запас, чтобы пользователь не ждал медленный вызов;
- stale-while-revalidate: отдаём то, что есть, свежую версию тянем в фоне. Вызов оборачивается в
context.WithTimeout, иначе зависший сервис держит горутину бесконечно; - защита от лавины:
singleflight.Groupизgolang.org/x/sync/singleflightсхлопывает одновременные промахи по одному ключу в один запрос; - джиттер: к TTL добавляется случайная доля, чтобы ключи, заполненные разом после старта, не истекали в одну секунду;
- stale-if-error: при ошибке сервиса отдаём старое значение до жёсткого предела и включаем circuit breaker, а не долбим упавший сервис ретраями;
- инвалидация по событию: если сервис шлёт вебхуки или события в очередь, TTL становится страховкой.
Хранилище под задачу: ristretto в памяти процесса, Redis, когда кэш общий для реплик. Проверяют всё метриками: доля попаданий, возраст отданных данных, латентность и ошибки внешнего вызова.
Где спотыкаются
Выводят TTL прямо из времени отклика: «сервис отвечает секунду, значит обновляем раз в секунду». Кэш превращается в постоянную нагрузку на сервис и почти не экономит вызовы.
Забывают про одновременный промах. Без singleflight истечение популярного ключа даёт всплеск одинаковых запросов, который и валит внешний сервис.
Выбрасывают данные при ошибке обновления. Пользователь получает 500 вместо чуть устаревшего ответа, хотя второе почти всегда лучше.