Twisted SFTP客户端大文件上传性能问题及优化咨询
解决Twisted SFTP大文件上传性能与超时问题
核心问题分析
你的两次实现本质上都是串行执行每个writeChunk请求:
- 第一个版本是同步读文件+逐个等待
writeChunk的Defer回调完成,才会发送下一个chunk - 修改后的
_async_map虽然用了递归写法,但逻辑还是一样——每次只有前一个writeChunk的Defer完成后,才会触发下一次调用,完全没有利用Twisted的异步并发能力
这种串行方式会把网络往返时间(RTT)无限叠加,700MB的文件需要发送近11200个64KB的chunk,光是等待每个chunk的服务器ACK就会耗尽时间,最终触发Defer超时。而paramiko之所以快,是因为它默认会批量发送多个写入请求,不需要等待单个请求完成再发下一个,充分利用了网络带宽。
方案一:改用Twisted的Producer/Consumer模式(最优解)
Twisted的ISFTPFile本身支持作为IConsumer,你可以用FileProducer直接把本地文件流推给远程SFTP文件,这是Twisted异步IO设计的原生最优方式,会自动处理并发批量写入,避免串行等待的问题。
修正后的代码实现
from twisted.protocols.basic import FileProducer from twisted.internet.defer import inlineCallbacks def _create_target_from_source(target, source): # 打开本地文件作为可读流 with open(source, 'rb') as fp: # 创建FileProducer,自动分块并异步推送(保持你原来的64KB chunk大小) producer = FileProducer(fp, chunkSize=1024*64) # 将producer注册到SFTP文件consumer,自动开始传输 producer.startProducing(target) # 返回Defer,等待传输完成 return target.deferred @inlineCallbacks def _upload(client): mode_write = FXF_WRITE | FXF_CREAT | FXF_TRUNC client_file = yield client.openFile(target, mode_write, {}) yield _create_target_from_source(client_file, source) defer.returnValue(client)
为什么这能提升性能?
FileProducer会在后台异步发送多个chunk请求,不需要等待前一个请求的ACK,充分利用网络带宽- 自动适配Twisted事件循环调度,避免手动串行调用带来的性能损耗
- 原生兼容Twisted SFTP的
IConsumer接口,稳定性更高
方案二:手动并发批量写入(如果不想用Producer/Consumer)
如果你坚持自己控制写入逻辑,可以用defer.gatherResults批量发送多个writeChunk请求,比如一次发10个chunk,等待这批完成后再发下一批,平衡并发量和内存占用:
from twisted.internet.defer import inlineCallbacks, gatherResults def _read_chunks_with_offset_from_file(filename, chunk_size=1024*64): with open(filename, 'rb') as fp: offset = 0 while True: chunk = fp.read(chunk_size) if not chunk: break yield (offset, chunk) offset += len(chunk) @inlineCallbacks def _create_target_from_source(target, source): chunk_generator = _read_chunks_with_offset_from_file(source) batch_size = 10 # 可根据网络情况调整(比如10-20) while True: batch = [] for _ in range(batch_size): try: batch.append(next(chunk_generator)) except StopIteration: break if not batch: break # 批量发送写入请求,并行执行 yield gatherResults([target.writeChunk(offset, chunk) for offset, chunk in batch]) return defer.succeed(True)
关于第二个问题:能否在client.openFile的Defer触发前开始上传?
答案是不行。因为client.openFile是向SFTP服务器发送创建/打开远程文件的请求,只有当服务器返回成功响应(Defer回调触发),你才会拿到有效的ISFTPFile实例(client_file)——没有这个实例,你没有任何合法的句柄来向服务器发送写入请求。
不过你可以优化等待期间的准备工作:比如提前把本地文件的前几个chunk读入内存,等openFile完成后立刻开始发送,但这对整体性能的提升非常有限,核心还是要解决写入阶段的并发问题。
额外排查点
如果用了Producer/Consumer后还是性能不佳,可以检查:
- 你的
chunkSize是否太小(比如默认的8KB太小,改成64KB或128KB试试) - Twisted SFTP客户端的
windowSize配置是否过低,可以在创建客户端时调整:from twisted.conch.client.ssh import SSHConnection # 创建连接时设置更大的窗口大小(比如10MB) conn = SSHConnection() conn.windowSize = 1024*1024*10 - 服务器端的SFTP配置(比如最大并发连接数、传输速率限制)
内容的提问来源于stack exchange,提问作者bartosz
相关产品推荐
相关产品推荐

