如何使用twisted来汇集HTTP连接?

时间:2014-08-28 15:18:49

标签: twisted connection-pooling twisted.web

我用一个非常简单的蜘蛛程序从单个站点获取网页。

这是最小化版本。

from twisted.internet import epollreactor  
epollreactor.install()
from twisted.internet import reactor
from twisted.web.client import Agent, HTTPConnectionPool, readBody

baseUrl = 'http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemCode='

start = 1001
end = 3500

pool = HTTPConnectionPool(reactor)
pool.maxPersistentPerHost = 10
agent = Agent(reactor, pool=pool)

def onHeader(response, i):
    deferred = readBody(response)
    deferred.addCallback(onBody, i)
    deferred.addErrback(errorHandler)
    return response

def onBody(body, i):
    print('Received %s, Length %s' % (i, len(body)))

def errorHandler(err):
    print('%s : %s' % (reactor.seconds() - startTimeStamp, err))

def requestFactory():
    for i in range (start, end):
        deferred = agent.request('GET', baseUrl + str(i))
        deferred.addCallback(onHeader, i)
        deferred.addErrback(errorHandler)
        print('Generated %s' % i)
        reactor.iterate(1)

    print('All requests has generated, elpased %s' % (reactor.seconds() - startTimeStamp))

startTimeStamp = reactor.seconds()
reactor.callWhenRunning(requestFactory)
reactor.run()

对于一些请求,如100,它工作正常。但对于大量请求,它将失败。

我希望所有请求(大约3000个)都应自动汇集,调度和流水线化,因为我使用HTTPConnectionPool,设置maxPersistentPerHost,用它创建一个Agent实例并逐步增加创建连接。

但事实并非如此,这些联系并不是保持活力,也不是合并。

在这个程序中,它确实以增量方式建立连接,但连接没有汇集,每个连接都会在收到主体后关闭,后来的请求永远不会在池中等待可用的连接。

因此,它需要数千个套接字,并且最终因超时而失败,因为远程服务器的连接超时设置为30秒。成千上万的请求不能在30秒内完成。

你能帮我个忙吗?

我已经尽力了,这是我的发现。

  • 在反应堆启动运行后30秒发生错误,不会受到其他因素的影响。
  • 让蜘蛛抓取我的服务器,我发现一些有趣的东西。
    1. HTTP协议版本是1.1 (我检查扭曲的文档,默认的HTTPClient是 1.0 而不是1.1)
    2. 如果我没有添加任何显式标头(就像最小化版本一样),请求标头不包含Connection: Keep-Alive,也可以执行响应标头。
    3. 如果我添加显式标头以确保它是保持连接,请求标头确实包含Connection: Keep-Alive,但响应标头仍然不是(我确信我的服务器行为正常,其他内容如Chrome,wget确实收到了Connection: Keep-Alive标题。)
  • 我在跑步过程中检查/proc/net/sockstat,它在开始时迅速增加,之后迅速减少。 (我已经增加了ulimit以支持大量的套接字)
  • 我用treq编写了一个类似的程序,这是一个基于扭曲的请求库。代码几乎相同,所以不要粘贴在这里。
    • 链接:https://gist.github.com/Preffer/dad9b1228fcd75cebd75
    • 它的行为几乎是一样的。没有合并。预计将按照treq的功能列表中的描述进行池化。
    • 如果我在其上添加了显式标头,则Connection: Keep-Alive永远不会出现在响应标头中。

基于以上所有内容,我对quirk Connection: Keep-Alive标题破坏程序非常怀疑。但是这个标头是HTTP 1.1标准的一部分,它确实报告为HTTP 1.1。我对此完全感到困惑。

2 个答案:

答案 0 :(得分:2)

我自己解决了这个问题,得到了IRC的帮助和stackoverflow中的另一个问题,Queue remote calls to a Python Twisted perspective broker?

总之,代理的行为与Nodejs中的行为非常不同(我在Nodejs方面有一些经验)。正如Nodejs doc

所述
  

agent.requests

     

包含尚未分配给套接字的请求队列的对象。

     

agent.maxSockets

     

默认设置为5.确定代理可以为每个源打开多少个并发套接字。 Origin可以是'host:port'或'host:port:localAddress'组合。

所以,这就是区别。

  • 扭曲:

    • 毫无疑问,如果使用HTTPConnectionPool实例进行构造,则代理可以对请求进行排队。
    • 但是如果在池中的连接用完后发出新请求,代理仍会创建新连接并执行请求,而不是将其放入队列
    • 实际上,它会导致丢弃池中的连接,并将新生成的连接推送到池中,保持连接数仍然等于maxPersistentPerHost
  • 的NodeJS:

    • 默认情况下,代理会使用隐式连接池对请求进行排队,该连接池的大小为5个连接。
    • 如果在池中的连接用完后发出新请求,代理将将请求排队agent.requests变量,等待可用连接

代理的扭曲行为导致代理能够对请求进行排队,但实际上却没有。

按照我们的直觉,一旦将连接池分配给代理,它就符合代理只使用池中的连接的直觉,并且如果池已经用完则等待可用连接。这与Nodejs中的代理完全匹配。

就我个人而言,我认为这是一种扭曲的错误行为,或者至少可以改进以提供设置代理行为的选项。

根据这一点,我必须使用DeferredSemaphore手动安排请求。

我向github上的treq项目提出了一个问题,并获得了类似的解决方案。 https://github.com/dreid/treq/issues/71

这是我的解决方案。

#!/usr/bin/env python
from twisted.internet import epollreactor
epollreactor.install()
from twisted.internet import reactor
from twisted.web.client import Agent, HTTPConnectionPool, readBody
from twisted.internet.defer import DeferredSemaphore

baseUrl = 'http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemCode='

start = 1001
end = 3500
count = end - start
concurrency = 10
pool = HTTPConnectionPool(reactor)
pool.maxPersistentPerHost = concurrency
agent = Agent(reactor, pool=pool)
sem = DeferredSemaphore(concurrency)
done = 0

def onHeader(response, i):
    deferred = readBody(response)
    deferred.addCallback(onBody, i)
    deferred.addErrback(errorHandler, i)
    return deferred

def onBody(body, i):
    sem.release()
    global done, count
    done += 1
    print('Received %s, Length %s, Done %s' % (i, len(body), done))
    if(done == count):
        print('All items fetched')
        reactor.stop()

def errorHandler(err, i):
    print('[%s] id %s: %s' % (reactor.seconds() - startTimeStamp, i, err))

def requestFactory(token, i):
    deferred = agent.request('GET', baseUrl + str(i))
    deferred.addCallback(onHeader, i)
    deferred.addErrback(errorHandler, i)
    print('Request send %s' % i)
    #this function it self is a callback emit by reactor, so needn't iterate manually
    #reactor.iterate(1)
    return deferred

def assign():
    for i in range (start, end):
        sem.acquire().addCallback(requestFactory, i)

startTimeStamp = reactor.seconds()
reactor.callWhenRunning(assign)
reactor.run()

是不是?请求指出我的错误和改进。

答案 1 :(得分:0)

  

对于一些请求,如100,它工作正常。但对于大量请求,   它会失败。

这是针对网络抓取工具的保护或针对DoS / DDoS的服务器保护,因为您在短时间内从同一IP发送了太多请求,因此防火墙或WSA将阻止您将来的请求。只需修改您的脚本,即可在一段时间内批量生成请求。你可以在每个X请求后的一段时间内使用callLater()。