我震惊了:Python、套接字和线程的奇怪问题

2 投票
5 回答
1556 浏览
提问于 2025-04-11 09:35

我有一个用Python写的HTTP服务器脚本:http://paste2.org/p/89701。我用ApacheBench(ab)来测试它的性能。当我设置的并发级别(-c选项)小于或等于我在代码中socket.listen()调用时指定的值,一切都运行得很好。但是,一旦我把ApacheBench中的并发级别设置得比socket.listen()中的值高,性能就会大幅下降。以下是一些例子:

  • 当socket.listen(10),并且用ab -n 50 -c 10 http://localhost/时,性能是1200请求/秒
  • 当socket.listen(10),并且用ab -n 50 -c 11 http://localhost/时,性能降到40请求/秒
  • 当socket.listen(100),并且用ab -n 5000 -c 100 http://localhost/时,性能是1000请求/秒
  • 当socket.listen(100),并且用ab -n 5000 -c 101 http://localhost/时,性能降到32请求/秒

在这两个调用之间,代码没有任何变化,我搞不清楚问题出在哪里,已经研究这个问题一天了。另外要注意的是:同样代码的多路复用版本(我写这个是为了和线程版本进行比较)无论socket.listen()设置成什么值,或者Apache的并发(-c选项)设置成什么值,运行得都很好。

我在IRC和Python文档上花了一天的时间,也在comp.lang.python和我的博客上发帖,但找不到任何人能告诉我可能出什么问题。帮帮我吧!

5 个回答

0

我找到了一篇关于Tomcat和Java的文章,里面提到了一些关于请求处理的有趣内容:

比如说,如果Java中的所有线程都在忙着处理请求,操作系统的内核会处理SYN和TCP的握手,直到它的请求队列满了。当队列满了,它就会直接丢掉后续的SYN请求。它不会发送一个重置连接的信号,也就是不会让客户端看到“连接被拒绝”的提示。相反,客户端会认为这个数据包丢失了,然后重新发送SYN请求。希望到那时候,请求队列能清空一些。

我理解的是,如果你用ab工具创建的同时连接数超过了你的套接字能处理的数量,数据包就会被丢掉,而不是被拒绝。我不太清楚ab是怎么处理这种情况的。可能它会重新发送SYN请求,但可能会等一段时间再发送。这可能在某个地方有说明(比如TCP协议?)。

总之,我不太确定,但希望这些信息能帮助你找到问题的原因。

祝你好运!

4

为了好玩,我还实现了一个异步版本:

import socket, Queue, select

class Request(object):
    def __init__(self, conn):
        self.conn = conn
        self.fileno = conn.fileno
        self.perform = self._perform().next

    def _perform(self):
        data = self.conn.recv(4048)
        while '\r\n\r\n' not in data:
            msg = self.conn.recv(4048)
            if msg:
                data += msg
                yield
            else:
                break
        reading.remove(self)
        writing.append(self)

        data = 'HTTP/1.1 200 OK\r\n\r\nHello World'
        while data:
            sent = self.conn.send(data)
            data = data[sent:]
            yield
        writing.remove(self)
        self.conn.close()

class Acceptor:
    def __init__(self):
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        sock.bind(('', 1234))
        sock.listen(10)
        self.sock = sock
        self.fileno = sock.fileno

    def perform(self):
        conn, addr = self.sock.accept()
        reading.append(Request(conn))

if __name__ == '__main__':
    reading = [Acceptor()]
    writing = list()

    while 1:
        readable, writable, error = select.select(reading, writing, [])
        for action in readable + writable:
            try: action.perform()
            except StopIteration: pass

这个版本执行了:

ab -n 10000 -c 10 http://127.0.0.1:1234/ --> 16822.13 [#/sec]
ab -n 10000 -c 11 http://127.0.0.1:1234/ --> 15704.41 [#/sec]
7

我无法确认你的结果,而且你的服务器代码看起来有点问题。我自己搭建了一个服务器,也没有遇到这个问题。我们来把讨论简化一下:

import thread, socket, Queue

connections = Queue.Queue()
num_threads = 10
backlog = 10

def request():
    while 1:
        conn = connections.get()
        data = ''
        while '\r\n\r\n' not in data:
            data += conn.recv(4048)
        conn.sendall('HTTP/1.1 200 OK\r\n\r\nHello World')
        conn.close()

if __name__ == '__main__':
    for _ in range(num_threads):
        thread.start_new_thread(request, ())

    acceptor = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    acceptor.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    acceptor.bind(('', 1234))
    acceptor.listen(backlog)
    while 1:
        conn, addr = acceptor.accept()
        connections.put(conn)

在我的机器上运行的结果是:

ab -n 10000 -c 10 http://127.0.0.1:1234/ --> 8695.03 [#/sec]
ab -n 10000 -c 11 http://127.0.0.1:1234/ --> 8529.41 [#/sec]

撰写回答