标签

#Nginx

13 则推送

Aaron Lau(韩栖)

Nginx 学习笔记 (8) —— 缓存加速指南

proxy_cache 运用局部性的原理,备存一些先前被访问过、料将被再度使用的资源,从而减少后端服务器的资源开销。缓存也是反代的用途之一。

1) 定义缓存目录,二级目录结构,10MB 的共享内存区存放缓存键和元数据,不活跃的缓存文件 1 小时后清除,磁盘上限 512MB;

proxy_cache_path /usr/local/nginx/cache levels=1:2 keys_zone=the_cache_zone:10m inactive=1h max_size=512m use_temp_path=off;

2) 启用缓存,并添加一响应头指示缓存状态(MISS / BYPASS / HIT …)。key 是决定缓存命中率的因素之一,要特别留心 Query String;

location / {
    proxy_pass http://www.example.com;
    proxy_cache the_cache_zone;
    # (default) proxy_cache_key $scheme$proxy_host$request_uri;
    add_header X-Cache-Status $upstream_cache_status;
}

3) 若希望抓取的源站内容总是新鲜,需要忽略上游的 Cache-Control 和 Expires,再来配置自己的缓存期限。proxy_cache_valid 控制的是有效期,inactive 决定的是保留期限,时间一到管你新不新鲜都要丢掉;

proxy_ignore_headers X-Accel-Expires Cache-Control Expires;
proxy_cache_valid 200 30m;
proxy_cache_valid any 1m;
proxy_cache_revalidate on;

4) 缓存一时爽,全家火葬场。一大波缓存同时失效,会导致缓存雪崩,使得大批请求涌向源站。网站不是特别大的话,可以加锁排队、暂时返回陈旧数据;但根本的还是要各别设定缓存期限,错峰更新缓存,不要一窝蜂。

proxy_cache_lock on;
proxy_cache_use_stale error timeout updating http_503;
proxy_cache_background_update on;

5) 倘若有手动清除缓存的需求,又用不起 NGINX Plus,不妨编译进 ngx_cache_purge。URI 前面加上 /purge 即为清除缓存接口(最好不要被外部访问)。

location ~ /purge(/.*) {
    allow 127.0.0.1;
    deny all;
    proxy_cache_purge the_cache_zone $scheme$proxy_host$1$is_args$args;
}

<small>原载 App-scope,有删改。</small>

Aaron Lau(韩栖)

Nginx 学习笔记 (a4) —— 浅谈负载均衡

Nginx 以高并发、低消耗而闻名,很适合作为一个负载均衡器,有策略地分发请求给不同的后端服务器。避免单点故障之余,亦增强整个系统的可用性,简单说不容易宕机。假设有一组名为 backend 的 upstream,调用方式是 proxy_pass http://backend。

热备:平时使用 www 提供服务,bad 暂时下线,bak 作为发生故障时兜底的一台机器。

upstream backend {
    server www.example.com;
    server bad.example.com down;
    server bak.example.com backup;
}

轮询是经(mò)典(rèn)的负载均衡算法,按照时间先后来分发请求。理论上会跳过不能用的 server,但预设的超时太长,实务上可把 proxy_connect_timeout 等改成 1。最小连接(least_conn)因应忙闲不均而生,把新请求分发至没那么忙的主机。源地址哈希(ip_hash)保证一个用户对应一台服务器,解决 Session 不一致的问题(通常表现为无法登录),但出于容灾理由,不建议大量使用。

集群中难免有算力不均的情况。不问性能的齐头式平等,很可能让配置最差的那个服务器决定整个集群的性能。可跑一次 bench.sh,再依据测试结果分配权重;max_fails 缺省值是 1,fail_timeout 缺省值是 10s。

upstream backend {
    server srv1.example.com weight=3;
    server srv2.example.com max_fails=2 fail_timeout=1d;
    server srv3.example.com;
}

即使某台后端返回了 500,它也会参与负载均衡,毕竟能收到状态码就表示它还活着。但这在用户眼里跟 Connection Refused 可没啥区别,所以把 500 一并纳入“在下一台服务器重试”的机制里。

proxy_next_upstream error timeout http_500;

<small>原载 App-scope,有删改。</small>

Aaron Lau(韩栖)

Nginx 学习笔记 (7) —— 启用 Brotli 压缩

