Что будет при запуске слишком большого количества параллельных скачиваний
Что проверяют
Понимаете ли вы, что дешёвая горутина не означает бесплатную операцию, и умеете ли ограничивать параллелизм. Вопрос практический: почти каждый сервис рано или поздно ходит пачкой запросов наружу.
Как отвечать
Горутина стоит пару килобайт стека, миллион горутин заведётся без проблем. Упрётесь вы не в планировщик, а во внешние ресурсы:
- файловые дескрипторы: каждое соединение это дескриптор, и после лимита процесса начинаются ошибки вида
socket: too many open files; - эфемерные порты и таблица соединений на хосте или на промежуточном NAT;
- память: буферы ответа плюс то, что вы читаете целиком в переменную;
- удалённая сторона: коды 429 и 503, троттлинг, бан по адресу;
- пул соединений: у транспорта по умолчанию скромный лимит простаивающих соединений на хост, лишние закрываются сразу после ответа, и каждая закачка платит за новое рукопожатие TCP и TLS.
Ограничение параллелизма делают семафором на канале или готовым лимитом в errgroup:
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(16)
for _, u := range urls {
g.Go(func() error { return download(ctx, u) })
}
err := g.Wait()К этому обязательны три вещи: клиент с таймаутом или контекстом с дедлайном, чтение тела ответа до конца перед закрытием, повторы с экспоненциальной паузой на временные ошибки.
Где спотыкаются
Пускают go в цикле по всему списку ссылок и ждут sync.WaitGroup, считая это параллельной загрузкой. На тысяче ссылок такой код упирается в дескрипторы и отдаёт пачку ошибок соединения вместо файлов.
Второе: закрывают тело ответа, но не дочитывают его. Соединение в этом случае не возвращается в пул, а закрывается, и вместо переиспользования вы получаете новое рукопожатие на каждую закачку.
Третье: берут клиент по умолчанию, у которого нет общего таймаута. Зависшая закачка держит горутину и дескриптор бесконечно, сервис не падает и не чинится сам, а просто тихо деградирует до полной остановки загрузок.