Как netpoller влияет на блокирующие сетевые вызовы в Go
Что проверяют
Знаете ли вы, почему в Go можно писать синхронный код с горутиной на каждое соединение и не упираться в потоки ОС. Следом спрашивают, что происходит, когда вызов блокирует по-настоящему: чтение файла, cgo, долгий системный вызов.
Как отвечать
Суть: для программиста conn.Read выглядит блокирующим, а под капотом он асинхронный.
- неблокирующие сокеты: рантайм переводит сокеты в режим
O_NONBLOCKи регистрирует их в netpoller, обёртке надepollв Linux,kqueueв macOS и BSD, IOCP в Windows; - парковка горутины: если данных нет, системный вызов возвращает
EAGAIN, горутина паркуется на дескрипторе, а поток M с процессором P берут следующую горутину. Поток ОС не простаивает; - пробуждение: планировщик опрашивает netpoller, когда ищет работу, а
sysmonпроверяет его, если опроса давно не было. Готовые горутины возвращаются в очередь выполнения; - дедлайны:
SetReadDeadlineи таймаутыhttp.Serverработают на таймерах того же механизма, без отдельного потока на ожидание.
Итог: десятки тысяч соединений обслуживает небольшое число потоков, порядка GOMAXPROCS плюс служебные, а горутина стоит несколько килобайт стека.
Потом покажите границу. Обычные файлы epoll не поддерживает, поэтому os.File.Read по файлу на диске занимает поток целиком. Так же ведут себя вызовы через cgo и часть системных вызовов. Рантайм отдаёт P другому потоку, чтобы остальные горутины не встали, но сам заблокированный поток занят. Тысяча горутин, одновременно висящих на медленном диске или в cgo, породит тысячу потоков, а при превышении лимита debug.SetMaxThreads (по умолчанию 10000) программа упадёт.
Где спотыкаются
Приводят nginx как пример netpoller. Nginx написан на C и использует epoll напрямую, к рантайму Go он отношения не имеет.
Считают, что netpoller делает неблокирующим любой ввод-вывод, включая файлы и cgo.
Предлагают вручную крутить epoll через syscall ради скорости. В обычном сервисе это ломает модель горутин и почти никогда не окупается.