Использование сельдерейных рабочих снижается с ростом числа рабочих

Я пытаюсь сделать тысячи запросов GET за минимально возможное время. Мне нужно сделать это масштабируемым способом: удвоение количества серверов, которые я использую для выполнения запросов, должно вдвое сократить время выполнения для фиксированного количества URL-адресов.

Я использую Celery с пулом событий и RabbitMQ в качестве брокера. Я создаю один рабочий процесс на каждом рабочем сервере с помощью --concurrency 100 и имею выделенный главный сервер, выдающий задачи (код ниже). Я не получаю ожидаемых результатов: время выполнения совсем не сокращается при удвоении числа используемых рабочих серверов.

Похоже, что по мере того, как я добавляю больше рабочих серверов, использование каждого рабочего сервера снижается (как сообщает Flower). Например, с 2 рабочими потоками во время выполнения количество активных потоков на одного рабочего колеблется в диапазоне от 80 до 90 (как и ожидалось, поскольку параллелизм равен 100). Однако при 6 рабочих потоках количество активных потоков на одного рабочего колеблется в диапазоне от 10 до 20.

Это похоже на то, что размер очереди слишком мал, или рабочие серверы не могут достаточно быстро вытаскивать задачи из очереди, чтобы их можно было полностью использовать, и по мере того, как вы добавляете больше рабочих, им становится все труднее быстро вытаскивать задачи из очереди.

urls = ["https://...", ..., "https://..."]
tasks = []
num = 0
for url in urls:
    num = num + 1
    tasks.append(fetch_url.s(num, url))

job = group(tasks)
start = time.time()
res = job.apply_async()
res.join()
print time.time() - start

Обновление: я приложил график успешно выполненных задач в зависимости от времени при использовании 1 рабочего сервера, 2 рабочих серверов и т. д. до 5 рабочих серверов. Как видите, скорость выполнения задачи удваивается при переходе от 1 рабочего сервера к 2 рабочим серверам, но по мере добавления дополнительных серверов скорость выполнения задачи начинает выравниваться.введите здесь описание изображения


person monstermac77    schedule 05.02.2018    source источник
comment
Как вы обеспечили, чтобы удаленный сервер выдерживал растущую нагрузку?   -  person temoto    schedule 07.02.2018
comment
Вы имеете в виду серверы, к которым я обращаюсь с моими запросами GET?   -  person monstermac77    schedule 07.02.2018
comment
Запросы GET на самом деле попадают на сотни разных серверов, каждый из которых определенно способен справиться с этой нагрузкой (они для этого предназначены). Я думаю, что может быть узкое место при добавлении задач в очередь; по сути, я думаю, что добавление большего количества рабочих, превышающих 3, не увеличивает скорость, потому что задачи не добавляются в очередь достаточно быстро, чтобы все рабочие могли быть полностью использованы. Любые идеи о том, как ускорить добавление задач, в идеале с python 2.7 (возможно, многопоточность, добавляющая задачи, чтобы я мог просто добавить больше процессоров)?   -  person monstermac77    schedule 07.02.2018
comment
Во-первых, попробуйте заменить http-запрос на eventlet.sleep(0.2). Во-вторых, попробуйте получить доступ к целевому сервису через небезопасный http, недавно была исправлена ​​соответствующая ошибка в eventlet. В-третьих, избавиться от rabbitmq, ненавижу это говорить, всегда хорошая идея, брокер Redis работает лучше. И, наконец, предлагаю избавиться от celery, если вам приходится обрабатывать каждый запрос по отдельности. В противном случае группируйте запросы и отправляйте в очередь небольшими партиями, это определенно поможет решить проблему с производительностью очереди (если она есть).   -  person temoto    schedule 08.02.2018
comment
@temoto, очень хорошие предложения. 1. Я смотрел на время выполнения задачи в Цветке, и почти все они возвращаются примерно через 0,1 секунды, но это хорошая идея для тех, кто может слишком сильно ударить по своим целевым серверам. 2. К сожалению, все эти целевые серверы перенаправляют на https. 3. После написания поста я переключился на Redis, и вы абсолютно правы: это быстрее. 4. Рассматривали переход на комбу, но ваше предложение группировать запросы было блестящим. Похоже, что узким местом было добавление задач в очередь, потому что использование фрагментации в Celery устранило это.   -  person monstermac77    schedule 11.02.2018
comment
@temoto, единственная проблема с фрагментацией заключается в том, что кажется, что рабочий не будет работать над задачами внутри фрагмента параллельно, а только последовательно. То есть, скажем, рабочий W получает чанк 1, содержащий задачи (A, B, C). Кажется, что рабочий W выполняет задачу A, затем ждет завершения задачи A перед запуском задачи B и т. д. Это ожидаемое поведение? Я могу ошибаться. Возможно, задачи A, B и C выполняются параллельно, но Flower ошибочно показывает время выполнения задачи для этого фрагмента как сумму времени выполнения отдельных задач, даже если задачи выполнялись параллельно.   -  person monstermac77    schedule 11.02.2018
comment
при параллельном выполнении задач см. расширенные сведения в ответе, а затем попробуйте HTTP (не https) на фиктивный сервер. Если проблема связана с https, попробуйте обходной путь отсюда github.com/eventlet/eventlet/ вопросы/457   -  person temoto    schedule 13.02.2018
comment
Спасибо. Из некоторых тестов мне удалось подтвердить, что использование встроенного в Celery механизма фрагментации не позволяет распараллеливать задачи внутри фрагмента, поэтому, используя встроенный механизм фрагментации Celery, вы действительно сокращаете скорость добавления задач в очередь. , но в ущерб распараллеливанию. Подробнее об этом ниже.   -  person monstermac77    schedule 26.02.2018


