ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Git over SSH 传输慢?瓶颈诊断、连接复用与按需克隆实战

Git over SSH 传输慢?瓶颈诊断、连接复用与按需克隆实战 1. 慢在哪先搞清楚瓶颈是网络还是协议用 SSH 连远程仓库git fetch、git pull、git push卡半天进度条一格一格挪甚至干脆卡在Resolving deltas或者Writing objects不动——这几乎是每个用 Git 做日常协作的人都遇到过的场景。我最早碰到这个问题是在一个跨地域协作的项目里本地仓库和远端隔着几千公里一次git pull要等两三分钟改一行代码推一次心态直接崩。后来花了不少时间翻日志、抓包、换协议、调参数才算把这套东西摸清楚。这篇文章想聊的就是这件事SSH 模式下 Git 传输慢的成因以及我实测下来真正有效的那几类解决办法。它适合所有需要频繁和远程仓库打交道的开发者不管你是刚学会git clone的新手还是天天跑 CI 的老手都能从这里找到能用上的东西。需要说明的是我不会给你一堆理论上可能有用的偏方只讲我自己验证过、并且能解释清楚原理的方案——因为 Git 传输慢的原因往往不是一个而是好几个叠加在一起找错方向调半天参数也是白搭。先建立一个基本认知Git over SSH 的完整链路是本地 Git 进程 → SSH 加密通道 → 远端 Git 服务进程可能是 git-receive-pack / git-upload-pack→ 磁盘 IO。这条链上任何一环出问题都会表现为慢但表象都是进度条不动所以第一步永远是定位瓶颈在链路的哪一段而不是上来就改配置。下面几节会按定位 → 优化传输 → 优化服务端 → 规避场景的顺序展开这也是我实际排查时用的顺序。2. 先做诊断几个能快速锁定瓶颈的命令2.1 用 GIT_TRACE 和 GIT_SSH_COMMAND 看清时间花在哪Git 自带了一个非常好用但很少人用的调试开关环境变量GIT_TRACE。它会把 Git 内部各个阶段的耗时打印出来尤其是连接建立、协商、传输这几个环节。用法很简单GIT_TRACE1 GIT_TRACE_PACKET1 git fetch origin输出里会看到类似trace: run_command: ssh -o ...这样的行后面跟着时间戳。我一般会重点看两个东西一是从开始执行 ssh 命令到连接真正建立之间的间隔二是从开始传输到传输完成之间的间隔。如果第一段间隔特别长比如十几秒说明问题在SSH 连接建立阶段多半和 DNS 解析、认证方式、服务端的反向解析有关如果第二段特别长那才是真正的数据传输慢得看网络带宽、压缩、协议版本这些。这个区分特别重要因为这两类问题的解法完全不一样。我见过太多人明明是连接建立慢却去折腾http.postBuffer之类的传输参数方向从一开始就错了。还可以配合time命令做粗粒度测量time git ls-remote originls-remote只做连接和列举引用不传实际对象。如果这个命令就要好几秒那你的瓶颈基本可以确定在连接建立而不是数据量上。我当时测出来ls-remote单独跑要 8 秒多而真正的 fetch 只多了 2 秒问题一下就锁定了。2.2 用 ssh -v 拆解握手过程中的每一个停顿在 Git 命令里临时指定一个带-v的 SSH 命令可以看到握手细节GIT_SSH_COMMANDssh -v git ls-remote origin-v会打印出debug1:开头的详细日志里面能看到 DNS 解析、TCP 连接、密钥交换、认证这几个阶段分别花了多久。我印象很深的一次是日志里卡在debug1: Authenticating to github.com:22 as git后面足足停了五六秒最后查出来是服务端在做一个反向 DNS 查询超时导致的。这种问题你在 Git 层面怎么调都没用。顺带说一句GIT_SSH_COMMAND这个环境变量在调试时特别好用因为它可以临时覆盖 SSH 参数不用去改全局的~/.ssh/config。调完之后直接不管它就行不会污染你的长期配置。2.3 一个简单对照换 HTTPS 跑一次诊断的最后一步用 HTTPS 协议跑同一个仓库的拉取git ls-remote https://github.com/用户名/仓库名.git如果 HTTPS 明显比 SSH 快那问题很可能出在 SSH 这一层加密协商、认证、连接复用如果两者差不多慢那更可能是网络链路本身的带宽或延迟问题或者是仓库体积太大。这个对照实验成本极低但能帮你砍掉一半的排查方向。3. 连接建立阶段为什么每次都要等好几秒3.1 DNS 解析和 TCP 握手的隐形成本很多人没意识到每执行一次 Git 命令Git 都会新起一个 SSH 进程去连远端这个连接是短命的用完就关。也就是说git pull一次就要经历一次完整的 DNS 解析、TCP 三次握手、SSH 密钥交换、用户认证。如果这些环节每个都要几百毫秒到几秒累积起来就非常明显。~/.ssh/config里有个参数能帮上忙Host github.com HostName github.com User git Compression yes ServerAliveInterval 30 ServerAliveCountMax 6但我个人更推荐先看看是不是 DNS 拖了后腿。可以在本机直接ping一下仓库域名看解析时间如果解析慢考虑在/etc/hosts里写死 IP注意仓库服务商的 IP 可能会变写死有风险这点后面会讲。此外SSH 的GSSAPIAuthentication在某些环境下会尝试做 Kerberos 认证白白等好几秒直接在~/.ssh/config里关掉Host * GSSAPIAuthentication no这一条我几乎在所有新机器上都会加上尤其是公司内网环境关掉之后连接建立时间能从五六秒降到不到一秒效果立竿见影。3.2 复用长连接ControlMaster 是收益最大的一招如果说有一招能让 SSH 连接速度产生质的飞跃那就是SSH 连接复用也就是ControlMaster。它的原理是第一个 SSH 连接建立后把它作为一个主连接留在后台后续所有到同一主机的 SSH 连接都直接复用这个已经建立好的通道不再重新做握手和认证。配置写在~/.ssh/config里Host * ControlMaster auto ControlPath ~/.ssh/sockets/%r%h-%p ControlPersist 600记得先建目录mkdir -p ~/.ssh/sockets。三个参数的含义分别是auto表示自动判断是否作为主连接ControlPath指定复用套接字的位置用%r%h-%p模板可以区分不同用户/主机/端口ControlPersist 600表示主连接在最后一个会话关闭后继续保留 600 秒供后续命令复用。实测效果我这边第一次git fetch还是要走完整握手但紧接着的第二次、第三次几乎瞬间开始传输ls-remote从 8 秒降到 0.3 秒左右。因为你日常开发时 Git 命令是一个接一个跑的这个复用带来的体感提升非常夸张。注意Windows 下原生 OpenSSH 对ControlMaster的支持不完全一致某些版本会报 Control socket ... already exists 之类的错误。如果遇到可以先确认自己用的是哪个 SSH 客户端或者干脆改用 Windows Terminal 里配合 Git Bash 的 OpenSSH。3.3 换用 Unix 套接字一些平台特有的提速技巧在一些部署脚本里如果你是通过 SSH 到远程服务器再执行 Git 操作可以考虑让 Git 通过 Unix 域套接字和远端服务通信——不过这个场景偏服务端日常开发用得少这里只作为一个可能方向提一下。对绝大多数人来说ControlMaster已经是连接建立阶段性价比最高的方案了。另外补充一个细节如果你在用 VSCode 的 Remote-SSH 连远程服务器做开发那个扩展本身也会维护一条 SSH 连接但 Git 命令行走的仍然是独立进程两者不共享连接池。所以即便 VSCode 的 SSH 是通的你在终端里跑git pull照样是冷启动。意识到这一点之后我一般会在开始工作前先跑一条git ls-remote把连接热起来。4. 数据传输阶段让 pack 更小更快4.1 理解 Git 传输的对象packfile 和 delta搞清楚连接问题之后如果还是慢那就是实际传输的数据量太大。这里需要理解 Git 的传输机制fetch和push传输的不是一个个独立文件而是打包好的packfile。Git 会把你需要的对象做差量压缩delta 压缩把相似的对象用引用和偏移表示从而减小体积。传输慢要么是网络带宽不够要么是 pack 本身太大。有个反直觉的点值得说pack 压缩是会消耗 CPU 的。服务端在准备 pack 时要做大量比对和压缩计算如果你的仓库很大、历史很乱服务端准备阶段就可能耗掉几十秒这段时间里客户端进度条一动不动看起来像网络卡住实际是服务端在算。判断方法用GIT_TRACE看客户端是否已经进入传输阶段如果没进入那慢在服务端准备。4.2 深度控制和浅克隆按需减少历史最有效的减量手段是不要拉取不需要的历史。Git 支持浅克隆git clone --depth 1 gitgithub.com:用户名/仓库名.git--depth 1表示只拉最近一次提交没有历史。对于只是要编译、部署、看最新代码的场景这能把传输量降到原来的一小部分。我有个前端项目仓库历史很长完整克隆要 400MB 以上浅克隆直接降到 30MB 左右clone时间从几分钟变成十几秒。已经在本地的大仓库也可以做浅化git fetch --depth 1 origin main不过要提醒一句浅克隆的仓库在后续操作上有限制比如某些merge、blame、log操作会受影响因为它没有完整历史。所以它适合特定场景CI 构建、临时查看不适合日常主力开发仓库。日常仓库我更倾向用局部克隆git clone --filterblob:none gitgithub.com:用户名/仓库名.git--filterblob:none表示只拉提交和树对象不拉实际文件内容文件内容在真正 checkout 时按需下载。这对大仓库非常有效而且后续操作基本不受影响是我现在克隆大仓库的默认姿势。4.3 压缩和批处理的取舍SSH 层本身有压缩Git 也有自己的压缩配置两者不要盲目叠加。SSH 的Compression yes对文本为主的对象有效但对已经压缩过的二进制比如图片、压缩包基本没收益反而浪费 CPU。所以你如果仓库里有大量二进制资源开着 SSH 压缩可能还不如关掉。Git 侧可以关注core.compression默认 -1也就是用 zlib 默认级别一般不用动。真正值得调的是pack.threads它控制打包时的并行线程数机器核心多的话可以提高git config --global pack.threads 4但注意这主要影响本地打包比如git gc、git repack对fetch的传输速度提升有限。我看到网上很多人推荐改这个来提速 pull实测下来效果不明显别抱太大期望。4.4 push 慢的特殊性远端钩子是隐形杀手push慢和fetch慢的成因不完全一样。push 时客户端要把本地对象打包发给服务端服务端接收后还要做校验、更新引用并且触发服务端的钩子hooks。很多团队在服务端配置了 pre-receive、post-receive 之类的钩子做代码检查、部署、通知这些钩子如果写得低效比如同步调用了一个慢接口你 push 时就会卡在最后一步。判断方法看 push 输出如果卡在Writing objects那是传输慢如果卡在remote:开头的那些输出之后那是服务端在处理客户端无能为力。这种情况只能推动服务端优化钩子比如把耗时操作改成异步。我踩过这个坑一个仓库 push 每次都卡十几秒最后发现是服务端的通知钩子在调一个响应很慢的接口。另一个 push 相关的配置是http.postBuffer但那个只对 HTTPS 有效SSH 模式下完全不起作用。经常有人 SSH 下 push 大文件报错去改这个参数纯属白费劲。5. 服务端与仓库本身那些你改不了但能规避的慢5.1 仓库膨胀历史里的巨型文件如果服务端准备 pack 特别慢一个常见原因是仓库历史里存在巨型文件。哪怕你后来把这个文件删了它依然留在历史对象里每次 clone/fetch 都要重新打包传输。判断仓库体积可以用git count-objects -vH看size-pack那一项。如果发现远大于当前工作区实际大小那历史里大概率有垃圾。这种情况的处理需要重写历史git filter-repo之类但这在共享仓库上是高风险操作会改变所有提交的哈希必须团队协调、提前通知所有人重新克隆。所以我不建议个人贸然操作而是先反馈给仓库维护者。日常能做的规避是本地不要提交大文件二进制资源用专门的存储方案管理.gitignore写扎实。这些习惯能防止仓库在你手里继续膨胀。5.2 服务端口和代理配置绕开拥堵链路有些自建 Git 服务比如内网的 GitLab会因为网络路径、代理、防火墙规则导致传输慢。这种时候可以考虑更换访问入口比如走不同的端口或者不同的地址。SSH 默认走 22 端口某些网络会对 22 端口做策略限制可以尝试服务提供方开放的备用端口。如果是通过公司网络访问外部仓库网络出口的带宽和策略往往是你控制不了的变量。我在一个项目里遇到过工作时间段 Git 特别慢、午休和晚上正常的情况基本可以判定是出口带宽被办公流量挤占了。这种情况下能做的有限要么错峰操作要么让大传输放到后台跑。5.3 SSH 配置项里那些真正有用的开关除了前面提到的ControlMaster和关闭 GSSAPI还有几个 SSH 参数值得调配置项作用我的建议值Compression传输压缩文本仓库 yes二进制多则 noServerAliveInterval保活心跳间隔30ServerAliveCountMax心跳失败容忍次数6TCPKeepAliveTCP 层保活yesGSSAPIAuthenticationKerberos 认证尝试noControlMaster连接复用autoControlPersist复用保留时长600这一组配下来对连接建立阶段和长连接稳定性都有明显改善。ServerAliveInterval配合ServerAliveCountMax能防止传输大 pack 时因空闲被中间设备掐断——传输大仓库时如果进度条突然中断并报连接重置多半就是这个原因。6. 常见问题速查与避坑心得6.1 常见现象对照排查表现象可能原因优先尝试每条 Git 命令都等好几秒连接建立慢GSSAPI 或 DNS关 GSSAPI配 ControlMaster卡在 Resolving deltas 很久服务端准备 pack 慢浅克隆 / 局部克隆卡在 Writing objects上行带宽不足或 pack 大分批提交检查网络传输中途连接重置空闲被掐断配 ServerAliveIntervalpush 卡在 remote 输出后服务端钩子慢推动服务端优化钩子只有某台机器慢该机器网络或 DNS 特殊对照 HTTPS查本机 DNS这张表是我从自己的排查记录里整理的覆盖了绝大多数我遇到过的场景。建议先按现象对号入座基本能省掉一半的试错时间。6.2 我踩过的几个坑第一个坑是盲目改http.postBuffer。刚接触这个问题时看网上说 push 大文件失败就改这个我照着做了结果 SSH 场景下毫无作用。后来才明白这个参数只走 HTTP 通道SSH 根本不读它。这个教训让我养成一个习惯调参数前先确认它属于哪条链路。第二个坑是在/etc/hosts里写死仓库 IP。一开始确实快了但仓库服务商的 IP 是可能变的某天突然就连不上了排查半天才发现是 hosts 里那条过期记录。现在如果一定要用 hosts我会加上注释和记录时间定期检查。第三个坑是在共享仓库上乱动历史。有次为了瘦身一个内网仓库我差点直接在共享的 origin 上跑历史重写命令还好在最后关头意识到这会让所有同事的本地仓库失效。共享仓库的历史改动永远是团队级决策个人不要擅自做。6.3 一套可以照着抄的收敛配置综合下来我现在新机器上的标准配置是这样的。~/.ssh/configHost * GSSAPIAuthentication no TCPKeepAlive yes ServerAliveInterval 30 ServerAliveCountMax 6 ControlMaster auto ControlPath ~/.ssh/sockets/%r%h-%p ControlPersist 600Git 全局配置里加上git config --global core.preloadindex true git config --global core.fscache true git config --global gc.auto 256core.preloadindex和core.fscache在 Windows 上对本地操作帮助明显gc.auto调大能减少自动 gc 打断的频率。克隆大仓库时我默认用--filterblob:none需要完整历史时才去掉这个参数。6.4 一点额外提醒别把调试配置留成长期配置调试阶段用GIT_TRACE、ssh -v这些都是临时的调完记得清掉否则大量日志会拖慢正常操作。GIT_SSH_COMMAND这种如果写进了 shell 配置文件而忘了删以后每条 Git 命令都会带上额外参数反而可能引入新问题。我自己就遇到过因为环境变量里残留了调试参数导致某台机器上 Git 行为诡异的案例排查了好久才想起这茬。说到底Git over SSH 慢这件事没有万能解药它是一组分层的问题连接建立、数据量、服务端处理每一层都有对应的解法。我自己的经验是先把ControlMaster和关 GSSAPI 这两条基础配置做上能解决大半日常卡顿剩下的大仓库慢靠按需克隆把数据量降下来再剩下的服务端问题就不是客户端能单方面解决的了。真正有效的排查永远从先看清瓶颈在哪一段开始——这一点想明白了比记住任何一条参数都值钱。
返回列表