Brotli 是 Google 研发的一种新兴压缩算法,可以加快网页的载入速度。相较于 Gzip 压缩率更高、性能也非常好,再基于 Chrome 的市场支配地位,Brotli 便得到迅速普及。不过要注意,它仅适用于 HTTPS。

1) 下载 Brotli 的源码;

yum install git && cd /usr/local/src
git clone https://github.com/google/ngx_brotli.git
pushd ngx_brotli
git submodule update --init
popd

2) 执行 nginx -V,configure arguments 即为现有的参数。追加 --add-module=../ngx_brotli,重新编译。如需平滑升级,make 之后请不要 make install,而是替换二进制后 make upgrade;

cd nginx-1.16.1
./configure \
--with-http_ssl_module \
--with-http_v2_module \
--with-http_sub_module \
--with-openssl=../openssl-1.1.1c \
--add-module=../ngx_brotli
make
mv /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.old
cp objs/nginx /usr/local/nginx/sbin/nginx
make upgrade

3) 修改 nginx.conf。缺省值就够用,不妨再留意一下 gzip_types 和 brotli_types,以允许压缩 text/html 以外的文件;

http {
    ...
    gzip on;
    brotli on;
    # brotli_types text/css text/javascript application/rss+xml;
    ...
}

4) 重载 Nginx,可借由 Brotli Test 等工具检验效果。

nginx -t && nginx -s reload

<small>原载 App-scope,有删改。</small>

Aaron Lau(韩栖)

Nginx 学习笔记 (a3) —— 让 sub_filter 替换 Gzip 内容

在微信小程序 web-view 跳转外部链接?甲方的需求千奇百怪,好在办法总比困难多。不是只允许业务域名吗?此时反代不失为一个办法。就算绕开了限制,由于内容经你方服务器转发,你方也须承担责任。

先 proxy_pass,再用 sub_filter 把对方域名换成我方域名,避免打开非业务域名。可有些网站无视 Accept-Encoding: '',不管三七二十一,统统返回压过的,所以替换失败。先来看人家怎么办到的:对方 frontend 向 upstream 请求 gzip,backend 则把 gzip_http_version 设成 1.0(Nginx 默认以 HTTP/1.0 连接上游)。

思路清晰了有没有?先解压、再替换就好了呗。Maxim Dounin 写了个 gunzip filter 模块,还有人直接修改 Nginx 源码。另一种 trick(感谢 ryd994 大大的原帖)比较直观:做两次 proxy_pass,先反代源站 gzip 过的内容,再反代一次即可获得未压缩的内容。

location /forward {
    proxy_pass https://example.com;
    # proxy_set_header Accept-Encoding 'gzip';
    # gunzip on;
}

location / {
    proxy_pass https://yourdomain.com/forward;
    proxy_set_header Accept-Encoding '';

    sub_filter_types *;
    sub_filter_once off;
    sub_filter 'example.com' 'yourdomain.com';
}

上面的 gunzip on 和下面的 Accept-Encoding '' 互为替代。即使禁用压缩就能顺利替换,也可以试看看这招。这种需求很能吃出站流量,带宽嘛,总是省一点算一点。

<small>原载 App-scope,有删改。</small>

Aaron Lau(韩栖)

Nginx 学习笔记 (6) —— sub_filter 替换响应内容

Accept-Encoding 设为空值以禁用压缩,是因为 sub_filter 只能处理未经压缩的内容;又 sub_filter 一般只替换 text/html 且仅工作一次,不符合需求,故对配置稍做微调。

location / {
    ...
    proxy_set_header Accept-Encoding '';
    sub_filter_types *;
    sub_filter_once off;
    sub_filter '查找内容:源站域名' '替换为:反代站域名';
    ...
}

如果禁用上游压缩的 trick 无效,或是心疼回源带宽,下一篇也许能帮到你。

<small>原载 App-scope,有删改。</small>

Aaron Lau(韩栖)

Nginx 学习笔记 (a2) —— $host、$http_host 与 $server_name

$host 的值,首先是请求行中的 host name,打个比方,GET http://example.com/ HTTP/1.1 中的 host name 就是 example.com;然后是请求头内 Host 字段的值;最后是匹配成功的 server name,有点儿“调剂”的意思。值得留意,$host 总是小写且不带端口号。

