在日常学习和工作中我们经常需要查阅一些文档,但大部分文档都部署在国外的服务器或 CDN 上,导致访问速度较慢甚至偶尔无法访问。为了解决这个问题,我们可以使用 Nginx 在自己的服务器上搭建一个镜像网站,本文以 Kubernetes 官网 为例,演示如何使用 Nginx 镜像网站实现「文档自由」。
首先我们需要准备一台云服务器并安装好 Nginx,本文使用的是 1.27 版本。另外需要一个域名作为镜像网站入口,这里以 mirror.lin2ur.cn
为例,目标是通过 kubernetes.mirror.lin2ur.cn
域名访问 Kubernetes 官网。为了提供 HTTPS 访问我们还需要为域名申请一个 SSL 证书。
0. 反向代理
镜像网站本质上就是反向代理目标网站,Nginx 反向代理的配置相信大家都很熟悉了,下面我们来编写配置文件:
1 | # /etc/nginx/conf.d/mirror.conf |
map $host $mirror_host
指令根据客户端请求域名匹配目标域名,方便后续添加新的镜像网站。
resolver 223.5.5.5
指定域名解析服务,在反向代理场景中这个配置是必须的。
proxy_ssl_server_name on
开启与上游服务器建立 SSL/TLS 连接时发送 SNI (Server Name Indication) 扩展字段,部分 CDN 厂商(如 Cloudflare) 强制要求发送 SNI。
proxy_set_header Host "$proxy_host"
设置反向代理时 Host
请求头为目标域名,默认情况下 Nginx 会将客户端请求的 Host 头发送给上游的服务。
proxy_redirect
上游服务返回 30x 跳转响应时重写 Location
响应头使客户端跳转到镜像网站域名,以 Kubernetes 官网为例,使用 HTTP 协议访问时会被重定向到 HTTPS 协议,先来看没有配置 proxy_redirect
的情况:
1 | curl -I http://kubernetes.mirror.lin2ur.cn |
将 proxy_redirect
指令的变量展开后这个指令等效于 proxy_redirect "https://kubernetes.io" "https://kubernetes.mirror.lin2ur.cn
,第一个参数指定匹配的字符串,第二个参数指定替换的字符串,因此 Location
响应头会被重写为:
1 | curl -I http://kubernetes.mirror.lin2ur.cn |
到这里我们就完成了初步的配置,接下来访问 https://kubernetes.mirror.lin2ur.cn
看一下效果:
可以看到大部分资源加载请求都都走了镜像网站,这部分资源使用的是相对路径因此我们无需干预。不过还是有一些「漏网之鱼」,这些资源是用绝对路径引入的因此需要特殊处理一下。
1. 处理绝对路径资源
在 Web 中 HTML 和 CSS 都可以引入外部资源,想要修改资源的引入地址我们必须从引入这些资源的资源入手。从上面的截图来看,这些外部资源的请求「发起者」都是 styleheet
,也就是说是在 CSS 中引用的,通过关键字搜索我们可以找到这个 CSS 文件:
这个 CSS 是用相对路径引入的,也就是说它会经过 Nginx 的代理,这就给我们提供了修改的机会,修改方式非常简单粗暴:对外部域名进行字符串替换。在 Nginx 中能完成这项工作的是 sub_filter
指令:
1 | map $host $mirror_host { |
上面的配置在基础配置上添加了 3 个镜像域名,接着就是关键指令 sub_filter
,它的用法非常简单,将第一个参数指定的字符串替换为第二个参数指定的字符串,这里对新增加的 3 个镜像域名都进行了替换。
sub_filter_once
指定每个请求是否只进行一次 sub_filter
替换,设置为关闭状态以便进行多次替换。
sub_filter_types
指定需要进行替换的 MIME 类型,默认情况下 Nginx 只会对 text/html
的资源进行替换,因此需要加上 text/css
。
proxy_set_header Accept-Encoding ""
用于清空 Accept-Encoding
请求头,如果客户端请求中包含有 Accept-Encoding
头,上游服务可能会对响应体进行压缩,而 sub_filter
无法对压缩后的内容进行替换。
再次请求可以看到所有资源都已经被代理到镜像网站了:
对于在 HTML 中使用 link
或 script
引入的资源我们也可以使用 sub_filter
进行替换,但需要注意的是 sub_filter
不支持正则表达式,因此一些复杂的替换可能需要借助 njs、ngx_http_substitutions_filter_module 等模块来完成。
2. 通用代理
虽然 map
指令能很方便地添加新的镜像域名,但如果目标网站引入了新的外部资源,我们还是得要手动添加,这显然不是一个完美的解决方案。为了解决这个问题我们可以搭建一个通用的代理,例如请求 https://any.mirror.lin2ur.cn/cdn.jsdelivr.net/vue.js
时,Nginx 会自动代理到 https://cdn.jsdelivr.net/vue.js
,接着再配合 sub_filter
指令,我们就可以实现「一劳永逸」了:
1 | server { |
在配置中添加了一个新的 location ~ /any/(.+)$
用于处理通用代理请求,然后修改了 sub_filter
指令在所有绝对路径资源前加了通用代理域名,再来看看效果:
虽然实现了一劳永逸但这种替换方式非常「简单粗暴」,这可能会导致一些问题,我们可以进行一些更精细化的配置,比如:
1 | sub_filter '@import "https://' '@import "https://any.mirror.lin2ur.cn/';' |
3. 代理缓存
有同学可能会有疑问做这些的目的是什么?确实,如果云服务器是在境内,那么这个镜像网站并不能起到多大的作用,但如果能在云服务器和目标网站之间加上一层代理缓存,那么镜像网站就能发挥出它的作用了。缓存生成之后即使云服务器和目标网站之间的网络不稳定,也不会影响到用户的访问体验,甚至可以实现「秒开」。
1 | proxy_cache_path /tmp/nginx keys_zone=mirror:10m; |
proxy_cache_valid
指令用于设置上游返回指定状态码时缓存的有效时间,这里将 200、302 状态设置为缓存 24 小时,其余状态缓存 1 分钟;proxy_cache_use_stale
指令用于设置在缓存失效时是否使用过期缓存;proxy_ignore_headers Cache-Control
指令用于忽略上游返回的 Cache-Control
头避免 Nginx 遵循上游的缓存策略,譬如 kubernetes.io 的缓存策略是 public,max-age=0,must-revalidate
,该策略允许缓存但必须验证缓存有效性,这意味着每次使用缓存 Nginx 都需要访问一次上游服务,这显然不是我们想要的,因此我们需要忽略这个响应头。
4. GZip 压缩
在给镜像站加上代理缓存后访问速度有了「质的飞跃」,不过这只是优化了 Nginx 与上游服务交互环节,别忘了我们在配置 sub_filter
时指令时清空了 Accept-Encoding
请求头,这意味着上游服务不会对内容进行任何压缩,Nginx 也会原样返回给客户端,对于一些「小水管」云服务器,动辄几十上百 KB 的资源还是会拖慢网站的加载速度,因此我们可以在 Nginx 中开启 GZip 压缩:
1 | server { |
Gzip 的配置和静态网站的配置类似,但默认情况下 Nginx 不会对反向代理请求进行压缩,因此需要加上 gzip_proxied any
指令。
5. 总结
到这里针对 Kubernetes 官网的镜像网站就搭建好了,以上只是针对单个网站提供一个思路,实际情况可能会更复杂,灵活使用 Nginx 的指令可以解决大部分问题。