Python套接字压力测试并发性能

9
我需要一个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] Connection reset by peer

客户端上的错误。
线程版本更加耐用,可以容纳超过4000个连接,但最终会开始显示

gaierror:[Errno -5] No address associated with hostname

测试是在我的本地机器上进行的,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框架?

为了了解背景,请搜索“C10K”和“C10K Python”。 - Robᵩ
2个回答

17

socketserver 没有能力处理接近 10k 个连接。在当前的硬件和操作系统上,无论是多线程还是多进程服务器都做不到这一点。成千上万的线程意味着你会花费更多时间在上下文切换和调度上,而不是真正工作。现代的 Linux 在调度线程和进程方面表现得非常好,Windows 在线程方面表现得不错(但在进程方面表现很差),但它所能做的也有限。

而且 socketserver 甚至没有尝试过追求高性能。

当然,CPython 的 GIL 会让情况变得更糟。如果你没有使用 3.2+ 版本,任何进行即使微不足道的 CPU 密集型工作的线程都会阻塞所有其他线程并阻塞你的 I/O。有了新的 GIL,如果避免使用非微不足道的 CPU,你就不会向问题中添加太多因素,但它仍然会使上下文切换比起原始的 pthreads 或 Windows 线程更加昂贵。


那么,你想要什么呢?

你需要一个单线程的“反应器”,它可以在一个循环中服务事件并启动处理程序。现代操作系统有非常好的多路复用 API 可以使用,如 BSD/Mac 上的 kqueue,Linux 上的 epoll,Solaris 上的 /dev/poll,Windows 上的 IOCP,即使在多年前的硬件上也可以轻松处理 10k 个连接。

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

并行性可以帮助,但只能将任何缓慢的工作任务分派到主要工作线程之外。首先让您的10K个连接起来,进行最小的工作。然后,如果您要添加的真正工作是I/O绑定的(例如读取文件或向其他服务发出请求),请添加线程池进行分派;如果您需要添加大量CPU绑定的工作,请添加进程池(或在某些情况下,甚至每种都添加一个)。

如果您可以使用Python 3.4,则标准库中有一种解决方法asyncio(对于3.3,有一个在PyPI上的后移版本,但无法后移到更早的版本)。

如果不介意Windows平台的话,你可以在3.4+版本上利用selectors创建自己的东西;如果只关注于Linux、*BSD和Mac平台,并且愿意编写两个版本的代码,你可以在2.6+版本上使用select,但这需要很多工作量。或者你可以用C语言编写核心事件循环(或者直接使用现有的库如libevlibuvlibevent)并将其包装为扩展模块。
但实际上,你可能想要求助于第三方库。有许多不同API的第三方库,从尝试使你的代码看起来像是预线程化代码但实际上在单线程事件循环中运行的gevent到基于显式回调和未来对象的Twisted,类似于许多现代JavaScript框架。
StackOverflow不是获取特定库建议的好地方,但我可以给你一个一般性的建议:仔细查看它们,选择最适合你的应用程序的API,测试它是否足够好,并且只有在你喜欢的那一个无法胜任时(或者你发现你对API的喜欢是错误的)才退而求其次。一些库(特别是geventtornado的粉丝)会告诉你他们最喜欢的库是“最快”的,但谁在乎呢?重要的是它们是否足够快并且可用于编写你的应用程序。

我能想到的,可以搜索以下关键词:geventeventletconcurrencecogentwistedtornadomonocledieselcircuits。这可能不是一个很好的列表,但是如果你将所有这些术语一起搜索,我敢打赌你会找到最新的比较结果,或者适当的论坛在上面发问。


哈,谷歌正是查询这组术语时,我发现了从2009年开始比较那些框架而排除其中一个的这篇文章,这意味着我的列表已经过时5年了。我还认为作者没有领会显式的“yield”可能比“gevent”风格的隐式更好,至少如果你有任何共享的可变数据或其他不确定性。 - abarnert

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个线程。考虑到他的硬件相对于今天的标准而言非常差,他能够通过将此脚本作为多个进程运行来达到15k线程的最大值,如下所示:

#!/usr/bin/env python

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

希望这能帮到你!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接