PyOne分块上传文件优化小记

回首

PyOne是什么?看之前的内容:

  1. 带你玩转PyOne:onedrive列表工具(一、安装篇)
  2. 带你玩转PyOne:onedrive列表工具(二、使用篇)

问题

话说,自己写了PyOne之后,一直在优化PyOne,在自己和网友的使用过程中,发现并解决了很多问题,本次记录一个令我头疼了很久的问题:那就是在小内存机器上使用PyOne上传大文件时,经常出现进程被killed的问题

这个脚本运行在一个1G内存的机器上,之前这台机器上传大文件(大姐姐)时,经常上传到一半进程就被Killed掉了,一开始怀疑是onedrive的问题。

但是后面在一台大内存机器上上传大文件,却没有这种情况,但是看监控,发现在上传过程中,内存是一直上升的,大内存机器在上传完之前内存没有到达100%,因此不会被killed掉,但是另一台1G内存机器就不行了,还没有上传完毕进程就killed掉了。

因为PyOne上传大文件时使用了递归,因此我怀疑是不是Python在递归上有需要优化的地方,在网上搜了下发现:这不是Python才会出现的问题,只要是使用了递归,不论什么语言,当递归深度太深的话就会出现内存暴增的现象。

假如上传一个2G的视频,分块上传,每块3.125M,那么递归的深度就是:

2048M/3.125M=655.36

也就是说,我上传一个2G文件就会有655层深度?

解决方法

Python有个内存自动回收机制(具体怎么回事,这就难为我了,野生Python爱好者也不在乎),一个对象什么时候会被回收,释放内存呢?简单来说就是这个对象没有被引用时。

来看一下之前的上传函数:

def UploadSession(uploadUrl, filepath, offset, length):
    token=GetToken()
    size=_filesize(filepath)
    offset,length=map(int,(offset,length))
    if offset>size:
        print('offset must smaller than file size')
        return False
    length=length if offset+length<size else size-offset
    endpos=offset+length-1 if offset+length<size else size-1
    print('upload file {} {}%'.format(filepath,round(float(endpos)/size*100,1)))
    filebin=_file_content(filepath,offset,length)
    headers={}
    # headers['Authorization']='bearer {}'.format(token)
    headers['Content-Length']=str(length)
    headers['Content-Range']='bytes {}-{}/{}'.format(offset,endpos,size)
    trytime=1
    while 1:
        try:
            r=requests.put(uploadUrl,headers=headers,data=filebin)
            data=json.loads(r.content)
            if r.status_code==202:
                offset=data.get('nextExpectedRanges')[0].split('-')[0]
                UploadSession(uploadUrl, filepath, offset, length)  #继续下一块上传
            elif data.get('@content.downloadUrl'):
                print('upload success!')
                return data.get('@content.downloadUrl')
            else:
                print('upload fail')
                print(data.get('error').get('message'))
                r=requests.get(uploadUrl)
                if r.status_code==404:
                    print('please retry upload file {}'.format(filepath))
                    requests.delete(uploadUrl)
                    return False
                data=json.loads(r.content)
                if data.get('nextExpectedRanges'):
                    offset=data.get('nextExpectedRanges')[0].split('-')[0]
                    UploadSession(uploadUrl, filepath, offset, length) #继续下一块上传
                else:
                    print('please retry upload file {}'.format(filepath))
                    requests.delete(uploadUrl)
                    return False
            break
        except Exception as e:
            trytime+=1
            print(u'error to opreate UploadSession("{}","{}","{}","{}"), try times {}'.format(uploadUrl, filepath, offset, length,trytime))
        if trytime>3:
            requests.delete(uploadUrl)
            break
    return False

看出什么问题了吗?之前考虑到需要分块上传大文件,因此优先使用了递归函数,重复得调用自己,但是在递归时,这个递归深度就会越来越深,最后OMM。

举个通俗的例子:任何生物都是会衰老死亡的(回收),因此地球上的生物数量在一定程度上是平稳的;但是如果生物是永生的,永远不会死亡,那么地球是不是迟早会挤爆?

可能例子不是很恰当,但是就是这个意思。因此我将这个分块上传函数拆分成了两块:

  • 一个是专门上传内容并返回响应信息的:
def _upload_part(uploadUrl, filepath, offset, length):
  size=_filesize(filepath)
  offset,length=map(int,(offset,length))
  if offset>size:
      print('offset must smaller than file size')
      return {'status':'fail','msg':'params mistake','code':1}
  length=length if offset+length<size else size-offset
  endpos=offset+length-1 if offset+length<size else size-1
  print('upload file {} {}%'.format(filepath,round(float(endpos)/size*100,1)))
  filebin=_file_content(filepath,offset,length)
  headers={}
  # headers['Authorization']='bearer {}'.format(token)
  headers['Content-Length']=str(length)
  headers['Content-Range']='bytes {}-{}/{}'.format(offset,endpos,size)
  try:
      r=requests.put(uploadUrl,headers=headers,data=filebin)
      data=json.loads(r.content)
      if data.get('@content.downloadUrl'):
          print(u'{} upload success!'.format(filepath))
          return {'status':'success','msg':'all upload success','code':0}
      elif r.status_code==202:
          offset=data.get('nextExpectedRanges')[0].split('-')[0]
          return {'status':'success','msg':'partition upload success','code':1,'offset':offset}
      else:
          print(data.get('error').get('message'))
          trytime+=1
          if trytime<=3:
              return {'status':'fail'
                      ,'msg':'please retry'
                      ,'sys_msg':data.get('error').get('message')
                      ,'code':2}
          else:
              return {'status':'fail'
                      ,'msg':'retry times limit'
                      ,'sys_msg':data.get('error').get('message')
                      ,'code':3}
  except Exception as e:
      trytime+=1
      print('error to opreate _upload_part("{}","{}","{}","{}"), try times {}'.format(uploadUrl, filepath, offset, length,trytime))
      if trytime<=3:
          return {'status':'fail','msg':'please retry','code':2}
      else:
          return {'status':'fail','msg':'retry times limit','code':3}
  • 一个是判断响应信息并重复上传操作的:
def UploadSession(uploadUrl, filepath):
  token=GetToken()
  length=327680*10
  offset=0
  while 1:
      result=_upload_part(uploadUrl, filepath, offset, length)
      code=result['code']
      #上传完成
      if code==0:
          break
          return True
      #分片上传成功
      elif code==1:
          offset=result['offset']
      #错误,重试
      elif code==2:
          offset=offset
      #重试超过3次,放弃
      elif code==3:
          break
          return False

效果

就是这么一个简单的优化,效果是很明显的:

很明显的,没有优化之前,上传大文件很容易内存就达到100%,然后OMM

优化之后,上传文件初期会有一段内存上升的区间,但是上升到一定程度后,内存就处于稳定波动了,最后轻松的上传了10G的大姐姐…

本文作者:Abbey

本文链接:https://www.abbeyok.com/archives/54

版权声明:本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0许可协议。转载请注明出处!

WebSocket实时数据获取... <<
0 条评论

请先登陆注册

已登录,注销 取消