$http_host 的值,其实就是请求头中 Host 字段的值。同理可得请求头中任意字段的值,如 User-Agent 就可以用 $http_user_agent 来调用。它可简单理解为(但不等同)$host:$server_port。另外 $proxy_port 为 backend 的端口,通常会隐藏起来,如 8080;$server_port 为客户端访问 frontend 使用的端口,如 80。

$server_name 的值,是实际处理该请求的 server block 的 server_name。Nginx 分发请求时,会依据客户端请求的 Host 按优先级逐次匹配:首先做精确匹配,其次查找通配符 * 开头的,再次查找通配符 * 结尾的,最后以正则表达式做模糊匹配。

若请求的 Host 匹配不上(或许根本是空值),则分派给默认的 server,可以在 listen 指令中附上 default_server 参数来指定。若没有指定,该请求将会走 http block 里面的第一个 server block。减少攻击面计,实务上应返回 444 以关闭连接。

server {
    listen 80 default_server;
    server_name _;
    return 444;
}

<small>原载 App-scope,有删改。</small>

Aaron Lau(韩栖)

Nginx 学习笔记 (a1) —— proxy_set_header 默认值

proxy_set_header 默认重定义两个 Header 头字段:

proxy_set_header Host       $proxy_host;
proxy_set_header Connection close;

Host 初始值 $proxy_host,这是因为 HTTP/1.1 必须包含 Host 字段以指定主机。$proxy_host 是 backend 即后端的主机名,$host 是 frontend 即自身的主机名。要不要改成 $host 或 $http_host,视后端会不会校验域名和端口而定。举个例子,proxy_pass 的目标是一组 upstream 时,$proxy_host 是这组 upstream 的名字,如果后端只接收 Host 是 www.example.com 的请求,那么后端不会响应,变通办法正是改写 Host 字段的值。

Connection 初始值 close,是因为在 HTTP/1.1 中所有连接都是长连接,而后端的 Web 应用程序(HTTP/1.0 或更古早的)未必支持连接复用,所以统统设成 close 以免出错。跟后端通讯真需要用到 keep-alive,先配置一组 upstream,回头再清空 Connection 字段即可。

upstream backend {
    server www.example.com;
    keepalive 16;
}

server {
    location / {
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Connection '';
        # proxy_set_header Host www.example.com;
    }
}

并没有 proxy_http_version 2.0,甚至于 nginx → upstream 也是默认 HTTP/1.0。HTTP/2 多路复用可减少客户端到 frontend 的连接数,不过在 frontend 到服务端的情境中,连接数并非瓶颈,共用一个连接反而更限制并发能力。

<small>原载 App-scope,有删改。</small>

Aaron Lau(韩栖)

Nginx 学习笔记 (5) —— 被反代被镜像了怎么办?

须知请求头的所有字段都是可以被人为修改的,镜像跟反镜像、反盗链跟反反盗链……都是一个斗智斗勇的过程,没有办法一劳永逸。以下几招,按需取用。

封锁 IP 段,也可以用来遵守特定国家的法令(譬如锁区、GDPR 等);屏蔽 UA 这招有点“防君子不防小人”,不过起码能挡住一批小白。

deny xxx.xxx.0.0/16;
if ($http_user_agent ~* (spider|bot)) { return 403; }
if ($http_user_agent ~* (Scrapy|Curl|HttpClient)) { return 403; }

禁止 IP 直接访问,防止 DNS 恶意解析:

server {
    listen 80 default_server;
    server_name _;
    return 444;
}

图片反盗链。如果 Referer 与 valid_referers 约定不符,$invalid_referer 变为 1,服务器返回 403 Forbidden。

location ~* \.(jpg|jpeg|png|gif|tiff|webp)$ {
    valid_referers none blocked yourdomain.com;
    if ($invalid_referer) { return 403; }
}

始终返回 Gzip 过的内容,能稍微增加对方替换的难度。最后是访问认证:借由 httpd-tools 里的 htpasswd 生成密码文件,再修改 server.conf。应当搭配 HTTPS;因浏览器会记住密钥,缺乏合理的注销手段,故不建议大量使用(通常应用于内网)。

yum install httpd-tools
cd /usr/local/nginx/conf && htpasswd -bc passwd admin password

auth_basic 'Confirm password to continue';
auth_basic_user_file passwd;

<small>原载 App-scope,有删改。</small>

