【问题标题】:Problem with multi threaded Python app and socket connections多线程 Python 应用程序和套接字连接的问题
【发布时间】:2011-06-14 14:24:16
【问题描述】:

我正在调查在具有 4G RAM 的 Ubuntu 机器上运行的 Python 应用程序的问题。该工具将用于审计服务器(我们更喜欢推出自己的工具)。它使用线程连接到大量服务器,并且许多 TCP 连接失败。但是,如果我在启动每个线程之间添加 1 秒的延迟,那么大多数连接都会成功。我已经使用这个简单的脚本来调查可能发生的事情:

#!/usr/bin/python

import sys
import socket
import threading
import time

class Scanner(threading.Thread):
    def __init__(self, host, port):
        threading.Thread.__init__(self)
        self.host = host
        self.port = port
        self.status = ""

    def run(self):
        self.sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sk.settimeout(20)
        try:
            self.sk.connect((self.host, self.port))
        except Exception, err:
            self.status = str(err)
        else:
            self.status = "connected"
        finally:
            self.sk.close()


def get_hostnames_list(filename):
    return open(filename).read().splitlines()

if (__name__ == "__main__"):
    hostnames_file = sys.argv[1]
    hosts_list = get_hostnames_list(hostnames_file)
    threads = []
    for host in hosts_list:
        #time.sleep(1)
        thread = Scanner(host, 443)
        threads.append(thread)
        thread.start()

    for thread in threads:
        thread.join()
        print "Host: ", thread.host, " : ", thread.status

如果我在 time.sleep(1) 注释掉的情况下运行此程序,例如 300 台主机,许多连接会因超时错误而失败,而如果我延迟一秒,它们不会超时。我是否在功能更强大的机器上运行的另一个 Linux 发行版上尝试了该应用程序并且没有那么多连接错误?是因为内核限制吗?我可以做些什么来让连接正常工作而不造成延迟?

更新

我还尝试了一个限制池中可用线程数的程序。通过将其减少到 20,我可以让所有连接正常工作,但它每秒只检查大约 1 个主机。因此,无论我尝试什么(进入睡眠(1)或限制并发线程的数量),我似乎都无法每秒检查超过 1 个主机。

更新

我刚刚发现了这个 question,看起来和我看到的很相似。

更新

我想知道使用 twisted 编写此内容是否有帮助?任何人都可以展示我的示例使用 twisted 编写的样子吗?

【问题讨论】:

  • 您是否看到许多处于 TIME_WAIT 状态的连接 (netstat)? stackoverflow.com/questions/410616/…
  • 在Python中使用线程时网络访问不是并发的,您需要使用多处理或类似gevent或eventlet的东西。查看 J.F. Sebastian 的回答
  • @cerberos:Python 通常会在执行 I/O 时释放 GIL,因此您可以使用线程,但您不应该创建数千个线程来连接数千个主机。

标签: python multithreading sockets


【解决方案1】:

你可以试试gevent:

from gevent.pool import Pool    
from gevent import monkey; monkey.patch_all() # patches stdlib    
import sys
import logging    
from httplib import HTTPSConnection
from timeit import default_timer as timer    
info = logging.getLogger().info

def connect(hostname):
    info("connecting %s", hostname)
    h = HTTPSConnection(hostname, timeout=2)
    try: h.connect()
    except IOError, e:
        info("error %s reason: %s", hostname, e)
    else:
        info("done %s", hostname)
    finally:
        h.close()

def main():
    logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")    
    info("getting hostname list")
    hosts_file = sys.argv[1] if len(sys.argv) > 1 else "hosts.txt"
    hosts_list = open(hosts_file).read().splitlines()    
    info("spawning jobs")
    pool = Pool(20) # limit number of concurrent connections
    start = timer()
    for _ in pool.imap(connect, hosts_list):
        pass
    info("%d hosts took us %.2g seconds", len(hosts_list), timer() - start)

if __name__=="__main__":
    main()

每秒可以处理多个主机。

输出

