ARTICLE DETAIL

资讯详情

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

自建GitHub镜像站:Nginx反代与Release下载缓存实战

自建GitHub镜像站:Nginx反代与Release下载缓存实战 大概每个和代码仓库打交道的人都经历过这种场景git clone一个知名深度学习仓库进度条跑到90%以上眼看就要完成突然报一个fatal: early EOF然后从头再来打开某个项目的Release页面下载按钮转了半天最后给你跑到一个堪比拨号时代的下载速度。次数多了人就会开始想与其忍不如自己动手搭一套 GitHub 镜像站。镜像站这个东西市面上有很多现成的方案但大多数只解决某一个场景比如单一网页加速、单一release下载加速。如果你有自己的服务器、有自己的使用场景完全可以自己搭建一个定制化的镜像站把连不上的问题、带宽瓶颈问题一次解决掉。这篇文章就基于我自己的实践把从域名规划、Nginx反代、git clone层镜像到release下载缓存的全过程写下来。适合有Linux操作经验、想给自己或团队搭一套稳定镜像入口的读者参考也适合那些正在纠结到底该镜像哪一层的人先看完再做决定。1. 动手之前先想清楚镜像站到底要镜像哪几层很多人一上来就想把整个github.com反代到我自己的域名上这个思路很容易翻车。原因很简单GitHub并不是一个单域名站点它是一大群域名的集合。页面、代码仓库、静态资源、下载文件分别跑在不同子域上DNS解析到不同IP走不同链路。只反代一条路最后大概率做出一个半残的镜像站。1.1 GitHub站点的三层结构比想象中要散我自己的理解里GitHub大致可以分为下面几层层级典型域名承载内容是否建议镜像页面层github.comHTML页面、仓库浏览、搜索、登录跳转可选静态资源层github.githubassets.comJS、CSS、图片、图标跟着页面层一起做Git数据层github.comgit clone、fetch、push依赖的智能HTTP协议强烈推荐下载层codeload.github.com、raw.githubusercontent.com、objects.githubusercontent.comRelease附件、tar.gz打包下载、Raw文件、LFS对象强烈推荐用户嘴上说打开GitHub但浏览器和git客户端在背后同时和多个子域建立连接。镜像站点只处理了github.com通常会出现两种情况网页能打开但样式全丢因为JS和CSS从github.githubassets.com加载失败Release页面能显示但点下载没反应因为真正的下载被重定向到codeload.github.com或者objects.githubusercontent.com而你的镜像完全没有覆盖这些域名。1.2 多数人真正需要的其实是git clone和release下载页面层虽然是最显眼的但我得说一句大实话对绝大多数个人开发者和小团队来说最痛的往往是两件事——仓库clone不稳定、release文件下载太慢。页面浏览这件事反而可以接受偶尔卡顿。如果你是开发者日常动作是clone、pull、看代码那么git数据层的稳定访问才是刚需。如果你是运维需要定期把上游几个依赖仓库同步到内网那么需要的是仓库镜像加内网托管。如果你只是想能快速下载某个二进制、某个训练好的模型文件release下载缓存是性价比最高的投入。页面级镜像适合团队内部做代码在线阅读、快速浏览但登录、发issue、提交pull request这类动态交互几乎没法镜像因为涉及GitHub的session和cookie体系反代之后很容易出现串号或写操作失败。所以搭镜像之前先问问自己我三个月内最频繁的操作是什么这个问题想清楚了再决定做哪一层的镜像。下面我就按这几个方向把搭建过程拆开讲。2. 基础设施与域名规划先想好这几点再动手2.1 服务器、磁盘、带宽三个硬指标镜像站其实不挑CPU挑的是磁盘和带宽。先说磁盘。Git仓库裸镜像和release缓存都会吃磁盘而且吃得很凶。一个热度中等的仓库裸镜像体积在几百MB到几个GB之间release缓存就更夸张动辄几十GB甚至上百GB因为很多项目会把几百MB的安装包、模型权重挂在Release下面。我的建议是如果条件允许单独挂一块1TB数据盘给缓存目录用如果只是小团队内部自用256GB也能转但你要在配置里把缓存上限压住否则跑几个月磁盘就满了。然后是带宽。镜像站的体验上限取决于你到GitHub源站的回源链路质量也取决于用户到你服务器的链路质量。如果你只是自己或团队几个人用10Mbps起步20~50Mbps体验就很不错如果你准备拿它做公共镜像那带宽成本会迅速变成最扎心的问题一台服务器的出口带宽很快会被下载流量占满。最后是服务器位置。我记得第一次搭的时候把服务器放在了一个离用户很近但到GitHub源站绕路的机房结果发现回源慢得离谱镜像站反而比直连还难受。这里给一个比较简单的经验服务器位置要同时考虑用户到它、它到GitHub源站这两段链路。服务对象在国内的大陆服务器通常需要先确认备案条件不想碰备案就选大陆以外、网络距离相对近的机房。动手之前先测链路质量用ping和curl -w %{speed_download}多测几次别想当然。2.2 子域名规划建议一次性到位如果你的镜像目标是尽量完整那域名规划建议直接按层级拆开。我用的是这种结构gh.example.com→github.comassets.example.com→github.githubassets.comraw.example.com→raw.githubusercontent.comcodeload.example.com→codeload.github.comobjects.example.com→objects.githubusercontent.comavatars.example.com→avatars.githubusercontent.com拆开的好处有三个。第一每个子域可以单独配缓存策略比如assets域里的静态文件URL带hash可以放心缓存很久objects和codeload是大文件下载需要单独设缓存目录和淘汰策略不能和JS、CSS混在一起。第二每个子域名可以分别观察流量和错误日志出问题的时候能最快定位到具体是哪个环节堵了。第三证书、回源地点、limit_rate限速都能各调各的互不影响。api.github.com不建议做镜像。原因是API接口大量携带token、身份信息你把API反代出去等于把用户身份校验的边界搅浑了安全风险很高而且GitHub对API有严格限流反代并不会帮你规避这个限制只会给自己找麻烦。2.3 HTTPS证书与备案问题每个子域都要配HTTPS证书。域名多最省事的办法是申请一张通配符证书比如*.example.com这样所有子域共用一张后续新增子域也不用重新签。发行工具我用的是acme.sh配合Lets Encrypt三个月自动续期一次。这里有一个容易被忽略的坑证书续期之后很多人忘了reload Nginx导致证书文件已经更新了但进程还在用旧证书镜像站到时间就悄悄失效。所以后面我会专门讲一下续期触发Nginx reload的配置。大陆服务器的安全责任中域名解析是需要先完成ICP备案的这个是玩服务器的基础操作不展开细说。如果你没有备案条件就优先选大陆以外区域来部署。3. 页面层反向代理配置让用户能正常打开你的镜像站3.1 最小可跑通的Nginx配置如果你决定先做页面层镜像那么下面这段配置是最小可运行版本。它把所有请求原样转发给github.com保留原始路径。server { listen 443 ssl; server_name gh.example.com; ssl_certificate /data/ssl/example.com/fullchain.pem; ssl_certificate_key /data/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_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_ssl_server_name on; proxy_ssl_protocols TLSv1.2 TLSv1.3; proxy_read_timeout 60s; proxy_connect_timeout 10s; } }注意一个细节proxy_pass https://github.com;结尾是没有斜杠的。这个写法和带斜杠的写法含义完全不同——带斜杠时Nginx会把location匹配到的前缀替换掉不带斜杠时则会把完整原始URI原样传给上游。对整站镜像来说我们要的是后者。proxy_ssl_server_name on;这行看起来不起眼但缺了它大概率会挂。原因是Nginx作为TLS客户端去连GitHub的时候如果不开SNI握手阶段就不会携带github.com这个域名GitHub那边看到的就是一个IP直连请求很可能会被拒绝或者握手失败。我在早期踩过这个坑日志里全是SSL handshake failed排查半天才想起来是SNI的问题。3.2 页面层的其他子域怎么补页面能打开只是一个开始。你会很快发现样式是乱的这时检查浏览器Network面板多半能看到github.githubassets.com的超时请求。处理方式就是在同一个Nginx里再加一个server块server { listen 443 ssl; server_name assets.example.com; ssl_certificate /data/ssl/example.com/fullchain.pem; ssl_certificate_key /data/ssl/example.com/privkey.pem; location / { proxy_pass https://github.githubassets.com; proxy_set_header Host github.githubassets.com; proxy_ssl_server_name on; proxy_cache assets_cache; proxy_cache_valid 200 24h; } }这里的proxy_cache_valid 200 24h可以放心大胆用因为assets域的静态资源URL基本都带内容hash文件内容变了URL就会变同一个URL对应的内容在长时间内是稳定不变的。raw.githubusercontent.com、codeload.github.com、objects.githubusercontent.com这几个域名也都是同样的套路换成各自域名和缓存策略就好。头像域avatars.githubusercontent.com技术上不难但那个域缓存价值一般如果觉得配置多得麻烦可以先不处理最多就是头像加载慢一点。3.3 页面层最坑的地方动态接口和登录态页面层的只读操作也就是打开仓库页、看README、浏览目录和issue列表反代过去基本都能工作。但一旦涉及登录、发帖、星标、release上传这类需要写session的操作情况就会变得非常诡异。GitHub的登录态是通过多个cookie和二次校验机制共同维护的。你把流量反代过去浏览器看到的域名是gh.example.comcookie会种在这个域名下面而GitHub服务端实际校验session时会去看请求里携带的官方域名相关cookie。两边对不上就会出现看起来登录成功了但刷新又变回未登录状态之类的问题。至于发issue、merge PR这种写操作失败概率更高。所以我对页面层镜像的建议是定位成只读浏览镜像别指望登录和交互功能。你甚至可以在镜像站首页放一个明显的说明条告诉用户这是只读镜像需要登录的操作请回到原站。这样至少不会让用户产生误会。3.4 用CDN还是自己硬扛如果你服务的用户分布很广可以考虑在镜像站前面再套一层CDN把静态资源缓存推到边缘节点。但要注意一个取舍CDN本身也是要成本的而且如果用户主要在同一个区域直接自己扛可能比套CDN更稳定、更容易排查问题。我的习惯是先不套CDN跑一段时间看页面层的流量特征确实有需要再加也不迟。4. Git Clone层URL重写和本地仓库镜像页面层只能解决看解决不了clone。因为克隆一个仓库的时候git客户端走的是GitHub的智能HTTP协议是要跟源站实时做数据交换的。Nginx反代只是一个转发通道除非你把GitHub那边的数据缓存下来否则用户每次clone仍然要从GitHub拉数据该断的还是会断。4.1 客户端URL重写先让团队用上镜像镜像站搭好之后最重要的问题是怎么让用户无感地走你的入口。一个最省事的办法是在客户端配置全局URL替换git config --global url.https://gh.example.com/.insteadOf https://github.com/这句话的意思是所有原本以https://github.com/开头的Git操作自动替换成https://gh.example.com/。配置完之后你照常写git clone https://github.com/foo/bar.git实际请求会发往gh.example.com。团队内部分发这份配置非常方便只要一行命令不用改任何已有脚本和文档。有一点要提醒这种替换方式依赖HTTPS证书链正确。你的镜像站必须能提供对gh.example.com的有效证书git客户端对证书的校验是很严格的。如果用户机器上没有信任你的自签证书连接会直接报错所以千万不要想省事用自签证书老老实实签Lets Encrypt就行。4.2 仓库定时镜像真正解决clone断连问题URL重写只能让流量走你的机器但如果你的Nginx每次还是回源GitHubclone的速度和稳定性并不一定比直连好多少。真正能显著改善clone体验的是在本地维护一份裸仓库用户clone的时候直接从本地裸仓库读取。思路其实很简单用git clone --mirror把远端仓库完整拉下来然后定时执行git remote update拉取增量。一个最基础脚本长这样#!/bin/bash # /data/git-mirror/sync.sh REPOS( https://github.com/owner/repo-a.git https://github.com/owner/repo-b.git ) for repo in ${REPOS[]}; do mirror_dir/data/git-mirror/$(basename $repo) if [ ! -d $mirror_dir ]; then git clone --mirror $repo $mirror_dir else cd $mirror_dir git remote update fi done--mirror的意思是完整镜像远端的所有分支、tag、refs都会保留下来而且以裸仓库形态存储没有工作区专门用来给clone提供数据。用cron跑定时同步比如每天凌晨拉一次就能保持基本新鲜。这里要说明一下这种镜像属于定时同步不是实时同步。GitHub上的push在同步时间点之前不会出现在镜像里。如果你的团队对新鲜度要求极高那就得把同步频次提上来比如每10分钟跑一次代价是同步脚本和GitHub源站之间的流量会变大。4.3 用git http-backend把裸仓库发布出去本地有裸仓库之后还要让用户能通过HTTP克隆它。办法是借助git http-backend这是git自带的一个CGI程序专门用于通过HTTP提供智能协议。先装依赖apt install fcgiwrap nginx git -y然后配置Nginxlocation ~ /git(/.*) { include fastcgi_params; fastcgi_param SCRIPT_FILENAME /usr/lib/git-core/git-http-backend; fastcgi_param GIT_HTTP_EXPORT_ALL 1; fastcgi_param GIT_PROJECT_ROOT /data/git-mirror; fastcgi_pass unix:/run/fcgiwrap.socket; }这样之后用户就能通过https://gh.example.com/git/owner/repo-a.git来clone了。注意裸仓库里如果有仓库名、描述文件可以自己维护一下方便展示。没有Gitea那套UI但胜在极轻量同步几十个仓库也不会有内存压力。4.4 用Gitea做可视化镜像仓库哪类团队更适合如果你不是只想clone还想让团队在网页上浏览代码、看commit历史、做权限控制那纯裸仓库的方式就不够用了。这时候可以用Gitea的仓库镜像功能。在Gitea后台新建仓库时可以直接选从远程镜像填上GitHub仓库地址Gitea会定期拉取更新并且自带一个完整的Web界面。两种方式的差异我列个表对比项裸仓库 git http-backendGitea镜像仓库资源占用极低只跑git和nginx中高需要数据库、多个进程网页代码浏览不支持完整支持权限控制只能靠Nginx的访问控制支持用户、团队、组织拉取实时性可高频同步同步周期有最短间隔限制维护复杂度低中等我的建议是5~10个人的技术团队裸仓库方式足够需要给几十上百人提供代码浏览和权限管理的再上Gitea。4.5 大仓库和LFS的镜像边界做git仓库镜像时还会遇到一个边界问题LFS对象。普通git镜像同步的是git的commit数据LFS对象不包含在内。如果你的上游仓库启用了Git LFS那么从你的镜像clone下来之后checkout LFS文件时仍然会回到objects.githubusercontent.com去拉对象。这就是为什么我在最开始强调下载层也要单独做——只有git数据层镜像、没有LFS对象缓存某些大仓库的体验还是差的。5. Release下载缓存性价比最高的一层镜像5.1 为什么release下载一定要单独做很多项目的使用频率里下载Release的比重远高于clone仓库。模型文件、预编译二进制、安装包全部挂在Release下面。Release下载看起来是简单的点一下按钮实际请求链路却很长浏览器先请求github.com/owner/repo/releases/download/v1.0.0/file.zipGitHub返回302重定向最终把流量导向objects.githubusercontent.com上的签名URL。用户体感上的速度慢绝大部分发生在这个最终下载阶段。这一层镜像做起来也是最划算的因为它是静态文件访问Nginx的proxy_cache天然适配不需要跑任何额外服务只要配好缓存目录和后端用户第一次下载回源之后所有命中缓存的请求都从你的磁盘走。5.2 Nginx的proxy_cache标准配置给codeload.example.com的配置大概是这样的proxy_cache_path /data/cache/rel levels1:2 keys_zonerelcache:200m max_size200g inactive60d use_temp_pathoff; server { listen 443 ssl; server_name codeload.example.com; ssl_certificate /data/ssl/example.com/fullchain.pem; ssl_certificate_key /data/ssl/example.com/privkey.pem; location / { proxy_pass https://codeload.github.com; proxy_set_header Host codeload.github.com; proxy_ssl_server_name on; proxy_cache relcache; proxy_cache_key $scheme$request_method$host$request_uri; proxy_cache_valid 200 24h; proxy_cache_valid 301 12h; proxy_cache_valid 302 0; proxy_cache_use_stale error timeout http_502 http_503; proxy_force_ranges on; } }这里几个关键点解释一下。第一proxy_cache_valid 302 0是我踩坑之后才改的。Release下载的第一步必然返回302这个302重定向URL是一个带签名参数的临时地址签名会过期。如果Nginx把302也缓存了那用户第二次来访问时拿到的重定向目标很可能是已经失效的地址表现为同一个文件第一次下载成功第二次开始一直报错。所以302绝不建议长时间缓存让每次请求都回源拿新鲜的重定向才是安全的。第二proxy_cache_key默认已经包含完整URI和参数但release这个场景签名URL尤其依赖请求参数显式写出来更稳妥也方便以后调试。第三proxy_force_ranges on用来开启缓存内容的Range请求支持。很多下载工具支持断点续传、多线程分段下载如果镜像端不处理Range请求大文件下载一旦中断就要整个重新来体验会非常糟糕。5.3 objects域名的重定向流量也要接住光处理codeload.example.com还不够因为302重定向之后流量会走到objects.githubusercontent.com如果没有对应的server块用户最终还是会绕回GitHub源站。所以你还得在Nginx里加一个objects.example.com的server块方式和上面几乎一样唯一需要注意的是证书、缓存目录可以复用同一个relcache也可以单独建一个objcache。我自己是单独建的原因是object域还存在大量LFS对象的下载流量我希望在监控上把它们分开统计。5.4 缓存淘汰和磁盘告警缓存有上限磁盘更是有上限。max_size200g的意思是缓存目录最大200GBNginx会在达到上限后根据活跃度淘汰旧数据。但你总不能等Nginx主动淘汰磁盘空间告警还是要自己盯着。一个简单脚本#!/bin/bash THRESHOLD80 USAGE$(df /data/cache | awk NR2 {print $5} | tr -d %) if [ $USAGE -gt $THRESHOLD ]; then echo $(date) /data/cache disk usage: $USAGE% /data/log/disk-alert.log # 这里可以接上你的告警推送 ficron每10分钟跑一次就够了。磁盘问题的处理方式优先级是先清掉inactive过期缓存再考虑扩容最后才是限制缓存体积。注意不要为了省空间把inactive时间设得太短否则热门release文件也会被频繁清掉失去缓存意义。6. 上线以后最容易踩的几个坑6.1 302被缓存下载突然全挂有一个非常经典的故障现象镜像站刚搭好的头几天release下载都挺快某一天用户突然反馈某个文件下载一直失败。我用curl -I https://codeload.example.com/owner/repo/releases/download/v1.0.0/file.zip一查响应头里出现了X-Cache-Status: HIT立刻意识到是302被缓存了。排查的思路很简单先确认有没有命中缓存再看缓存了哪个状态码最后想这个状态码的缓存策略是否合理。修复方法就是把proxy_cache_valid 302改成0然后清理掉相关缓存key之后类似问题就再也没有出现过。6.2 Nginx反代的SNI问题这个问题第一次遇到会很莫名其妙镜像站页面偶尔能开但时不时报502或者日志里大量SSL handshake failed。原因是Nginx去连接GitHub源站的时候如果TLS客户端不带上SNIGitHub侧无法确定访问的是哪个域名会拒绝握手。解决办法就是前面配置里那行proxy_ssl_server_name on;写配置的时候顺手加上能少折腾好几个小时。6.3 大仓库clone还是老是断的话别在缓存上死磕Nginx反代对页面、release下载这种静态内容很有用但对git的clone流程帮助有限。原因是我在第四章讲的clone过程中git客户端和远端建立的是有状态的数据交换过程返回的数据并不能简单地缓存成一个文件供后续复用。如果你已经把反代配置全调完了clone大型仓库依然时断时续那就放弃用反代做git加速的执念老老实实上本地裸仓库镜像把用户clone的对象切到本地裸仓库上稳定性才会有质的提升。6.4 证书续期之后忘了reloadLets Encrypt证书的有效期是三个月所以这个问题每隔三个月就有机会碰一次。哪怕你用acme.sh自动续期如果续期脚本不触发Nginx reloadNginx进程手里还是旧证书。某天用户突然说镜像站打不开了一看浏览器的证书错误提示你才意识到是证书和进程没对上。正确做法是在acme.sh的续期命令里加上reloadacme.sh --install-cert -d example.com -d *.example.com \ --key-file /data/ssl/example.com/privkey.pem \ --fullchain-file /data/ssl/example.com/fullchain.pem \ --reloadcmd systemctl reload nginx这样每次证书更新完Nginx会自动reload加载新证书彻底解决这个问题。6.5 不做访问控制就等着流量把自己榨干镜像站一旦被公开很容易被各种下载请求刷爆尤其是那些大文件release。我的做法是在镜像入口加一层Basic Auth或者IP白名单只服务自己团队。如果你确实需要公开至少要做单IP限速location /download/ { limit_rate 2m; }limit_rate 2m的意思是对单个连接限速到2MB/s能有效防止某个用户一次性榨干所有带宽。这个度自己灵活掌握总之别给裸奔。7. 镜像早点做做得薄一点最后聊点相对务虚但重要的事。镜像站不是越全越好做得薄一点、范围小一点维护压力会小很多。上面提到的所有组件我现在的线上组合并不是全部都开着。实际保留的是release下载缓存加几个核心仓库的裸镜像页面层的全站反代我在跑过一段时间后关掉了——原因很现实登录态问题解决不了静态资源和动态接口的耦合太深总是时不时冒出一些奇怪的小毛病而我投入维护这个镜像站的时间又有限。另外一个容易被忽视的点是边界。做镜像站不是把GitHub的内容拿来挂到自己域名下就完了仓库本身的License、README里对转载镜像的态度、GitHub的品牌使用规范这些都要留意。内部使用基本上没这些问题一旦做成公开服务就要考虑清楚内容合规和品牌混淆问题。至少应该在页面上写明这是非官方镜像、原始内容来自GitHub对应仓库。如果你正处在要不要自建镜像站的观望状态我个人的建议是先别急着上全套。用我前面提到的客户端URL重写方式配一个release缓存子域跑一周看看是不是真的解决了你遇到的痛点。如果痛点确实解决了再逐步加仓库镜像层、页面层。反过来一步到位配置全套后续返工的成本会高得多。
返回列表