ARTICLE DETAIL

资讯详情

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

自建GitHub镜像站:从Nginx反代到仓库同步的完整实践

自建GitHub镜像站:从Nginx反代到仓库同步的完整实践 1. 先想清楚你需要的到底是不是镜像站很多人在GitHub页面转圈圈、git clone龟速、release大文件下到一半断掉的时候第一反应就是我要搭一个GitHub镜像站。但以我搞过几套镜像服务的经验来看这个决定往往做得太急了。镜像站不是万灵丹搭错了方向你得到的只是一个花了三天时间搭建、最后自己都不想用的玩具。1.1 GitHub访问慢的三个真实瓶颈先说现象。GitHub访问慢通常慢在三处网页打开慢、git clone慢、release附件下载慢。这三者的瓶颈其实完全不一样。网页打开慢多半卡在DNS解析和首屏资源加载。GitHub首页和仓库页面依赖大量静态资源这些资源如果每次都要跨洋请求首屏自然慢。git clone慢核心瓶颈在.git仓库数据的传输尤其是大仓库全量历史下来动辄几百MB甚至上GB链路稍有抖动就超时。release附件下载慢则是另一个问题大文件走的是跟网页不同的对象存储链路Socket超时、连接重置都是家常便饭。你如果能判断自己卡在哪一类就知道该搭哪种镜像而不是一股脑全上。1.2 镜像站的三种形态对应三种需求我把镜像站拆成三种形态分别解决上面三个瓶颈第一类是网页与静态资源加速。用Nginx之类的Web服务器做反向转发并缓存静态资源。适合我只是想让网页打开快一点的场景。这类镜像不涉及仓库数据本身实现最简单但要注意缓存策略动态页面、登录态请求不能乱缓存。第二类是仓库数据镜像。这是真正意义上的镜像站把GitHub上的仓库完整同步到自己的服务器提供git clone、git fetch的替代地址。适合我要频繁拉某个仓库不想每次都被网络折磨的场景。Gitea、Gitea的镜像仓库功能或者裸仓库加定时同步都是常用方案。第三类是release大文件缓存。针对github.com/xxx/xxx/releases/download/...这类地址做缓存让大文件下载走自己的服务器。适合我需要经常下载编译好的二进制包的场景。这类缓存的难点在于大文件缓存淘汰策略和Range请求兼容。这三种形态可以单独用也可以组合成一套完整体系。关键是你要先明白自己的需求属于哪一类。1.3 什么情况下别急着自建镜像站不是免费午餐。它需要一台长期在线的服务器、一块够用的磁盘、一个域名和证书以及你持续的维护精力。如果你的实际情况是下面这些我建议你先别折腾只是偶尔打不开GitHub页面刷新一下就能用。这种情况直接换公共镜像站更快没必要自建。只需要拉两三个仓库且仓库不大。手动git clone下来本地维护完全够用不要上来就搞全家桶。没有固定公网IP或服务器。搭出来只能本机访问价值大打折扣。期望搭完就万事大吉。镜像站需要定期同步、清理缓存、盯日志属于持续运营类项目不是一锤子买卖。想明白这些问题之后你再往下看才知道该选哪条路。2. 搭建前的准备硬件、域名与方案选型既然要搭就得按能跑起来且跑得稳的标准来准备。这一节把我踩过的选型坑和估算方法写出来尽量让你一次到位。2.1 算好你的盘子带宽、磁盘与内存估算很多人搭镜像站翻车翻在低估了磁盘和带宽需求。磁盘空间方面GitHub仓库镜像的膨胀系数很惊人。仓库的.git目录体积通常会比工作区大不少而且每次git fetch增量更新都可能带来新的历史对象。按我的经验你要给目标仓库预估出源仓库体积2到3倍的空间才稳妥。如果你镜像的是带release附件的仓库那还要额外算附件存储这部分往往比仓库本身大一个数量级。带宽方面做网页缓存还好做release缓存就要认真算。假设你期望单个用户下载100MB的release包耗时30秒那这条下载链路至少需要约27Mbps的带宽。如果同时有5个人下载峰值就要135Mbps。你自己掂量一下服务器的带宽上限别等到下载并发起来才发现带不动。内存方面Nginx反代加缓存512MB内存的机器完全能跑。如果上Gitea建议至少2GB内存因为Gitea自带Web界面、数据库和Git后台进程内存太小会在仓库同步时频繁交换内存导致卡顿。2.2 域名、证书与DNS配置顺序域名方面我强烈建议用一个独立的域名或子域名来提供镜像服务比如git-mirror.example.com。不要直接拿主域名裸跑因为镜像站的访问量和日志量都不小独立域名方便你做访问控制、证书管理和后续迁移。证书方面直接上Lets Encrypt免费证书泛域名证书更省心。如果你打算同时提供git clone的HTTPS地址和release下载的HTTPS地址泛域名证书可以一证多用。注意配置自动续期Lets Encrypt证书有效期90天忘了续期会导致镜像站突然不可用——这个坑我踩过。DNS配置的顺序也有讲究。我建议先把域名解析到服务器IP确认解析生效后再申请证书之后再启动Nginx并加载证书。如果顺序反了容易在Nginx启动时报证书不存在的错误排查半天。2.3 三条技术路线横评这一节把主流方案放在一起对比。我实际用过Nginx反代方案和Gitea方案对象存储缓存方案我见过同事搭过也参考过它的设计思路下面表格结合了这些实践。方案核心机制适用场景维护成本典型局限Nginx反代缓存网页静态资源与release文件缓存个人/团队网页访问加速、下载加速低改配置文件即可不支持仓库语义无法提供稳定的git clone地址裸仓库镜像定时同步git clone --mirror 定期fetch针对少量特定仓库的clone加速中需要写同步脚本每个仓库单独管理规模化后脚本复杂Gitea镜像仓库Gitea内置仓库镜像功能大量仓库的集中镜像管理中高依赖Gitea自身运维资源占用偏高需要配套数据库对象存储下载缓存网关大文件转存到对象存储release/大附件下载频次高的场景高需要额外存储服务只解决大文件问题不覆盖仓库镜像如果你只需要解决clone太慢选裸仓库镜像最直接。如果你想做的是一个看起来像GitHub分站的东西能给团队提供网页浏览和clone入口选Gitea。如果你只是被release大文件下载折磨那就用Nginx缓存大文件路径就够了没必要上Gitea。3. 动手第一步Nginx反向代理与页面缓存这一节从最轻量的方案讲起。Nginx反代加缓存两个小时就能跑通适合作为整套镜像体系的第一块拼图。3.1 最小可用的反向代理配置先给出一份可以跑通的基础配置。我以自己的服务器实践为例假设镜像服务域名为git-mirror.example.comHTTPS证书放在/etc/nginx/ssl/下。proxy_cache_path /var/cache/nginx/github levels1:2 keys_zonegithub_cache:10m max_size20g inactive60d use_temp_pathoff; server { listen 443 ssl; server_name git-mirror.example.com; ssl_certificate /etc/nginx/ssl/example.com/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/example.com/privkey.pem; location / { proxy_pass https://github.com; proxy_set_header Host github.com; proxy_set_header X-Real-IP $remote_addr; proxy_cache github_cache; proxy_cache_key $scheme://$host$request_uri; # 从源站缓存而不是从磁盘缓存返回过期数据 proxy_cache_lock on; proxy_cache_valid 200 301 302 10m; # 关键透传Range请求给源站 proxy_pass_request_headers on; proxy_set_header Range $http_range; } # 忽略特定路径的缓存避免缓存到动态内容 location ~ ^/(login|logout|session|search) { proxy_pass https://github.com; proxy_set_header Host github.com; proxy_cache off; } }这份配置的思路很简单所有请求转发到github.com响应按URL缓存。但注意我把login、logout、search这几个路径排除掉了原因后面细说。3.2 缓存规则公开静态资源可以缓存登录态绝不能碰Nginx缓存最忌讳的就是缓存到不该缓存的内容。GitHub的页面分两类公开仓库的静态页面和登录用户的个性化页面。前者是公开数据缓存了没问题还能大幅提高访问速度后者涉及用户会话一旦缓存轻则显示错乱重则把A用户的私有信息返回给B用户——这绝对不可接受。所以缓存的生效范围要严格限定。我的做法是只对公开仓库的常规路径做缓存对任何带/session、/login、/logout的请求直接透传。另外很多读者会忽略HTTP响应头里的Set-CookieNginx默认对带Set-Cookie的响应不做缓存这是从源头上保护你的最后一道防线。不要为了追求缓存命中率而去关掉这个默认行为。还有一个细节GitHub的git clone走的是https://github.com/owner/repo.git这类路径响应内容是git协议数据。这类动态数据不能按静态页面缓存策略处理否则会出现缓存内容错乱。如果要给clone加速请用下一节的仓库镜像方案不要硬塞在Nginx缓存里。3.3 Release大文件的下载缓存release下载是另一个高频痛点。GitHub的release附件实际存储在objects.githubusercontent.com等对象存储域名上最终下载地址会302跳转。如果你只想缓存最终的文件内容需要让Nginx跟踪跳转。我的做法是在反代配置里加上proxy_set_header Range $http_range配合proxy_cache实现对大文件的分块缓存。这样用户下载时如果中断了浏览器重新发起带Range的请求Nginx能直接从缓存返回剩余部分实现断点续传体验会好很多。大文件缓存的淘汰策略要单独调。默认的inactive60d对频繁更新的小型release没问题但如果你镜像的仓库发布很频繁旧版本的大文件会一直躺在缓存里占用磁盘空间。我建议对大文件路径单独设置inactive参数比如30天不访问就淘汰同时配合下面的清理任务定期清理过期文件。4. 仓库数据镜像让clone和fetch变快Nginx缓存解决的是网页和下载的提速。但如果你的真实痛点是git clone慢那还得靠仓库数据镜像。这一节说两种方案裸仓库镜像和Gitea镜像仓库。4.1 裸仓库镜像一条命令拉全量历史裸仓库镜像的思路极其简单——在服务器上克隆一份完整的.git裸仓库然后让用户从这个地址clone。# 在服务器上执行 git clone --mirror https://github.com/owner/repo.git /data/mirror/repo.git--mirror参数会拉取所有分支、标签和远程引用等同于给源仓库做了一个完整备份。之后用户就能这样加速clonegit clone https://git-mirror.example.com/owner/repo.git初次clone时直接与GitHub源同步全量历史拉完之后后续更新靠增量fetch。增量fetch的脚本可以这样写#!/bin/bash cd /data/mirror/repo.git git remote update --prune把这个脚本交给cron或systemd timer定期执行就能保持镜像仓库基本跟源同步。4.2 用Gitea托管镜像仓库如果你要镜像的仓库很多手动管理一堆裸仓库会越来越吃力。这时候Gitea这类自托管Git服务更合适。Gitea内置了仓库镜像功能操作路径很简单在Gitea后台新建仓库时选择从GitHub镜像填入源仓库URL即可。Gitea会定期拉取源仓库的数据并在仓库页面上显示此仓库为镜像的标记。用户浏览代码、切分支、看标签体验跟GitHub几乎一样clone地址也统一指向你自己的服务器。我实际用Gitea镜像过大约30个仓库稳定性不错。但要注意两点一是Gitea的镜像同步是串行的仓库多了之后同步一轮可能要几个小时。二是Gitea的镜像同步只同步git数据不同步release附件。想缓存release附件还得搭配第一节里的Nginx大文件缓存。4.3 自动化同步与增量更新不管用裸仓库还是Gitea同步频率都取决于你对数据新鲜度的要求。我自己用的是每小时同步一次。太频繁会消耗Gitea的连接数和GitHub那边的资源不划算太稀疏会导致用户clone到过期数据。关键经验是先fetch验证再对外暴露。用裸仓库方案时我会先执行fetch确认引用更新成功后再让Nginx切换到新仓库目录。如果你让服务直接指向正在fetch的仓库目录用户在fetch中途clone可能拿到半同步状态的引用造成一致性错误。systemd timer比cron更适合干这个因为可以精准控制任务间隔、日志输出和失败告警。一个简单的timer配置如下[Unit] DescriptionMirror Sync Timer [Timer] OnCalendarhourly Persistenttrue [Install] WantedBytimers.target配合对应的service文件日志统一走journald排查问题比cron发邮件方便多了。5. 排错实录搭建过程中最容易踩的五个坑镜像站搭起来容易但让它长期稳定运行坑都在细节里。下面五个问题是我实际遇到过的逐个说现象、排查思路和解决方案。5.1 子模块拉取失败要镜像的不仅是主仓库现象用户clone镜像仓库成功但执行git submodule update --init时报错提示无法访问子模块地址。根因很多仓库用了submodule子模块地址写的是源站https://github.com/owner/child.git。用户从你的镜像站clone主仓库后git会按源地址去访问子模块如果用户访问GitHub本身就慢这一步就会卡死。解决方案镜像主仓库的同时一并镜像所有子模块仓库并确保子模块地址也指向你的镜像站。手工做这件事很繁琐我建议写脚本解析.gitmodules文件把所有子模块URL替换成镜像地址。预防在镜像仓库的README或访问页面上明确提示使用前请先设置git的url.mirror.insteadOf重写规则我用的规则是git config --global url.https://git-mirror.example.com/.insteadOf https://github.com/这样连子模块的源地址也会被自动重写到镜像站省去逐个替换的麻烦。5.2 大文件断流Range请求与缓存冲突现象release大文件下载到一半断开重新下载又从0开始无法断点续传。根因Nginx的proxy_cache默认不会缓存带Range请求头的响应或者说缓存了但重新请求时处理不当导致缓存无法命中用户每次都要重新拉全量数据。解决方案需要确保proxy_cache支持Range请求。Nginx官方推荐的做法是设置proxy_cache_revalidate on并确保上游响应包含ETag或Last-Modified头。我实测过GitHub的release下载响应支持Range配置后断点续传就正常了。再补一个细节如果你发现Range请求始终不命中缓存检查一下请求头是否被Nginx吃掉了。proxy_set_header Range $http_range这行必须显式声明否则Nginx默认不会把客户端的Range头传给源站。5.3 证书链不完整导致git clone报错现象git clone https://git-mirror.example.com/owner/repo.git时报错SSL certificate problem: unable to get local issuer certificate。根因我最初只部署了域名证书没有把中间证书链合入fullchain.pem导致部分git客户端无法验证证书链直接拒绝连接。这和镜像站本身无关纯粹是证书配置不完备。解决方案Lets Encrypt默认签发的fullchain.pem已经包含完整链但如果你用自签证书或其他证书签发工具一定要把服务器证书中间证书按顺序拼接成fullchain.pem。验证方法是openssl s_client -connect git-mirror.example.com:443 -servername git-mirror.example.com看输出的证书链是否完整Verify return code是否为0 (ok)。5.4 缓存过期策略太激进的后果现象网页访问速度上去了但用户反馈代码更新了页面还是旧的。根因缓存有效期设置太长。我一开始给HTML页面设了proxy_cache_valid 200 10m但这会导致仓库文件列表、README等内容最长有10分钟延迟。如果调用频率不高10分钟延迟是可接受的但如果你的团队拿它当开发入口10分钟延迟非常难受。解决方案把HTML页面的缓存时间降到1分钟或者直接按响应头里的Cache-Control来缓存不要自己硬设。GitHub自身的响应头对静态资源、页面、API给出了不同的缓存控制指令尊重这些指令比拍脑袋设置更准确。5.5 磁盘被缓存撑爆现象跑了半个月后Nginx报错No space left on device服务直接不可用。根因proxy_cache的max_size设置得太大或者inactive时间太长导致磁盘被缓存文件占满。我遇到过release大文件缓存把/data分区塞满的情况当时40GB的缓存目录里躺着好几个大型安装包。解决方案把max_size设置为磁盘容量的60%到70%留出系统、日志和仓库数据的空间。同时加一个每日清理任务find /var/cache/nginx/github -type f -mtime 30 -delete这个任务每天凌晨跑一次清理30天未访问的缓存文件。当然Nginx自身的淘汰机制也会处理但手动清理能确保磁盘水位在可控范围内。6. 上线后的日常维护与安全加固镜像站搭好只是开始真正考验人的是它上线之后的稳定运行。这一节说说我日常做哪些维护以及安全上要注意的底线。6.1 监控清单日志、缓存命中率与磁盘水位我建议至少盯三个指标。第一个是缓存命中率。Nginx的$upstream_cache_status变量可以区分命中、未命中、过期等状态你可以在access_log里加上这个字段然后统计命中率。如果命中率长期低于30%说明缓存策略有问题用户其实是在直连源站镜像站白搭了。可以临时停用镜像让用户直接访问源站没有实际损失的场景下不要为一个低命中率的镜像花太多电费。第二个是同步成功率。Gitea的镜像仓库同步失败时会在仓库页面上显示异常标记但你不可能天天人工翻页面。我的做法是写脚本检查Gitea后台的同步任务日志失败时通过Webhook通知到群聊或邮箱。第三个是磁盘水位。用脚本监控磁盘使用率超过80%就告警。磁盘满了对镜像站是致命打击缓存写不进去、仓库同步中断服务处于半瘫痪状态。别等到满了才处理。6.2 磁盘清理与缓存淘汰磁盘管理是镜像站运维里最日常的工作。除了前面说的Nginx缓存清理还要注意仓库数据本身的增长。Gitea的仓库镜像同步会不断累积git对象如果源仓库经常触发rebase、force push历史对象会大量堆积仓库体积持续膨胀。我的做法是每季度对重点镜像仓库做一次垃圾回收git gc --aggressive --prunenow注意操作前先停掉该仓库的同步任务避免与回收操作冲突。Gitea后台也提供仓库维护功能可以直接触发垃圾回收比命令行的容错性更好。6.3 访问控制与安全底线镜像站本质上是一个对外开放的只读数据服务。安全底线分三层强制HTTPS、限定访问范围、保持系统更新。HTTPS没得商量git clone和网页访问都应该只走443端口。我遇到过有读者图省事直接开80端口跑明文下载的代码包在中途被注入脚本这种事故一旦发生整个团队都会受影响。访问范围要看你的使用场景。如果镜像站只给团队用建议在防火墙层面限制来源IP或者在Nginx层做Basic Auth。如果做的是个人公开镜像至少要对写请求彻底拒绝——镜像站只提供只读服务任何写路径都应该关闭。Gitea后台要关闭用户注册功能否则任何人都能注册账号并创建仓库这会耗尽你的磁盘和数据库资源。最后系统安全补丁要跟上。镜像站核心依赖的Nginx、Gitea、Git都存在历史漏洞保持系统和软件包更新是最基本的安全习惯。我见过很多镜像站被人薅去做恶意下载中转原因就是跑的是几年前的Nginx配置老漏洞随手一打就开。我在实际使用中的一个体会是镜像站最怕的不是搭不起来而是搭起来之后当成一次性项目丢在那里。定时同步要盯、缓存要清、证书要续、日志要看这套流程跑顺之后镜像站才能真正成为你日常开发里那个不起眼但可靠的基础设施。如果你只是自己偶尔用一用做好第三、四章的轻量方案就够了如果打算给团队用那就认真按第六章的维护清单来运营。
返回列表