2011-01-31 11:08:29,052 getting hostname list
2011-01-31 11:08:29,052 spawning jobs
2011-01-31 11:08:29,053 connecting www.yahoo.com
2011-01-31 11:08:29,053 connecting www.abc.com
2011-01-31 11:08:29,053 connecting www.google.com
2011-01-31 11:08:29,053 connecting stackoverflow.com
2011-01-31 11:08:29,053 connecting facebook.com
2011-01-31 11:08:29,054 connecting youtube.com
2011-01-31 11:08:29,054 connecting live.com
2011-01-31 11:08:29,054 connecting baidu.com
2011-01-31 11:08:29,054 connecting wikipedia.org
2011-01-31 11:08:29,054 connecting blogspot.com
2011-01-31 11:08:29,054 connecting qq.com
2011-01-31 11:08:29,055 connecting twitter.com
2011-01-31 11:08:29,055 connecting msn.com
2011-01-31 11:08:29,055 connecting yahoo.co.jp
2011-01-31 11:08:29,055 connecting taobao.com
2011-01-31 11:08:29,055 connecting google.co.in
2011-01-31 11:08:29,056 connecting sina.com.cn
2011-01-31 11:08:29,056 connecting amazon.com
2011-01-31 11:08:29,056 connecting google.de
2011-01-31 11:08:29,056 connecting google.com.hk
2011-01-31 11:08:29,188 done www.google.com
2011-01-31 11:08:29,189 done google.com.hk
2011-01-31 11:08:29,224 error wikipedia.org reason: [Errno 111] Connection refused
2011-01-31 11:08:29,225 done google.co.in
2011-01-31 11:08:29,227 error msn.com reason: [Errno 111] Connection refused
2011-01-31 11:08:29,228 error live.com reason: [Errno 111] Connection refused
2011-01-31 11:08:29,250 done google.de
2011-01-31 11:08:29,262 done blogspot.com
2011-01-31 11:08:29,271 error www.abc.com reason: [Errno 111] Connection refused
2011-01-31 11:08:29,465 done amazon.com
2011-01-31 11:08:29,467 error sina.com.cn reason: [Errno 111] Connection refused
2011-01-31 11:08:29,496 done www.yahoo.com
2011-01-31 11:08:29,521 done stackoverflow.com
2011-01-31 11:08:29,606 done youtube.com
2011-01-31 11:08:29,939 done twitter.com
2011-01-31 11:08:33,056 error qq.com reason: timed out
2011-01-31 11:08:33,057 error taobao.com reason: timed out
2011-01-31 11:08:33,057 error yahoo.co.jp reason: timed out
2011-01-31 11:08:34,466 done facebook.com
2011-01-31 11:08:35,056 error baidu.com reason: timed out
2011-01-31 11:08:35,057 20 hosts took us 6 seconds

