You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python环境下S3分片上传未完整完成的排查及异常捕获方案

解决S3大文件分片上传不完整且无报错的问题

嘿,我完全懂你这种困扰——大文件上传到S3,偶尔传一半就停了,脚本还不报错,排查起来真的挠头。作为Python新手,咱们一步步拆解问题,搞定它!

为什么会出现“无声失败”?

首先,boto3的upload_file在分片上传时默认会尝试重试,但如果遇到临时网络波动、连接中断,或者某个分片上传失败后没被正确检测到,就可能出现文件传了一部分却没有抛出异常的情况。另外也要排除本地备份文件在上传过程中被意外修改的可能(不过备份文件一般生成后就不动了,这个可能性较低)。

你需要捕获这些异常

目前你只捕获了botocore.exceptions.ClientError,但还有很多可能导致上传中断的异常要覆盖:

  • botocore.exceptions.EndpointConnectionError:S3端点连接失败(比如网络断了)
  • botocore.exceptions.ReadTimeoutError:读取S3响应超时
  • botocore.exceptions.ConnectTimeoutError:连接S3超时
  • OSError:本地文件读取失败(比如文件被删除、权限问题)
  • 通用的Exception:作为兜底,避免漏抓未知异常

另外,ClientError里还包含具体的错误代码(比如IncompleteUpload、InvalidPart),你可以在捕获时打印这些细节,方便定位问题。

核心解决方案:上传后强制校验文件完整性

这是解决“无声失败”最关键的一步!即使脚本没报错,也要主动对比本地文件和S3上文件的大小,或者校验哈希值:

  1. 上传完成后,调用S3的head_object接口获取远程文件的大小
  2. 和本地文件的os.path.getsize()结果对比,如果不一致,就标记为上传失败并告警

完善日志记录

别只依赖Slack通知,把详细日志写到本地文件里,方便事后排查。用Python的logging模块记录:

  • 上传开始/结束时间
  • 本地文件路径、大小
  • S3文件路径、上传后的大小
  • 异常的完整堆栈信息(不是只打印错误消息,要打印traceback)

修改后的代码示例

from progress import ProgressPercentage # class file progress.py
from slack import * # function file for Slack notifications
import glob
import os
import boto3
import botocore
import logging
import traceback
from boto3.s3.transfer import TransferConfig

# 配置日志
logging.basicConfig(
    filename='/path/to/upload_logs/s3_backup.log',
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

bucket = "my-s3-backup"
s3 = boto3.resource('s3')

# 获取最新备份文件
pattern = "/path/to/backup/file/xb_*"
files = list(filter(os.path.isfile, glob.glob(pattern)))
files.sort(key=lambda x: os.path.getmtime(x))
file_to_upload = files[-1]
file_name = file_to_upload.replace('/path/to/backup/file/', '')
key_path = 'physical_db_backups/' + file_name
local_file_size = os.path.getsize(file_to_upload)

# 分片上传函数
def multi_part_upload():
    config = TransferConfig(
        multipart_threshold=1024 * 25, 
        max_concurrency=10, 
        multipart_chunksize=1024 * 25, 
        use_threads=True
    )
    try:
        logging.info(f"开始上传文件: {file_to_upload},大小: {local_file_size} bytes")
        
        s3.meta.client.upload_file(
            file_to_upload, 
            bucket, 
            key_path, 
            Config=config, 
            Callback=ProgressPercentage(file_to_upload)
        )
        
        # 上传后校验文件大小
        remote_file_info = s3.meta.client.head_object(Bucket=bucket, Key=key_path)
        remote_file_size = remote_file_info['ContentLength']
        
        if remote_file_size != local_file_size:
            error_msg = f"文件上传不完整!本地大小: {local_file_size} bytes,S3大小: {remote_file_size} bytes"
            logging.error(error_msg)
            sendslackerror(f"Physical Backup to S3 Failed:\n{file_name}\n{error_msg}")
            return
        
        # 校验通过,发送成功通知
        logging.info(f"文件上传完成: {key_path},S3大小: {remote_file_size} bytes")
        sendslacksuccess(f"Physical Backup to S3 Complete:\n{file_name}")
        
    except botocore.exceptions.ClientError as error:
        error_msg = f"S3客户端错误: {error.response['Error']['Message']},错误代码: {error.response['Error']['Code']}"
        logging.error(f"上传失败: {error_msg}\n{traceback.format_exc()}")
        sendslackerror(f"Physical Backup to S3 Failed:\n{file_name}\n{error_msg}")
    except (botocore.exceptions.EndpointConnectionError, 
            botocore.exceptions.ReadTimeoutError,
            botocore.exceptions.ConnectTimeoutError) as error:
        error_msg = f"网络连接错误: {str(error)}"
        logging.error(f"上传失败: {error_msg}\n{traceback.format_exc()}")
        sendslackerror(f"Physical Backup to S3 Failed:\n{file_name}\n{error_msg}")
    except OSError as error:
        error_msg = f"本地文件操作错误: {str(error)}"
        logging.error(f"上传失败: {error_msg}\n{traceback.format_exc()}")
        sendslackerror(f"Physical Backup to S3 Failed:\n{file_name}\n{error_msg}")
    except Exception as error:
        error_msg = f"未知错误: {str(error)}"
        logging.error(f"上传失败: {error_msg}\n{traceback.format_exc()}")
        sendslackerror(f"Physical Backup to S3 Failed:\n{file_name}\n{error_msg}")

if __name__ == '__main__':
    multi_part_upload()

额外建议

  • 如果你想更精确地校验,可以计算本地文件的MD5,然后和S3文件的ETag对比(注意分片上传的ETag格式是分片MD5拼接-分片数,需要特殊处理,网上有现成的计算方法)
  • 可以给TransferConfig增加重试配置,比如num_download_attempts(上传重试也会用到),提高容错性

内容的提问来源于stack exchange,提问作者DevOpsSauce

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 20:42:41