
一次 GitLab 大仓库 Clone 中断排查从 OpenVPN 到 Nginx 代理超时写在前面这次案例的表象很常见研发在家连 OpenVPN 后从自建 GitLab clone 大仓库时总是在 80% 左右中断。最开始很容易被归因为“办公室网络慢”或者“VPN 不稳定”但实际排查后发现问题的关键点在 Nginx 反向代理 GitLab 时的默认超时配置以及 VPN 场景下长连接传输产生的瞬时停顿。本文不会重点讲 GitLab 如何部署而是围绕一次真实的故障排查过程梳理为什么 Web 页面正常但git clone大仓库会失败。为什么平均下载速度不慢仍然会触发中断。proxy_read_timeout到底控制的是什么。为什么不能为了 clone 直接对整个 GitLab 站点关闭proxy_buffering。最终如何调整 Nginx 配置并避免二次故障。环境与脱敏说明文中已对内部域名、公网/内网 IP、仓库路径、Nginx 日志目录等做脱敏处理保留排查方法、故障链路和处置思路。脱敏项示例占位GitLab 域名gitlab.example.comGitLab 公网 IPgitlab-public-ipGitLab 内网 IP10.x.x.xOpenVPN 服务端 IPopenvpn-server-ip仓库路径example-group/example-frontend-repoNginx 日志目录/data/company/nginx/log/一、问题背景研发同事在家通过办公室 OpenVPN访问自建 GitLab使用HTTP方式 clone 前端等大仓库。办公室内网同一 GitLab、同一仓库HTTP clone长期正常家里 VPNWeb 页面访问 GitLab正常但git clone大包传到约 80% 左右中断该问题很早就存在一度被归因为「办公室网速慢限速以及vpn不稳定的情况」本次在家复现时平均速度约3 MiB/s仍会在中途失败本次复现环境客户端Windows Git BashMINGW64VPN办公室 OpenVPN 线路UDP端口1194服务端openvpn-server-ipClone 地址http://gitlab.example.com/example-group/example-frontend-repo.git请求路径80 端口Nginx 反代 → GitLab 内网http://10.x.x.x:80这类问题比较迷惑的一点是Web 页面可以正常打开说明 VPN、DNS、登录认证看起来都没问题但只要 clone 大仓库就会在传输中后段失败。二、故障现象1. Git 客户端报错Cloning into example-frontend-repo... remote: Enumerating objects: 179, done. remote: Counting objects: 100% (179/179), done. remote: Compressing objects: 100% (103/103), done. Receiving objects: 82% (131906/159078), 781.40 MiB | 3.65 MiB/s error: RPC failed; curl 18 transfer closed with outstanding read data remaining error: 2968 bytes of body are still expected fetch-pack: unexpected disconnect while reading sideband packet fatal: early EOF fatal: fetch-pack: invalid index-pack output2. 关键特征特征说明进度卡在 ~80% 附近非一开始就失败说明链路大部分时间可用平均速度并不慢34 MiB/s排除「整体带宽不足」仅差少量字节如2968 bytes still expected属于连接被提前关闭Web 正常说明 VPN 连通、DNS、GitLab 认证无问题OpenVPN 日志无 AEAD 报错本次复现隧道层无明显解密错误与同事联通线路场景不同从报错看curl 18 transfer closed with outstanding read data remaining的含义是客户端预期还应该继续收到数据但连接被提前关闭了。它不是 Git 仓库不存在也不是认证失败而是典型的传输链路中断。3. 其他同事场景参考部分同事使用其他 VPN 线路时OpenVPN 日志曾出现AEAD Decrypt error: bad packet ID (may be a replay)该场景更偏向UDP 隧道丢包/乱序本次华为 office 线路复现未出现此类日志根因以Nginx 代理超时 客户端背压为主。三、排查过程1. 先排除 GitLab 服务端不可用GitLab 主机负载不高办公室网络 HTTP clone同一仓库正常说明 GitLab 进程、仓库数据本身无持续性故障这一步的意义是先确定问题不在 GitLab 仓库本身。如果同一个仓库在办公室网络可以长期正常 clone说明 GitLab 服务和仓库数据本身没有持续性故障。2. 重新理解 Nginx 的proxy_read_timeoutNginx 默认proxy_read_timeout为60 秒含义是从 upstreamGitLab连续 60 秒读不到任何新数据 → Nginx 主动断开不是「整次 clone 总时长超过 60 秒才超时」。只要数据持续流动计时器会不断重置中间出现 60s 的数据空窗才会触发。这里是本次排查的关键点。很多人看到60s会误以为是“一次 clone 总时长超过 60 秒就会失败”但实际不是。proxy_read_timeout控制的是Nginx 从 upstream 读取两次数据之间允许空闲多久。只要 upstream 持续有数据返回计时器会不断刷新真正触发超时的是中间出现连续一段时间没有新数据。3. 确认请求实际走的是 80 端口 NginxClone 使用http://gitlab.example.com/...对应 Nginx80的server块非 443。原 80 配置片段问题版本server { listen 80; server_name gitlab.example.com; location / { client_max_body_size 800m; keepalive_timeout 600s; proxy_buffer_size 64k; proxy_buffers 32 32k; proxy_busy_buffers_size 128k; proxy_set_header X-Forwarded-Proto https; # HTTP 入口硬写 https建议改为 $scheme proxy_pass http://10.x.x.x:80; # 缺少 proxy_read_timeout / proxy_send_timeout使用 Nginx 默认 60s } }已有keepalive_timeout 600s只影响客户端和 Nginx 之间的连接保活不能替代 Nginx 到 GitLab upstream 的读超时。4. 查 Nginx 日志grep-itimed out\|upstream/data/company/nginx/log/gitlab.example.com.logaccess log 中未搜到 timeout 记录失败与成功时均无。可能原因upstream timed out通常写在error.log不在 access log日志已轮转改配置后问题不再复现不再产生 timeout 日志这一步没有直接在 access log 中找到 timeout 记录但不能因此排除 Nginx 超时。Nginx 的 upstream timeout 通常会写到 error log而不是 access log如果日志已经轮转或者调整配置后不再复现也可能查不到历史错误。5. 在家 VPN 环境复现对比场景结果修改 Nginx 前VPN HTTP clone~82% 中断curl 18首次加全量参数后VPN HTTP clone100% 成功约 1.06 GiB3.17 MiB/s加全量参数后同事访问 Web 登录502 Bad Gateway删除 buffering 相关参数、仅保留超时Web 登录恢复clone 仍正常这组对比很关键加大超时后 clone 可以成功说明方向基本正确但加入过于激进的 buffering 参数后 Web 登录 502说明解决 clone 问题时不能粗暴影响整站请求。四、根因分析1. 直接原因Nginx 反向代理 GitLab 时使用默认proxy_read_timeout 60s无法适应Git smart HTTP 大包 clone长连接上的阶段性停顿中间 60s 无新数据即断开。clone 中断的主因是超时过短不是必须关闭proxy_buffering。2. 为什么 GitLab 负载不高也会触发 60 秒无数据不是 GitLab 进程挂死 60 秒而是 Nginx 在 upstream 连接上连续 60 秒没有完成一次 read。常见机制机制 A客户端通过 VPN 收包变慢引发反向背压家里客户端 ──VPN──► Nginx ──► GitLab 收不动 缓冲满暂停读 GitLabVPN 路径偶发抖动客户端瞬时收包变慢Nginxproxy_buffers约 1MB 很快写满Nginx 暂停从 GitLab 读取GitLab 写满 TCP 窗口也暂停发送upstream 连接上超过 60s 无新 read →proxy_read_timeout 触发Nginx 断开 → Git 报curl 18办公室网络稳定、客户端收包快 → 缓冲不堵 → 从不触发。家里 VPN 平均速度可以很快但80% 附近抖一下就足够触发。机制 BGitLab pack 流本身存在阶段性停顿Git clone 的 pack 流是burst pause对象枚举、压缩、读盘两批数据之间可能 tens of seconds 无新字节。默认 60s 阈值在边界上容易被踩中。机制 CUDP VPN 丢包或乱序会放大问题OpenVPN UDP 出现AEAD Decrypt error时长连接大包更容易失败与机制 A 可叠加。本次华为线路复现未看到 AEAD 日志但机制 A/B 已足够解释。3. 故障链路总结家里 OpenVPN clone 大仓库HTTP → 传输至 ~80% 时客户端或 pack 出现短暂 stall → Nginx proxy_buffers 满 / upstream 60s 无新数据 → Nginx proxy_read_timeout默认 60s断开 upstream → Gitcurl 18 / early EOF五、二次故障clone 修好了Web 登录却 5021. 现象在location /中增加以下全部参数并重载后同事反馈 GitLab 域名登录即 502删除后恢复正常proxy_connect_timeout 300s; proxy_send_timeout 3600s; proxy_read_timeout 3600s; proxy_buffering off; # ← 导致 Web 异常的高风险项 proxy_request_buffering off; # ← 同上2. 502 原因分析502 表示 Nginx 从 GitLab upstream 未拿到合法响应。不是超时时间「太长」导致而是Git clone 优化参数误用在整站location /参数对 clone对 Web 登录/APIproxy_read_timeout 3600s等有益一般无害proxy_buffering off可选优化易引发 502Rails/workhorse 响应与反代不兼容proxy_request_buffering off对 clone 下载几乎无必要POST/上传/OAuth 可能异常GitLab 同一location /同时承载/users/sign_in、/api/* ← Web 登录HTML POST Cookie 302 /*.git/info/refs ← git clone 大包 /assets/* ← 静态资源将proxy_buffering off加在整站clone 可能受益但Web 登录链路会被破坏→ 502。因此clone 断线只需加大 proxy 超时不必整站关闭 buffering。这个二次问题也提醒我们同一个 GitLab 域名下不仅有 Git 大包下载还有登录、API、静态资源等不同类型的请求。不能因为一个场景需要优化就把高风险参数直接加到整站location /中。3. 参数必要性结论参数是否保留说明proxy_read_timeout 3600s保留解决 clone 中间 stall 60s 断开proxy_send_timeout 3600s保留长连接/大 push 更稳proxy_connect_timeout 300s可选连 upstream 慢时用proxy_buffering off整站不要加易 502非 clone 断线主因proxy_request_buffering off不必加HTTP clone 以下载为主与断线关系不大六、最终处置方案1. 核心调整只增加 proxy 超时在location /中仅增加超时不要关闭 buffering不要加Connection location / { # ... 原有 include、add_header、client_max_body_size 等保持不变 ... keepalive_timeout 600s; # 仅加超时解决 git clone 中断 proxy_connect_timeout 300s; proxy_send_timeout 3600s; proxy_read_timeout 3600s; # proxy_buffering 保持默认 on不要 off # 不要加 proxy_request_buffering off # 不要加 proxy_set_header Connection proxy_buffer_size 64k; proxy_buffers 32 32k; proxy_busy_buffers_size 128k; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; # 80 端口勿硬写 https proxy_pass http://10.x.x.x:80; }80 与 443 两份 server 同步修改。校验并重载nginx-tnginx-sreload2. 如果未来仍偶发失败再考虑拆分 Git 路径仅对git 路径单独关 buffering不要动整站location /location ~ ^.\.git(/|$) { proxy_connect_timeout 300s; proxy_send_timeout 3600s; proxy_read_timeout 3600s; proxy_buffering off; proxy_request_buffering off; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_pass http://10.x.x.x:80; }当前现网仅保留超时即可无需此拆分除非复测 clone 仍失败。3. 研发/流水线侧采用分批拉取降低单次传输压力除了调整 Nginx 代理超时外也可以从研发或流水线打包侧降低单次git clone的数据量。对于打包场景很多时候并不需要完整历史记录只需要当前分支的最新代码。这时可以采用浅克隆、单分支拉取、按需拉取等方式减少一次 clone 产生的大包传输从而降低中途 stall 后被代理断开的概率。常见方式如下。只拉取指定分支的最新提交gitclone--depth1--single-branch-bbranch-namehttp://gitlab.example.com/example-group/example-frontend-repo.git如果流水线需要更明确地控制拉取过程也可以拆成init fetch checkoutgitinit example-frontend-repocdexample-frontend-repogitremoteaddorigin http://gitlab.example.com/example-group/example-frontend-repo.gitgitfetch--depth1originbranch-namegitcheckout FETCH_HEAD如果后续确实需要更多历史记录可以再逐步加深gitfetch--deepen100originbranch-name对于特别大的仓库还可以结合 partial clone 或 sparse checkout只拉取打包需要的目录gitclone--filterblob:none --no-checkout http://gitlab.example.com/example-group/example-frontend-repo.gitcdexample-frontend-repogitsparse-checkout init--conegitsparse-checkoutsetneed-build-dirgitcheckoutbranch-name这种方式的作用是减少单次 clone/fetch 的数据量 - 缩短长连接持续时间 - 降低 VPN 抖动或代理空窗触发中断的概率需要注意的是分批拉取是研发/流水线侧的优化和规避手段不能替代 Nginx 代理层的根因修复。如果代理层仍然使用默认 60s 超时大仓库、慢网络或 VPN 抖动场景后续仍可能复现。4. 客户端 Git 配置只能作为辅助gitconfig--globalhttp.postBuffer524288000gitconfig--globalhttp.lowSpeedLimit1000gitconfig--globalhttp.lowSpeedTime600不能替代 Nginx 修改但可缓解低速 stall 被 Git 主动断开。5. VPN 侧优化方向措施说明客户端mssfix 1360缓解 MTU/分片问题提供 TCP 模式 OpenVPN 备选家宽 UDP 不稳时使用Split tunnel仅路由内网/GitLab 网段减少全隧道抖动6. 绕过 Nginx 做对比测试# 仅内网/VPN 可达时gitclone http://10.x.x.x/example-group/example-frontend-repo.git若直连 GitLab IP 成功、走域名失败 → 确认是 Nginx 层问题。七、恢复验证1. Clone 验证在家 VPN 环境复测Receiving objects: 100% (159090/159090), 1.06 GiB | 3.17 MiB/s, done. Resolving deltas: 100% (111303/111303), done. Updating files: 100% (7002/7002), done.同一仓库、同一路径、同一 VPN完整 clone 成功验证加大 proxy 超时可解决断线。2. Web 验证验证项期望浏览器登录gitlab.example.com正常无 502家里 VPNgit clone大仓库仍能完整拉取access / error log登录时段无新增 502最终生效配置location /仅保留proxy_*_timeout不加proxy_buffering off。八、后续建议GitLab 反代标准80/443 的location /只加大proxy_read_timeout/proxy_send_timeout勿整站proxy_buffering off变更后必测两项Web 登录 大仓库 clone避免只验证 clone 忽略 Weberror.log 监控upstream timed out与502不要只在 access log 里搜若 clone 仍偶发断再考虑单独location ~ \.git关 buffering不要动整站流水线打包场景优先使用--depth 1 --single-branch等浅克隆方式减少单次大包拉取VPN 问题与 Nginx 问题可叠加无 AEAD 日志不等于 VPN 无影响九、复盘总结这次问题最值得记录的地方不是简单地把proxy_read_timeout改大而是排查过程中几个容易误判的点。keepalive_timeout≠proxy_read_timeout前者是客户端到 Nginx后者是 Nginx 到 GitLabproxy_read_timeout看的是「连续无数据的空窗」不是 clone 总时长clone 断线主因是默认 60s 太短加大超时即可不必整站proxy_buffering off整站关 buffering 会导致 GitLab Web 502git 优化与 Web 登录不能混在同一location /的激进参数里改 Nginx 后 clone 好了、登录 502典型误伤回退 buffering 相关项只留超时Web 能开、git clone 大包断长连接 反代默认 60s 不匹配平均网速快仍可能失败瞬时 stall 默认 60s 即触发打包流水线可以分批/浅克隆减少单次传输数据量降低长连接中断概率access log 无 timeout 不代表没超时查 error.log以「clone 登录」双验证为准整体来看这类故障的排查思路可以概括为先确认问题发生在哪一段链路 → 再区分是连接问题、认证问题、传输问题还是代理超时 → 修改配置时只改最小必要项 → 验证时同时覆盖原故障场景和普通 Web 场景对运维来说最重要的是不要只看“改完 clone 成功了”还要确认 GitLab 的 Web 登录、API、静态资源都没有被误伤。生产环境中的 Nginx 反向代理往往承载多类请求配置参数的影响范围一定要控制住。附录GitLab 侧超时参数若直连 GitLab 内网 IP 仍失败再查 GitLab Omnibus# /etc/gitlab/gitlab.rbnginx[proxy_read_timeout]3600nginx[proxy_send_timeout]3600gitlab_workhorse[api_max_duration]3600gitlab-ctl reconfigure本次案例在最外层 Nginx 加大 proxy 超时后 clone 恢复去掉整站proxy_buffering off后Web 登录同步恢复。未动 GitLab 本机配置。