CDN核心就是“缓存”和“加速”两个功能。主流的大规模CDN大多基于Apache Trafffic Server,属于是高攀不起的知识盲区。在个人能够配置的小规模CDN场景中,cdnflygoedge是比较常见的。但是这些工具在更低的需求面前,还是不够轻量。

NGINX除了作为最常用的Web Server,也是一个高性能的反向代理工具。事实上,早年的CloudFlare和当下的GCORE都是基于NGINX开发的CDN系统,只是在一键脚本盛行的今天很少有人再去关注它与实现这些CDN功能的联系,现在就聊聊NGINX反向代理中的一些小技巧。

一、Zone规则

NGINX的http段位于nginx.conf(即NGINX管理-配置修改)文件中,将该参数添加在下方中括号{后的适当位置即可。在这里建议一站一规则,vhsot.conf文件(即站点修改-配置文件)是include到http段下的,可以如图添加在vhost配置中server段之前,这样较为清晰明了。这里演示内容的解释以宝塔为例。
nginxcache001.png

#全局缓存设置,需要加在NGINX配置文件的HTTP段
proxy_cache_path /www/wwwlogs/ngx_cache levels=1:2 keys_zone=cache_api:10m inactive=30d max_size=1g;

以下是关于该段参数的解释:

  • proxy_cache_path:NGINX缓存的储存位置,无需自行新建该文件夹,但不可与其他缓存配置的路径重复,清理时可直接删除该目录或使用第三方nginx_cache_purge模块。
  • levels=1:2:缓存目录设置,第一级目录名1 byte(A-Z & 0-9)、第二级目录名2 byte(如AA、A5等)。该参数会间接影响缓存读取的性能,当缓存文件数过多时可以适当提升至1:32:3等。
  • keys_zone=cache_test:键值区域,以cache_test为例可自行命名,是用于反向代理配置中调用的唯一值。
  • 10m:用于缓存响应状态的临时空间,一般大小设置为10m
  • inactive=30d:不活跃过期时间,当缓存超过该期限没有命中访问时,该缓存会过期以减少占用。可设置为1y1d30m等,若不想让其过期,可以设置为一个极大值如100y
  • max_size=1g:该区域最大占用的缓存大小,可设置为100m1g等。

在这里可以看到在缓存全局的配置中有inactive参数控制不活跃的缓存过期时间,边缘节点上的该参数是很多小型站点缓存命中率难以提高的重要原因。CDN也有一个误区是“套上CDN一定就快”,以国内腾讯云的北上广BGP为例,在不触及带宽上限的情况下其全国访问性能是明显好于托管在二三线城市的CDN运营商节点的。

腾讯轻量提供了一个较为廉价的机会让我们接触这样的网络,也为各种需求配备了充裕的流量包。合理利用3-5M带宽能做的事还是很多的,我的博客日PV在400左右、CDN流量约1G,报表回馈的实际峰值带宽很少超过2Mbps。

二、目录缓存

反向代理规则是添加在vhost.confserver段下的,宝塔做了一个include到proxy.conf的文件夹,所以可以直接编辑反向代理页面的配置文件。对整站的反向代理加速一般第一级需要配置一个针对/的全局代理,此处默认不做缓存较为合理,将缓存交由后面更为细致的目录、后缀配置中。本文中的所有示例均以104.20.20.20为服务器、cdnjs.cloudflare.net为反代的目标站点、static.test.com为访问站点,请根据自己的实际情况进行修改。

# 主目录的反向代理,设置为不缓存
location / {
    proxy_pass https://cdnjs.cloudflare.com;
    proxy_set_header Host cdnjs.cloudflare.com;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header REMOTE-HOST $remote_addr;
    # 当源站校验sni时(如CLOUDFALRE)需添加如下字段
    proxy_ssl_server_name on;
    proxy_ssl_name cdnjs.cloudflare.com;
    
    # 设置缓存状态header,用于判断访问时是否命中(该状态为buypass)
    add_header lms_cache $upstream_cache_status;
    # 设置主目录不进行缓存
    proxy_no_cache $uri;
    proxy_cache_bypass $uri;
}

针对目录的缓存以Gravatar为例,该路径下返回的图片并不会以jpg结尾,并且末尾的参数控制了返回图片的大小,所以需要以对目录配置+保留参数的方式来实现。当然你也可以按照这样的方法,只修改缓存段的配置实现对某个路径的不缓存,原理是相同的。

# 缓存gravatar头像文件
location /avatar {
    proxy_pass http://secure.gravatar.com;
    proxy_set_header Host secure.gravatar.com;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header REMOTE-HOST $remote_addr;
 
    # 设置缓存状态header,用于判断访问时是否命中
    add_header lms_cache $upstream_cache_status;
    # 不遵循源站的缓存头
    proxy_ignore_headers Set-Cookie Cache-Control expires;
    # 指定缓存键区,需要与1中区域设置对应
    proxy_cache cache_api;
    # 保留请求参数进行缓存,不保留设置为$host$uri即可
    proxy_cache_key $host$uri$is_args$args;
    # 缓存指定的状态码,缓存有效期30天
    proxy_cache_valid 200 304 301 302 30d;
    # 浏览器缓存有效期30天
    expires 30d;
}

以下是针对路径使用的一些高级配置,用于单站点访问多网站以及修改网页中的内容。proxy_set_header用于增加向源站请求的请求头,proxy_hide_header用于向访客隐藏来自源站的请求头。sub_filter则是用于对网页中内容的修改,具体使用示例见注释。

# 反代站点的特定目录,并对网页中内容进行修改
location /home/ {
    # 当location路径与proxy_pass地址末尾均以/封闭时,访问cdnjs.cloudflare.com/home/1.txt即为static.test.com/1.txt
    # 当location路径与proxy_pass地址末尾均不封闭时,访问cdnjs.cloudflare.com/home同static.test.com/home
    proxy_pass http://104.20.20.20/;
    proxy_set_header Host cdnjs.cloudflare.com;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header REMOTE-HOST $remote_addr;
 
    # 用于设置向源站请求提交的Header,可用于绕开防盗链
    proxy_set_header Referer 'https://cloudflare.com/';
    # 隐藏源站提供的某请求头,可根据实际需要设置(比如隐藏对象存储特征)
    proxy_hide_header access-control-expose-headers;
 
    # 关闭GZIP,同时接受接受所有编码
    gzip off;
    proxy_set_header Accept-Encoding '*';
    # 关闭仅替换一次
    sub_filter_once off;
    # 替换内容,将ContentA替换为ContentB
    sub_filter 'ContentA' 'ContentB';
    # 针对引导跳转的修改
    proxy_redirect https://cdnjs.cloudflare.com/ https://static.test.com/;
    
    # 设置缓存状态header,用于判断访问时是否命中(该状态为buypass)
    add_header lms_cache $upstream_cache_status;
    # 设置主目录不进行缓存
    proxy_no_cache $uri;
    proxy_cache_bypass $uri;
}

三、后缀缓存

针对特定后缀的缓存只需要配置好location字段限制好范围即可,如下配置可以实现缓存特定后缀的文件,或缓存特定路径下某后缀的文件。location字段使用正则表达式来圈定范围,若有更为复杂的需求请以NGINX、正则表达式为关键词进行检索和学习。需要注意的是,在具有包含关系的正则匹配中,位于配置文件靠前的会优先触发匹配。因此请确保更为细致的配置靠前放置,比如第2行开头的配置是包含于第三行的配置中的,需要安置在以第3行开头的配置之前,以避免被表达更大范围的正则表达式覆盖。

# 缓存指定后缀的静态文件,可以添加路径来进行更为细致的设置
# 例如针对/public下的html进行缓存设置:location ~* ^/public/.*\.(html|htm)$
location ~* \.(mtn|moc|webp|jpg|jpeg|gif|png|bmp|psd|ttf|pix|tiff|js|css|woff|woff2|shtm|html|htm)$
{
    proxy_pass http://104.20.20.20;
    proxy_set_header Host cdnjs.cloudflare.com;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header REMOTE-HOST $remote_addr;
    
    # 添加跨域请求头,若限制网站可以将*替换为完整的https://test.com
    add_header Access-Control-Allow-Origin *;
    
    # 设置缓存状态header,用于判断访问时是否命中
    add_header lms_cache $upstream_cache_status;
    # 不遵循源站的缓存头
    proxy_ignore_headers Set-Cookie Cache-Control expires;
    # 指定缓存键区,需要与1中区域设置对应
    proxy_cache cache_api;
    # 忽略缓存参数
    proxy_cache_key $host$uri;
    # 缓存指定的状态码,缓存有效期30天
    proxy_cache_valid 200 304 301 302 30d;
    # 浏览器缓存有效期30天
    expires 30d;
}

四、防盗链

防盗链的内容很简单,只需要在location中加上valid_referers配置的字段。需要配置的要点有四个:①选定防盗链的location范围、②是否允许空refer、③设置允许的refer、④选择禁止的方式(返回403或返回特定文件)。黑名单的匹配则在valid_referers字段只需包含拉黑的refer,去掉默认的blocked参数即可。示例配置及注释如下,可参考进行修改。

# 以图片后缀为例,添加防盗链规则
location ~* \.(webp|jpg|jpeg|gif|png|bmp)$
{
    proxy_pass http://104.20.20.20;
    proxy_set_header Host cdnjs.cloudflare.com;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header REMOTE-HOST $remote_addr;
    
    # 添加防盗链设置
    # 设置白名单header,若允许空refer则将none去掉,多个允许的refer间隔空格添加
    valid_referers none blocked *.test.com; 
    # 设置黑名单header,多个阻止的refer间隔空格添加
    # valid_referers *.thieft.com;
    if ($invalid_referer) {
        # 直接返回403阻止访问
        return 403;
        # 也可以rewrite到错误页
        # rewrite ^/ https://static.test.com/forbiden.jpg;
        break;
     }
    
    # 设置缓存状态header,用于判断访问时是否命中
    add_header lms_cache $upstream_cache_status;
    # 不遵循源站的缓存头
    proxy_ignore_headers Set-Cookie Cache-Control expires;
    # 指定缓存键区,需要与1中区域设置对应
    proxy_cache cache_api;
    # 忽略缓存参数
    proxy_cache_key $host$uri;
    # 缓存指定的状态码,缓存有效期30天
    proxy_cache_valid 200 304 301 302 30d;
    # 浏览器缓存有效期30天
    expires 30d;
}

五、WAF

添加WAF(Web Application Firewall)的CDN属于SCDN的范畴,在NGINX中可以使用lua-nginx-module加载lua脚本对请求进行处理,进而实现对异常请求的拦截。该模块由OpenResty项目开发,并未内置在NGINX项目中,因此有这方面的需求尽量选择内置该模块的OpenResty。原版NGINX安装该模块涉及到LuaJIT、NDK的调用,如果有兴趣可以按照官方的说明进行安装。

以上两个是较为优秀的LuaWAF项目,能够在一定程度上实现防cc、防注入、ip黑名单、URL黑名单等功能。WAF一般情况下会使得访问性能下降20-40%左右,如果不是容易遭到攻击的站点可以不必配置WAF功能。具体使用方法在这里就不细讲了,若有需要可以根据GitHub项目中的指引进行实践。

五、结语

在修改以上反向代理+缓存的示例时,我将步骤大致总结为以下五步:

  • 配置全局的缓存的规则,设定缓存路径等参数
  • location的正则表达式指向配置的目标
  • 设置好源站的IP、域名,根据实际决定是否添加sni头
  • 配置缓存细则,决定是否缓存、是否保留参数、缓存有效期等
  • 添加防盗链、修改双向的请求头等,完善反向代理的配置

最后,也贴上一个较为简单的JSdelivr反代示例供大家参考,其中只对需要的npm和gh路径以不同的方式代理并缓存,请根据自己的实际需要来进行修改。此处无需添加跨域请求头,因为jsdelivr已设置好。

# 全局缓存设置,需要加在NGINX配置文件的HTTP段
proxy_cache_path /www/wwwlogs/jsd_cache levels=1:2 keys_zone=cache_jsd:10m inactive=100y max_size=1g;
 
# 缓存npm相关的路径,npm包相对质量较高可不必过分考虑安全问题
location /npm {
    proxy_pass https://104.17.17.17;
    proxy_set_header Host cdn.jsdelivr.net;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header REMOTE-HOST $remote_addr;
    proxy_ssl_server_name on;
    proxy_ssl_name cdn.jsdelivr.net;
 
    # 可添加白名单,允许空refer
    valid_referers blocked *.test.com; 
    if ($invalid_referer) {
        return 403;
        break;
     }
 
    # 设置缓存状态header,用于判断访问时是否命中
    add_header lms_cache $upstream_cache_status;
    # 不遵循源站的缓存头
    proxy_ignore_headers Set-Cookie Cache-Control expires;
    # 指定缓存键区
    proxy_cache cache_jsd;
    # 保留请求参数进行缓存
    proxy_cache_key $host$uri$is_args$args;
    # 缓存指定的状态码,缓存有效期30天
    proxy_cache_valid 200 304 301 302 30d;
    # 浏览器缓存有效期30天
    expires 30d;
}
 
# 缓存github相关的路径,GitHub鱼龙混杂,建议只允许特定需要的后缀通过
location ~* ^/gh/.*\.(mtn|moc|webp|jpg|jpeg|gif|png|bmp|psd|ttf|pix|tiff|js|css|woff|woff2|shtm|html|htm)$
{
    proxy_pass https://104.17.17.17;
    proxy_set_header Host cdn.jsdelivr.net;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header REMOTE-HOST $remote_addr;
    proxy_ssl_server_name on;
    proxy_ssl_name cdn.jsdelivr.net;
    
    # 可添加白名单,不允许空refer展示GitHub内容
    valid_referers none blocked *.test.com; 
    if ($invalid_referer) {
        return 403;
        break;
     }
    
    # 设置缓存状态header,用于判断访问时是否命中
    add_header lms_cache $upstream_cache_status;
    # 不遵循源站的缓存头
    proxy_ignore_headers Set-Cookie Cache-Control expires;
    # 指定缓存键区
    proxy_cache cache_jsd;
    # 保留请求参数进行缓存
    proxy_cache_key $host$uri$is_args$args;
    # 缓存指定的状态码,缓存有效期30天
    proxy_cache_valid 200 304 301 302 30d;
    # 浏览器缓存有效期30天
    expires 30d;
}

针对海外访客建议通过DNS分线路解析使海外用户解析至CloudFlare,如图通过页面规则设置一个301/302跳转回到JSdelivr官方。这样的话,既节省了提供国内访问服务器的流量,又保障了海外用户的体验。

题外话

如何优化CDN的使用有很多技巧,需要从①访客>边缘节点、②边缘节点>源站、③缓存有效性这三个方面来综合考虑。

  • 访客到边缘节点网络不佳:典型例子比如CloudFlare和CloudFront的自选IP(即针对性选择对大陆访问速度较为良好的节点解析给访客)。国内节点则不需要担心这个问题,尤其是公有云北上广BGP ≥ 公共CDN的网络质量。
  • 边缘节点到源站的网络不佳:这一点很容易被忽略的,例如使用国内的CDN回源境外站点依然要通过三大运营商的国际出口。对于这样的需求,建议中置一个对大陆友好的中间节点作为中间源。
  • 缓存的有效性不佳:一方面是对边缘节点的高估(一般认为CDN的缓存能力<20m),另一方面来自于访问不达标(即使定时预热,但是访问量不能维持边缘节点缓存有效)。对于少量使用的需求,类似于轻量COS这样的对象存储、亦或是本文如此的单节点缓存能够提供更好的体验。

宝塔关闭反向代理全局缓存

宝塔默认会开启全局缓存,这导致没有开启缓存的网站也会被缓存,这里还是建议一站一Zone规则,不开启全局缓存。
/www/server/nginx/conf/proxy.conf里的proxy_cache cache_one;注释掉,
在宝塔对反代网站打开缓存开关时,proxy_cache cache_one; 还是会自动加在反代的配置代码里,这时缓存会针对该反代网站单独生效。

proxy_temp_path /www/server/nginx/proxy_temp_dir;
proxy_cache_path /www/server/nginx/proxy_cache_dir levels=1:2 keys_zone=cache_one:20m inactive=1d max_size=5g;
client_body_buffer_size 512k;
proxy_connect_timeout 60;
proxy_read_timeout 60;
proxy_send_timeout 60;
proxy_buffer_size 32k;
proxy_buffers 4 64k;
proxy_busy_buffers_size 128k;
proxy_temp_file_write_size 128k;
proxy_next_upstream error timeout invalid_header http_500 http_503 http_404;
#proxy_cache cache_one;注释掉,这样全局缓存就关闭了

希望以上博客的内容能够对您有所启发~

参考文献:https://luotianyi.vc/7050.html

Last modification:July 13, 2023
如果觉得我的文章对你有用,请随意赞赏