Python socket 压力并发

9 投票
2 回答
6090 浏览
提问于 2025-04-21 01:04

我需要一个Python的TCP服务器,能够处理至少几万个同时连接的套接字。我尝试测试Python的SocketServer包在多进程和多线程模式下的性能,但两者的表现都远远达不到我的期望。

首先,我会描述客户端,因为这两种情况的客户端是一样的。

client.py

import socket
import sys
import threading
import time


SOCKET_AMOUNT = 10000
HOST, PORT = "localhost", 9999
data = " ".join(sys.argv[1:])


def client(ip, port, message):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((ip, port))
    while 1:
        sock.sendall(message)
        time.sleep(1)
    sock.close()


for i in range(SOCKET_AMOUNT):
    msg = "test message"
    client_thread = threading.Thread(target=client, args=(HOST, PORT, msg))
    client_thread.start()

多进程服务器:

foked_server.py

import os
import SocketServer


class ForkedTCPRequestHandler(SocketServer.BaseRequestHandler):

    def handle(self):
        cur_process = os.getpid()
        print "launching a new socket handler, pid = {}".format(cur_process)
        while 1:
            self.request.recv(4096)


class ForkedTCPServer(SocketServer.ForkingMixIn, SocketServer.TCPServer):
    pass


if __name__ == "__main__":
    HOST, PORT = "localhost", 9999

    server = ForkedTCPServer((HOST, PORT), ForkedTCPRequestHandler)
    print "Starting Forked Server"
    server.serve_forever()

多线程服务器:

threaded_server.py

import threading
import SocketServer


class ThreadedTCPRequestHandler(SocketServer.BaseRequestHandler):

    def handle(self):
        cur_thread = threading.current_thread()
        print "launching a new socket handler, thread = {}".format(cur_thread)
        while 1:
            self.request.recv(4096)


class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
    pass


if __name__ == "__main__":
    HOST, PORT = "localhost", 9999

    server = ThreadedTCPServer((HOST, PORT), ForkedTCPRequestHandler)
    print "Starting Threaded Server"
    server.serve_forever()

在第一种情况下,使用forked_server.py时,只创建了40个进程,但大约有20个进程在一段时间后就出现了以下错误:

错误:[Errno 104] 连接被对方重置

这个错误出现在客户端。

而多线程版本则要稳定得多,可以保持超过4000个连接,但最终也会出现

gaierror: [Errno -5] 没有与主机名关联的地址

这些测试是在我的本地机器上进行的,系统是Kubuntu 14.04 x64,内核版本是v3.13.0-32。为了提高系统的整体性能,我做了以下几步:

  1. 提高文件句柄的内核限制:sysctl -w fs.file-max=10000000
  2. 增加连接的等待队列:sysctl -w net.core.netdev_max_backlog = 2500
  3. 提高最大连接数:sysctl -w net.core.somaxconn = 250000

所以,我有几个问题:

  1. 这些测试是否正确,我能相信这些结果吗?我对网络和套接字的知识还很浅薄,请纠正我的结论。
  2. 在高负载的系统中,真的不适合使用多进程或多线程的方法吗?
  3. 如果是的话,那我们还有什么选择?异步的方法?Tornado、Twisted、Gevent框架?

2 个回答

0

这个人似乎找到了一种不错的解决方案,他使用了 threadingsubprocess 这两个工具。

#!/usr/bin/env python
# ssl_load.py - Corey Goldberg - 2008

import httplib
from threading import Thread

threads = 250
host = '192.168.1.14'
file = '/foo.html'

def main():
    for i in range(threads):
        agent = Agent()
        agent.start()

class Agent(Thread):
    def __init__(self):
        Thread.__init__(self)

    def run(self):
        while True:
            conn = httplib.HTTPSConnection(host)
            conn.request('GET', file)
            resp = conn.getresponse()

if __name__ == '__main__':
    main()

由于Windows XP的限制,他每个进程最多只能有250个线程。考虑到他的硬件条件比现在的标准差很多,他通过将这个脚本作为多个进程运行,成功达到了15000个线程的上限,如下所示:

#!/usr/bin/env python

