Ребята, вопрос к тем, кто реально тащил прод с singleflight из Go standard lib. Я часто вижу в коде эту штуку как панацею от cache stampede, но лично у меня было пару случаев, когда при высоком конкурентном доступе (RPS > 10k на ноду) она начинала вести себя странно — то ли утечка горутин, то ли дедлоки на Do().
Поделитесь опытом: кто-то реально юзал это в продакшене под нагрузкой? Стоит ли оборачивать в свой пул воркеров или лучше сразу на Redis-блокировки переползать? А то читаю доки — всё гладко, а на практике нюансы вылезают.
👍 1
Да, было дело. Singleflight — штука удобная, но только пока нагрузка не перевалит за определённый порог. На 10k+ RPS я тоже видел странное: Do() начинал тупить, горутины подвисали на sharedFuture, особенно если бэкенд тупил и Do() висел секундами. Это не дедлок в чистом виде, а скорее эффект «залипания» — один медленный запрос блокирует всех, кто пришёл за тем же ключом.
На практике я бы сказал так: singleflight ок для умеренной нагрузки и простых кейсов, но под high load лучше не полагаться на него как на серебряную пулю. Либо оборачивай в свой пул с таймаутами и circuit breaker, либо сразу на Redis-блокировки с TTL — они предсказуемее и легче отлаживаются. Я в итоге перешёл на комбинацию: локальный singleflight для горячих ключей с коротким TTL + Redis для остального. Так и нагрузка на сеть ниже, и риски дедлоков меньше.
Ну да, классика. Singleflight — это как раз тот случай, когда простая идея «давай объединим одинаковые запросы» разбивается о реальность с блокировками на sharedFuture. Утечка горутин — не миф, я такое видел, когда Do() висел на медленном бэкенде, а новые запросы всё подваливали. Де-факто это не дедлок, а эффект «залипания»: один тормозной колл душит всех, кто пришёл за тем же ключом, и GC потом не сразу разгребёт висящие горутины. На 10k+ RPS это становится проблемой, особенно если бэкенд лагает — ты получаешь не просто деградацию, а полное зависание на критическом пути.
Я бы сказал, что singleflight ок ровно до тех пор, пока ты не доверяешь ему жизнь под high load. Redis-блокировки с TTL — да, предсказуемее, но за них платишь сетью. Твой подход с гибридом — локальный singleflight для горячих ключей + Redis для остального — это разумный компромисс. Только не забудь обернуть Do() в таймаут через context.WithTimeout, иначе одна тормозная база данных положит тебе всю ноду. А если хочешь совсем изврата — замени sharedFuture на канал с буфером 1 и явным отстрелом по таймеру, тогда контроль над горутинами будет ручками.