Ответы (1)


Для будущих читателей. Действия, которые помогли, сначала самое значительное преимущество:

  • Сгруппируйте несколько небольших рабочих единиц в одну задачу сельдерея
  • Переключите брокера Celery с RabbitMQ на Redis

Более полезные подсказки, не упомянутые в первоначальном обсуждении комментариев, поэтому неизвестное значение выгоды для этого вопроса.

  • Используйте httplib2 или urllib3 или лучшую библиотеку HTTP. requests сжигает процессор без уважительной причины
  • Использовать пул соединений HTTP. Проверьте и убедитесь, что вы повторно используете постоянные подключения к целевым серверам.

Чанкинг объяснил.

Перед разделением

urls = [...]

function task(url)
  response = http_fetch(url)
  return process(response.body)

celery.apply_async url1
celery.apply_async url2
...

Таким образом, очередь задач содержит N=len(urls) задач, каждая задача состоит в том, чтобы получить один URL-адрес, выполнить некоторые вычисления в ответ.

С разбивкой

function chunk(xs, n)
  loop:
  g, rest = xs[:n], xs[n:]
  yield g

chunks = [ [url1, url2, url3], [4, 5, 6], ... ]

function task(chunk)
  pool = eventlet.GreenPool()
  result = {
    response.url: process(response)
    for response in pool.imap(http_fetch, chunk)
  }
  return result

celery.apply_async chunk1
celery.apply_async chunk2
...

Теперь очередь задач содержит задачи M=len(urls)/chunksize, каждая задача состоит в том, чтобы получить URL-адреса в размере фрагмента и обработать все ответы. Теперь вам нужно мультиплексировать одновременные выборки URL внутри одного фрагмента. Здесь это сделано с помощью Eventlet GreenPool.

Обратите внимание, поскольку Python, вероятно, будет выгодно сначала выполнить весь сетевой ввод-вывод, а затем выполнить все вычисления ЦП для всех ответов в чанке, амортизируя загрузку ЦП с помощью нескольких обработчиков celery.

Весь код в этом ответе показывает только общее направление. Вы должны реализовать лучшую версию с меньшим количеством копирования и распределения.