import subprocess
processes = 60
for i in range(processes):
    subprocess.Popen('python ssl_load.py') 

希望这对你有帮助!

17

socketserver 处理不到 10,000 个连接。现在的硬件和操作系统上,没有任何线程或分叉的服务器能做到这一点。成千上万的线程意味着你花在切换和调度上的时间比实际工作还多。现代的 Linux 在调度线程和进程方面做得很好,Windows 在处理线程方面也不错(但在进程方面表现糟糕),不过它们都有一个极限。

而且 socketserver 甚至连高性能的尝试都没有。

当然,CPython 的全局解释器锁(GIL)让事情变得更糟。如果你使用的版本不是 3.2 以上,任何一个线程即使只做一点点 CPU 密集型的工作,都会影响到其他线程并阻塞你的输入输出。使用新版本的 GIL,如果你避免做复杂的 CPU 工作,就不会加重这个问题,但它仍然让上下文切换比原始的 pthreads 或 Windows 线程更耗费资源。


那么,你到底想要什么呢?

你需要一个单线程的“反应器”,它在循环中处理事件并启动处理程序。(在 Windows 和 Solaris 上,使用“主动反应器”会有一些好处,这是一组线程共同处理同一个事件队列,但因为你在 Linux 上,所以我们不必担心这个。)现代操作系统有非常好的多路复用 API 可以使用——在 BSD/Mac 上是 kqueue,在 Linux 上是 epoll,在 Solaris 上是 /dev/poll,在 Windows 上是 IOCP——这些都能轻松处理 10,000 个连接,即使是几年前的硬件也没问题。

socketserver 不是一个糟糕的反应器,只是它没有提供好的方式来调度异步工作,只有线程或进程。理论上,你可以在 socketserver 的基础上构建一个 GreenletMixIn(使用 greenlet 扩展模块)或 CoroutineMixIn(假设你有或知道如何编写一个跳板和调度器),这可能不会太复杂。但我不确定在那种情况下你能从 socketserver 中获得多少好处。

并行处理可以帮助,但只是为了将任何慢的工作从主工作线程中分离出去。首先让你的 10,000 个连接建立起来,做最少的工作。然后,如果你想添加的实际工作是 I/O 密集型的(例如,读取文件或向其他服务发请求),就添加一个线程池来处理;如果你需要添加大量 CPU 密集型的工作,就添加一个进程池(或者在某些情况下,甚至各添加一个)。

如果你能使用 Python 3.4,标准库中有一个解决方案 asyncio(还有一个可以在 PyPI 上找到的 3.3 的回溯版本,但本质上无法回溯到更早的版本)。

如果不能……那么你可以在 3.4+ 的 selectors 上自己构建一些东西,如果你不在乎 Windows,或者在 2.6+ 的 select 上,如果你只关心 Linux、*BSD 和 Mac,并且愿意编写两个版本的代码,但这会是很多工作。或者你可以用 C 语言编写核心事件循环(或者直接使用现有的像 libevlibuvlibevent),然后将其封装成一个扩展模块。

但实际上,你可能想转向第三方库。有很多这样的库,它们的 API 各不相同,从 gevent(它试图让你的代码看起来像是抢占式线程代码,但实际上在单线程事件循环中运行)到 Twisted(它围绕显式回调和未来对象构建,类似于许多现代 JavaScript 框架)。

StackOverflow 不是获取特定库推荐的好地方,但我可以给你一个一般性的建议:看看这些库,选择一个 API 听起来最适合你的应用,测试它是否足够好,如果你喜欢的那个不行,再考虑换另一个(或者如果你发现自己对 API 的喜好是错的)。一些库的粉丝(尤其是 geventtornado 的粉丝)会告诉你他们的库是“最快的”,但谁在乎呢?重要的是它们是否“足够快”并且能用来写你的应用。

我随便想想,建议你搜索 geventeventletconcurrencecogentwistedtornadomonocledieselcircuits。这可能不是一个很好的列表,但如果你把这些词一起谷歌一下,我敢打赌你会找到一个最新的比较,或者一个合适的论坛来询问。

撰写回答