ARTICLE DETAIL

资讯详情

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

自建GitHub镜像站实战:Nginx反向代理与缓存策略优化指南

自建GitHub镜像站实战:Nginx反向代理与缓存策略优化指南 前阵子帮团队搭了一个 GitHub 镜像站起因很实际持续集成流水线每次拉第三方依赖都慢得让人心慌release 里的大文件动不动就中断同一份制品被十几台构建机反复下载浪费了不少时间。折腾了一周左右把 Nginx 缓存、主动预热、监控告警这些环节都过了一遍总算稳定下来。这篇把完整的搭建和优化思路整理出来给和我一样需要自建 GitHub 镜像站的朋友做参考。自建镜像站并不神秘核心就两件事一是让重复下载的流量尽可能打在本地二是让跨地区访问 GitHub 源站时常见的超时和中断问题得到缓解。换句话说镜像站 Nginx 反向代理 磁盘缓存 定时预热 必要的保护措施。你可能不需要一上来就搞得很复杂但先想清楚自己的场景和资源边界再动手能少走很多弯路。1. 自建镜像站前先想清楚这几件事1.1 镜像站解决的四个真实问题很多人一想到 GitHub 镜像站就默认是为了“访问更快”。其实从实际运维角度看真正值得用镜像站解决的问题更具体重复拉取代码和依赖CI/CD 流水线里每次构建都要从 GitHub 拉取同一个公共仓库或 release 包网络一波动就失败。大文件下载中断release 页面里的二进制安装包动辄几百 MB直接从源站下载时常半路断掉没有断点续传就非常痛苦。API 速率限制自动化脚本如果频繁调用 GitHub API 获取版本信息、下载列表很容易触发限流影响业务。多机共享缓存团队里有多台构建机或开发机各自独立下载相同资源白白消耗带宽和时间。所以自建镜像站的首要目标不是什么“绕过限制”而是把重复流量就近拦截下来让回源的次数降到最低。这个思路适用于任何网络环境纯粹从工程效率出发。1.2 三种落地方案的取舍动手之前先选架构。我见过三种主流方案各有取舍方案优点缺点适合场景Nginx 反向代理 磁盘缓存配置灵活、完全可控、可以精确设计缓存策略需要自己处理 URL 映射、302 重定向等细节有运维能力想要高可控性的团队开源专用工具如 ghproxy部署快、开箱即用URL 规则清晰功能固定二次开发空间小更新依赖上游个人或小团队快速上线仓库同步Gitee/Gitea/GitLab仓库完全复制clone 速度快非实时同步release 大文件支持弱只是需要固定仓库的代码备份我最后选了 Nginx 反向代理方案原因很简单镜像站需要同时满足git clone、release 下载、API 请求转发三件事Nginx 的缓存模块和 rewrite 能力可以应对所有这些场景后续调优空间也大。如果你只是想要一个给同事用的简单下载加速入口ghproxy 这类工具会更省事但别指望它在流量变大后还能保持灵活。2. 服务器到域名证书准备工作里的隐藏细节2.1 服务器配置怎么选才不浪费镜像站是典型的高带宽、中低 CPU 应用。它的大部分工作都在转发流量和写缓存真正吃资源的是磁盘 IO 和网络带宽。CPU2 核足够起步。如果你不用 OpenResty/Lua 做复杂逻辑Nginx 对 CPU 的要求不高。内存4GB 起步。内存主要被系统的 Page Cache 占用来加速磁盘读取缓存文件越大内存越有用。带宽建议 5Mbps 以上。如果面向团队使用10Mbps 会更稳。带宽不是越高越好而是要根据实际的下载流量预算来配。磁盘这是最容易忽略的。release 大文件缓存和访问日志都落在磁盘上系统盘不能和缓存目录共用否则日志一膨胀就把根分区塞满。一句话磁盘容量和带宽决定了镜像站的上限CPU 内存够用就行。2.2 域名、DNS 与 HTTPS 证书的自动化镜像站必须用独立域名不要用 IP 裸奔。独立域名带来的好处是后续接 CDN、做多区域分流、调整 DNS 都很方便。域名选好后DNS 记录直接解析到服务器 IP 即可。如果服务器在国内建议做好备案相关的合规检查如果服务器在境外那访问延迟会受到地区影响需要结合团队分布选择区域。HTTPS 证书我推荐用 Lets Encrypt 自动签发配合acme.sh或certbot配置一条 cron 或 systemd timer 做自动续期。很多镜像站事故不是源站出问题而是证书过期导致全站不可用所以这一步一定要自动化。2.3 磁盘挂载与目录规划我习惯把镜像站所有数据放在一个独立数据盘下命名清晰/data/github-mirror/cache /data/github-mirror/logs /data/github-mirror/tmp缓存目录单独挂载到数据盘并且挂载参数加上noatime可以减少不必要的磁盘写入对 IO 有一点帮助。文件系统用 ext4 或 xfs 都行。日志目录和缓存目录分开是因为日志轮转和缓存清理的触发条件不同混在一起容易出现“日志把磁盘塞满缓存写不进去”的连锁故障。3. Nginx 核心配置从能用到好用的距离3.1 起步配置一个能用的反向代理先看一段最基础的反向代理配置作用是让github-mirror.example.com/owner/repo/...反向代理到github.com/owner/repo/...server { listen 443 ssl http2; server_name github-mirror.example.com; # SSL 证书配置省略 ssl_certificate /etc/letsencrypt/live/github-mirror.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/github-mirror.example.com/privkey.pem; location / { resolver 8.8.8.8 ipv6off; set $backend https://github.com; proxy_pass $backend$request_uri; 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; } }注意这里用resolver定义 DNS 解析地址是为了让 Nginx 能够根据变量动态解析$backend避免proxy_pass配置中的固定域名在启动时解析一次后就不再更新。这个配置对普通的仓库页面浏览没问题但距离“好用”还差很远因为 GitHub 的 release 下载会涉及 302 重定向到objects.githubusercontent.com必须单独处理。3.2 处理 release 下载的 302 重定向GitHub 的 release 下载流程是这样客户端请求https://github.com/owner/repo/releases/download/v1.0.0/file.zipGitHub 服务器返回 302Location 指向https://objects.githubusercontent.com/...客户端跟随重定向从对象存储下载文件。直接在 Nginx 里反向代理客户端会在最外层就拿到 302 响应于是它绕过你的镜像站直连 GitHub 的对象存储镜像缓存完全失效。解决办法是让 Nginx 把对象存储也代理了并改写响应头里的 Location。这里我给出一个实用配置思路是将 release 下载路径反向代理到github.com使用proxy_redirect把响应头里的https://objects.githubusercontent.com/改写成自己的镜像域名增加一个 location 专门反向代理objects.githubusercontent.com并开启缓存。# 处理 release 下载路径 location / { proxy_pass https://github.com; proxy_set_header Host github.com; # 将 objects.githubusercontent.com 的重定向改写为本站地址 proxy_redirect https://objects.githubusercontent.com/ /objects/; } # 代理 GitHub 对象存储 location /objects/ { resolver 8.8.8.8 ipv6off; set $backend https://objects.githubusercontent.com; proxy_pass $backend$request_uri; proxy_set_header Host objects.githubusercontent.com; # 开启缓存 proxy_cache github_cache; proxy_cache_valid 200 7d; proxy_cache_key $scheme://$host$uri; }这样客户端实际下载路径变成https://github-mirror.example.com/objects/...文件内容会通过镜像站回源到对象存储然后被 Nginx 缓存下来。第一次请求会比较慢后续请求就直接命中本地缓存了。3.3 缓存策略与 Range 请求大文件下载最怕断点续传失效。客户端通常会携带Range: bytes0-或一个具体的区间去请求Nginx 需要把 Range 头透传给上游同时保证缓存命中。Nginx 的proxy_cache默认会缓存完整响应但如果你直接让客户端和源站之间做 Range 请求Nginx 的缓存行为需要额外配置。关键参数proxy_cache github_cache; proxy_cache_valid 200 7d; proxy_cache_valid 206 1h; proxy_cache_key $scheme://$host$uri; proxy_set_header Range $http_range; proxy_ignore_headers Cache-Control Expires Set-Cookie;这里的proxy_ignore_headers是常见的必备项。GitHub 对象存储返回的响应头里可能带有Set-Cookie或Cache-Control: private如果不忽略Nginx 会因为“不允许缓存”而放弃缓存导致所有请求都回源。3.4 用 OpenResty 提升缓存命中率如果直接用 Nginx 官方版缓存 key 匹配规则比较死板。GitHub 的很多 URL 带有查询参数例如?raw1、?tokenxxx这些参数没有实际意义却会让缓存 key 变化导致同一份文件被缓存多份命中率直线下降。用 OpenResty 可以更灵活地处理。例如动态去除无意义的 query 参数location / { set_by_lua $cache_key local uri ngx.var.uri local args ngx.req.get_uri_args() -- 只保留有限参数 return uri ; proxy_cache_key $scheme://$host$cache_key; proxy_pass https://github.com; }这样即使客户端请求/owner/repo?tokenabc和/owner/repo?tokenxyz最终缓存 key 都是/owner/repo命中率明显提升。OpenResty 的 LuaJIT 性能很强这步操作对整体吞吐影响很小。4. 缓存命中率优化与主动预热4.1 日志里的三个关键数字配好之后第一件事不是继续调配置而是看日志。我在 Nginx 日志格式里加了一个变量$upstream_cache_status用来记录每次请求的缓存状态HIT、MISS、EXPIRED、BYPASS 等。log_format main $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent $http_range cache:$upstream_cache_status;通过日志你需要关注三个数字缓存命中率统计 HIT / 总请求正常应在 60% 以上。如果长期低于 30%说明缓存策略有问题或者请求 URL 中的随机参数太多。MISS 请求的 top URL找到回源最多的路径优先做预热。5xx 和 429 状态码数量如果源站开始返回 429说明回源请求太频繁可能触发了限流。统计命令可以用awk快速处理awk {print $NF} /data/github-mirror/logs/access.log | sort | uniq -c | sort -rn | head4.2 写一个预热脚本被动缓存有个缺点新文件第一次被请求时依然要回源如果这个文件体积很大第一个下载者会等很久。为了避免“冷启动慢”可以针对热门仓库做主动预热。预热脚本的逻辑很简单定期请求你关心的 release 下载 URL强制 Nginx 缓存。比如用 Python 脚本拉取 GitHub API 获取某个仓库的最新 release 下载地址然后依次请求镜像站 URL。#!/usr/bin/env python3 import requests import time GITHUB_API https://api.github.com/repos/{owner}/{repo}/releases/latest MIRROR_BASE https://github-mirror.example.com repos [ (owner, repo), (owner2, repo2), ] for owner, repo in repos: try: r requests.get(GITHUB_API.format(ownerowner, reporepo), timeout10) if r.status_code ! 200: continue for asset in r.json().get(assets, []): url asset[browser_download_url] mirror_url MIRROR_BASE url.replace(https://github.com, ) print(Warm up:, mirror_url) requests.get(mirror_url, timeout30, streamTrue) time.sleep(1) except Exception as e: print(Error:, e)注意预热请求最好限制并发和频率否则源站可能认为你在攻击它反而把 IP 封掉。我通常在每次请求之间加 1-2 秒间隔。4.3 限流与超时保护镜像站一旦公开出去就会面临各种突发流量。Nginx 本身有很好的防护能力关键是用起来# 限制客户端请求速率平均每秒 5 个请求突增不超过 10 limit_req_zone $binary_remote_addr zonemirror_limit:10m rate5r/s; location / { limit_req zonemirror_limit burst10 nodelay; proxy_connect_timeout 10s; proxy_send_timeout 60s; proxy_read_timeout 60s; }proxy_connect_timeout要短一点避免客户端等待太久proxy_read_timeout要足够长因为大文件下载可能持续几分钟。这个平衡需要根据实际业务调整。5. 实战中遇到的网络怪问题5.1 release 路径 404一个 rewrite 的锅我第一次配置时下载release文件一直返回 404但仓库首页能访问。排查了很久最后发现是 Nginx 的location ~正则写得太激进把/owner/repo/releases/download/...里的/releases匹配掉了一段。优化前location ~ ^/([^/])/([^/])/releases/download/(.*)$ { proxy_pass https://github.com/$1/$2/releases/download/$3; }这看起来没问题但如果是多层目录的情况$3可能不完整。GitHub 的 release download 路径实际上就是/{owner}/{repo}/releases/download/{tag}/{filename}如果 tag 里包含斜杠或文件名被 URL 编码简单的正则就会漏。我的经验是能用前缀匹配就尽量用前缀匹配少用复杂正则。直接location / { proxy_pass https://github.com$request_uri; }简单的透传反而最不容易出错。只有当需要特殊改写时才用 rewrite并且改完之后必须实测几个典型路径。5.2 403 权限问题缓存目录的“隐形权限”有段时间镜像站经常随机出现 403但刷新一下又好了。查看错误日志发现是 Nginx 的proxy_cache写缓存失败因为缓存目录属主不是 Nginx 工作进程。Nginx 的 master 进程一般是 root但 worker 进程通常以www-data或nginx用户运行。如果你手工把缓存目录mkdir在/data下属主是 rootworker 进程没有写权限自然无法写入缓存文件于是请求直接失败返回 403。解决方法是把缓存目录属主改成 Nginx worker 用户sudo chown -R www-data:www-data /data/github-mirror/cache sudo chmod -R 755 /data/github-mirror/cache这个坑很容易被忽略因为裸 Nginx 代理不配缓存时完全正常一旦开缓存就间歇性报错。5.3 缓存穿透与回源风暴恶意请求如何拖垮源站有次我发现源站突然开始返回大量 429查看镜像站访问日志后发现有人在用扫描工具探测路径生成大量随机 URL比如/owner/nonexistent/releases/download/v1.0.0/random.zip。这些请求全部回源导致 GitHub API 和对象存储限流。解决方案有两个前置校验如果 URL 路径不符合/{owner}/{repo}/releases/download/的格式直接返回 404不进行反向代理。缓存锁开启proxy_cache_lock on避免多个相同请求同时回源。这样即使有热点流量也只有一个请求会真正回源。proxy_cache_lock on; proxy_cache_lock_timeout 10s;配合limit_req基本能抵御常见扫描流量。5.4 大文件缓存不完整Nginx 临时文件上限大文件下载到一半就断明明已经缓存过但每次请求还是回源。查了 Nginx 文档发现是proxy_max_temp_file_size限制导致的。Nginx 缓存文件时会先把上游响应写入临时文件如果临时文件的大小超过某个阈值Nginx 就决定不缓存直接流式转发给客户端。默认的proxy_max_temp_file_size是 1024m如果你的 release 包超过 1GB超出了这个值缓存就不会生效。解决办法是调大这个参数并确保临时目录proxy_temp_path所在磁盘有足够空间proxy_temp_path /data/github-mirror/tmp; proxy_max_temp_file_size 8192m;大文件缓存对磁盘 IO 的要求比较高建议临时目录和缓存目录放在同一块高速磁盘上避免多次跨盘拷贝。6. 安全加固与日常运维6.1 防盗链与限速控制流量成本镜像站公开后很容易被第三方网站直接引用。如果有别人在热门页面嵌入你的下载链接你的带宽成本会瞬间飙升。可以通过valid_referers限制来源location /objects/ { valid_referers none blocked github-mirror.example.com *.example.com; if ($invalid_referer) { return 403; } }但要注意很多下载命令行工具wget/curl不会带 Referer所以none要保留否则会误伤正常用户。只针对浏览器请求做防盗链命令行工具一般不需要。同时可以限制单个 IP 的下载速率location /objects/ { limit_conn addr 4; limit_rate 5m; }这里limit_conn addr需要先在http层定义 zonelimit_conn_zone $binary_remote_addr zoneaddr:10m;6.2 日志轮转与缓存清理访问日志增长很快尤其是大文件下载场景每行日志虽然小但量一多就会占满磁盘。我用logrotate按天切分保留 30 天/data/github-mirror/logs/access.log { daily rotate 30 compress delaycompress missingok notifempty create 0640 www-data www-data sharedscripts postrotate /usr/sbin/nginx -s reopen endscript }对于 Nginx 缓存目录依靠proxy_cache_path自带的 manager 机制定期清理proxy_cache_path /data/github-mirror/cache levels1:2 keys_zonegithub_cache:10m max_size50g inactive30d use_temp_pathoff;max_size50g表示缓存总量超过 50GB 时manager 进程会自动清理不活跃文件inactive30d表示 30 天没有被访问的缓存会被清掉。这个配置能保证缓存目录不会无限膨胀。6.3 监控告警的整体思路镜像站在没有告警的情况下运行是很危险的。你可能登录服务器才发现磁盘满了或者缓存命中率已经掉到 30% 以下。我用的方案是 Prometheus nginx_exporter暴露 Nginx Stub Status 数据再配合node_exporter采集磁盘、CPU、内存指标。在 Nginx 配置里开启 status 需要在编译时带--with-http_stub_status_module一般发行版都默认带了location /stub_status { stub_status; allow 127.0.0.1; deny all; }然后nginx_exporter会定期抓取stub_status把连接数、请求数等转成 Prometheus 指标。告警规则我这里给几个重点5xx 响应占比超过 1%缓存命中率HIT 占比低于 50%/data/github-mirror磁盘使用率超过 80%Nginx 进程关闭或无法探活有了这几个告警镜像站的基本健康状态就能实时掌握。最后说几句自建 GitHub 镜像站最大的感受是“配好只是开始”。缓存策略要观察真实请求来调整重定向规则要根据源站行为变化来维护磁盘和带宽消耗要持续监控。如果你没有真实流量建议先用测试脚本模拟几轮下载再切正式域名不然一上线就被存量请求打爆缓存目录是很常见的事。一个小技巧把所有 location、缓存策略写在独立的配置片段里用include引入主配置。这样每次调整只需要改一个文件还能方便地做 A/B 对比。镜像站这种服务简单透明比花哨重要得多。
返回列表