Aaron Lau(韩栖)

Nginx 学习笔记 (4) —— 自动跳转 https 或 www

单单开启 HTTPS 不足以保障安全,若仍保留 HTTP 更是门户大开,况且从 SEO 的角度来说,协议不同也会视为内容重复的两个网站。比较稳妥的做法是,重定向 HTTP 请求至 HTTPS,这是服务器端跳转。

server {
    listen 80;
    server_name yourdomain.com;
    return 301 https://$host$request_uri;
}

更进一步地,建议开启 HSTS 以防范 SSL 剥离攻击。HSTS 会告知浏览器今后访问该站点都得使用 HTTPS,即使用户在地址栏输入的 HTTP 亦然。这是浏览器端跳转,不用再 301。

add_header Strict-Transport-Security 'max-age=63072000; includeSubDomains; preload';

不过还是有盲点存在。新访客首次访问,可不晓得 HSTS 的规矩,得经历一次 HTTP → HTTPS 的过程才行,这中间攻击者就大有文章可做。所以有一个 HSTS 预加载列表,第一次访问就教你做人。合法证书、全站 HTTPS、子域名全数 HTTPS、响应头带 Strict-Transport-Security,条件满足就向 hstspreload.org 提交。但要提醒,启用了 HSTS 的网站恢复原状比较困难。

至于 www,虽说带 www 俨然是一种 old-school 的做法,但实务上显得更 formal,同时免除裸域不支持 CNAME 记录且 Cookie 作用域过大的尴尬。www 和 non-www 最好两者择其一,以免权重分散。

server {
    listen 443 ssl;
    server_name yourdomain.com www.yourdomain.com;

    if ($host != www.yourdomain.com) {
        return 301 https://www.yourdomain.com$request_uri;
    }
}

<small>原载 App-scope,有删改。</small>

Aaron Lau(韩栖)

Nginx 学习笔记 (a0) —— HTTP/2、TLS 1.3 和 HSTS

不过瘾?再折腾 HTTP/2、TLS 1.3 和 HSTS,力求在 SSL Server Test 达到 A+!

1) 要启用 TLS 1.3,OpenSSL 起码得 1.1.1 以上才行。安装新版并替换掉旧版,尔后以 openssl version 判断是否升级完成;

cd /usr/local/src
wget https://www.openssl.org/source/openssl-1.1.1d.tar.gz && tar zxf openssl-1.1.1d.tar.gz && cd openssl-1.1.1d
./config --prefix=/usr/local/openssl
make && make install

mv /usr/bin/openssl /usr/bin/openssl.old
mv /usr/include/openssl /usr/include/openssl.old
ln -s /usr/local/openssl/bin/openssl /usr/bin/openssl
ln -s /usr/local/openssl/include/openssl /usr/include/openssl
echo "/usr/local/openssl/lib/" >> /etc/ld.so.conf
ldconfig -v

2) 追加参数 --with-openssl=../openssl-1.1.1d 后,重新编译 Nginx 并平滑升级;

cd /usr/local/src/nginx-1.16.1
./configure \
--with-http_ssl_module \
--with-http_v2_module \
--with-http_sub_module \
--with-openssl=../openssl-1.1.1d
make
mv /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.old
cp objs/nginx /usr/local/nginx/sbin/nginx
make upgrade

3) 回去编辑 server.conf。另外提醒 HSTS 是浏览器端直接强制 HTTPS,而非 HTTP 301 重定向至 HTTPS,如不确定后续会否继续使用 HTTPS,请注释掉那行代码。

server {
    ...
    listen 443 ssl http2;
    ssl_protocols TLSv1.3;
    add_header Strict-Transport-Security 'max-age=63072000; includeSubDomains; preload';
    ...
}

如果前面使用的 Certbot,要么注释掉 include /etc/letsencrypt/options-ssl-nginx.conf;,要么不用加 ssl_protocols TLSv1.3;。

<small>原载 App-scope,有删改。</small>

Aaron Lau(韩栖)

Nginx 学习笔记 (3) —— Let's Encrypt 证书

倘源站只支持 HTTP,也许做到上一篇就够了。但都 9102 年,还不支持 HTTPS 的网站很少了。即使后端不支持 HTTPS,经过反代服务器的中转,也能实现 HTTPS 加密访问。

