оптимальное колличество горутин

783 views
Skip to first unread message

Ilia Kandrashou

unread,
Oct 16, 2015, 1:53:20 PM10/16/15
to Golang Russian
Доброго времени суток дорогие инженеры.

В данный момент работаю над http crawler'ом, 
Реквесты к страничкам естественно планирую делать в а параллельных горутинах.

И тут встаёт вопрос, ограничивать ли количество параллельно висящих горутин? 
если да, то как посчитать/прикинуть сколько их может быть? 
а может есть патерн/методология что бы это колличество саморегулировалось ?

Этот вопрос я не решил, поэтому и задаю, но имеются некоторые размышления:
Производительность в данной классической

Алексей Акулович

unread,
Oct 16, 2015, 2:00:14 PM10/16/15
to Golang Russian
Вы быстрее упретесь в дефолтные лимиты по числу файловых дескрипторов на сокеты соединений, чем лимины на число горутин.
А саморегуляция простая: пул коннектов, имеющий минимальный и максимальный пределы.

пятница, 16 октября 2015 г., 20:53:20 UTC+3 пользователь Ilia Kandrashou написал:

Ilia Kandrashou

unread,
Oct 16, 2015, 2:07:11 PM10/16/15
to Golang Russian
*(простите не дописал)
так вот, размышления:
производительность в данной классической программе упирается в два ресурса процессорное время (ЦПУ) и/или пропускная способность сети (ПС).
если упираемся в ЦПУ :
 во первых это вряд ли, но если всё же упрёлись, то можно заюзать нехитрый паттерн - всю обработку страниц складываем отправляем (по каналу) в отдельные горутины (пул горутин). Их колличество равно колличеству ядер. Когда все ядра будут задействованы на 100% вычислениями этих горутин , горутины с реквестами (пул воркеров) автамотически остановятся выжидая на записи в канал (его надо сделать немного буферезированым)
С ЦПУ всё просто, но что делать если упираемся в сеть ?


пятница, 16 октября 2015 г., 20:53:20 UTC+3 пользователь Ilia Kandrashou написал:
Доброго времени суток дорогие инженеры.

Ilia Kandrashou

unread,
Oct 16, 2015, 2:11:35 PM10/16/15
to Golang Russian
мой вопрос как раз в том как найти этот предел и чем грозит если выставить непомерно большой предел, наприрер 100К воркеров на 2Mbit канал.

пятница, 16 октября 2015 г., 21:00:14 UTC+3 пользователь Алексей Акулович написал:

Denis Bakhtin

unread,
Oct 16, 2015, 5:24:15 PM10/16/15
to gola...@googlegroups.com
Вот здесь вопрос с лимитами хорошо описан на похожей задаче http://talks.golang.org/2013/advconc.slide#1, может там найдете что полезное.
И здесь https://blog.golang.org/pipelines (но уже на примере работы с файловой системой вместо сети, но много интересного).

16 октября 2015 г., 21:11 пользователь Ilia Kandrashou <loo...@gmail.com> написал:

--
Вы получили это сообщение, поскольку подписаны на группу "Golang Russian".
Чтобы отменить подписку на эту группу и больше не получать от нее сообщения, отправьте письмо на электронный адрес golang-ru+...@googlegroups.com.
Чтобы настроить другие параметры, перейдите по ссылке https://groups.google.com/d/optout.

Ilia Kandrashou

unread,
Oct 17, 2015, 9:31:15 AM10/17/15
to Golang Russian
может я плохо объясняю проблему но ваши ссылки ничего общего с ней не имеют.

между тем я продолжаю изучать вопрос, на некоторые мысли натолкнула статья http://www.futurechips.org/chip-design-for-all/littles-law.html
При работе с сетью мы имеем дело с latency и throughput. Предположим что любой запрашиваемый сайт подключён к высокоскоростной линии, т.е. скорость скачки ограничена только с нашей стороны. И скорость приёма == скорости отдаче.
Тогда если бы latency==0 то можно было бы все запросы слать последовательно в одной горутине. Пусть одну страничку мы скачиваем за 1сек, тогда при последовательно схеме 2 странички - за 2 сек, логично. (временем запроса принебрегаем)
Теперь в эту систем добавим задержку (latency) в 2 сек для каждой страничке. Тогда время скачки 2х страниц станет: (1+2) + (1+2) = 6 сек.
Что происходит если мы запрашиваем две странички параллельно: два запроса почти одновременно уходят в сеть, затем стадия задержки, затем стадия закачки каждой странички (последовательно) т.е. общее время станет : 2(задержка) +1(страничка) +1(страничка) = 4 сек. Причём TCP пакеты этих двух страничек перемешаются произвольным образом и по факту мы будем видеть как будто каждая страничка скачивается за 2 сек, но параллельно. Если Было бы 5 страничек - 5 сек для каждой странички, если 10 - то 10 сек, если 60 - то таймаут ! 

