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上文件的大小,或者校验哈希值:
- 上传完成后,调用S3的
head_object接口获取远程文件的大小 - 和本地文件的
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
相关产品推荐
相关产品推荐