【讨论】:

    【解决方案2】:

    我想知道使用 twisted 编写此内容是否有帮助?任何人都可以展示我的示例使用 twisted 编写的样子吗?

    这个变种比the code that uses gevent快得多:

    #!/usr/bin/env python
    import sys
    from timeit import default_timer as timer
    
    from twisted.internet import defer, protocol, reactor, ssl, task
    from twisted.python   import log
    
    info = log.msg
    
    class NoopProtocol(protocol.Protocol):
        def makeConnection(self, transport):
            transport.loseConnection()
    
    def connect(host, port, contextFactory=ssl.ClientContextFactory(), timeout=30):
        info("connecting %s" % host)
        cc = protocol.ClientCreator(reactor, NoopProtocol)
        d = cc.connectSSL(host, port, contextFactory, timeout)
        d.addCallbacks(lambda _: info("done %s" % host),
                       lambda f: info("error %s reason: %s" % (host, f.value)))
        return d
    
    def n_at_a_time(it, n):
        """Iterate over `it` concurently `n` items at a time.
    
        `it` - an iterator creating Deferreds
        `n`  - number of concurrent iterations
        return a deferred that fires on completion
        """
        return defer.DeferredList([task.coiterate(it) for _ in xrange(n)])
    
    def main():
        try:
            log.startLogging(sys.stderr, setStdout=False)
    
            info("getting hostname list")
            hosts_file = sys.argv[1] if len(sys.argv) > 1 else "hosts.txt"
            hosts_list = open(hosts_file).read().splitlines()
    
            info("spawning jobs")
            start = timer()        
            jobs = (connect(host, 443, timeout=2) for host in hosts_list)
            d = n_at_a_time(jobs, n=20) # limit number of simultaneous connections
            d.addCallback(lambda _: info("%d hosts took us %.2g seconds" % (
                len(hosts_list), timer() - start)))
            d.addBoth(lambda _: (info("the end"), reactor.stop()))
        except:
            log.err()
            reactor.stop()
    
    if __name__=="__main__":
        reactor.callWhenRunning(main)
        reactor.run()
    

    这是一个使用t.i.d.inlineCallbacks 的变体。它需要 Python 2.5 或更高版本。它允许以同步(阻塞)方式编写异步代码:

    #!/usr/bin/env python
    import sys
    from timeit import default_timer as timer
    
    from twisted.internet import defer, protocol, reactor, ssl, task
    from twisted.python   import log
    
    info = log.msg
    
    class NoopProtocol(protocol.Protocol):
        def makeConnection(self, transport):
            transport.loseConnection()
    
    @defer.inlineCallbacks
    def connect(host, port, contextFactory=ssl.ClientContextFactory(), timeout=30):
        info("connecting %s" % host)
        cc = protocol.ClientCreator(reactor, NoopProtocol)
        try:
            yield cc.connectSSL(host, port, contextFactory, timeout)
        except Exception, e:
            info("error %s reason: %s" % (host, e))
        else:
            info("done %s" % host)
    
    def n_at_a_time(it, n):
        """Iterate over `it` concurently `n` items at a time.
    
        `it` - an iterator creating Deferreds
        `n`  - number of concurrent iterations
        return a deferred that fires on completion
        """
        return defer.DeferredList([task.coiterate(it) for _ in xrange(n)])
    
    @defer.inlineCallbacks
    def main():
        try:
            log.startLogging(sys.stderr, setStdout=False)
    
            info("getting hostname list")
            hosts_file = sys.argv[1] if len(sys.argv) > 1 else "hosts.txt"
            hosts_list = open(hosts_file).read().splitlines()
    
            info("spawning jobs")
            start = timer()        
            jobs = (connect(host, 443, timeout=2) for host in hosts_list)
            yield n_at_a_time(jobs, n=20) # limit number of simultaneous connections
            info("%d hosts took us %.2g seconds" % (len(hosts_list), timer()-start))
            info("the end")
        except:
            log.err()
        finally:
            reactor.stop()
    
    if __name__=="__main__":
        reactor.callWhenRunning(main)
        reactor.run()
    

    【讨论】:

      【解决方案3】:

      Python 3.4 为异步 IO 引入了新的 provisional API -- asyncio module

      这种方式类似于twisted-based answer:

      #!/usr/bin/env python3.4
      import asyncio
      import logging
      from contextlib import closing
      
      class NoopProtocol(asyncio.Protocol):
          def connection_made(self, transport):
              transport.close()
      
      info = logging.getLogger().info
      
      @asyncio.coroutine
      def connect(loop, semaphor, host, port=443, ssl=True, timeout=15):
          try:
              with (yield from semaphor):
                  info("connecting %s" % host)
                  done, pending = yield from asyncio.wait(
                      [loop.create_connection(NoopProtocol, host, port, ssl=ssl)],
                      loop=loop, timeout=timeout)
                  if done:
                      next(iter(done)).result()
          except Exception as e:
              info("error %s reason: %s" % (host, e))
          else:
              if pending:
                  info("error %s reason: timeout" % (host,))
                  for ft in pending:
                      ft.cancel()
              else:
                  info("done %s" % host)
      
      @asyncio.coroutine
      def main(loop):
          logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
          limit, timeout, hosts = parse_cmdline()
      
          # connect `limit` concurrent connections
          sem = asyncio.BoundedSemaphore(limit)
          coros = [connect(loop, sem, host, timeout=timeout) for host in hosts]
          if coros:
              yield from asyncio.wait(coros, loop=loop)
      
      if __name__=="__main__":
          with closing(asyncio.get_event_loop()) as loop:
              loop.run_until_complete(main(loop))
      

      twisted 变体一样,它使用NoopProtocol,除了在成功连接后立即断开连接之外什么都不做。

      使用信号量限制并发连接数。

      代码是coroutine-based

      示例

      要了解我们可以与前 1000 台 Alexa 列表中的前 1000 台主机建立多少成功的 ssl 连接:

      $ curl -O http://s3.amazonaws.com/alexa-static/top-1m.csv.zip
      $ unzip *.zip
      $ /usr/bin/time perl -nE'say $1 if /\d+,([^\s,]+)$/' top-1m.csv | head -1000 |\
          python3.4 asyncio_ssl.py - --timeout 60 |& tee asyncio.log
      

      结果是不到一半的连接是成功的。平均而言,它每秒检查约 20 个主机。许多网站在一分钟后超时。如果主机与服务器证书中的主机名不匹配,则连接也会失败。它包括 example.comwww.example.com 类似的比较。

      【讨论】:

        【解决方案4】:

        真正的线程池怎么样?

        #!/usr/bin/env python3
        
        # http://code.activestate.com/recipes/577187-python-thread-pool/
        
        from queue import Queue
        from threading import Thread
        
        class Worker(Thread):
            def __init__(self, tasks):
                Thread.__init__(self)
                self.tasks = tasks
                self.daemon = True
                self.start()
        
            def run(self):
                while True:
                    func, args, kargs = self.tasks.get()
                    try: func(*args, **kargs)
                    except Exception as exception: print(exception)
                    self.tasks.task_done()
        
        class ThreadPool:
            def __init__(self, num_threads):
                self.tasks = Queue(num_threads)
                for _ in range(num_threads): Worker(self.tasks)
        
            def add_task(self, func, *args, **kargs):
                self.tasks.put((func, args, kargs))
        
            def wait_completion(self):
                self.tasks.join()
        

        例子:

        import threadpool
        pool = threadpool.ThreadPool(20) # 20 threads
        pool.add_task(print, "test")
        pool.wait_completion()
        

        它在 python 3 中,但转换为 2.x 应该不会太难。如果这能解决您的问题,我并不感到惊讶。

        【讨论】:

        • 有问题的实际应用程序使用队列。我只是想用我的例子中的代码来简化。我想知道它是否与套接字有关?
        • 注意:在 Python 2+ 中有multiprocessing.ThreadPool。以及 Python 3 中的 concurrent.futures.ThreadPoolExecutor
        【解决方案5】:

        首先,尝试使用非阻塞套接字。 另一个原因是您正在使用所有临时端口。 尝试取消限制。

        【讨论】:

          猜你喜欢
          • 2012-11-14
          • 2013-10-14
          • 2016-12-27
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多