Итак задача заключается в том что бы обеспечить непрерывное поступление страничек в канал, не доводя до таймаутов. Т.е допустим обычная страничка (100Kb) должна быть скачана не больше чем за 10 сек. для этого мы должны были запросить не более чем 10 страничек. Т.е. скорость запросов = 10 стр/10 сек = 1стр/сек. (верно исключительно для данного примера).
Либо можно сказать по другому: 
Скорость отправки запросов в единицу времени должна быть равна скорости приёма страничке за то же время. Единица времени берётся такая что бы не превышались таймауты.

Таким образом вырисовывается система с обратной отрицательной связью:
 1) задаём желаемое время приёма страничек, снизу ограничено скоростью канала, сверху таймаутом. 
 2) в коде мониторим реально время загрузки, если меньше - добавляем реквест воркеров, если больше - убавляем.

Что думаете о таком подходе? давайте обсуждать или критиковать, в дебатах рождается истина (с)




суббота, 17 октября 2015 г., 0:24:15 UTC+3 пользователь Denis Bakhtin написал:

Silent

unread,
Oct 17, 2015, 10:04:56 AM10/17/15
to Golang Russian
Что-то вы заморачиваетесь, имхо. Вы же странички скачивать будете не просто так, а что-то с ними делать - парсить, сохранять в базу, как-то еще обрабатывать. Вот и заведите себе параметр rps, количество обработанных страниц в секунду. запустили пул горутин, померили rps; добавили еще горутин, снова померяли. rps повысился - отлично, можно еще добавлять сколько-то; понизился - уперлись либо в цпу, либо в сеть, нагрузку нужно скинуть.
У меня на подобной задаче для железяки i3+8Гб+100Мб/с выходило 3000 запущенных горутин и 300 rps (то есть в среднем 10сек на страничку) - упирался в проц

Ilia Kandrashou

unread,
Oct 17, 2015, 6:51:58 PM10/17/15
to Golang Russian
вариант конечно, можно подтюнить, анализируя метрики или профайл, но хочется что бы программа работала максимально эффективно на любом железе. т.е. была бы переносимой.


суббота, 17 октября 2015 г., 17:04:56 UTC+3 пользователь Silent написал:

Igor Yurchenko

unread,
Oct 18, 2015, 5:33:39 PM10/18/15
to Golang Russian


суббота, 17 октября 2015 г., 16:31:15 UTC+3 пользователь Ilia Kandrashou написал:
может я плохо объясняю проблему но ваши ссылки ничего общего с ней не имеют.

между тем я продолжаю изучать вопрос, на некоторые мысли натолкнула статья http://www.futurechips.org/chip-design-for-all/littles-law.html
При работе с сетью мы имеем дело с latency и throughput. Предположим что любой запрашиваемый сайт подключён к высокоскоростной линии, т.е. скорость скачки ограничена только с нашей стороны. И скорость приёма == скорости отдаче.
Тогда если бы latency==0 то можно было бы все запросы слать последовательно в одной горутине. Пусть одну страничку мы скачиваем за 1сек, тогда при последовательно схеме 2 странички - за 2 сек, логично. (временем запроса принебрегаем)

У вас очень странные представления как работает сетевая подсистема. Работа идет через сокеты. Вы можете считать что получили страницу тогда, когда всё прочитали из сокета. Если вы не читаете, то протокол TCP останавливает передачу страницы. То есть про действительно высокоскоростном канале,  скорость сети по факту определяется скоростью вашего чтения из сокетов. 
 
Теперь в эту систем добавим задержку (latency) в 2 сек для каждой страничке. Тогда время скачки 2х страниц станет: (1+2) + (1+2) = 6 сек.
Что происходит если мы запрашиваем две странички параллельно: два запроса почти одновременно уходят в сеть, затем стадия задержки, затем стадия закачки каждой странички (последовательно) т.е. общее время станет : 2(задержка) +1(страничка) +1(страничка) = 4 сек.

