Python:通过FTP上传大量文件

时间:2013-05-06 18:55:48

标签: python upload ftp ftplib

我正在开发一个python脚本,它监视目录(使用libinotify)获取新文件,并为每个新文件执行一些处理,然后将其复制到存储服务器。我们使用的是NFS挂载,但有一些性能问题,现在我们正在使用FTP进行测试。看起来FTP使用的资源远少于nfs(负载总是低于2,nfs高于5)。

我们现在遇到的问题是在TIME_WAIT状态下保持打开的连接数量。存储在等待时间内具有约15k连接的峰值。

我想知道是否有某种方法可以重新使用以前的连接进行新的转移。

任何人都知道有没有办法做到这一点?

由于

2 个答案:

答案 0 :(得分:1)

这是一个新的答案,基于对前一个的评论。

我们将使用单个TCP套接字,并通过交替发送每个文件的名称和内容(netstrings)来发送每个文件,所有文件都在一个大流中。

我假设Python 2.6,双方的文件系统使用相同的编码,并且您不需要大量并发客户端(但您可能偶尔需要,例如,两个 - 例如,真正的客户端,以及测试员)。而且我再次假设你有一个模块filegenerator,其generate()方法注册inotify,排队通知,yield一个接一个。

client.py:

import contextlib
import socket
import filegenerator

sock = socket.socket()
with contextlib.closing(sock):
    sock.connect((HOST, 12345))
    for filename in filegenerator.generate():
        with open(filename, 'rb') as f:
            contents = f.read()
            buf = '{0}:{1},{2}:{3},'.format(len(filename), filename, 
                                            len(contents), contents)
            sock.sendall(buf)

server.py:

import contextlib
import socket
import threading

def pairs(iterable):
    return zip(*[iter(iterable)]*2)

def netstrings(conn):
    buf = ''
    while True:
        newbuf = conn.recv(1536*1024) 
        if not newbuf:
            return
        buf += newbuf
        while True:
            colon = buf.find(':')
            if colon == -1:
                break
            length = int(buf[:colon])
            if len(buf) >= colon + length + 2:
                if buf[colon+length+1] != ',':
                    raise ValueError('Not a netstring') 
                yield buf[colon+1:colon+length+1]
                buf = buf[colon+length+2:]

def client(conn):
    with contextlib.closing(conn):
        for filename, contents in pairs(netstrings(conn)):
            with open(filename, 'wb') as f:
                f.write(contents)

sock = socket.socket()
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
with contextlib.closing(sock):
    sock.bind(('0.0.0.0', 12345))
    sock.listen(1)
    while True:
        conn, addr = sock.accept()
        t = threading.Thread(target=client, args=[conn])
        t.daemon = True
        t.start()

如果你在Windows上需要超过200个客户端,在Linux和BSD上有100个客户端(包括Mac),在不太好的平台上有十几个客户端,你可能想要使用事件循环设计而不是线程设计,使用{{ Linux上有1}},BSD上有epoll,Windows上有IO完成端口。这很痛苦,但幸运的是,有一些框架可以为您提供一切。两种流行(和非常不同)的选择是Twistedgevent

关于kqueue的一个好处是,你今天可以编写线程代码,并通过一些简单的更改将其转换为基于事件的代码,如魔法。

另一方面,如果你最终想要基于事件的代码,那么从一开始就学习和使用框架可能会更好,所以你不必处理{{的所有繁琐的部分。围绕gevent循环并循环,直到您收到完整的消息并干净地关闭等等,然后编写您关心的部分。毕竟,上面一半以上的代码基本上都是每个服务器共享的东西的样板,所以如果你不必写它,为什么还要费心呢?


在评论中,你说:

  

这些文件也是二进制文件,因此如果客户端编码与服务器编码不同,我可能会遇到问题。

请注意,我以二进制模式(acceptrecv)打开了每个文件,故意选择了一个可以处理二进制字符串的协议(netstrings),而不试图将它们解释为字符或处理嵌入式NUL字符作为EOF或类似的东西。而且,虽然我使用'rb',但在Python 2.x中不会进行任何隐式编码,除非您提供'wb'字符串或给它基于语言环境的格式类型,我都不知道我在做。 (请注意,在3.x中,您需要使用str.format而不是unicode,这会改变一些代码。)

换句话说,客户端和服务器编码不会进入;你正在进行二进制传输,与FTP的I模式完全相同。


但是,如果你想要相反的话,为目标系统自动传输文本和重新编码呢?有三种简单的方法可以做到:

  1. 发送客户端的编码(一次在顶部,或每个文件一次),在服务器上,从客户端解码并重新编码到本地文件。
  2. 以文本/ unicode模式执行所有操作,甚至是套接字。这很愚蠢,在2.x中也很难做到。
  3. 定义线编码 - 比如UTF-8。客户端负责将文件和编码解码为UTF-8进行发送;服务器负责解码接收和编码文件的UTF-8。
  4. 使用第三个选项,假设文件将采用默认文件系统编码,更改的客户端代码为:

    bytes

    在服务器上:

    str

    默认情况下,with io.open(filename, 'r', encoding=sys.getfilesystemencoding()) as f: contents = f.read().encode('utf-8') 函数也使用通用换行符,因此客户端会将任何内容转换为Unix风格的换行符,服务器将转换为自己的本机换行符类型。

    请注意,FTP的T模式实际上进行任何重新编码;它只进行换行(以及更有限的版本)。

答案 1 :(得分:0)

是的,您可以重复使用ftplib的连接。你所要做的就是不要关闭它们并继续使用它们。

例如,假设您有filegenerator方法向generate()注册的模块inotify,将通知排队,并逐个yield:< / p>

import ftplib
import os
import filegenerator

ftp = ftplib.FTP('ftp.example.com')
ftp.login()
ftp.cwd('/path/to/store/stuff')

os.chdir('/path/to/read/from/')

for filename in filegenerator.generate():
    with open(filename, 'rb') as f:
        ftp.storbinary('STOR {}'.format(filename), f)

ftp.close()

我有点困惑:

  

我们现在遇到的问题是在TIME_WAIT状态下保持打开状态的连接数量。

听起来你的问题并不是你为每个文件创建一个新连接,而是你永远不会关闭旧文件。在这种情况下,解决方案很简单:只需关闭它们。


或者,或者你试图并行完成所有这些,但是没有意识到这就是你正在做的事情。

如果你想要一些并行性,但不是无限制的,你可以轻松地,例如创建一个包含4个线程的池,每个线程都有一个打开的ftplib连接,每个都从队列中读取,然后是一个刚刚推入该队列的inotify线程。