【问题标题】:Python: Upload huge amount of files via FTPPython:通过 FTP 上传大量文件
【发布时间】:2013-04-30 14:17:08
【问题描述】:

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

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

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

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

谢谢

【问题讨论】:

  • 使用ftplib 重用现有连接很简单。如此琐碎,我什至不知道如何解释它。我可以编写一个快速示例,创建一个线程池 ftplib 连接,所有这些连接都服务于一个充满路径的队列或其他东西,如果这有帮助的话。
  • 不清楚为什么您会在存储上看到 15K TIME-WAIT。如果 TIME-WAIT 的原因是大量 ftp 数据连接(由于storbinary() 调用),则客户端应启动 tcp 连接关闭(以指示 EOF),因此 TIME-WAIT 应位于客户端。如果 TIME-WAIT 为 60 秒 (Linux),则 15K 对应于每秒持续数分钟的约 250 个连接(文件上传)。你真的看到这么多上传吗? TIME-WAIT 仅消耗内存(以防止与相同(源 ip、源端口、目标 ip、目标端口)元组的新连接)。
  • 是的,我们每分钟上传大约 125 个文件(也许我们的等待时间是 120 秒,我会检查一下)。无论如何,我认为使用 TFTP 应该是解决我们问题的更好方法,因为 tftp 比 ftp 更轻量级。我只需要修补 tftp 服务器以在上传文件时自动创建完整路径。
  • 我添加了一个 TFTP 客户端,结果发现 TFTP 比 FTP 慢得多,因为协议的实现方式,它不会像 FTP 那样打开太多连接,但它有一个非常慢的隐式流控制。所以我回到了我之前的 FTP 实现,试图提高它的性能。

标签: python upload ftp ftplib


【解决方案1】:

这是一个新的答案,基于上一个的 cmets。

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

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

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(包括 Mac)上需要 100 个以上,在不太好的平台上需要十几个,您可能希望使用事件循环设计而不是线程设计,使用 @987654330 @ 在 Linux 上,kqueue 在 BSD 上,以及 IO 完成端口在 Windows 上。这很痛苦,但幸运的是,有一些框架可以为您解决所有问题。 Twistedgevent 是两个流行(并且非常不同)的选择。

gevent 的一个特别好的地方是,您现在可以编写线程代码,只需进行一些简单的更改,就可以将其变成基于事件的代码,就像魔术一样。

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


在评论中,你说:

而且文件是二进制的,所以如果客户端编码与服务器不同,我可能会遇到问题。

请注意,我以二进制模式('rb''wb')打开每个文件,并特意选择了一种协议(网络字符串),该协议可以处理二进制字符串而不试图将它们解释为字符或将嵌入的 NUL 字符视为 EOF 或任何类似的东西。而且,当我使用str.format 时,在 Python 2.x 中不会进行任何隐式编码,除非您将其提供给它unicode 字符串或给它基于区域设置的格式类型,我都没有这样做。 (请注意,在 3.x 中,您需要使用 bytes 而不是 str,这会更改一些代码。)

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


但是如果您想要相反的情况,为目标系统自动传输文本和重新编码怎么办?有三种简单的方法可以做到这一点:

  1. 发送客户端的编码(在顶部一次,或每个文件一次),然后在服务器上,从客户端解码并重新编码到本地文件。
  2. 以文本/Unicode 模式执行所有操作,包括套接字。这很愚蠢,在 2.x 中也很难做到。
  3. 定义有线编码,例如 UTF-8。客户端负责将文件解码并编码为UTF-8进行发送;服务器负责对接收和编码文件进行 UTF-8 解码。

使用第三个选项,假设文件将采用您的默认文件系统编码,更改后的客户端代码为:

with io.open(filename, 'r', encoding=sys.getfilesystemencoding()) as f:
    contents = f.read().encode('utf-8')

在服务器上:

with io.open(filename, 'w', encoding=sys.getfilesystemencoding()) as f:
    f.write(contents.decode('utf-8'))

默认情况下,io.open 函数也使用通用换行符,因此客户端会将任何内容转换为 Unix 样式的换行符,而服务器将转换为其自己的本机换行符类型。

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

【讨论】:

  • 这是一个很好的例子。我明天试试,但问题是我需要很多并发连接(大约有 40 个客户端同时发送文件)。而且文件是二进制的,所以如果客户端编码与服务器不同,我可能会遇到问题
  • 首先:40 对于线程来说并不过分,至少在 linux、Windows 和 Mac/BSD 上不会。如果您将来可能需要几百个,那就是另一回事了。在这种情况下,请考虑使用 epoll/kqueue 事件循环 (linux/Mac/BSD) 或 IO 完成端口 (Windows)。最简单的方法是使用良好的网络框架。如果您从简单的线程代码开始,只需进行少量更改即可将其转换为简单的 gevent 代码。另一方面,您可能想从一开始就扭曲。随便你舒服。重点是自定义协议。
  • 第二:FTP 对编码没有任何作用。它确实对行尾有一些神奇的作用,但在 Python 2.x 中自己做这件事很简单,在 3.x 中并不难。如果您确实需要即时重新编码……我将编辑答案以说明如何进行。
  • 在 FTP 中,我以二进制文件的形式传输,这就是我不需要关心编码的原因。 python示例使用python的字符串函数来格式化输出字符串,所以可能会出现一些编码问题......
  • @filipenf:在 Python 2.x 中,str.format 不进行任何编码,除非您将其传递给 unicode 对象(或使用基于区域设置的格式类型,如 n),我不做。 (这在 Python 3.x 中并非如此,但在 3.x 中你不会使用str,你会使用bytes。)
【解决方案2】:

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

例如,假设您有一个模块filegenerator,其generate() 方法向inotify 注册,将通知排队,yield 将它们一一发送:

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 线程。

【讨论】:

  • 我已经有了线程池(实际上是 3 个)和一个使用 libinotify 的主线程。每个线程都有自己的 FTP 连接,但是对于每个 storbinary,它都会产生一个新的套接字(这就是 ftp 的工作方式,它为被动和主动模式下的传输分配一个新连接)。我想要什么,但我不知道是否有可能仅在第一次调用 storbinary 时生成“传输”连接,并在后续调用中重用。
  • @filipenf:我认为做你想做的事情并不完全合法,因为数据连接是用于你想要一次传输的一批文件......但它可能适用于大多数服务器,至少在被动模式下。另一方面,如果您想批量上传整个目录,我认为合法的。
  • 所以,如果你想利用它,而 ftplib 不能……你需要 fork the source,monkeypatch 它,或者使用不同的 FTP 库。您是否需要这方面的帮助,或者您只是询问ftplib 中是否有一些您找不到的隐藏功能?
  • 最后还是没看出问题。除非您要传输大量小文件,否则设置/拆卸成本与传输成本相比应该是微不足道的。而且,即使您正在传输大量小文件,只要您正确关闭数据连接,无论如何都不会有问题。
  • @filipenf:您是否尝试过按照我之前的建议编写自己的琐碎协议?如果您只需要尽可能快地发送大量小文件(尽管我不认为 1MB 这么小),那么在每个文件之前发送单个大小前缀将很难被击败,整个单个 TCP 套接字。 (如果 TCP 的问题只是因为您打开和关闭 600 个套接字,我不会费心尝试在 UDP 之上构建,因为只需使用一个连续的套接字就可以解决这个问题。)
猜你喜欢
  • 2012-09-18
  • 2012-04-26
  • 2013-03-24
  • 2018-01-05
  • 2012-02-22
  • 1970-01-01
相关资源
最近更新 更多