从urllib.request向HTTPServer发出许多并发请求时的神秘异常

Eli*_*ria 20 python urllib httpserver python-multithreading python-3.x

我正在尝试做这个Matasano加密挑战,涉及对人为减慢字符串比较功能的服务器进行定时攻击.它说使用"​​你选择的web框架",但我不想安装Web框架,所以我决定使用模块中内置的HTTPServer类http.server.

我想出了一些工作,但它是非常缓慢的,所以我尝试使用内置的(不良的记录)线程池来加速这一过程multiprocessing.dummy.它的速度要快得多,但我注意到一些奇怪的事情:如果我同时发出8个或更少的请求,它就可以了.如果我有更多,它会工作一段时间,并在看似随机的时间给我错误.这些错误似乎是不一致的,而不是总是相同的,但他们通常有Connection refused, invalid argument,OSError: [Errno 22] Invalid argument,urllib.error.URLError: <urlopen error [Errno 22] Invalid argument>,BrokenPipeError: [Errno 32] Broken pipe,或urllib.error.URLError: <urlopen error [Errno 61] Connection refused>在其中.

服务器可以处理的连接数有限制吗?我不认为线程本身的数量是问题,因为我编写了一个简单的函数,它在没有运行Web服务器的情况下进行了减慢的字符串比较,并使用500个并发线程调用它,并且它工作正常.我不认为只是从那么多线程发出请求就是问题所在,因为我已经使用了超过100个线程的爬虫(都同时向同一个网站发出请求)并且它们工作正常.看起来HTTPServer可能并不是为了可靠地托管能够获得大量流量的生产网站,但我很惊讶它很容易让它崩溃.

我尝试逐渐从我的代码中删除看起来与问题无关的东西,正如我在诊断这样的神秘错误时通常所做的那样,但在这种情况下这并不是很有帮助.看起来我正在删除看似无关的代码,服务器可以处理的连接数量逐渐增加,但没有明确的崩溃原因.

有谁知道如何增加我一次可以提出的请求数量,或者至少为什么会发生这种情况?

我的代码很复杂,但我想出了这个简单的程序来演示这个问题:

#!/usr/bin/env python3

import os
import random

from http.server import BaseHTTPRequestHandler, HTTPServer
from multiprocessing.dummy import Pool as ThreadPool
from socketserver import ForkingMixIn, ThreadingMixIn
from threading import Thread
from time import sleep
from urllib.error import HTTPError
from urllib.request import urlopen


class FancyHTTPServer(ThreadingMixIn, HTTPServer):
    pass


class MyRequestHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        sleep(random.uniform(0, 2))
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b"foo")

    def log_request(self, code=None, size=None):
        pass

def request_is_ok(number):
    try:
        urlopen("http://localhost:31415/test" + str(number))
    except HTTPError:
        return False
    else:
        return True


server = FancyHTTPServer(("localhost", 31415), MyRequestHandler)
try:
    Thread(target=server.serve_forever).start()
    with ThreadPool(200) as pool:
        for i in range(10):
            numbers = [random.randint(0, 99999) for j in range(20000)]
            for j, result in enumerate(pool.imap(request_is_ok, numbers)):
                if j % 20 == 0:
                    print(i, j)
finally:
    server.shutdown()
    server.server_close()
    print("done testing server")
Run Code Online (Sandbox Code Playgroud)

由于某种原因,上面的程序工作正常,除非它有超过100个线程左右,但我的真正的挑战代码只能处理8个线程.如果我用9运行它,我通常会遇到连接错误,而对于10,我总是会遇到连接错误.我试过使用concurrent.futures.ThreadPoolExecutor,concurrent.futures.ProcessPoolExecutormultiprocessing.pool不是multiprocessing.dummy.pool那些似乎没有帮助.我尝试使用一个普通的HTTPServer对象(没有ThreadingMixIn),只是让事情运行得很慢,并没有解决问题.我尝试使用ForkingMixIn,但也没有解决它.