Если вы делаете это действительно параллельно, то у вас формально общее время будет 3 сек. Да, сетевая карта не многозадачная. Она в один момент времени может отдавать только один блок данных. Но вы делаете слишком много обобщений и упрощений. Запросы у вас почему-то уходят мгновенно и одновременно, а страницы вы считываете последовательно...  
 
Причём TCP пакеты этих двух страничек перемешаются произвольным образом и по факту мы будем видеть как будто каждая страничка скачивается за 2 сек, но параллельно. Если Было бы 5 страничек - 5 сек для каждой странички, если 10 - то 10 сек, если 60 - то таймаут !

На нормальных серверах вы не будете этого видеть. Серверные сетевые карты имеют свои буферы и если сеть достаточно быстрая, то всё будет определяться скоростью вашего чтения из сокета. Там дофига нюансов связанных с возможностями и особенностями сетевых карт.
Но они на практике очень редко когда не имеют смысл, потому что в реале, вы всегда будете ждать поступление данных в сокет со стороны сетевой карты. Если у вас слишком много открытых сокетов то вы реально будете тратить время на опрос сокетов есть ли данные или нет...
То есть с ростом числа запросов/открытых сокетов у вас суммарная эффективность будет сначала расти, а потом падать из-за потерь времени на опрос большого количества сокетов...
Всё что вам надо, это найти где находится вершина. Это зависит от многих факторов. Методы machine learning вам в помощь...

Alex Lurye

unread,
Oct 19, 2015, 3:45:32 AM10/19/15
to gola...@googlegroups.com
Итак задача заключается в том что бы обеспечить непрерывное поступление страничек в канал, не доводя до таймаутов. Т.е допустим обычная страничка (100Kb) должна быть скачана не больше чем за 10 сек. для этого мы должны были запросить не более чем 10 страничек. Т.е. скорость запросов = 10 стр/10 сек = 1стр/сек. (верно исключительно для данного примера).

Не понял, зачем вы делите одно на другое? Если у вас канал, скажем, 100 мегабит, то у вас в секунду пролезет порядка 100 таких страничек без всяких задержек на уровне сети.

И ещё - не доводить до таймаутов - это какое-то внешнее требование? Если его убрать (и просто со временем пытаться загрузить страничку снова), то можно использовать сеть более эффективно, см. ниже. И от таймаутов всё равно 100% вы не избавитесь, т.к. есть много не зависящих от вас факторов. Так что, если логика повторных попыток заложена в систему, то сильно подумайте, так ли вам страшны таймауты.
 
Таким образом вырисовывается система с обратной отрицательной связью:
 1) задаём желаемое время приёма страничек, снизу ограничено скоростью канала, сверху таймаутом. 

Все серверы разные (одни тормозят несколько секунд перед выдачей данных, другие сразу отвечают, одни вам мегабайт в секунду легко выпихнут, другие будут по 10кб/с тошнить), все странички разные (одни маленькие и влезут в один пакет, другие могут быть размером в мегабайты или гигабайты). Число запросов в секунду - совершенно не показательная метрика.

Считать лучше по-другому - делите пропускную способность канала на число соединений, получаете максимальную пропускную способность связи с каждым сервером. Если вы хотите 100 кбайт принимать за 10 секунд, то вам нужо 10 кбайт/с c каждым сервером. Значит делаете 100 горутин, и загрузите в пике (если все серверы одновременно отвечают и не тормозят) 100-мегабитный канал. В реальности серверы тормозят ещё как. Соответственно ваш канал окажется недонагружен (вы это легко в мониторинге сможете увидеть).

Это момент принятия решения. Или вы увеличиваете число горутин и общую пропускную способность кролера, но тогда есть риск, что больше серверов ответит одновременно, чем обычно, пропускная способность канала с каждым сервером упадёт ниже 10 кб/с, и при неблагоприятном стечении обстоятельств ваш SLO (скачать 100 килобайт за 10 секунд) будет нарушаться. Либо вы можете не увеличивать число горутин - тогда сеть будет недогружена, но таймаутов будет меньше.
 
 2) в коде мониторим реально время загрузки, если меньше - добавляем реквест воркеров, если больше - убавляем.

