Git push GitHub 网络错误排查:分层定位、分批推送与 SSH 配置
很多人第一次把本地代码往 GitHub 远程仓库推的时候都会经历同一个画面git push敲下去光标卡住不动进度条走到 40%、60%然后冷不丁蹦出一行error: 上传失败:网络请求错误再或者fatal: the remote end hung up unexpectedly。手动重试一次可能到 80% 断掉第三次重试反而又过了。你完全不知道刚才那两次到底发生了什么下次遇到还是只能靠运气。这篇就聚焦这件事本身Git 上传文件到 GitHub 远程仓库时反复出现的网络错误卡点究竟在哪一层以及我这些年摸索出来的一套相对可靠的连接与推送方法。文章最后会附一个从空目录到远端仓库出现文件的完整示例命令可以直接照抄如果你的目录里塞了几百兆甚至几个 G 的素材这套思路会更省事。1. 报错信息其实在告诉你卡在哪一层1.1 五层结构从域名解析到引用更新大部分人处理 push 失败的方式是再试一次但 Git 的报错文本其实相当诚实它基本能定位到问题发生在哪一层。我把一次git push拆成五个阶段来看域名解析层把github.com换成一个 IP。失败时报Could not resolve host。TCP 连接层和那个 IP 的 443 端口建立连接。失败时报Connection refused、Connection timed out。TLS 握手层协商加密参数。失败时报SSL_ERROR_SYSCALL、schannel: failed to receive handshake、OpenSSL SSL_read: Connection was reset。HTTP 传输层把打包好的对象流 POST 上去。失败时报error: RPC failed; curl 56、curl 18、HTTP code 411、the remote end hung up unexpectedly。Git 引用层服务端校验完成、更新分支指针。失败时报failed to push some refs、non-fast-forward、remote: Repository not found。这个分层的意义在于能修的层次完全不同。第 1、2 层的问题你改代码没用那是链路本身的事第 4 层的问题可以通过调整配置、缩小包体积来绕过去第 5 层根本不是网络问题是分支状态没对齐你把网络重试一百次也没用。我见过太多人把第 5 层的failed to push some refs当成网络错误在那边反复 push实际上远端有人先提交了你缺一次git pull --rebase。1.2 有代表性的几条报错对照表下面这张表是我自己踩过、也帮别人看过的报错按报错文本 → 所在层 → 通常的诱因 → 我第一时间做什么整理出来你可以对着自己的报错找报错文本片段所在层常见诱因我会先做的动作Could not resolve host: github.com域名解析本地 DNS 配置异常nslookup github.com看能不能解析Failed to connect to github.com port 443TCP 连接本地网络环境受限换网络环境测试确认是不是全局性SSL_ERROR_SYSCALL/schannel: failed to receive handshakeTLS 握手系统 TLS 实现与证书链不匹配排查 Windows 下的 TLS 后端curl 18: transfer closed with outstanding read dataHTTP 传输连接被中途掐断降低单次推送体积curl 56: Recv failure: Connection was resetHTTP 传输传输流被中断换协议、分小批推error: RPC failed; HTTP 411HTTP 传输服务端不接收分块传输调整http.postBufferthe remote end hung up unexpectedlyHTTP 传输同上或大包超时分批 调超时阈值remote: Repository not found引用/鉴权仓库名写错、凭证过期检查 remote 地址与凭证! [rejected] ... (non-fast-forward)引用更新远端有新提交git pull --rebase后再推看这张表你会发现真正的网络问题集中在第 3、4 层而第 4 层占了绝大多数。这也是为什么很多人调http.postBuffer有效、换 SSH 有效——它们改的都是第 4 层的行为。1.3 先做一次不看玄学的自检在动手改配置之前我习惯先跑几条命令把当前状态摸清楚避免瞎改。第一步是看 remote 到底配的是什么git remote -v # origin https://github.com/user/repo.git (fetch) # origin https://github.com/user/repo.git (push)第二步是看这次推送要传多少东西这是最容易被忽略的一步。很多人不知道自己要推的是 20MB 还是 2GB# 看本次待推送的提交数 git log --oneline origin/main..HEAD | wc -l # 看仓库对象占用情况 git count-objects -vH # count: 12 # size: 48.00 KiB # in-pack: 8421 # size-pack: 356.42 MiB -- 这个数字才是关键 # 看当前 HEAD 到远端之间的差异体积 git rev-list --objects origin/main..HEAD | wc -lsize-pack一旦超过两三百 MBgit push失败的几率就会显著上升因为 Git 在推送时需要在本地把对象重新打包成一个 pack 文件再整块 POST 出去中间任何一次抖动都会导致整个连接报废。第三步是确认一下链路本身通不通用 Git 自带的探测方式比 ping 更有意义GIT_CURL_VERBOSE1 git ls-remote origin 21 | head -40这条命令不走推送只做一次轻量的引用列表拉取会把 DNS 解析、TCP 连接、TLS 握手、HTTP 响应头全打出来。如果ls-remote都卡住那问题一定在链路层往下调推送参数是白费力气。提示git ls-remote是我排查任何远端相关问题的第一手段。它足够轻几乎不会因为体积大而失败可以把网络不通和包太大这两类原因干净地分离开。2. 把一次大推送拆成可控的小步2.1 为什么本地先成型、远端后同步更省事绝大多数失败场景本质上是把三件事挤在一次操作里做了写文件、提交、以及传输。一旦传输失败前面两步的成果在心理上就变得不确定于是你容易在慌乱中反复重来。我后来固定成一个顺序先在本地把仓库长成最终形状再考虑怎么把它送出去。这个顺序的好处是跨越失败边界的无论传输失败多少次本地的提交历史是完整的、可验证的。git log、git fsck、git status都能给你确定的答案。剩下的只是一个纯粹的搬运问题而搬运问题是可以换方式、分批次、慢慢磨的不会因为一次失败就前功尽弃。另一个好处是分批推送变得非常自然。因为提交已经在本地了我可以选择先推最老的一批提交成功之后再推下一批每一次推的量都可控。而如果你一边改一边推提交历史是长的、飘的很难切出干净的批次。2.2 我的仓库初始化与提交节奏假设我手上有个素材目录里面是几百 MB 的图片和几个脚本。我的做法是# 1. 在目录里初始化注意不要先在网页上建空仓库再 clone cd my-assets git init -b main # 2. 先把 .gitignore 写好再 git add .顺序很重要 cat .gitignore EOF node_modules/ dist/ *.log .DS_Store Thumbs.db EOF # 3. 第一次提交只放文本类小文件 git add .gitignore README.md scripts/ git commit -m chore: 项目骨架与脚本 # 4. 之后按目录分批提交每批控制在一个可接受的体积 git add assets/images/ git commit -m feat: 补充图片素材 git add assets/models/ git commit -m feat: 补充模型文件这里有个细节值得强调.gitignore一定要在git add .之前落地。我见过太多人先git add .把node_modules整个吞进去提交完之后才发现然后只能删掉重新来。几十万个小文件进了历史git push不失败才怪而且这个历史除非重写否则会一直背着。分批提交的粒度我一般按单批 100MB 到 150MB 打包后体积来控制。怎么估算git count-objects -vH里的size-pack在每次提交后会增长看增量的数量级就够了不需要精确。2.3 分批推送的两种写法提交都在本地之后推送就有讲究了。第一种是按提交逐个推进# 先看本地分支上从远端往后数有哪些提交 git log --oneline --reverse origin/main..HEAD # 逐个推把远端 main 指向某个中间提交 git push origin commit-sha:refs/heads/main # 全部推完之后再推一次完整分支 git push origin main这里用的是本地对象:远端引用的语法不需要在本地真的切出分支直接告诉远端把这个提交放上去。中途某个提交推失败前面的都在远端站住了你只需要重试这一条。第二种是边推边更新本地引用缓存适合提交数量多的情况git fetch origin git push origin main --verbose--verbose会把传输统计打出来包括对象数量、压缩后体积、写入速率。这些数据比进度条有用得多——如果每秒只有几十 KB那基本可以判断链路质量本身不行这时候再怎么调http.postBuffer都没意义该做的是缩小单次体积。注意分批推送时不要用--force。分批本身就是为了不破坏远端状态一旦用强制推送把中间状态盖上去如果最后一批失败远端就停在一个半成品引用上反而更难收拾。3. 真正影响上传成功率的几个 Git 配置3.1 http.postBuffer该不该调调到多少http.postBuffer是这个问题里被引用最多、也被误解最多的一个参数。它的默认值是 1MiB1048576 字节含义是当要 POST 的数据小于这个阈值时Git 会把整个 pack 在内存里拼好带上Content-Length一次性发出去一旦超过阈值就改用 HTTP/1.1 的 chunked 传输一块一块地发。所以早期那个经典的error: RPC failed; HTTP 411报错原因是服务端不接收分块传输解法就是把postBuffer调得比包还大强制走整块发送这条路git config --global http.postBuffer 524288000 # 500MB但这里有两个坑必须说清楚。第一值设得过大毫无意义反而吃内存。Git 真的会在内存里开这么大一块缓冲来组装 pack你设 2GB遇到大仓库时进程可能直接被系统干掉。第二这个参数在 HTTP/2 下基本不起作用因为 HTTP/2 有自己的数据帧分片机制不再依赖 chunked。所以如果你的链路已经在用 HTTP/2调它等于白调。我的实际用法是分两种情况确认走 HTTP/1.1 且报 411 或hung up时临时设成 500MB 试一次其他情况保持默认把精力放在缩小包体积上。这个取舍的逻辑是——缩小包体积是确定有效的调缓冲是概率有效的。3.2 低速断开阈值让卡死的连接早点失败比postBuffer更值得调、却很少人知道的是这两个参数git config --global http.lowSpeedLimit 1024 git config --global http.lowSpeedTime 60含义是如果传输速率低于 1024 字节/秒并且持续超过 60 秒就主动断开这次请求。为什么这个反而重要因为默认情况下 Git 对慢速传输是没有下限的。你看到的那种光标卡住十分钟不动、最后报hung up的情况很可能链路早就死了只是 Git 一直傻等 TCP 超时。主动设一个阈值能让失败来得快一点你也能快一点进入重试或换方案的环节——30 秒失败重试三次比 10 分钟失败一次的成功率要高得多。默认值在不同 Git 版本里不完全一样所以最稳妥的做法是先查git config --get http.lowSpeedLimit git config --get http.lowSpeedTime返回空就说明没设此时按上面的值补上。我自己长期用的是 1024 / 60 这一组在弱链路下重试效率提升很明显。3.3 HTTP 版本、压缩级别与 Windows 下的 TLS 后端HTTP 版本回退如果容器、路由器或某些中间设备对 HTTP/2 的支持不完整多路复用的流会被莫名掐断表现为推送到某个百分比就 reset。这时候强制回退git config --global http.version HTTP/1.1这个改动是可逆的用完可以git config --global --unset http.version撤掉。压缩级别core.compression默认是 -1走 zlib 默认级别约等于 6。如果链路带宽很珍贵、CPU 又富余可以调到 9 让包更小git config --global core.compression 9反过来说如果本地 CPU 是瓶颈比如在旧笔记本上打包几百 MB调到 0 关掉压缩能让打包快不少但传输量会变大。这个取舍要看你带宽和 CPU 哪边更紧张我个人在窄带宽场景下选 9。Windows 下的 TLS 后端Windows 版 Git 默认用系统的 schannel 做 TLS遇到schannel: failed to receive handshake这类报错时切到 OpenSSL 后端经常能直接解决git config --global http.sslBackend openssl关于http.sslVerify这个参数确实存在把它设为false能跳过证书校验。我不建议这么做它等于放弃了对你在跟谁通信的确认。如果确实在某些内网环境下遇到证书链问题正确做法是把对应的根证书导入信任库而不是关校验。3.4 一份可以抄的配置清单把上面这些整理成一份可直接执行的配置按需取用# 基础减少交互避免凭证过期导致的中断 git config --global credential.helper manager git config --global push.default simple git config --global core.autocrlf input # 传输层解决卡死与中途断开 git config --global http.version HTTP/1.1 git config --global http.lowSpeedLimit 1024 git config --global http.lowSpeedTime 60 git config --global http.postBuffer 524288000 # 打包层弱带宽下换更小的包 git config --global core.compression 9 git config --global pack.threads 1pack.threads 1这一条比较特殊默认 Git 会用多个线程并行打包在多核机器上更快但内存占用也更高。在内存吃紧的机器上把它限制成 1 反而能避免打包阶段被 OOM 打断——打包阶段一挂推送自然也就废了。改完之后用git config --list --show-origin确认一下每一项的来源避免全局配置和仓库级配置互相打架。4. 换用 SSH把手握成本降下来4.1 密钥生成与公钥登记当 HTTPS 这条路反复失败时换 SSH 是我第二优先做的事。原因不在于哪种更快而在于 SSH 的连接模型更简单一次认证之后复用长连接没有 HTTP 那一堆分块、缓冲、代理头的中间层出问题的环节少。生成密钥用 ed25519比 RSA 更短更快ssh-keygen -t ed25519 -C your-emailexample.com # 一路回车默认落在 ~/.ssh/id_ed25519 和 ~/.ssh/id_ed25519.pub然后把~/.ssh/id_ed25519.pub的内容整段复制粘到 GitHub 的 Settings → SSH and GPG keys → New SSH key 里。注意是.pub结尾的那个私钥文件永远不要外传。4.2 验证链路与首次连接加完之后第一件事是验证ssh -T gitgithub.com # Hi username! Youve successfully authenticated...第一次连接会提示确认主机指纹输入yes即可这会往~/.ssh/known_hosts里写一条记录。如果这一步卡住不返回用-v看详细过程ssh -vT gitgithub.com 21 | tail -30输出里会明确显示卡在哪一步是 DNS、是 TCP、还是密钥交换。这比猜要快得多。验证通过之后把 remote 换成 SSH 地址git remote set-url origin gitgithub.com:user/repo.git git remote -v # 确认已切换4.3 同机多账号的 config 写法如果你同时用个人账号和团队账号直接生成两把密钥然后靠~/.ssh/config做区分Host github-personal HostName github.com User git IdentityFile ~/.ssh/id_ed25519_personal IdentitiesOnly yes Host github-work HostName github.com User git IdentityFile ~/.ssh/id_ed25519_work IdentitiesOnly yes对应的 remote 地址要写成别名形式git remote set-url origin gitgithub-personal:user/repo.gitIdentitiesOnly yes这一行很关键。不加它的时候SSH 会把本地所有密钥依次试一遍服务端一般限制认证尝试次数试到一半被拒绝是常有的事表现出来就是明明密钥没错却认证失败。提示切换协议之后记得把原来的凭证缓存清一下否则 Git 可能还在用旧的 HTTPS 凭证尝试认证报错信息会变得很有误导性。5. 传到一半断了先别急着重来5.1 确认远端到底收到了什么这是最关键、也最容易被跳过的一步。git push失败之后很多人的第一反应是重新git push但如果远端已经被这次失败的推送改动了状态重来可能会撞上新的错误。先看远端现状git ls-remote origin输出是远端所有引用及其指向的提交 SHA。把refs/heads/main那一行的 SHA 和你本地的比一下git rev-parse HEAD git rev-parse origin/main如果远端的 SHA 还是上次成功推送的那个说明这次失败没有留下任何痕迹直接重推就行。Git 的推送有一个很好的性质服务端只有在收到完整、校验通过的 pack 之后才会更新引用。所以半截上传的包会被丢弃不会污染仓库Git LFS 是例外LFS 对象是单独上传的可能留下孤立对象但那也不影响仓库一致性。5.2 我的重试顺序确认远端干净之后我按这个顺序来每一步都观察结果再决定下一步原样重试一次。链路抖动是随机的第二次就过的概率不低。连着失败两次以上再进入下一步。确认当前 HTTP 版本如果是 HTTP/2 就回退到 HTTP/1.1 重试。调低单次推送体积。用第 2 章说的分批推送把这次的量砍成原来的一半甚至四分之一。调整postBuffer与低速阈值然后重试。换协议HTTPS 换 SSH 或反过来说。这一步会重置所有 HTTP 层的假设是很有价值的对照实验。换本地的打包方式把core.compression调到 9pack.threads设成 1重新打包再推。这套顺序的逻辑是从成本最低、影响面最小的动作开始每一步只改一个变量这样你能知道到底是哪个改动起了作用而不是一通乱改之后碰巧成功下次还得从头猜。5.3 大文件与大二进制目录的专门处理如果你的仓库里本来就是大量二进制素材前面这些手段只能缓解根治要靠结构上的调整。方案一Git LFS。把大文件交给 LFS 管理仓库里只存指针git lfs install git lfs track *.psd git lfs track *.mp4 git add .gitattributes git commit -m chore: 启用 LFS 跟踪大文件代价是远端必须支持 LFS而且一旦某个文件已经进了历史后来才启用 LFS 并不能把它从历史里挪出去得配合历史重写。方案二历史清理。如果历史上曾经误提交过大文件即使用git rm删掉了它仍然躺在历史里每次 clone 和 push 都要带着走。清理用git filter-repo比老的filter-branch快很多# 找出历史里最大的几个对象 git rev-list --objects --all \ | git cat-file --batch-check%(objecttype) %(objectname) %(objectsize) %(rest) \ | awk /^blob/ {print $3, $4} \ | sort -rn | head -20 # 清理指定路径 git filter-repo --path assets/huge/ --invert-paths方案三拆仓库。如果某个目录天然就该独立用git subtree split把它拆成单独的历史单独托管git subtree split --prefixassets/ -b assets-branch git checkout assets-branch git remote add assets-origin 新仓库地址 git push assets-origin assets-branch:main我个人的判断标准很简单单次推送的 pack 超过 300MB就该考虑上面三条中的一条了继续硬推只是在和概率较劲。6. 完整示例从空目录到远端出现文件6.1 示例场景与目录结构假设我手上有一个素材项目结构是这样的总计约 320MBmy-assets/ ├── README.md (2 KB) ├── .gitignore ├── scripts/ │ ├── build.py (6 KB) │ └── convert.py (3 KB) ├── assets/ │ ├── images/ (约 180 MB1200 张 png) │ └── models/ (约 140 MB8 个 bin)目标是把它推到一个新建的远程仓库。直接git init git add . git commit git push是我试过的第一种做法结果就是标题里说的那种情况。6.2 一步步敲下来第一步本地初始化并配好忽略规则cd my-assets git init -b main cat .gitignore EOF __pycache__/ *.pyc .DS_Store *.log EOF git add .gitignore README.md scripts/ git commit -m chore: 初始化仓库骨架与构建脚本这一步只有 11KB 的东西本地提交是瞬间完成的跟网络无关。第二步分批加入素材git add assets/images/ git commit -m feat: 加入图片素材 git add assets/models/ git commit -m feat: 加入模型文件此时先看一眼体积确认心里有数git count-objects -vH | grep size-pack # size-pack: 312.68 MiB第三步本地打一次包确认打包本身不会失败这一步能提前暴露内存问题git gc git repack -adf --window50 --depth50第四步配置远程地址先配 SSH 版git remote add origin gitgithub-personal:user/repo.git git remote -v第五步从最小的一批开始推。先把第一次提交推上去让远端有个合法起点git push origin $(git rev-list --max-parents0 HEAD):refs/heads/main这一条推的是根提交体积最小几乎一定成功。之后逐条推进git push origin HEAD~1:refs/heads/main # 推图片那一批 git push origin HEAD:refs/heads/main # 推模型那一批最后确认远端状态git fetch origin git log --oneline -3 origin/main git status # On branch main # Your branch is up to date with origin/main.到这一步本地和远端就对齐了。6.3 中途断了的那一次我是怎么救回来的第一次尝试的时候我是直接git push origin main推到约 62% 报curl 56: Recv failure: Connection was reset。按前面第 5 章的顺序走先git ls-remote origin看到refs/heads/main仍然指向第一次推上去的根提交说明那次失败没留下痕迹。然后回退 HTTP 版本重试还是断。于是改成分批推送也就是 6.2 里第五步那两条命令。推图片那一批的时候又断了一次这次我没有立刻重试而是先看要推的对象到底有多少git rev-list --objects origin/main..HEAD~1 | wc -l数字比我预想的大说明单批还是太重。于是临时用了更细的粒度——不按提交分而是按路径临时组一批提交出来git checkout -b temp-push # 按目录切分每次只提交一部分图片 git push origin temp-push:refs/heads/main全部推完之后再回到 main用一次快进推送把最终状态对齐git checkout main git push origin main那次最后成功了。事后复盘真正起作用的是三件事把单次推送的对象数量压下来、把 HTTP 回退到 1.1、以及每次失败后先确认远端状态再动手。调参数那些反而是辅助。注意用临时分支做分批推送时远端main的引用会被推成一条看起来不完整的历史。如果你的仓库是多人协作的最好提前打个招呼或者干脆推到一个临时分支上最后用一次合并把结果并进 main。7. 让它长期不再是问题的几个习惯7.1 提交粒度和 .gitignore 前置说到底Git 上传失败的频率和你的提交习惯强相关。我在几个长期维护的仓库里坚持下来的做法是.gitignore永远先写。新建仓库的第一件事就是写忽略规则不要等git add .之后补救。单次提交控制在 50MB 以内。超过这个量就拆拆的成本远低于事后清理历史的成本。每天至少推一次。让本地和远端的差距保持在小范围内避免积累出一个几百 MB 的待推包。二进制素材和大文本分开管理。文档、脚本走主仓库几 MB 的图片可以考虑直接进几十 MB 的模型和视频放 LFS 或单独仓库。这几条听起来像老生常谈但它们直接决定了你面对的是一次几十 MB 的推送还是一次 800MB 的推送而这两件事的失败概率差着数量级。7.2 仓库瘦身与历史清理定期看一眼仓库体积别等它膨胀到难以收拾git count-objects -vH # 本地垃圾回收合并松散对象 git gc --aggressive --prunenow # 检查是否有游离对象 git fsck --lost-foundgit gc --aggressive比较耗时我一般一个月跑一次或者在一次大的历史清理之后跑。日常用不带参数的git gc就够了。如果发现size-pack的增长和实际文件大小严重不匹配说明历史里藏着已经删掉但没清理的大文件按 5.3 的方法用git filter-repo处理。7.3 备用远端与离线快照这是我用得最多、也最推荐的一个兜底手段不要把所有依赖都押在一条链路上。第一招是本地裸仓库做镜像# 在本地磁盘或移动硬盘上建一个裸仓库 git init --bare /Volumes/backup/my-assets.git # 加成一个远端 git remote add mirror file:///Volumes/backup/my-assets.git git push mirror main这个镜像不经过网络永远能推成功。等你网络条件好的时候再从镜像推一份到远程仓库git push origin main第二招是离线快照用git bundle把整个仓库打成一个文件# 打包全部历史 git bundle create my-assets.bundle --all # 之后从 bundle 恢复或克隆 git clone my-assets.bundle my-assets-restored git bundle verify my-assets.bundlegit bundle生成的是单个文件可以随便用什么方式传——U 盘、移动硬盘、邮件附件、内部文件服务都行。到了目标机器上git clone一下历史一条不少。我在带宽条件很差的环境里处理大仓库时经常先打 bundle 把数据搬过去再在那边做一次推送比在这边硬扛传输要快得多、也稳得多。第三招是后台推送加日志避免人在那儿干等nohup git push origin main push.log 21 tail -f push.log这样即使终端断了推送进程还在失败了日志也留着可以回头分析是哪一类错误。最后一招如果你的项目里本就有一部分适合放在国内平台托管把主仓库和备份仓库分散在不同服务商上本身就是一种降低单点故障的实践。远端不是只能有一个。我在这些事情上踩过的坑总结成一句最实在的话能提前拆小的就不要留到传输时解决。网络错误的随机性你控制不了但单次要传多少东西、用哪种协议、远端有几个这些都是你能控制的。把这些控制住之后你会发现那种重试三次靠运气的情况慢慢就不出现了。