服务器上的脚本和数据越来越多,每天备份到 OneDrive 越来越慢,跨国上传经常超时断线。这篇记录我把 2.5GB 备份压缩到 312MB、传输从超时变成秒传的完整过程。
痛点:备份越来越慢,最后干脆失败 我的服务器每天凌晨自动跑全量备份到 OneDrive(用 Microsoft Graph API)。最开始一切正常,但随着时间推移:
/root/.hermes 目录越来越大
Python 虚拟环境、Node 依赖、系统快照、回收站……这些备份根本不需要的东西 也一起被打包
打包文件膨胀到 2.5GB+
跨国直连上传,碰上大文件经常超时,被网关掐断
即使没超时,大分片(10MB+)一抖动就丢包,重试率高得离谱
最后索性直接失败。
第一步:分析为什么这么大 我先看了下备份内容到底是啥:
1 2 tar -tzf /tmp/last_backup.tar.gz | head -50 du -sh /root/.hermes/* | sort -hr | head -10
发现真正占空间的是这些”垃圾”:
目录
大小
是不是真的需要备份?
venv
~500 MB
❌ 重新装就行
node_modules
~800 MB
❌ 重新 npm install 就行
.hermes/node
~300 MB
❌ 系统级二进制
.hermes/lsp
~150 MB
❌ 语言服务器
.hermes/state-snapshots
~1 GB
❌ 运行时快照
.hermes/trash
~50 MB
❌ 回收站
.hermes/checkpoints
~200 MB
❌ 历史 checkpoint
真实业务数据
~300 MB
✅ 必须备份
结论:真正需要备份的只有约 12% ,其他全是”环境 + 缓存 + 历史”。
第二步:手术刀式排除(核心优化) 在 tar 命令里精准加入 --exclude 参数:
1 2 3 4 5 6 7 8 9 tar -czf backup.tar.gz \ --exclude='*venv*' \ --exclude='*node_modules*' \ --exclude='.hermes/node' \ --exclude='.hermes/lsp' \ --exclude='.hermes/checkpoints' \ --exclude='.hermes/state-snapshots' \ --exclude='.hermes/trash' \ /root/.hermes /root/scripts
实测效果 :
项目
优化前
优化后
缩减率
打包体积
2.5 GB
312 MB
88%
打包时间
~30 秒
< 5 秒
83%
上传耗时
超时失败
8 分钟
∞
体积直接打了 1 折,效果立竿见影。
几个细节 :
*venv* 用通配符匹配各种虚拟环境(venv、.venv、myenv 等)
--exclude 可以写多次,每次一个模式
注意 exclude 的顺序 :tar 是按命令行顺序匹配的,靠前的规则先生效
排除 state-snapshots 后,再加个清理逻辑 :保留最近 3 个快照,旧的删掉
第三步:解决上传超时 打包体积小了,但跨国上传还是不稳定。继续优化。
关键点 1:免代理直连 我服务器上配置了 Xray 代理(127.0.0.1:10808),Python 的 requests 库默认会读环境变量 HTTP_PROXY,导致 OneDrive 走代理上传,反而更慢 。
解法:建一个专属的 Session,关掉 trust_env :
1 2 3 4 import requestsdirect_session = requests.Session() direct_session.trust_env = False
这样 OneDrive 的 Graph API 调用 100% 走物理直连。
关键点 2:5MB 黄金分片 OneDrive 上传大文件需要分片,原本我用的是 10MB 分片。改成 5MB 后:
单个分片小 → 抖动丢包的影响小
重试成本低 → 单分片重传只要几秒
并发友好 → 可以同时上传多个 5MB 分片
1 CHUNK_SIZE = 5 * 1024 * 1024
关键点 3:重试 + 抖动退避 加上重试逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import timedef upload_with_retry (session, url, data, max_retries=5 ): for attempt in range (max_retries): try : response = session.put(url, data=data) if response.status_code in (200 , 201 , 202 ): return response except requests.exceptions.RequestException as e: print (f"上传失败 (第 {attempt+1 } 次): {e} " ) time.sleep(3 + attempt) raise Exception(f"上传失败,已重试 {max_retries} 次" )
退避时间设置也很重要:
第 1 次重试:等 3 秒
第 2 次重试:等 4 秒
第 3 次重试:等 5 秒
…
避免重试风暴给服务端压力。
第四步:完整的优化脚本 下面是核心上传逻辑(简化版):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 import osimport tarfileimport requestsimport timeONEDRIVE_BACKUP_PATH = "backups/full_backup" CHUNK_SIZE = 5 * 1024 * 1024 def create_backup_tar (source_dirs, output_file ): """打包 + 排除垃圾""" excludes = [ "*venv*" , "*node_modules*" , ".hermes/node" , ".hermes/lsp" , ".hermes/checkpoints" , ".hermes/state-snapshots" , ".hermes/trash" , ] with tarfile.open (output_file, "w:gz" ) as tar: for source in source_dirs: tar.add(source, arcname=os.path.basename(source), exclude=lambda name: any (ex in name for ex in excludes)) return output_file def upload_to_onedrive (file_path, access_token ): """分片上传到 OneDrive""" session = requests.Session() session.trust_env = False session.headers.update({ "Authorization" : f"Bearer {access_token} " , "Content-Type" : "application/octet-stream" }) file_size = os.path.getsize(file_path) file_name = os.path.basename(file_path) create_url = f"https://graph.microsoft.com/v1.0/me/drive/root:/{ONEDRIVE_BACKUP_PATH} /{file_name} :/createUploadSession" resp = session.post(create_url, json={"item" : {"@microsoft.graph.conflictBehavior" : "replace" }}) upload_url = resp.json()["uploadUrl" ] with open (file_path, "rb" ) as f: offset = 0 while offset < file_size: chunk = f.read(CHUNK_SIZE) end = offset + len (chunk) - 1 headers = { "Content-Length" : str (len (chunk)), "Content-Range" : f"bytes {offset} -{end} /{file_size} " } for attempt in range (5 ): resp = session.put(upload_url, data=chunk, headers=headers) if resp.status_code in (200 , 201 , 202 ): break print (f"分片 {offset} -{end} 重试 {attempt+1 } /5" ) time.sleep(3 + attempt) else : raise Exception(f"分片 {offset} -{end} 上传失败" ) offset = end + 1 print (f"已上传 {offset} /{file_size} bytes ({100 *offset//file_size} %)" ) return True tar_file = create_backup_tar(["/root/.hermes" , "/root/scripts" ], "/tmp/backup.tar.gz" ) print (f"打包完成: {os.path.getsize(tar_file) / 1024 / 1024 :.1 f} MB" )upload_to_onedrive(tar_file, access_token="..." ) print ("✅ 上传成功" )
实测结果
指标
优化前
优化后
打包体积
2.5 GB
312 MB
打包时间
30 秒
< 5 秒
上传耗时
超时失败
8 分钟
重试次数
50+
0
成功率
60%
100%
打包时间从 30 秒缩到 5 秒,传输成功率 100%。
一句话总结 备份不只是”把东西打包传上去”,更重要的是”只备份值得备份的东西” 。先做排除规则(按目录/模式),再做传输优化(直连 + 小分片 + 重试),这两步做完一般都能把跨国备份从”经常失败”变成”稳定秒传”。
如果你也在搞自动化备份,评论区聊聊你踩过的坑~