python的paramiko模块下载大文件失败问题解决

python的paramiko模块下载大文件失败问题解决

1.发现问题

  使用python的paramiko(2.7.2版本)模块从sftp服务器上面下载文件,发现一个很奇怪的现象,下载大小为29.5M的文件时,没有发现异常,但是下载大小为37.6M的文件时,大概率会出现中途卡住的情况。实际调用方法如下:

python3.6\Lib\site-packages\paramiko\sftp_client.py

class SFTPClient(BaseSFTP, ClosingContextManager):
     ......
	def get(self, remotepath, localpath, callback=None)
	......

发现该问题后,下意识地先在网上进行了相关问题的搜索,搜索良久,找到一些相关的信息,但没有找到具体的解决方案。最后,自己尝试解决问题,下面记录一下相关过程。

2.分析解决问题

  首先,增加了上述方法中callback回调方法打印已经传递的字节数,发现出现问题时每次都是传递到30M左右时卡住不动。于是先从接收数据相关代码开始分析,增加了打印接收数据状态、读取数据状态的信息,发现每次卡住都是在读取数据时一直在那死等,考虑到我是在windows 7 64位机器上验证发现的该问题,不确定在linux上是否也存在该问题,于是在一台centos 7.6 64位的机器上进行了验证,问题依旧,大体判断可能是第三方库paramiko实现逻辑的问题,首先想到是不是出现了死锁,于是仔细查看了相关代码,没有发现什么异常,然后查看从sftp获取数据的相关代码,看到了触发获取数据逻辑如下:

python3.6\Lib\site-packages\paramiko\sftp_file.py

    def _prefetch_thread(self, chunks):
        # do these read requests in a temporary thread because there may be
        # a lot of them, so it may block.
        for offset, length in chunks:
            num = self.sftp._async_request(
                self, CMD_READ, self.handle, long(offset), int(length)
            )
            with self._prefetch_lock:
                self._prefetch_extents[num] = (offset, length)

  直观感觉这个地方可能存在并发过大的问题,作者的注释,也说明了可能存在该问题,保险起见,又看了相关的实现逻辑,最后决定实际验证一下该想法,修改代码如下:

python3.6\Lib\site-packages\paramiko\sftp_file.py

    def _prefetch_thread(self, chunks):
        # do these read requests in a temporary thread because there may be
        # a lot of them, so it may block.
        max_request_num = 512
        to_wait = False
        for offset, length in chunks:
            num = self.sftp._async_request(
                self, CMD_READ, self.handle, long(offset), int(length)
            )
            with self._prefetch_lock:
                self._prefetch_extents[num] = (offset, length)
                if len(self._prefetch_extents) >= max_request_num:
                    to_wait = True
            
            if to_wait:
                time.sleep(1)
                to_wait = False

总体思路就是限制同时存在的异步请求总数,最开始是将max_request_num设置为1024,实际验证发现问题依然存在,有点小失落,以为是前面猜测的问题点不成立,接下来,将max_request_num设置为512,验证发现可以正常下载,稳妥起见,又尝试下载多次,并且下载sftp服务器上其他大文件进行验证,发现都工作正常,至此确定前面猜测的问题点是对的,且该解决方案能解决问题。

3.进一步研究

  问题解决了,有点兴奋,又想到再次上网搜索一下相关信息,这次使用“paramiko 下载大文件”关键字进行了搜索,有一些收获,发现这个问题以前就有人定位了,并且给出了解决方案,相关信息如下,可以进行参考。

需要修改源文件或配置其他参数的方案:
https://bbs.csdn.net/topics/392299886

不需要修改源文件,亲测有效的方法:
https://zhuanlan.zhihu.com/p/102372919
关键信息摘录如下:

        # 旧方法下载大文件会出现Server connection dropped
        # self.sftp.get(filename, filename_fullpath, callback=self.sftp_get_callback)
        
        # 新方法下载大文件成功
        with self.sftp.open(filename, 'rb') as fp:
            shutil.copyfileobj(fp, open(filename_fullpath, 'wb'))

  上述不需要修改源文件的方法,实际测试时,感觉耗时较长,于是进行了具体的量化分析,从sftp服务器下载49.6M大小的文件,使用我修改源代码的方式,耗时5秒,使用上面不需要修改源文件的方式,耗时24秒,性能差别还是很大的,所以,实际使用时,可根据自己的需求选择相应的方式,如果采用修改源代码的方式,可以根据实际情况,调整max_request_num的值,达到最优效果。

4.过程备忘

  出错代码堆栈信息:

File "/data/dev/python/credittools/ftp_download.py", line 48, in _download_single_file
    sftp_handler = sftp_util.sftp_download_file(sftp_cli, remote_file_path, local_file_path, force=force, callback=callback)
  File "/home/tom/work/anaconda3/lib/python3.6/site-packages/creditutils/sftp_util.py", line 184, in sftp_download_file
    sftp_cli.get(sftp_file_path, local_file_path, callback=callback)
  File "/home/tom/.local/lib/python3.6/site-packages/paramiko/sftp_client.py", line 802, in get
    size = self.getfo(remotepath, fl, callback)
  File "/home/tom/.local/lib/python3.6/site-packages/paramiko/sftp_client.py", line 782, in getfo
    reader=fr, writer=fl, file_size=file_size, callback=callback
  File "/home/tom/.local/lib/python3.6/site-packages/paramiko/sftp_client.py", line 678, in _transfer_with_callback
    data = reader.read(32768)
  File "/home/tom/.local/lib/python3.6/site-packages/paramiko/file.py", line 219, in read
    new_data = self._read(read_size)
  File "/home/tom/.local/lib/python3.6/site-packages/paramiko/sftp_file.py", line 182, in _read
    data = self._read_prefetch(size)
  File "/home/tom/.local/lib/python3.6/site-packages/paramiko/sftp_file.py", line 162, in _read_prefetch
    self.sftp._read_response()
  File "/home/tom/.local/lib/python3.6/site-packages/paramiko/sftp_client.py", line 843, in _read_response
    t, data = self._read_packet()
  File "/home/tom/.local/lib/python3.6/site-packages/paramiko/sftp.py", line 201, in _read_packet
    x = self._read_all(4)
  File "/home/tom/.local/lib/python3.6/site-packages/paramiko/sftp.py", line 185, in _read_all
    x = self.sock.recv(n)
  File "/home/tom/.local/lib/python3.6/site-packages/paramiko/channel.py", line 699, in recv
    out = self.in_buffer.read(nbytes, self.timeout)
  File "/home/tom/.local/lib/python3.6/site-packages/paramiko/buffered_pipe.py", line 160, in read
    self._cv.wait(timeout)
  File "/home/tom/work/anaconda3/lib/python3.6/threading.py", line 295, in wait
    waiter.acquire()

你可能感兴趣的:(python,app开发,python,sftp,get)