Число воркеров ещё ограничено размером TCP-буферов. При большом числе соединений это будет заметно. Если у вас, скажем, буфер каждого соединения 800 кб, то 10K открытых соединений - и привет 8 гигабайт. Настраивать буфер для интернет-кролера тоже надо аккуратно, т.к. он сильно влияет на пропускную способность соединений с далёкими и быстрыми серверами (см "Bandwidth-delay product").
 
Что думаете о таком подходе? давайте обсуждать или критиковать, в дебатах рождается истина (с)

Для автоматической подстройки параметров (размера буферов, числа соединений, потребляемой памяти) нужно иметь очень качественную модель сети. Это тема для отдельного исследования.

Ilia Kandrashou

unread,
Oct 22, 2015, 4:13:59 PM10/22/15
to Golang Russian
Спасибо за комментарии, коллеги.

Действительно я  сделал достаточно много упрощений, главное из них ,пожалуй, о ресурсе процессора. Но очевидно что он всё таки будет вносить некоторую задержку при многочисленных коннектах, даже не смотря на то что не которые ядра могут простаивать. кстати, предполагаю что сокеты хранятся в одной очереди (типа epoll) и опрашиваются все в одном потоке.

Итак, всё таки хочется смоделировать поведение при увеличении нагрузки на сеть, наверно можно разбить на несколько фаз:
1) пока канал и проц не нагружены, с увеличение числа параллельных запросов, мы будем видеть пропорциональное увеличение пропускной способности (ПС). при этом нагрузка на проц будет возрастать, но влияние будет не существенно, latency (или задержка) будет на постоянном уровне.
2.1) Следующая фаза, одна из двух возможных. Мы ещё не исчерпали  возможности канала, но нагрузка на проц уже привносит заметные задержки. На этой фазе замедление роста ПС с одновременным увеличение задержки.
2.2) Если мы всё таки достигаем насыщения сети, а проц ещё не вносит значительных задержек - будем наблюдать как растёт latency для каждой отдельной странички, при этом общая ПС будет на постоянном уровне.
3) многочисленные ошибки сети из-за задержек, вызванных либо слишком большим количеством пакетов, либо проц ничего не успевает.

Напомню что специфика задачи - много мелких реквестов (html странички с разных серверов) .
Поэтому изначальный мой план был в том что бы задать какое-то максимально допустимое значение для latency (10 сек). измерять его - это время между отправкой реквеста, и получением хидеров, без вычитвания боди. Агригировать множество значений меьтодом скользящего среднего. И добавлять/убавлять воркеров что бы это значение соответствовало заданному. План не учитывал процессор.

Новый план)
Мэнеджер воркеров больше не будет мерять latency, он будет мерять общее время закачки тестовой странички. Тестовая страничка находится на том же хосте что и мэнеджер, и даже хэндлиться в том же процессе. Оброзцовое время - полученное на момент старта программы. Увеличиваем количество воркеров, пока это время существенно не ухудшается.

Не претендую на истинность в последней инстанции, поэтому как вам идея ? давайте обсуждать, дискутировать. Спасибо.


понедельник, 19 октября 2015 г., 10:45:32 UTC+3 пользователь Alex Lurye написал:

Daniel Podolsky

unread,
Oct 23, 2015, 1:51:08 AM10/23/15
to gola...@googlegroups.com
2015-10-22 23:13 GMT+03:00 Ilia Kandrashou <loo...@gmail.com>:
> давайте обсуждать, дискутировать

давайте для начала сформулируем - чего вы добиваетесь?

Евгений Л

unread,
Oct 23, 2015, 3:16:35 AM10/23/15
to Golang Russian
Как уже отвечали, вы сначала будете наблюдать рост производительности с увеличением горутин. Но потом столкнётесь с задержками и загрузкой ЦПУ или пробкой в канале из-за барьеров блокировки (если обработка в горутине будет простая и не требовательная по ресурсам). На разных машинах везде результаты будут разные. Поэтому разрабатывайте паттерн который будет вам помогать и саморегулироваться. То есть, например как у себя сделал, ведёте статистику кол-ва горутин, среднее время обработки горутинами, поток заявок на входе и т.п.; далее выбираете динамически политику планирования воркеров, пула или что там у вас.

Также проблема может быть, как я выше описывал в пропускной способности канала, хоть с любым максимальным буфером, он просто будет простаивать и не заполняться. Тогда придумывайте как это обойти, вариантов много.
Reply all
Reply to author
Forward
0 new messages