使用 Nginx 镜像网站实现「文档自由」

541 阅读4分钟

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

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

0. 反向代理

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


# /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 的情况:


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 响应头会被重写为:


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

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

1. 处理绝对路径资源

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

20240715112200

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


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

对于在 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 指令,我们就可以实现「一劳永逸」了:


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

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


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. 代理缓存

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


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 压缩:


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 的指令可以解决大部分问题。