在日常学习和工作中我们经常需要查阅一些文档,但大部分文档都部署在国外的服务器或 CDN 上,导致访问速度较慢甚至偶尔无法访问。为了解决这个问题,我们可以使用 Nginx 在自己的服务器上搭建一个镜像网站,本文以 Kubernetes 官网 为例,演示如何使用 Nginx 镜像网站实现「文档自由」。

首先我们需要准备一台云服务器并安装好 Nginx,本文使用的是 1.27 版本。另外需要一个域名作为镜像网站入口,这里以 mirror.lin2ur.cn 为例,目标是通过 kubernetes.mirror.lin2ur.cn 域名访问 Kubernetes 官网。为了提供 HTTPS 访问我们还需要为域名申请一个 SSL 证书。

0. 反向代理

镜像网站本质上就是反向代理目标网站,Nginx 反向代理的配置相信大家都很熟悉了,下面我们来编写配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# /etc/nginx/conf.d/mirror.conf

map $host $mirror_host {
kubernetes.mirror.lin2ur.cn kubernetes.io;
default "";
}

server {
listen 80;
listen 443 ssl;

server_name *.mirror.lin2ur.cn;

ssl_certificate conf.d/tls.crt;
ssl_certificate_key conf.d/tls.key;

resolver 223.5.5.5 ipv6=off;

proxy_ssl_server_name on;

proxy_set_header Host "$proxy_host";

location / {
if ( "$mirror_host" = "" ) {
return 404;
}

proxy_redirect https://$proxy_host https://$host;

proxy_pass "${scheme}://${mirror_host}";
}
}

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
2
3
4
curl -I http://kubernetes.mirror.lin2ur.cn
HTTP/1.1 301 Moved Permanently
Location: https://kubernetes.io/
...

proxy_redirect 指令的变量展开后这个指令等效于 proxy_redirect "https://kubernetes.io" "https://kubernetes.mirror.lin2ur.cn,第一个参数指定匹配的字符串,第二个参数指定替换的字符串,因此 Location 响应头会被重写为:

1
2
3
4
curl -I http://kubernetes.mirror.lin2ur.cn
HTTP/1.1 301 Moved Permanently
Location: https://kubernetes.mirror.lin2ur.cn/
...

到这里我们就完成了初步的配置,接下来访问 https://kubernetes.mirror.lin2ur.cn 看一下效果:

20240713160655
20240713160655

可以看到大部分资源加载请求都都走了镜像网站,这部分资源使用的是相对路径因此我们无需干预。不过还是有一些「漏网之鱼」,这些资源是用绝对路径引入的因此需要特殊处理一下。

1. 处理绝对路径资源

在 Web 中 HTML 和 CSS 都可以引入外部资源,想要修改资源的引入地址我们必须从引入这些资源的资源入手。从上面的截图来看,这些外部资源的请求「发起者」都是 styleheet,也就是说是在 CSS 中引用的,通过关键字搜索我们可以找到这个 CSS 文件:

20240715112200
20240715112200

这个 CSS 是用相对路径引入的,也就是说它会经过 Nginx 的代理,这就给我们提供了修改的机会,修改方式非常简单粗暴:对外部域名进行字符串替换。在 Nginx 中能完成这项工作的是 sub_filter 指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
map $host $mirror_host {
kubernetes.mirror.lin2ur.cn kubernetes.io;
fonts-googleapis.mirror.lin2ur.cn fonts.googleapis.com;
jsdelivr.mirror.lin2ur.cn cdn.jsdelivr.net;
fonts-gstatic.mirror.lin2ur.cn fonts.gstatic.com;
}

server {
...

sub_filter '//fonts.googleapis.com' '//fonts-googleapis.mirror.lin2ur.cn';
sub_filter '//cdn.jsdelivr.net' '//jsdelivr.mirror.lin2ur.cn';
sub_filter '//fonts.gstatic.com' '//fonts-gstatic.mirror.lin2ur.cn';

sub_filter_once off;
sub_filter_types text/css;

proxy_set_header Accept-Encoding "";

location / {
...
}
}

上面的配置在基础配置上添加了 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 无法对压缩后的内容进行替换。

再次请求可以看到所有资源都已经被代理到镜像网站了:

20240715113134
20240715113134

对于在 HTML 中使用 linkscript 引入的资源我们也可以使用 sub_filter 进行替换,但需要注意的是 sub_filter 不支持正则表达式,因此一些复杂的替换可能需要借助 njsngx_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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
server {
...

# sub_filter '//fonts.googleapis.com' '//fonts-googleapis.mirror.lin2ur.cn';
# sub_filter '//cdn.jsdelivr.net' '//jsdelivr.mirror.lin2ur.cn';
# sub_filter '//fonts.gstatic.com' '//fonts-gstatic.mirror.lin2ur.cn';

sub_filter 'https://' 'https://any.mirror.lin2ur.cn/';
sub_filter 'http://' 'http://any.mirror.lin2ur.cn/';

location / {
if ( "$host" = "any.mirror.lin2ur.cn" ) {
rewrite /(.+)$ https://$1 last;
}
...
}

location ~ /any/(.+)$ {
internal;

set $target "$1";
proxy_redirect ~(http|https)://(.+)$ $1://any.mirror.lin2ur.cn/$2;
proxy_pass "${scheme}://$target?$args";
}
}

在配置中添加了一个新的 location ~ /any/(.+)$ 用于处理通用代理请求,然后修改了 sub_filter 指令在所有绝对路径资源前加了通用代理域名,再来看看效果:

20240715115659
20240715115659

虽然实现了一劳永逸但这种替换方式非常「简单粗暴」,这可能会导致一些问题,我们可以进行一些更精细化的配置,比如:

1
2
3
4
5
sub_filter '@import "https://' '@import "https://any.mirror.lin2ur.cn/';'
sub_filter 'url("https://' 'url("https://any.mirror.lin2ur.cn/';

sub_filter '<script src="https://' '<script src="https://any.mirror.lin2ur.cn/';
sub_filter '<link href="https://' '<link href="https://any.mirror.lin2ur.cn/';

3. 代理缓存

有同学可能会有疑问做这些的目的是什么?确实,如果云服务器是在境内,那么这个镜像网站并不能起到多大的作用,但如果能在云服务器和目标网站之间加上一层代理缓存,那么镜像网站就能发挥出它的作用了。缓存生成之后即使云服务器和目标网站之间的网络不稳定,也不会影响到用户的访问体验,甚至可以实现「秒开」。

1
2
3
4
5
6
7
8
9
10
11
12
proxy_cache_path /tmp/nginx keys_zone=mirror:10m;

server {
...
proxy_cache mirror;
proxy_cache_valid 200 302 24h;
proxy_cache_valid any 1m;
proxy_cache_use_stale error timeout updating http_502;

proxy_ignore_headers Cache-Control;
...
}

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
2
3
4
5
6
7
8
9
server {
...
gzip on;
gzip_comp_level 5;
gzip_min_length 5000;
gzip_proxied any;
gzip_types text/html text/css text/javascript application/javascript image/svg+xml;
...
}

Gzip 的配置和静态网站的配置类似,但默认情况下 Nginx 不会对反向代理请求进行压缩,因此需要加上 gzip_proxied any 指令。

5. 总结

到这里针对 Kubernetes 官网的镜像网站就搭建好了,以上只是针对单个网站提供一个思路,实际情况可能会更复杂,灵活使用 Nginx 的指令可以解决大部分问题。