1) 启用 EPEL 源。官方源因版权顾虑,移除了有争议的软件包,而 Extra Packages for Enterprise Linux 的尺度更宽一些,算是个有益补充; 2) 安装并配置 Certbot,应指定 Nginx 的安装目录。Certbot 拥有傻瓜式的交互,键入邮箱、接受条款、选取主机名、决定 HTTP 是否重定向至 HTTPS,视情况操作即可;

yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
yum install certbot python2-certbot-nginx
certbot --nginx --nginx-server-root=/usr/local/nginx/conf

3) 创建一个 cron 定时任务,以实现自动续期;

echo "0 0,12 * * * root python -c 'import random; import time; time.sleep(random.random() * 3600)' && certbot renew" | sudo tee -a /etc/crontab > /dev/null

4) 完事了。就这么简单。来看看 Certbot 都动了哪些配置:

listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

options-ssl-nginx.conf 里默认是 ssl_protocols TLSv1.2 TLSv1.3。如需开启 HTTP/2、TLS 1.3 和 HSTS,见下一篇。

<small>原载 App-scope,有删改。</small>

Aaron Lau(韩栖)

Nginx 学习笔记 (2) —— 反向代理基本配置

1) Nginx 配置文件的路径,可透过 nginx -t 命令找出。nginx.conf 的内容相当庞杂,为日后维护方便,建议拆分出 server block,即在 http block 引用外部配置文件;

http {
    ...
    include server.conf;
    ...
}

2) 以反代 www.example.com 为例。proxy_pass 遇有变量是不会走系统 /etc/hosts 和 /etc/resolv.conf 的,而是走 resolver 指定的 DNS 服务器,好处是可以实现域名的动态解析。

出于防止跨站攻击和反盗链的目的,对方一般会校验 Referer,不是他们的域名统统返回 403,作出修改可绕过此限制。X-Real-IP 用于记录客户端的 IP(但不一定是真实的),X-Forwarded-For 则反映链路信息,两者的伪造相当容易,毕竟作弊在应用层是难免的。借由传递 Set-Cookie,既可反代又不影响注册登录等功能运作。

server {
    listen 80;
    server_name yourdomain.com;

    location / {
        resolver 8.8.8.8;
        set $TARGET 'www.example.com';
        proxy_pass http://$TARGET;

        # proxy_set_header Host $TARGET;
        proxy_set_header Referer http://$TARGET;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        proxy_pass_header Set-Cookie;
        # proxy_cookie_domain www.example.com yourdomain.com;
    }
}

3) 先检测配置文件语法,再热加载或重启 Nginx;从外部访问,还需要打开 firewalld 防火墙的相应端口。

nginx -t && (nginx -s reload || service nginx restart)
firewall-cmd --add-service=http --add-service=https --permanent
firewall-cmd --reload

<small>原载 App-scope,有删改。</small>

Aaron Lau(韩栖)

Nginx 学习笔记 (1) —— 源码安装与开机自启

测试系统为 CentOS 7.x,在其他 Linux 发行版的操作略有不同。为行文简洁起见,省去非 root 用户的 sudo 赋权命令,下同。

1) 准备好编译环境,安装依赖:PCRE (for regex)、zlib (for gzip) 和 OpenSSL;

yum groupinstall 'Development Tools'
yum install pcre pcre-devel zlib zlib-devel openssl openssl-devel

2) 工作目录可以是任何路径,本系列文依惯例选定 /usr/local/src。源码有 mainline 和 stable 之分,本系列文选择稳定版;

cd /usr/local/src
wget https://nginx.org/download/nginx-1.16.1.tar.gz
tar zxf nginx-1.16.1.tar.gz
cd nginx-1.16.1

3) 编译安装,通常是安装到 /usr/local/nginx。Nginx 的模块一般是静态编译,如果需求有变更,都需要修改参数(可借由 nginx -V 列出现有的)并重编译;

./configure --with-http_ssl_module --with-http_v2_module --with-http_sub_module
make && make install

4) 加入至 systemd 守护进程,实现开机自启,再创建软连接以方便使用。

cd /lib/systemd/system/
wget https://raw.githubusercontent.com/Surfine/Code-Snippets/master/nginx.service
systemctl enable nginx.service
ln -s /usr/local/nginx/sbin/nginx /usr/local/bin

<small>原载 App-scope,有删改。</small>