python - gevent/requests 在发出大量 head 请求时挂起

标签 python urllib2 python-requests gevent grequests

我需要发出 100k head 请求,并且我在请求之上使用 gevent。我的代码运行了一段时间,但最终挂起。我不确定它为什么挂起,或者它是否卡在请求或 gevent 中。我在请求和 gevent 中都使用了超时参数。

请查看下面我的代码片段,让我知道我应该更改哪些内容。

import gevent
from gevent import monkey, pool
monkey.patch_all()
import requests

def get_head(url, timeout=3):
    try:
        return requests.head(url, allow_redirects=True, timeout=timeout)
    except:
        return None

def expand_short_urls(short_urls, chunk_size=100, timeout=60*5):
    chunk_list = lambda l, n: ( l[i:i+n] for i in range(0, len(l), n) )
    p = pool.Pool(chunk_size)
    print 'Expanding %d short_urls' % len(short_urls)
    results = {}
    for i, _short_urls_chunked in enumerate(chunk_list(short_urls, chunk_size)):
        print '\t%d. processing %d urls @ %s' % (i, chunk_size, str(datetime.datetime.now()))
        jobs = [p.spawn(get_head, _short_url) for _short_url in _short_urls_chunked]
        gevent.joinall(jobs, timeout=timeout)
        results.update({_short_url:job.get().url for _short_url, job in zip(_short_urls_chunked, jobs) if job.get() is not None and job.get().status_code==200})
    return results 

我已经尝试过 grequests,但它已被放弃,并且我已经查看了 github pull requests,但它们也都有问题。

最佳答案

您观察到的 RAM 使用情况主要源于存储 100.000 个响应对象时堆积的所有数据,以及所有底层开销。我已经复制了您的应用案例,并针对 Alexa 排名靠前的 15000 个 URL 发出了 HEAD 请求。没关系

  • 无论我使用的是 gevent 池(即每个连接一个 greenlet)还是一组固定的 greenlet,都请求多个 URL
  • 我将池大小设置为多大

最后,RAM 使用量随着时间的推移增长到相当大的数量。但是,我注意到从 requests 更改为 urllib2 已经导致 RAM 使用量减少了大约两倍。也就是我换了

result = requests.head(url)

request = urllib2.Request(url)
request.get_method = lambda : 'HEAD'
result = urllib2.urlopen(request)

其他一些建议:不要使用两种超时机制。 Gevent 的超时方法非常可靠,您可以像这样轻松地使用它:

def gethead(url):
    result = None
    try:
        with Timeout(5, False):
            result = requests.head(url)
    except Exception as e:
        result = e
    return result

可能看起来很棘手,但要么返回 None(恰好在 5 秒后,并指示超时),要么返回任何表示通信错误的异常对象,要么返回响应。效果很好!

虽然这可能不是问题的一部分,但在这种情况下,我建议让 worker 活着并让他们每个人处理多个项目!生成小绿叶的开销确实很小。仍然,这将是一个非常简单的解决方案,带有一组长生命周期的 greenlets:

def qworker(qin, qout):
    while True:
        try:
            qout.put(gethead(qin.get(block=False)))
        except Empty:
            break

qin = Queue()
qout = Queue()

for url in urls:
    qin.put(url)

workers = [spawn(qworker, qin, qout) for i in xrange(POOLSIZE)]
joinall(workers)
returnvalues = [qout.get() for _ in xrange(len(urls))]

此外,您确实需要意识到这是您正在解决的一个大规模问题,会产生非标准问题。当我以 20 秒的超时时间和 100 个工作人员和 15000 个要请求的 URL 重现您的场景时,我很容易获得大量套接字:

# netstat -tpn | wc -l
10074

也就是说,操作系统有超过 10000 个套接字需要管理,其中大部分处于 TIME_WAIT 状态。我还观察到“太多打开的文件”错误,并通过 sysctl 调高了限制。当您请求 100.000 个 URL 时,您也可能会达到这样的限制,您需要采取措施防止系统耗尽。

还要注意您使用请求的方式,它会自动遵循从 HTTP 到 HTTPS 的重定向,并自动验证证书,所有这些肯定会消耗 RAM。

在我的测量中,当我将请求的 URL 数量除以程序的运行时间时,我几乎从未超过 100 个响应/秒,这是与世界各地的外国服务器的高延迟连接的结果。我猜你也受到这样的限制的影响。将架构的其余部分调整到此限制,您可能能够生成从 Internet 到磁盘(或数据库)的数据流,而中间的 RAM 使用量不会那么大。

我应该回答您的两个主要问题,具体而言:

我认为 gevent/您使用它的方式不是您的问题。我认为您只是低估了任务的复杂性。它伴随着严重的问题,并将您的系统推向极限。

  • 您的 RAM 使用问题:如果可以,请从使用 urllib2 开始。那么,如果事情仍然积累太多,你就需要去积累。尝试产生稳定状态:您可能想要开始将数据写入磁盘,并且通常会努力处理对象可能被垃圾收集的情况。

  • 您的代码“最终挂起”:这可能是您的 RAM 问题。如果不是,则不要生成那么多 greenlet,而是按照指示重复使用它们。此外,进一步降低并发性,监控打开的套接字数量,必要时增加系统限制,并尝试找出确切您的软件挂起的位置。

关于python - gevent/requests 在发出大量 head 请求时挂起,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/28223414/

相关文章:

python - 使用 Beautifulsoup 和选择器检索内容

python - OpenCV Python - 替换图像中的 channel

python - pytest和 flask : keep liver server alive for test class

python - UrlLib2 - 在工作场所网络上请求 ASP.NET 网站时访问被拒绝

Python - 使用 HTTPS 的 urllib2 异步/线程请求示例

Python urllib2 无法在备用端口(不是 80)上打开本地主机?错误 10013

python - requests 的 stream=True 选项如何一次一个 block 地流式传输数据?

python - 基于 mixin 列表的交叉以编程方式定义 python 类的好方法?

python - Try-Except-Else-Finally 返回 else 替代方案?

python - 确定请求的速率限制