本文所述内容不具普遍性,可能因操作环境差异而与实际有所出入,故请勿照搬照抄,仅供参考。
CDN 的相关概念可以参考之前的文章《 如何加快网站访问速度》,了解了什么是 CDN 后,我们知道 为了提高网站相关体验,网站一般都会上 CDN,但线路质量好一点的 CDN 费用一般也不低,所以就会想 着,从技术角度上看我们自己是否能够搭建 CDN?
概念
Nginx
Nginx 是一个高性能的 HTTP 和反向代理服务器,也是一个 IMAP/POP3/SMTP 服务器,Nginx 具有很多非常优越的特性:
- 在连接高并发的情况下,具有较好的性能,支持负载均衡,实现可扩展的浏量管理;
- 使Web服务器更灵活,更高效,更安全;
- 能够快速灵活且可靠的传送流视频和音频内容;
- 支持强大的 Web 加速和移动端性能的解决方案;
- 不仅只保护数据安全,也保证网站在面对恶意流量的攻击中能正常运行,从而保护应用安全;
- 可管理的安全的基于 HTTP API 流量的可信平台,为 API 提供安全保障和使用策略;
- 完整的软件应用分发平台,能够取代昂贵的 ADC 硬件负载平衡器,节省成本。
智能解析
域名智能解析是指域名解析服务器根据来访者的 IP 类型,对同一域名作出相应不同解析。
例如:
对 IP 来自电信的访问者,将域名解析到该域名对应 IP 地址为电信的服务器上;
对 IP来自联通的访问者,将域名解析到该域名对应 IP 地址为联通的服务器上;
对 IP 来自香港的访问者,将域名解析到该域名对应 IP 地址为香港的服务器上;
以保证访问者不因电信线路瓶颈而造成联通、香港的访客无法访问。
反向代理
反向代理(Reverse Proxy)是指用代理服务器来接收 Internet 上的连接请求,然后将请求转发给内部网络上的服务器,并将从内部服务器上得到的结果返回给 Internet 上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。
CDN 工作流程
如上图所示,这是一个完整的流程图:客户端访问域名时,先向 DNS 请求域名 IP,DNS 查询到 CNAME 记录(如果没有 DNS 直接回复源服务器 IP),则进一步解析 CNAME 智能解析服务器, 智能解析收到请求根据客户端来源按规则判断并回复 CDN 节点 IP,客户端此时访问域名就连接到了回复的 CDN 节点,如果 CDN 节点没有缓存,则 CDN 就会发起连接到负载均衡器(如果没有则直接连接源服务器,一般在有多个源服务器后端时才会有负载均衡器),然后负载均衡器根据规则分流到源服务器,将内容返回给 CDN 节点,CDN 节点再返回给客户端,完成整个访问流程。
从流程上看使用 CDN 后整个过程中增加了很多路由,看起来客户端访问速度可能会变慢,但实际上,从智能解析到负载均衡,客户端几乎是没有感知的,也就是说客户端能感受到的速度就是到 CDN 节点的速度。
搭建
通过概念讲解我们知道了 CDN 工作方式,其中智能解析、负载均衡都是可以单独拿出来长篇讲解的比较复杂的内容,我们这里先以如下简化版的流程来讲解 Nginx 搭建 CDN 节点的过程,后续有时间再一起来看看智能解析、负载均衡的内容。
编译安装
CDN 最重要的功能之一就是缓存,但像博客等内容的网站会经常更新,而 Nginx 默认是不支持删除某个文件的缓存,因此我们需要额外安装缓存清除插件 ngx_cache_purge
;如果准备建多个节点,我们希望能看到是由哪个节点提供服务的,这时候就可以通过插件ngx_http_substitutions_filter_module
和 headers-more-nginx-module
来配合实现。
参考以下命令编译
# 新建 Nginx 运行用户及用户组
$ groupadd www
$ useradd -s /sbin/nologin -g www www
# 下载 Nginx 源码
$ wget https://nginx.org/download/nginx-1.14.2.tar.gz
$ tar zxf nginx-1.14.2.tar.gz && cd nginx-1.14.2/src/
# 下载并编译 pcre 运行库
$ wget https://ftp.pcre.org/pub/pcre/pcre-8.42.tar.bz2
$ tar jxf pcre-8.42.tar.bz2 && cd pcre-8.42 && ./configure
$ make && make install && cd .. && rm -rf pcre-8.42*
$ echo /usr/local/lib >> /etc/ld.so.conf.d/usrlib.conf
$ ldconfig
# 下载并解压缓存清除插件 ngx_cache_purge
$ wget https://github.com/FRiCKLE/ngx_cache_purge/archive/2.3.tar.gz && tar zxf 2.3.tar.gz && rm -rf 2.3.tar.gz
# 下载并解压字符串替换插件 ngx_http_substitutions_filter_module/
$ wget https://github.com/yaoweibin/ngx_http_substitutions_filter_module/archive/v0.6.4.tar.gz && tar zxf v0.6.4.tar.gz && rm -rf v0.6.4.tar.gz
# 下载并解压头部参数设置插件 headers-more-nginx-module
$ wget https://github.com/openresty/headers-more-nginx-module/archive/v0.33.tar.gz && tar zxf v0.33.tar.gz && rm -rf v0.33.tar.gz && cd ..
# 编译并安装
$ ./configure--user=www --group=www --prefix=/usr/local/nginx --with-http_stub_status_module --with-http_ssl_module --with-http_v2_module --with-http_gzip_static_module --with-http_sub_module --with-stream --with-stream_ssl_module --with-http_flv_module --with-http_addition_module --with-http_realip_module --with-http_mp4_module --with-ld-opt=-Wl,-E --with-pcre --add-module=/usr/local/nginx/src/src/ngx_cache_purge-2.3 --add-module=/usr/local/nginx/src/src/ngx_http_substitutions_filter_module-0.6.4 --add-module=/usr/local/nginx/src/src/headers-more-nginx-module-0.33
$ make && make install
配置缓存
配置
主要是为了缓存静态文件,而缓存需要指定位置及大小,参见如下配置:
......
http {
......
proxy_cache_path /usr/local/nginx/cache levels=1:2 keys_zone=mycache:20m max_size=5g inactive=1d;
proxy_temp_path /usr/local/nginx/cache/tmp;
.......
由于缓存是写入硬盘的,硬盘的 I/O 将会对速度产生重大影响,故建议选择 I/O 好一点的主机,比如华为云学生机。
说明
- proxy_cache_path :用于缓存的本地磁盘目录是
/usr/local/nginx/cache/
。 - levels:在
/usr/local/nginx/cache/
设置了一个两级层次结构的目录。将大量的文件放置在单个目录中会导致文件访问缓慢,所以推荐使用两级目录层次结构,如果levels
参数没有配置,则 Nginx 会将所有的文件放到同一个目录中。 - keys_zone:设置一个共享内存区,该内存区用于存储缓存键和元数据,有些类似计时器的用途。将键的拷贝放入内存可以使 Nginx 在不检索磁盘的情况下快速决定一个请求是
HIT
还是MISS
,这样大大提高了检索速度。一个 1MB 的内存空间可以存储大约 8000 个 key,那么上面配置的 20MB内存空间可以存储差不多 160000 个 key。 - max_size:设置了缓存的上限(在上面的例子中是 5G),这是一个可选项。如果不指定具体值,那就是允许缓存不断增长,占用所有可用的磁盘空间,当缓存达到这个上线,处理器便调用
cache manager
来移除最近最少被使用的文件,这样把缓存的空间降低至这个限制之下。 - inactive:指定了项目在不被访问的情况下能够在内存中保持的时间。在上面的例子中,如果一个文件在 1 天之内没有被请求,则缓存管理将会自动将其在内存中删除,不管该文件是否过期。该参数默认值为 10 分钟(
10m
)。注意,非活动内容有别于过期内容,Nginx 不会自动删除由缓存控制头部(Cache-Control
)指定的过期内容,过期内容只有在inactive
指定时间内没有被访问的情况下才会被删除,如果过期内容被访问了,那么 Nginx 就会将其从原服务器上刷新,并更新对应的inactive
计时器。 - proxy_temp_path:Nginx 最初会将注定写入缓存的文件先放入一个临时存储区域,
use_temp_path=off
命令指示 Nginx 将在缓存这些文件时将它们写入同一个目录下,我们强烈建议你将参数设置为 off 来避免在文件系统中不必要的数据拷贝。如果没有配置use_temp_path=off
或者没有配置则按proxy_temp_path
设置来给定存储目录,如果proxy_temp_path
也没配置,则直接缓存到proxy_cache_path
目录下 。
配置代理
配置
$ sudo echo '1.1.1.1 www.vircloud.net' >> /etc/hosts
server {
listen 80;
server_name www.vircloud.net;
more_set_headers "X-Node: CN-BJ01";
location / {
proxy_pass https://www.vircloud.net;
proxy_redirect off;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_cache mycache;
proxy_cache_key $uri$is_args$args;
}
location ~ /clear(/.*) {
proxy_cache_purge mycache $1$is_args$args;
error_page 405 =200 /clear$1;
}
}
说明
由于 CDN 节点必须知道源站 IP,否则无法正常工作,所以我们需要先在 hosts
指定源码 IP。
这一段反代服务的配置可以说是最简单的配置了,我们来看一下各个参数的作用:
- server_name:指定反代的域名,可以与源站不同;
- more_set_headers:由
headers-more-nginx-module
插件提供的参数,可以自由地添加头部信息,我们这里设置了节点位置; - proxy_pass:要反代的域名;
- proxy_redirect:修改从被代理服务器传来的应答头信息。比如
proxy_redirect http://localhost:8000/blog/ /;
是将源站响应的http://localhost:8000/blog/
替换成/
,这样就可以直接通过https://www.vircloud.net/
来访问http://localhost:8000/blog/
; - proxy_set_header X-Forwarded-For:将实际访客的 IP 添加到发送给源站的请求头信息中,不然源站看到的访问 IP 都是代理服务器的 IP;
- proxy_cache:指定缓存使用的空间,与
proxy_cache_path
配置的空间一致; - proxy_cache_key:指定缓存使用的 key 值,方便定位清除缓存;
- location ~ /clear(/.*):指定访问
/clear/
目录为清除缓存的入口; - proxy_cache_purge:由
ngx_cache_purge
插件提供的参数,解析要清除的缓存实际存储的位置; - error_page 405 =200 /clear$1:重写清除缓存的状态码。
完整配置
到这里,不出意外反代已经可以正常工作了,即已经实现了 CDN 节点的搭建。出于实际作业需求,我们可能还需要更多的配置来实现,比如源站挂了,而节点有缓存,那么我们就会希望让节点正常返回,而不是跟着源站返回错误。
下面我们就来看看 Nginx 对反代还提供了哪些参数,以实际完整配置为例:
server {
# 监听端口。
listen 80;
listen 443 ssl http2;
# 监听 SNI 域名。
server_name www.vircloud.net;
# SSL 配置。
ssl_certificate vircloud.net.crt;
ssl_certificate_key vircloud.net.key;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
ssl_ciphers 'TLS13-AES-256-GCM-SHA384:TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-128-GCM-SHA256:TLS13-AES-128-CCM-8-SHA256:TLS13-AES-128-CCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# 获取真实 IP。
include realip.conf;
# 反垃圾。
include deny-ip.conf;
include deny-bots.conf;
# SSL 跳转。
if ($server_port !~ 443){
rewrite ^(/.*)$ https://$host$1 permanent;
}
# 指定错误页面,并从当前服务器取,而不向源站服务器请求。
proxy_intercept_errors on;
error_page 403 404 500 502 /error.html;
location /error.html {
root /usr/local/nginx/html;
}
# 添加响应头信息。
more_set_headers "X-Node: CN-BJ01";
# 设置缓存清除入口。
location ~ /clear(/.*) {
# 只允许特定 IP 执行清除动作。
include allowip.conf;
deny all;
# 缓存 key 转换。
proxy_cache_purge mycache $1$is_args$args;
error_page 405 =200 /clear$1;
}
# 反代配置正式开始。
location / {
proxy_pass https://www.vircloud.net;
proxy_redirect off;
# 向源站传送主机头、客户端真实 IP 等特定信息。
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 忽略源站响应的 Cache-Control 头信息。
proxy_ignore_headers Cache-Control
# 与源站建立连接的超时时间,通常不要超过 75s。
proxy_connect_timeout 60;
# 与源站通信时节点发送请求的超时时间,超时只在两次连续的写入操作之间作用, 而不是用于传输整个请求,如果源站在此时间内没有收到任何内容,则连接将关闭。
proxy_send_timeout 60;
# 与源站通信时源站响应数据的超时时间,超时只在两次连续的读操作之间起作用,而不是用于传输整个响应,如果源站在此时间内没有传输任何内容,则连接将关闭。
proxy_read_timeout 60;
# 开启代理缓冲区。
proxy_buffering on;
# 响应头的缓冲区大小。
proxy_buffer_size 128k;
# 网页内容缓冲区大小(4*256k)。
proxy_buffers 4 256k;
# Nginx 会在没有完全读完后端响应的时候就开始向客户端传送数据,所以它会划出一部分缓冲区来专门向客户端传送数据,然后它继续从后端取数据,缓冲区满了之后就写到磁盘的临时文件中。
proxy_busy_buffers_size 512k;
# 指定当响应内容大于 proxy_buffers 指定的缓冲区时,写入硬盘的临时文件的大小,如果超过了这个值,Nginx 将与源服务器同步的传递内容,而不再缓冲到硬盘,设置为 0 时则直接关闭硬盘缓冲。
proxy_max_temp_file_size 512k;
# 一次访问能写入的临时文件的大小。
proxy_temp_file_write_size 512k;
# 源站返回错误信息自动重试。
proxy_next_upstream error timeout invalid_header http_500 http_503;
# 若后端返回错误则返回已缓存的。
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
# 指定缓存使用的空间。
proxy_cache mycache;
# 缓存支持的方法。
proxy_cache_methods GET HEAD;
# 添加 If-Modified-Since 头信息,如果客户端的请求项已经被缓存过了,但是在缓存控制头部中定义为过期,那么 Nginx 就会在向源站 GET 请求中包含 If-Modified-Since 字段,源站只会在该文件请求头中 Last-Modified 记录的时间内被修改时才将全部文件一起发送。
proxy_cache_revalidate on;
# 多个 MISS 只有第一个会连接服务器。
proxy_cache_lock on;
# 客户端设置 Pragma:no-cache 时节点应当直接请求源站,添加此配置可使 Nginx 支持该请求(默认忽略)。
proxy_cache_bypass $http_pragma $cookie_nocache;
# 指定对 200、301 或者 302 等有效代码缓存的时间长度,特定参数 any 表示对任何响应都缓存一定时间长度。
proxy_cache_valid 200 24d;
proxy_cache_valid 301 24h;
proxy_cache_valid 302 304 403 1h;
proxy_cache_valid any 1s;
# 指定缓存使用的 key 值,方便定位清除缓存。
proxy_cache_key $uri$is_args$args;
# 响应头信息添加当前请求资源的缓存状态,如命中 HIT、没有缓存 MISS、过期 UPDATING 等。
add_header X-Cache $upstream_cache_status;
# 指定请求多少次才开始缓存。
proxy_cache_min_uses 1;
# 统一添加请求的资源过期时间头信息
expires max;
}
真实 IP
使用反代功能后,实际上对于源站来说,访客是代理服务器而不是真实的访客,Nginx 针对这种情况也给了解决方案,在源站 Nginx 配置中添加如下配置:
......
set_real_ip_from 反代服务器 IP;
real_ip_recursive on;
......