Concurrency 芹菜工人';工人越多,利用率就越低

Concurrency 芹菜工人';工人越多,利用率就越低,concurrency,rabbitmq,celery,distributed-computing,eventlet,Concurrency,Rabbitmq,Celery,Distributed Computing,Eventlet,我试图在尽可能短的时间内发出数千个GET请求。我需要以一种可扩展的方式来实现这一点:将我用于发出请求的服务器数量增加一倍,就可以将完成固定数量URL的时间减少一半 我使用芹菜和eventlet池以及RabbitMQ作为代理。我在每个工作服务器上生成一个工作进程,具有--concurrency 100,并且有一个专用的主服务器来发布任务(下面的代码)。我没有得到我期望的结果:当使用的工作服务器数量翻一番时,完成时间根本没有减少 似乎随着我添加更多的工作服务器,每个工作服务器的利用率都会下降(如Fl

我试图在尽可能短的时间内发出数千个GET请求。我需要以一种可扩展的方式来实现这一点:将我用于发出请求的服务器数量增加一倍,就可以将完成固定数量URL的时间减少一半

我使用芹菜和eventlet池以及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个工作服务器,翻了一番,但随着我添加更多服务器,任务完成率开始趋于平稳。

供未来读者参考。有帮助的行动,最重要的是首先受益:

  • 将几个小的工作单元组合成一个芹菜任务
  • 将芹菜代理从RabbitMQ切换到Redis
在最初的评论讨论中没有提到更多有用的提示,因此对这个问题的益处不得而知

  • 使用
    httplib2
    urlib3
    或更好的HTTP库<代码>请求无正当理由烧坏CPU
  • 使用HTTP连接池。检查并确保重新使用到目标服务器的永久连接
Chunking解释道

分块前 所以任务队列包含N=len(url)个任务,每个任务都要获取单个url,对响应执行一些计算

带块 现在任务队列包含M=len(URL)/chunksize任务,每个任务都要获取chunksize URL并处理所有响应。现在,您必须在单个块内多路传输并发url抓取。这里是Eventlet GreenPool

请注意,由于使用Python,因此首先执行所有网络IO,然后对块中的所有响应执行所有CPU计算,通过多个芹菜工人分摊CPU负载可能是有益的


此答案中的所有代码仅显示大致方向。您必须使用更少的复制和分配来实现更好的版本。

您如何确保远程服务器能够承受不断增加的负载?您是指我使用GET请求访问的服务器吗?GET请求实际上访问了数百台不同的服务器,每台服务器都绝对能够处理此负载(它们的设计目的是这样的)。我认为向队列中添加任务可能存在瓶颈;从本质上说,我认为在3个以上添加更多的工作人员不会加快速度,因为任务添加到队列的速度不够快,无法充分利用所有工作人员。关于如何加快添加任务的速度,最好是使用Python2.7(可能是多线程添加任务,这样我就可以添加更多CPU)?首先,尝试用
eventlet.sleep(0.2)
替换http请求。第二,尝试通过不安全的http访问目标服务,eventlet中最近修复了一个相关的bug。第三,摆脱rabbitmq(我讨厌这么说)永远是个好主意,redis broker工作得更好。最后,如果你必须单独处理每个请求,我建议你扔掉芹菜。否则,分组请求并以小批量发送到队列,这肯定会有助于解决队列性能问题(如果存在问题)。@temoto,非常好的建议。1.我一直在关注任务完成时间,几乎所有任务都会在0.1秒左右完成,但对于那些可能对目标服务器打击过大的人来说,这是个好主意。2.不幸的是,所有这些目标服务器都重定向到https。3.自从我写了这篇文章后,我确实转到了redis,你说得绝对正确:它更快。4.我曾考虑过去kombu,但你对分组请求的建议非常棒。似乎瓶颈在于向队列中添加任务,因为在芹菜中使用分块解决了这个问题。这太棒了!有趣的是,我实现了您建议的这个自定义分块解决方案(让一个分块中的任务并行执行),在大约12000个URL上只能获得7%的速度。我认为这是因为在最坏的情况下,在块内并行化只会真正帮助您提高整体性能,也就是说,当您碰巧在同一块中放置多个慢速URL时。因此,除非您的程序的整体执行一直被同一块中的两个掉队者减慢,否则在块中并行将不会产生什么好处。关于代理选择:正如您所建议的,Redis的执行速度始终比RabbitMQ快30%。也就是说,使用Redis与RabbitMQ相比,从将任务添加到队列到.get()返回的时间要快30%。有没有具体的想法来解释为什么会这样,或者这只是因为RabbitMQ有更多的开销?关闭耐久性对性能没有可测量的影响。这是一个显著的加速,所以我想使用Redis,但我知道RabbitMQ在生产方面经过了更多的战斗测试,并且我遇到了可能是由于Redis造成的稳定性问题(不确定,仍在测试)。RabbitMQ是消息队列应用程序,而Redis(在本场景中)是通过套接字链接的列表。所以兔子当然应该这么做
urls = [...]

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

celery.apply_async url1
celery.apply_async url2
...
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
...