我该怎么办呢?我在运行OS X 10.11.3的2013年末MacBook Pro上运行Python 3.5.1.

编辑:我尝试了一些其他的东西,包括在一个进程而不是一个线程中运行服务器,作为一个简单的HTTPServer,与ForkingMixIn,和ThreadingMixIn.这些都没有帮助.

编辑:这个问题比我想象的更奇怪.我尝试用服务器创建一个脚本,另一个用很多线程发出请求,然后在终端的不同选项卡中运行它们.服务器的进程运行正常,但发出请求的进程崩溃了.例外情况是的混合ConnectionResetError: [Errno 54] Connection reset by peer,urllib.error.URLError: <urlopen error [Errno 54] Connection reset by peer>,OSError: [Errno 41] Protocol wrong type for socket,urllib.error.URLError: <urlopen error [Errno 41] Protocol wrong type for socket>,urllib.error.URLError: <urlopen error [Errno 22] Invalid argument>.

我尝试使用上面的虚拟服务器,如果我将并发请求的数量限制为5或更少,它工作正常,但有6个请求,客户端进程崩溃.服务器出现了一些错误,但它仍然存在.无论我是使用线程还是进程来发出请求,客户端都崩溃了.然后我尝试将减速功能放在服务器中,它能够处理60个并发请求,但它与70崩溃.这似乎可能与服务器问题的证据相矛盾.

编辑:我尝试了大多数我描述的东西,requests而不是urllib.request遇到类似的问题.

编辑:我现在正在运行OS X 10.11.4并遇到同样的问题.

Ped*_*eck 9

您正在使用默认的listen()积压值,这可能是导致很多错误的原因.这不是已建立连接的并发客户端数,而是在建立连接之前等待侦听队列的客户端数.将您的服务器类更改为:

class FancyHTTPServer(ThreadingMixIn, HTTPServer):
    def server_activate(self):
        self.socket.listen(128)
Run Code Online (Sandbox Code Playgroud)

128是合理的限制.如果要进一步增加它,可能需要检查socket.SOMAXCONN或您的操作系统somaxconn.如果在重负载下仍然存在随机错误,则应检查ulimit设置并在需要时增加.

我用你的例子做到了这一点,我运行了1000多个线程,所以我认为这应该可以解决你的问题.


更新

如果它有所改进,但它仍然会同时崩溃200个客户端,那么我很确定你的主要问题是积压大小.请注意,您的问题不是并发客户端的数量,而是并发连接请求的数量.简要解释这意味着什么,而不是深入到TCP内部.

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))
s.listen(BACKLOG)
while running:
    conn, addr = s.accept()
    do_something(conn, addr)
Run Code Online (Sandbox Code Playgroud)

在此示例中,套接字现在接受给定端口上的连接,并且s.accept()调用将阻塞,直到客户端连接.您可以让许多客户端同时尝试连接,并且根据您的应用程序,您可能无法s.accept()像客户端尝试连接那样快速地调用和分派客户端连接.待处理的客户端排队,该队列的最大大小由BACKLOG值确定.如果队列已满,则客户端将因连接拒绝错误而失败.

线程没有帮助,因为ThreadingMixIn类所做的是do_something(conn, addr)在单独的线程中执行调用,因此服务器可以返回到mainloop和s.accept()调用.

您可以尝试进一步增加积压,但是会有一点无济于事,因为如果队列增长太大,某些客户端将在服务器执行s.accept()调用之前超时.

因此,正如我上面所说,您的问题是同时连接尝试的次数,而不是同时客户端的数量.对于您的实际应用程序来说,128就足够了,但是您的测试会出现错误,因为您尝试同时连接所有200个线程并充斥队列.

ulimit除非您收到Too many open files错误,否则不要担心,但如果您想将积压增加到128以上,请进行一些研究socket.SOMAXCONN.这是一个好的开始:https://utcc.utoronto.ca/~cks/space/blog/python/AvoidSOMAXCONN