做开发这几年,几乎人人都被GitHub坑过那么几次:克隆一个大仓库,进度条走到99%卡死;下载几百MB的Release包,动不动就断流重来;网页偶尔能开,偶尔转圈圈。于是很多人把"GitHub镜像站"当成救命稻草——但说实话,网上那些公开镜像站良莠不齐,要么同步不及时,要么有安全风险,根本不适合当核心依赖。本文就从实际运维角度出发,聊聊镜像站到底是怎么建的、有哪几种正宗做法、自建一套到底要动哪些手,以及我踩过的一堆坑。适合想彻底解决GitHub访问体验问题的开发者、技术团队负责人,也适合刚入门想搞懂镜像原理的新手。
1. 先想清楚:镜像站到底要解决什么问题
很多人一上来就喊着"搭个镜像站",但你要问他具体要加速什么,他往往说不清楚。这一点不搞明白,后面方案选型就是瞎摸。GitHub的访问慢,其实慢在三个完全不同的场景,每种场景的解法天差地别。
1.1 代码仓库拉取:慢在增量对象传输
git clone慢,是最经典的痛点。尤其是一些历史悠久的仓库,.git目录动辄几个GB,里面塞满了每次commit的快照。Git在传输时要做对象协商、压缩、增量传输,这个过程的耗时和网络延迟关系极大。
还有个隐蔽问题:GitHub的github.com域名本身解析出来有多组泛播IP,各家运营商、各条线路访问同一组IP的延迟可能差出好几倍。我实测过,在同一台国内服务器上,访问不同IP段的延迟波动能从30ms跳变到300ms。所以仓库拉取慢,不是单纯的带宽问题,而是多轮交互带来了大量往返请求,任何一次握手超时都会导致整体失败。
镜像站解决的就是这个场景:因为仓库内容已经在镜像站上有一份完整的拷贝,用户只需要和镜像站通信,而镜像站和GitHub之间则走机房线路。用户侧网络质量再差,只要到镜像站够快就行;镜像站到GitHub的同步,则可以通过脚本、定时任务、API等多重手段保证。
1.2 Release大文件下载:慢在CDN链路
很多人不知道,GitHub的Release文件下载并不走github.com主站,而是通过objects.githubusercontent.com和release-assets.githubusercontent.com这类专门域名下发。每次下载都是一次大型文件传输,对链路稳定性要求极高,但这恰恰是国内网络的弱项。
这类大文件下载,特点就是"单线程大流量",不太注重多轮交互,更看重连续稳定传输。镜像站对这个场景的解法也很直接:提前把Release文件下载到本地缓存或远端同步存储,用户直接从镜像站拉取,避开漫长且飘忽的国际链路。同步本身可以走断点续传工具或CDN回源,一次性搬到位,剩下的都是国内/局域网的稳定传输。
1.3 网页浏览:慢在静态资源加载
如果你只是想在浏览器里看代码、搜仓库、读Issue,那痛点又不一样。GitHub网页端会加载一大堆JavaScript、CSS、图片资源,这些资源分布在不同的CDN子域名上。网页能不能流畅打开,就看这些静态资源能不能快速加载完。
针对这个场景,有人会做整站静态缓存,有人会专门缓存某些高频访问的静态文件。但说实话,这个场景是三个里面最不建议自建镜像来解的,因为GitHub网页几乎每天都在变,HTML是动态渲染的,静态缓存很容易失效,维护成本极高。真遇到这种需求,多半是临时应急,或者在公司内网搭一层只读缓存,并不适合做成长期公共服务。
2. 四种主流的镜像方案选型
弄清楚要解决哪个场景之后,选型就顺理成章了。我把市面上常见的做法分成四类,各有各的适用场景。
2.1 平台一键导入:Gitee/GitCode镜像
这是门槛最低的做法。国内的代码托管平台大多提供"仓库迁移"或"镜像仓库"功能,你在Gitee或GitCode上创建一个新仓库,选择"从GitHub导入",输入上游仓库地址,平台会自动帮你把远端仓库完整拉一份过来。整个过程不需要命令行,也不需要服务器,三分钟搞定。
但缺点也很明显:一是同步频率不受你控制,有些平台只允许手动触发或低频定时同步,你可能在平台上看到的代码永远是几天前的;二是大仓库导入经常失败,平台有自己的超时和大小限制,那些几百MB到几个GB的仓库很容易卡住;三是平台偶尔还会限制单仓库导入次数。所以这个方案更适合个人临时用,或者用来做公开项目的"读镜像"入口,给访客提供一个快速clone的备选地址,不适合做团队的正式基础设施。
2.2 自建轻量托管:Gitea做镜像底座
Gitea是一个非常轻量的自托管Git平台,单二进制文件就能跑起来,一台1核2G的入门云服务器都绰绰有余。把它和GitHub搭配使用,你可以自己控制同步节奏、同步哪些分支、保存多少历史,还能给每个仓库单独配置权限。
这个方案的核心逻辑是:Gitea这边维护一份仓库的完整镜像,用户访问Gitea而不是GitHub。你既可以用Gitea自带的"迁移仓库"功能手动拉取,也可以在服务器上写脚本做定时同步。对技术团队来说,这种方法最灵活,而且数据完全掌握在自己手里。
2.3 下载资源加速:为Release加缓存
如果你痛点的核心是下载Release压缩包、二进制产物,那真正的思路是给这些资源做缓存。你可以用nginx做一层反向缓存,把高频访问的Release文件缓存到本地磁盘;也可以用专门的下载加速服务,先把文件同步到国内对象存储,再生成一个国内访问的加速地址。
这个方案最"轻",因为它不涉及git协议的复杂交互,只是把"文件搬运"这层做好。操作上也不难:先下载好目标文件,放到nginx的静态文件目录下,配合优良缓存策略即可。但它解决不了clone慢的问题,属于场景定向优化,适合"团队几个人频繁下载同一个大文件"这种明确的需求。
2.4 自动化同步:GitHub Actions定时驱动
把这套东西串起来的,是同步机制。除了自己写cron脚本,另一个很主流的手段就是利用GitHub Actions。你可以新建一个workflow,定时触发,把仓库内容推送到Gitea、Gitee或者任何你指定的远端。
这样做的好处是,同步任务的"执行环境"在GitHub的服务器上,天然拥有和国际网络良好的连接,拉取上游仓库几乎不会失败;坏处是GitHub Actions对仓库有免费额度限制,仓库数量多、体积大的话可能跑得不够频繁或触发次数受限。但作为一个辅助缓解手段,它和自建Gitea配合起来非常舒适。
| 方案 | 解决场景 | 部署成本 | 同步控制能力 | 适合对象 |
|---|---|---|---|---|
| 平台一键导入 | 网页浏览、clone | 最低 | 弱 | 个人临时使用 |
| 自建Gitea镜像 | clone、代码阅读 | 中 | 强 | 技术团队、正式使用 |
| Release文件缓存 | 大文件下载 | 低 | 中 | 高频下载特定资源 |
| Actions定时同步 | 配合前三者 | 中 | 较强 | 已有GitHub工作流 |
3. 实操:从零搭建一个自用仓库镜像站
选定了Gitea + 定时同步 + nginx缓存这个组合之后,下面就是完整的落地过程。我自己用这套组合跑了快两年,服务过团队里几十号人,稳定性和维护成本都可控。
3.1 环境准备:服务器与域名
先准备一台能公网访问的服务器。如果主要用户在国内,建议用国内云厂商的地域节点,毕竟镜像站的初衷就是"离用户近";但如果你的同步逻辑比较重、经常要拉取大仓库,我还额外建议用一台海外节点做"跳板",用来跑同步任务,因为海外节点访问GitHub的稳定性更好。两台机器之间再用内网或公网做仓库同步,能兼顾"拉得动"和"下载快"。
域名方面,如果你用的是国内服务器,并且直接用80端口对外提供下载服务,那域名按规定需要完成ICP备案;如果只是拿到服务器IP自己临时测试,或者使用非标准端口,可以先不备案,但正式使用还是建议把备案搞定,或者退一步用海外服务器绕开这个环节。这些属于常规运维范畴,按各家云厂商指引操作即可。
磁盘要提前规划好。一个仓库的镜像体积大约是仓库原始体积的1.2到1.5倍,因为裸仓库.git里包含所有历史对象。我建议至少准备上游仓库总体积的两倍磁盘空间,给对象压缩、临时文件和未来增长留余量。
3.2 用Docker Compose部署Gitea
Gitea部署很简单,我用的是Docker Compose方式。先创建一个目录,写一个docker-compose.yml:
version: "3.8" services: gitea: image: gitea/gitea:1.22 container_name: gitea restart: always environment: - USER_UID=1000 - USER_GID=1000 - GITEA__server__DOMAIN=git.example.com - GITEA__server__HTTP_PORT=3000 - GITEA__server__SSH_PORT=2222 - GITEA__database__DB_TYPE=sqlite3 - GITEA__service__DISABLE_REGISTRATION=true volumes: - ./data:/data ports: - "3000:3000" - "2222:2222"几个关键点说一下。DISABLE_REGISTRATION我强烈建议设置为true,如果你只是给团队内部当镜像站用,就关闭开放注册,避免陌生人进来乱建仓库占用磁盘。SSH端口映射到2222,是因为宿主机22可能有其他用途,而且Gitea内置SSH和系统SSH冲突很常见,直接换端口更省心。
启动之后,访问http://服务器IP:3000,完成初始化安装。这里有个容易踩的坑:如果域名还没解析或者HTTPS还没配好,初次安装时不要把"域名"字段填成最终正式域名,先填IP或临时域名,等全部配置好之后再通过配置文件改回来。因为这个值会被写进所有仓库的克隆地址里,一旦填错,后面每个仓库的SSH地址都是错的,改起来会非常痛苦。
数据库直接用SQLite就够了。Gitea对SQLite支持得不错,仓库数量不过千、并发不过百的话完全够用,没必要单独再上MySQL/PostgreSQL,少一个组件就少一份维护负担。
3.3 批量同步脚本:把上游仓库完整镜像过来
Gitea部署好之后,核心就是同步。我先说一个思路:不要在Gitea服务器上执行git clone去拉GitHub仓库,除非你有一个离GitHub网络质量很好的跳板。否则同步任务一旦中途断掉,整个脚本就卡住,新增的仓库也无法及时同步。
更好的做法是写一个可重入的同步脚本。这里提供我一直在用的bash版本:
#!/usr/bin/env bash # 上游仓库列表,格式:owner/repo REPOS=( "octocat/Hello-World" "torvalds/linux" "vuejs/core" ) GITEA_URL="http://gitea-host:3000" GITEA_USER="mirror-bot" GITEA_TOKEN="${GITEA_TOKEN}" # 从环境变量读取,不要硬编码 WORK_DIR="/data/mirror-tmp" mkdir -p "$WORK_DIR" for repo in "${REPOS[@]}"; do repo_dir="$WORK_DIR/$(echo "$repo" | tr '/' '_').git" if [ ! -d "$repo_dir" ]; then echo "首次镜像 $repo" git clone --mirror "https://github.com/$repo.git" "$repo_dir" else echo "增量更新 $repo" git -C "$repo_dir" remote set-url origin "https://github.com/$repo.git" git -C "$repo_dir" remote update --prune fi echo "推送 $repo 到 Gitea" git -C "$repo_dir" push --mirror "http://$GITEA_USER:$GITEA_TOKEN@$GITEA_URL/$GITEA_USER/$(basename $repo).git" done这个脚本的逻辑是:首次同步用git clone --mirror拉取完整的裸仓库,之后每次只要remote update --prune就能增量更新,最后用git push --mirror把所有分支、标签、引用列表完整推送到Gitea。
有两个细节容易被忽略。第一,写脚本之前,先要在Gitea里给镜像机器人创建一个单独账号,并且在账号设置里生成一个访问令牌。这个令牌用作push认证,权限只开"写仓库"就够了,不要给管理员权限。第二,目标仓库在Gitea上要提前建好,或者用Gitea API先创建。如果需要全自动,可以在脚本里加一段调用Gitea API创建仓库的逻辑:
curl -X POST "http://$GITEA_URL/api/v1/user/repos" \ -H "Authorization: token $GITEA_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"name\": \"$(basename $repo)\", \"private\": false, \"auto_init\": false}"3.4 nginx文件缓存:加速Release下载
仓库镜像解决了clone问题,但Release附件的下载还是走的GitHub的CDN。为了让团队下载大文件更快,我在镜像站前面加了一层nginx,对Release文件做缓存。
思路是:Gitea本身的仓库元数据和git协议走正常路径;针对Release下载的URL,统一转发到一个缓存层。如果缓存里有文件就直接读本地磁盘,没有就回源到GitHub下载一份,存下来供下次使用。
简化版的nginx配置长这样:
proxy_cache_path /data/nginx-github-cache levels=1:2 keys_zone=gh_cache:50m max_size=50g inactive=30d use_temp_path=off; server { listen 80; server_name download.example.com; location /releases/ { proxy_cache gh_cache; proxy_cache_valid 200 30d; proxy_pass https://github.com; proxy_set_header Host github.com; proxy_set_header User-Agent "Mirror-Bot/1.0"; } limit_req_zone $binary_remote_addr zone=dl_limit:10m rate=5r/s; location / { limit_req zone=dl_limit burst=10; proxy_pass http://gitea-host:3000; } }这个配置里,/releases/路径专门处理Release下载,缓存有效期设成30天。需要注意的坑是:单文件较大时,nginx默认的proxy缓冲可能把整个文件读进内存,导致内存吃紧。最好调整proxy_buffering和相关缓冲参数,让文件直接流式写盘,避免内存抖动。
另外,只缓存Release文件是不够的,大文件下载最怕的就是断流。我给下载响应额外加上了proxy_ignore_headers Expires Cache-Control之类的配置,保证缓存策略只由我的nginx控制,不会因为上游跳过了缓存而白白浪费这一步。
3.5 定时任务与健康监控
同步脚本写完,让它跑起来很简单:加一个crontab条目,比如每两个小时同步一次。
0 */2 * * * /opt/mirror-sync/sync.sh >> /var/log/mirror-sync.log 2>&1但光有cron是不够的,镜像站最大的风险是"同步悄悄失败"——上游仓库被删了、脚本里某个仓库名字写错了、token过期了,任何一个小问题都可能导致某个仓库永远停在旧版本。所以要有监控。
我这里的做法是给脚本最后加一段"变更计数":每次同步后记录新增推送的仓库数和对象数,如果某次同步结果是0更新,就发个日志提醒一下,区别"真的没变化"和"同步任务根本没跑起来"。更进一步,可以用云厂商的监控服务定期探测镜像站首页,一旦返回码不是200就告警到企业微信群或电话。
4. 常见问题与排查技巧实录
这套镜像站跑起来之后,肯定会遇到各种问题。我把这两年实际踩过、处理过的典型问题整理出来,每一个都是我亲手调过的,不是纸面经验。
4.1 镜像不同步或对象缺失
最典型的现象是:Gitea上能看到仓库目录,但Clone下来缺分支,或者某几个tag不见了。这种多半是同步脚本执行了一半崩掉,git push --mirror没跑完。
排查步骤很简单,二话不说先看裸仓库的完整性:
cd /data/mirror-tmp/example_repo.git git fsck --full git show-ref | head -50git fsck --full会检查所有对象的完整性和关联关系,如果发现"missing object"或"broken link",基本可以判定裸仓库本身有问题。这种情况不要试图修复,直接把目录删了,让脚本下一次走完整clone流程。个别对象缺失的修复成本远高于重新拉一次,尤其是大仓库,修复过程可能越修越乱。
另外一个常见原因是我在3.3里提到过的:脚本没有做成可重入。如果同步脚本里用了git clone而没有判断目录是否已存在,第二次运行就会报错退出。所以脚本开头必须判断目录存在与否,这看起来很简单,但能帮你省掉无数垃圾日志。
4.2 大仓库频繁中断
同步一个几GB的仓库时,git clone --mirror在中途很容易因为网络抖动断掉。Git本身虽然支持断点续传,但--mirror配合--depth之类参数时,恢复行为并不总是理想。
我的做法是:优先保证完整clone,而不是为了偷那点时间去做浅克隆。浅克隆看起来能快速拿到最新代码,但后面每次增量更新都需要额外fetch,反而容易把仓库搞成残缺状态。对于超大的历史仓库,更稳的做法是分步骤:先完整clone一次,后续只fetch新增引用,不要在首次同步时就贪快。
还有就是不要并发同步多个大仓库。我试过同时开4个克隆任务,结果每个都慢吞吞,反而是串行跑成功率更高。GitHub也会对瞬时并发请求做限流,串行更贴合上游策略。
4.3 磁盘占用膨胀
跑一段时间后,最容易忽视的就是磁盘慢慢被塞满。裸仓库本身占空间,nginx缓存也在涨,日志文件更是一直膨胀。这三个加起来,一个活跃镜像站跑半年,吃掉一两个T一点也不夸张。
我的建议是给每个仓库的裸仓库目录设置配额和告警,用du -sh /data/mirror-tmp/*定期看谁最大。同时把nginx的max_size设成一个合理值,比如说镜像站总磁盘的一半。日志方面,crontab任务跑起来后会无限增长,别忘了加日志轮转,用系统自带的logrotate按天切分加压缩。
对大仓库还有一个技巧:仓库确实不再需要历史时,可以用git clone --mirror配合git gc --aggressive --prune=now,手动触发一次垃圾回收。但注意只对确定不再有增量的大仓库做,否则白白增加服务器CPU负担。
4.4 认证失败与权限错乱
同步脚本跑着跑着突然报403或401,一般是token的问题。GitHub API token有有效期,Gitea的token也建议定期轮换,一旦过期,所有推送都会失败。
另一个容易踩的坑是Gitea的仓库可见性设置。我用镜像机器人推送时,如果目标仓库是private,但用户看到的克隆地址是公网匿名地址,就会出现"网页能看,但clone要密码"的混乱状态。建议要么都设成公开,要么在团队内部建立标准的SSH访问通道,别混着来。
顺便提一句,如果你在Gitea里改了账号名称,旧的推送地址会全部失效。这就是我为什么推荐用一个固定服务账号(比如名为mirror-bot)来做同步,不要用个人账号,否则人员离职或改名,同步全断。
4.5 缓存命中率低怎么调试
nginx配好缓存后,可以通过响应头和访问日志判断命中情况。在nginx配置里加上add_header X-Cache-Status $upstream_cache_status;,然后直接curl看响应头:
curl -I https://download.example.com/releases/some-file.zip如果返回的X-Cache-Status是MISS,说明没命中;是HIT,说明走了本地缓存。如果长期MISS,第一件事就是检查URL是否稳定:GitHub上的Release文件URL里有签名参数,有时同文件URL都会变,导致缓存永远命不中。解决办法是把URL重写规则做稳,把签名部分从缓存key里剥离,否则你配的缓存就是个摆设。
5. 合规与可持续运维:镜像站的边界
镜像站不是"把别人的东西搬过来盖个章"那么简单,这里头有些原则性问题,值得在动手之前想明白。
5.1 许可证、版权与署名:镜像不是搬家
GitHub上绝大多数开源仓库都带有许可证,比如MIT、Apache-2.0、GPL等。做镜像站的目的是"让更多人方便获取",而不是让你把别人的代码改头换面当成自己的东西发布。镜像仓库务必保留上游的许可证文件和版权声明,一般同步完的完整裸仓库本身就会带上这些内容,所以只要别乱删文件,就没什么大问题。
另外,如果你直接把上游仓库设成private,又在团队内部分发,那就要谨慎核对许可证是否允许这个用途。大多数开源许可证允许复制和分发,但有些带有附加限制,比如不能商用、不能修改后不披露。这种细节最好由团队里懂法务的同事把关,我这边只能提醒一句:别把"镜像"当"完全白嫖"的挡箭牌。
5.2 服务安全:防止镜像站被当成对象存储
镜像站一旦对外公开,就会有人拿它当免费CDN用,把你当成下载源,疯狂拉取大文件。我见过最夸张的,是把一个镜像站当作某个软件的自动更新源,天天几十万次请求,直接把服务器打挂。
所以访问控制和限流一定要从第一天就做好。我在3.4的nginx配置里加过limit_req,方法很简单但很有效。如果服务只面向团队内部,就直接用防火墙或nginx的allow/deny把外网隔离掉;如果真的要公开,那就要有成熟的可观测性和配额体系。不要期待"没什么人知道我的站",你只要一放到公网,扫描器几小时内就会找上门。
另外一个容易被忽略的细节是:镜像站的用户自注册功能一定要关掉,这不光是防滥用,也是防止对方创建大量仓库来耗尽你磁盘。开启组织模式、按项目创建仓库、强制走令牌认证,这些都是低成本但极其有效的运维手段。
跑镜像站这些年,我最大的体会是:它不是一个"装好就完了"的工具,而是一个需要持续运维的基础设施。很多时候你花在调试同步脚本、清理磁盘、修证书上的时间,远比你从镜像站中节省下来的要多。所以动手之前,一定先想清楚自己的场景是不是真的需要一套完整镜像站——如果只是偶尔下载一两个Release包,那直接用官方加速渠道或者临时代理缓存就够用了;只有当你的团队每天高频访问GitHub、clone大仓库、分发资源都变成了日常工作,才值得投入时间来做这套东西。
最后再分享一个小技巧:如果你只是想救急某个仓库的clone速度,根本不用搭整套Gitea,直接在本地先git clone --mirror到服务器,再把服务器上的裸仓库地址发给同事,他们直接git clone http://你的服务器/仓库.git就完事了。这一招省掉了Gitea、nginx、域名这些所有中间层,是镜像站最朴素的形态,也是最容易临时顶上的方案。