person temoto    schedule 12.02.2018
comment
Это здорово! Интересно, что я реализовал предложенное вами специальное решение для фрагментации (чтобы задачи внутри фрагмента выполнялись параллельно) и смог увеличить скорость только на 7% примерно для 12 000 URL-адресов. Я думаю, это связано с тем, что распараллеливание внутри фрагмента действительно помогает вам с общей производительностью только в худшем случае, то есть когда вы помещаете несколько медленных URL-адресов в один и тот же фрагмент. Таким образом, если общее выполнение вашей программы постоянно не замедляется из-за пары отстающих в одном и том же фрагменте, распараллеливание внутри фрагмента не принесет большой пользы. - person monstermac77; 26.02.2018
comment
Что касается выбора брокера: как вы предполагаете, Redis может стабильно работать на 30% быстрее, чем RabbitMQ. То есть время от добавления задач в очередь до возврата .get() на 30 % быстрее при использовании Redis по сравнению с RabbitMQ. Любые конкретные идеи о том, почему это может быть, или это просто ожидается, потому что у RabbitMQ больше накладных расходов? Отключение долговечности не оказало заметного влияния на производительность. Это значительное ускорение, поэтому я хотел бы использовать Redis, но я знаю, что RabbitMQ больше протестирован для производства, и у меня были проблемы со стабильностью, которые могли быть связаны с Redis (не уверен, все еще тестирую это). - person monstermac77; 26.02.2018
comment
RabbitMQ — это приложение очереди сообщений, а Redis (в этом сценарии) — связанный список через сокет. Так что, конечно, Кролик должен иметь избыточные накладные расходы. Часть этих накладных расходов должна быть реализована в коде брокера на стороне клиента (комбу или что-то в этом роде), и неудивительно, что Python медленнее, чем (что-либо) Erlang. Конкретная идея (извините, не та, которую вы просили) - измерить общую производительность системы и решить, довольны ли вы ею. Я знаю, что это трудное решение, но иначе вы либо переплатите, либо попадете в бездонную оптимизационную яму. - person temoto; 27.02.2018
comment
Теперь я подтвердил, что Redis постоянно терпит неудачу для меня после отличной работы в течение нескольких часов. Поскольку вы, кажется, знакомы с Redis, возможно, для вас есть что-то очевидное, из-за чего .get() в Celery зависает на неопределенный срок: stackoverflow.com/q /49006182/2611730. Если я не смогу заставить Redis работать, мне придется довольствоваться значительно более медленным (но, надеюсь, более стабильным) RabbitMQ. У меня просто заканчиваются вещи, которые я могу придумать, чтобы попытаться заставить Redis работать надежно. - person monstermac77; 28.02.2018
comment
Так что я снова попробовал RabbitMQ. По какой-то странной причине после запуска новых серверов (без изменений кода) RabbitMQ работал в два раза быстрее, чем раньше. На старых серверах для выполнения задач RabbitMQ требовалось 70 с, а Redis — около 55 с, а с новыми серверами это сократилось до 35 и 30 с соответственно. Любые идеи о том, почему это могло произойти? Оба набора серверов находились в одном и том же центре обработки данных. - person monstermac77; 10.03.2018
comment
@ monstermac77 извини, приятель, теперь это широкое предположение. Они могут использовать другое/неподвижное оборудование (без вашего ведома), сетевое подключение, недостаточно информации по определенной причине. - person temoto; 11.03.2018
comment
спасибо, да, я знаю, что не даю тебе много работы. В итоге после этого я протестировал целую кучу других условий и не смог воспроизвести гораздо более медленную производительность (пробовал разные конфигурации сервера (меньше ЦП/меньше ОЗУ), пытался переместить половину рабочих мест в другой центр обработки данных в том же городе. , пытались перемещать их по стране и т. д.). Итак, я предполагаю, что это была какая-то временная аппаратная проблема с нашим провайдером, или, возможно, RabbitMQ создал какое-то неприятное сочетание соединений во время нашего тестирования, которое вызывало замедление? Не уверен, что это возможно. - person monstermac77; 14.03.2018