Nginx 学习笔记

零、写在前面


1、背景

nginx 用了起码有六七年了(最早期做 Java Web,用的是 apache tomcat),一直也没能专门整理一下,有时候遇到问题搜索引擎查找到就粘贴上去,也没好好深入的研究机理。

本文参考 《 NGINX —— A PRACTICAL GUIDE TO HIGH PERFORMANCE 》 (英文版) 和 《实战Nginx —— 取代Apache的高性能Web服务器》。

前者为 Preview Edition(预览版,只有1-5章),网上搜了一大圈都没搜到完整版,估计是作者还没出,不过即使这样,内容仍然尤其棒(后者就不推荐了)。

2、环境

  • CentOS 7.6.1810 (Core)
  • Nginx 1.17.10

一、入门


1、介绍

(1)历史

Igor Sysoev 于2000年代创建了 Nginx,并于2004年将其开源。在2015年,他创立了Nginx 同名公司,后来被 F5 Networks 以6.7亿美元收购。

(2)介绍

nginx 属于 HTTP 服务器。(也可以叫 Web 服务器

官网:

文档:
https://docs.nginx.com/

(3)原理与优缺点

原理:

  • nginx 是事件驱动的服务器。
  • nginx 采用的是多进程,(每个进程都是)单线程
  • nginx 采用 IO 多路复用的事件模型。在Linux操作系统下,Nginx使用epoll事件模型(得益于此,Nginx在Linux操作系统下效率相当高)。在OpenBSD或FreeBSD操作系统上采用类似于epoll的事件模型kqueue。

    Nginx 支持 select、poll、kqueue、epoll、rtsig 和 /dev/poll。当然,你不用特别指定事件模型,Nginx 会自动选择最佳。

优点:

  • 高性能
  • 资源消耗低
  • 模块化设计
  • 模块编写简单
  • 配置文件简洁

缺点:( nginx 性能提升的代价是降低了其他方面)

  • 稳定性,不如 apache 和 Windows Server 下的 Nginx
  • 没有像 Apache 使用.htaccess 那样的访问设置
  • 添加模块复杂(在版本 1.9.11 中增加了动态模块加载。但模块需要与 Nginx 同时编译)
(4)竞品
  • Nginx:由俄国人开发,轻量级,适合高并发。
  • Apache:重量级,速度、性能没有 nginx 快。
  • Lighttpd:由德国人开发,也是轻量级,是 Nginx 的竞争对手之一。
  • Tomcat:基于 Java ,是运行 servlet 的 JSP Web 服务器。
  • IIS:基于 C# ,只在 windows 操作系统上,是运行 .NET 的 ASP Web 服务器。

2021-04-07-00-14-16

2、安装

(1)依赖
  • PCRE (for the HTTP Rewrite module)
  • Zlib (for the HTTP Gzip module)
  • OpenSSL (for HTTPS protocol support)
(2)源代码安装

缺点:安装步骤繁琐
优点:安装时可以自定义所需模块

步骤1:下载源码

下载地址:http://nginx.org/en/download.html

版本分为:

  • Mainline version(本文写于 2020-05-27,最新为 v1.19.0)
  • Stable version【更早更稳定,推荐生产环境使用】(本文写于 2020-05-27,最新为 v1.18.0)

步骤2:装依赖

apt-get install libpcre3-dev zlib1g-dev libssl-dev

步骤3:安装

$ tar -zxvf nginx-1.18.0.tar.gz
$ cd nginx-1.18.0
$ ./configure
$ make
$ sudo make install

步骤4:安装成功

默认执行文件路径:/usr/sbin/nginx

(3)包管理工具安装

缺点:版本过时。安装时不能自定义模块。
优点:安装步骤简单。自动跟 systemctl 集成,方便管理。

sudo yum install epel-release

sudo yum install nginx

sudo systemctl enable nginx

3、查看版本

(1)nginx -v 简略
nginx version: nginx/1.17.0
(2)nginx -V 详细(+配置选项)
nginx version: nginx/1.17.0
built by gcc 4.8.5 20150623 (Red Hat 4.8.5-36) (GCC)
built with OpenSSL 1.0.2k-fips  26 Jan 2017
TLS SNI support enabled
configure arguments: --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib64/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -m64 -mtune=generic -fPIC' --with-ld-opt='-Wl,-z,relro -Wl,-z,now -pie'

4、模块

(1)查看模块支持

查看(此版本) nginx 源码支持安装哪些模块:

./configure --help

  • --without 开头,后面标记 disable 的,代表是 nginx 默认安装的模块(可卸载)
  • --with 开头,后面标记 enable 的,代表是 nginx 支持的模块(可安装)
(2)安装模块

可以通过 ./configure 结合(上面提到的)--without/--with 来按需 安装/移除 模块。

① 首次安装 nginx

$ ./configure --with-http_ssl_module \
 --with-http_spdy_module \
 --with-http_realip_module \
 --with-http_stub_status_modul 

$ make
$ make install

注:如果是包管理工具安装,那这种方式不支持。


② 已经安装过 nginx

因为 nginx 是不支持动态的安装模块的,所以必须得重新构建 nginx。

跟(上面的)首次安装 nginx 一样的操作,只不过最后的 make install 最好不要加,否则就是覆盖安装(不光是 nginx 执行文件,配置文件什么的都会被覆盖),推荐先备份原有的 nginx 执行文件,然后再手动把新构建生成的 nginx 执行文件覆盖过去。

注:如果是包管理工具安装,原理一样,但得确保下载相同版本的nginx源码包。

(3)查看安装了哪些模块

nginx -V 2>&1 | tr -- - '\n' | grep module

(4)官方模块 vs 第三方模块

上面介绍的都是官方模块,第三方模块待写。

第三方模块list:https://www.nginx.com/resources/wiki/modules/index.html

二、管理


1、check nginx 是否启动正常

查看 nginx 进程:ps aux | grep nginx

2、signal


nginx 跑起来后,支持下面的 Signal:

Signal Description
TERM INT Quick shutdown
QUIT Graceful shutdown
KILL Halts a stubborn process
HUP Configuration reload
USR1 Reopen the log €les (useful for log rotation)
USR2 Upgrade executable on the fly
WINCH Gracefully shutdown worker processes

常用命令的对应关系:

Signal systemctl systemctl
kill -QUIT cat /var/run/nginx.pid nginx -s quit
kill -TERM cat /var/run/nginx.pid nginx -s stop systemctl stop nginx.service
kill -KILL cat /var/run/nginx.pid ---- systemctl kill nginx.service
kill -HUP cat /var/run/nginx.pid nginx -s reload systemctl reload nginx.service

注1:QUIT 跟 TERM / KILL 的区别在于,是否在 Nginx 退出前完成已经接受的连接请求。

注2:nginx 的 reload ,即热部署(此后新生成的 worker 进程,会使用新的配置,至于老的 worker 进程,要等他们把手上的请求处理完毕后再退出。)

三、基本配置


1、配置文件 —— nginx.conf 文件

(1)nginx -t

nginx -t:查找 nginx.conf 配置文件在哪 + 检测配置文件是否合法。

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

用途:改了配置文件,但是不想 reload nginx ,就先用这个试试,免得影响正式环境。

(2)启动时指定配置文件

nginx -c

2、配置结构

events {
}

http {
 server {
 }
}

3、指令

以这段简单的配置为例:

http {
    server {
        listen *:80;
        server_name "";
        root /usr/share/nginx/html;
    }
}
(1)指令分类

简单指令 Simple Directives

listen *:80;,是由 名称(listen)-参数(*:80)-分号(;) 构成。


上下文指令 Context Directives

server {
    
}

注:简单指令只能包含在上下文指令中。(最高层的简单指令可以理解成在一个隐藏的上下文指令中。)

(2)指令继承 Directive Inheritance

指令总是向下继承,除非有内部重名的会覆盖外层。

这跟编程语言变量作用域的规则一样。

4、具体指令介绍

(1)user

user nginx;

执行 ps aux | grep nginx 命令可以看到:

root      5791  0.0  0.4  47184  2108 ?        Ss   18:22   0:00 nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf
nginx     5807  0.0  0.4  47188  2248 ?        S    18:22   0:00 nginx: worker process

Nginx 主进程(master process)会先以 root 权限运行,之后主进程读取 nginx.conf 文件中的 user 模块的配置,使用此用户启动 工作进程(worker process)。

问:为什么 主进程 需要使用 root 用户运行?

答:因为主进程需要很多权限,例如:

  • 监听小于1024的端口
  • 读取和写入日志文件,
  • 存储临时数据
  • ……
(2)worker_processes

worker_processes 1;

指定工作进程数。推荐 auto,它会自动检测逻辑CPU内核数。

如果 cpu 使用了超线程技术,例如双核4线程,可以设置为4。

拓展:

  • 物理cpu数:主板上实际插入的cpu数量。
  • cpu核数:单块CPU上面能处理数据的芯片组的数量,如双核、四核等。
  • 逻辑cpu数:一般情况下,逻辑cpu=物理CPU数×cpu核数。
(3)Events 上下文指令

Events 这个上下文指令只能在配置文件里出现一次。

events {
 worker_connections 512;
}

① worker_connections

单个工作进程可以允许同时建立连接的数量。(此为调优的重要参数)

注:如果往上调整太多,记得有可能需要同时调整操作系统的最大打开文件数限制(ulimit)。

(4)HTTP 上下文指令

① sendfile + tcp_nopush / tcp_nodelay

1、sendfile

sendfile on;

sendfile 是 Linux 2.0+ 推出的一个系统调用方法。

  • 原来:硬盘 >> kernel buffer >> user buffer >> kernel socket buffer >> 协议栈(传递数据需要从用户空间经过)
  • 现在:硬盘 >> kernel buffer >> kernel socket buffer >> 协议栈(传递数据直接在内核空间进行,更快)

所以,sendfile 是个比 read 和 write 更高性能的方法。

适用范围:

  • 当 Nginx 是作为一个静态文件服务器来使用的时候,开启 sendfile 能大大提高性能。
  • 当 Nginx 是作为一个反向代理服务器来使用的时候,开启 sendfile 没什么用。(因为 sendfile 方法只支持文件句柄,不支持 socket)
  • 当想开启 tcp_nopush 的时候,sendfile 必须打开(下面会介绍)

建议:一般我们会把 nginx 即当静态文件也当反向代理,所以建议开着。

2、tcp_nopush / tcp_nodelay

在我之前一篇文章 朝花夕拾——《网络是怎样连接的》 中的 TCP 章节,我提到了为了解决 糊涂窗口综合症 SWS 而采用的两种算法,它们分别对应:

  • Nagle 算法 —— tcp_nodelay
  • Cork 算法 —— tcp_nopush

它们分别的适用场景也请看我的那篇文章。

# tcp_nodelay on; # 默认 on ,即 Nagle 算法关闭
# tcp_nopush on; # 默认 off ,即 Cork 算法关闭(注意:tcp_nopush 开启的前提是 sendfile 也得开启,原因未知,待写)

5、虚拟主机

虚拟主机是一种在单一主机或主机群上,实现多网域服务的方法,可以运行多个网站或服务的技术。

  • 方法1、 基于IP地址。前提是你的主机得有多个 IP 地址,这个成本较大。
  • 方法2、 基于端口号。但一般网址都是默认 80/443,弄别的端口体验会很差。
  • 方法3、 基于主机名。HTTP1.1 新增 Host 头,用来填写主机名。
(1)方法1

通过 listen 对应 IP。

server {
 listen 172.19.205.200:80; 
}
server {
 listen 172.19.205.201:80; 
} 

如果不知道 本机 IP ,可以通过 ip addr 查看。

更多操作:

# 监听所有

server {
 listen *:80;  
}

server {
 listen 0.0.0.0:80;  
}
 
server {
 listen 80;  
}

如果本机只有一个IP,那 监听那一个IP = 监听所有。

(2)方法2

通过 listen 对应端口。

server {
 listen 80; 
}
server {
 listen 81; 
} 
(3)方法3

通过 server_name,对应 HTTP 的 Host 头。

注1:虽然 HTTP 对 Host 头的定义是 域名:端口(可选),但是 nginx 匹配的时候不会管 Host 的端口。
注2:若不传 Host 头,nginx 会返回 400 Bad Request。

server {
 listen 80; 
 server_name  test1.xjnotxj.com;
}
server {
 listen 80; 
 server_name  test2.xjnotxj.com;
} 

高级操作:

# 支持多个值
server_name example.com www.example.com;

# 支持通配符 
server_name example.com *.example.com www.example.*;
server_name ~^www[0-9]\.example\.com$;

问:网址上不是有域名吗?为什么还要多引入一个 Host 头来存域名?

网址上的域名会被(例如浏览器)先通过 DNS 解析成 IP 地址,然后发送 HTTP 报文的时候,报文头部是不包含任何域名的,只有域名后的路径(例如 http://example.com/api/shop 的报文头部第一行是 GET /api/shop HTTP/1.0 ),所以 HTTP/1.1 引入了 Host 头部,用来填写域名(浏览器默认行为就是填入网址的域名),可以实现虚拟主机功能。

Host 头部也可以随便填写,未必非要是网址的域名

(4)3 种方法的匹配规则

① default_server

先介绍下 default_server 的写法(下面会提有什么用)

server {
 listen 80 default_server;
 server_name foobar.com;
}

② 匹配规则

1、先匹配 listen

如果这都匹配不到,则根本就不会有响应,因为 HTTP 连接没法建立

2、然后匹配 server_name

  • 准确值优先级大于通配符
  • 如果没找到,有 default_server 则匹配
  • 如果没有 default_server,就找第一个出现的 server

建议在负责兜底的匹配规则里:

  • 首先有 default_server
  • 然后 server_name 写成 _(只是惯例,你写成 @#$ 也没事,反正不会被匹配上),
  • 返回的话,一、写上 return 444,这是 nginx 自己独有的 HTTP 响应码,含义是服务器不向客户端返回任何信息,并关闭连接(有助于恶意软件的威胁)。二、可以重定向到你定制的错误页面或者官网首页。

6、Location Block

location 即 url 的匹配规则。

(1)修饰符

2021-04-07-00-14-21


先是最普通的写法:

location /api {} # 前缀匹配

然后支持上面表的修饰符

location = /api {} # 精确匹配

location ~ \.php$ {} # 正则匹配(区分大小写)

location ~* \.php$ {} # 正则匹配(不分大小写)

location ^~ /api {} # 非正则的前缀匹配


(2)匹配

匹配规则(按顺序依次匹配):

  • 精确匹配 =
  • 非正则的前缀匹配 ^~
  • 正则匹配 ~~*
  • 前缀匹配

注1:

  • 一个网址中,域名不分大小写(例如 chrome 地址栏中输入大写,会自动变小写),但是后面的路径分大小写, nginx 也分。
  • 注意有两种前缀匹配,且在这两种前缀匹配下,匹配越长优先级越高,与 location 所在位置的顺序无关
  • 正则匹配如果匹配了多个,会选择出现顺序的首个,所以建议越精细的放的越靠前。
  • 最终还是没找到,nginx 响应 404。

注2:关于网址斜杠的问题

  • 一个域名的最后一定要有一个斜杠,这是 RFC1738 规定的,如 https://www.baidu.com/ 是对的,而 https://www.baidu.com 不对,但日常大家都习惯不加,是因为如浏览器等都会帮我们自动加上。(对应 nginx 的精确匹配就是 location = / {}
  • 一个网址(域名+路径)的最后的斜杠可有可没有。没有代表一个文件,有代表一个目录
  • 域名不看,nginx 只会把路径当成待比较的值,去参与上面介绍的匹配规则。(如 https://www.baidu.com/api/abc ,只会用 /api/abc 去参与匹配)
  • nginx 用来匹配的路径,会忽略 ? 及后面。(如 https://www.baidu.com/api?param1&param2,只会用 /api 去参与匹配)
(3)Named Location Blocks

Named Location 不能直接用于常规请求处理,并且永远不会与请求URI匹配。相反,它们只适用于内部重定向,搭配 try_files 指令。

try_files 指令检查是否存在所提供的文件名(按顺序检查,从左到右),try_files 的最后一个参数便是 Named Location。

try_files 是 nginx 在 0.7 以后的版本中加入的指令(放置在 server 或 location 块中),配合 Named Location,可以部分替代原本常用的 rewrite 。(rewrite 下面会详细介绍)

使用:

location / {
 try_files maintenance.html index.html @foobar;
}
location @foobar {
 ...
}

拓展:try_files 不一定非要搭配 location,直接返回响应码也是可以的:

location / {
 try_files maintenance.html index.html =404
}
(4)Location Block 内的指令

① 指定文件系统路径 —— root / alias

1、root 指令:

location /foobar/resource_root { 
    root /etc/nginx/static/;
} 

http://example.org/foobar/resource_root/b.jpg 的请求将解析为文件系统路径 /etc/nginx/static/foobar/resource_root/b.jpg。

原理:文件系统目录 = root + url_path(即 /etc/nginx/static/ + /foobar/resource_root/b.jpg = /etc/nginx/static//foobar/resource_root/b.jpg )

注:上面的结果 /etc/nginx/static//foobar/resource_root/b.jpg 注意中间是有两个斜杠。root 路径最后的斜杠可带可不带,因为 url_path 的开头肯定是有斜杠的,最终都会合法。即 /etc/nginx/static/; 也可以,/etc/nginx/static; 也可以。


2、alias 指令:

location /foobar/resource_alias {  
    alias /etc/nginx/static/;
} 

https://xjnotxj.com/foobar/resource_alias/img/a.jpg 的请求将解析为文件系统路径 /etc/nginx/static/img/a.jpg。

原理:文件系统目录 = alias + (url_path - location)(即 /etc/nginx/static/ + /img/a.jpg = /etc/nginx/static//img/a.jpg )

注:上面的结果 /etc/nginx/static//img/a.jpg 注意中间是有两个斜杠。alias 路径最后的斜杠带不带要仔细考量,因为 (url_path - location) 的开头不一定有斜杠,最终结果不一定合法。


注:root 和 alias 还有一个区别,即:alias 只能位于 location 块中,而 root 可以在 location 块也可以在 server 块。(原因很简单,从它们两者的原理就可以看出,因为 alias 计算文件系统路径需要 location 的参与,而 root 不需要。)


② 指定网站默认页 —— index 指令

默认值:index index.html

原理:内部重定向【重点】

内部重定向即再一次搜索同一个 server 下的 location

最早的时候这个文件被叫作“主页” (home page),意思就是当省略文件名时访问的那个默认的页面。随着 Web 的普及,这个词的意义似乎并没有被正确理解,现在不光是默认页面,似乎随便什么网页都可以被叫作主页了。

7、Configuring SSL

(1)SSL 证书

① 证书介绍

SSL证书需要国际公认,得从证书认证机构(简称CA,Certificate Authority)申请。

证书有3种类型:

  • 域名型SSL证书(DV SSL):一般用于自建网站
  • 企业型SSL证书(OV SSL):一般用于普通企业
  • 增强型SSL证书(EV SSL):一般用于银行证券等金融机构。

这几种证书的区别:

  • 审核严格程度
  • 是否支持多域名(单域名 / 通配符域名(子域名)/ 多域名)
  • 证书公信等级
  • 加密强度
  • 浏览器及兼容性
  • 是否支持安全保险

② 证书购买

以在阿里云买为例:

因为我搭的是个人站点,所以选择最便宜的证书,即:Digicert 这家的免费版 DV 证书(单域名 + 一年有效期)

注:需要备案。


③ 证书下载

包含两个文件:

  • .pem:证书文件。
  • .key:证书的密钥文件。
(2)配置 HTTPS

① 上传证书

在 Nginx 的配置文件所在目录(/etc/nginx)下创建cert目录,并将上面下载的证书文件和密钥文件拷贝进去。


② 修改配置文件

  server {
  listen 443;
  server_name localhost;
  ssl on;
  ssl_certificate cert/3921616_xjnotxj.com.pem;
  ssl_certificate_key cert/3921616_xjnotxj.com.key;
  ssl_session_timeout 5m;
  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
  ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;
  ssl_prefer_server_ciphers on;
 

注1、开启 ssl 的两种方式:

① 推荐
listen 443 ssl;

② deprecated,不建议用
listen 443;
ssl on;

注2、ssl_certificate 和 ssl_certificate_key 可以使用相对路径 or 绝对路径。

记得 reload nignx。


(3)设置 HTTP 自动跳转到 HTTPS(nook)
server {
 listen 80; 
 rewrite ^(.*)$ https://$host$1 permanent; 
} 

8、日志

(1)基本用法

① 访问日志 access_log

需要设置 format:

access_log  /var/log/nginx/access.log  main;

log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';


② 错误日志 error_log

需要设置 级别:

error_log  /var/log/nginx/error.log warn; 

级别按严重程度分:debug,info,notice,warn,error,crit,alert 和 emerg,只有等于或高于此级别才会写入错误日志中。

(2)日志切割

待写。

9、配置范例

https://www.nginx.com/resources/wiki/start/topics/examples/full/

四、动态程序


上面介绍的是 nginx 处理静态文件,nginx 也可以运行程序,然后将程序输出的数据返回给客户端,即动态页面

1、CGI、FastCGI

实现步骤:

  • 一般通过 url 路径中的文件扩展名来进行判断是静态还是动态页面,例如将 .cgi、.php 等扩展名设置为动态页面。
  • 接着通过配置 CGI / FastCGI 来运行程序,返回结果。

    与 Apache 不同,nginx 不能嵌入编程语言解释器进入 Web 服务器,就像 Apache 使用 mod_php 一样。只能通过 CGI / FastCGI,这让 nginx 轻量很多。

详细介绍请参考我的旧文:CGI + FastCGI(PHP-FPM)联系与区别 【图解 + 注释】

2、反向代理

其他大多数语言(Ruby,Node,Go等),则不用 CGI / FastCGI 之流,而是使用反向代理(下面会介绍)。

五、反向代理


1、用处

  • 实现动态Web应用程序
  • 负载均衡

    下面会专门介绍

2、用法

以 nodejs 版本为例:

http {

    upstream node_app {
        server 127.0.0.1:3000;  # focus here
    }

    server {
            listen *:80;
            root /path/to/application/public;
            location / {
                try_files $uri $uri/index.html @node;
            }
            location @node {
                proxy_pass http://node_app;   # focus here
            }
    }
    
}

注1:这里的 upstream 中的 server 只有一个,如果是多个的话,就起到负载均衡的功能了。

注2:server 支持三种形式:

upstream backend {
    # IP Address with Port
    server 192.0.2.10:443;
    # Hostname
    server app1.example.com;
    # Unix Socket(下面会专门介绍)
    server unix:/u/apps/my_app/current/tmp/unicorn.sock
}
[拓展] nginx 中关于 url 的变量

如果 url 是 http://example.com/stat.php?id=1585378&web_id=1585378 ,则:

  • $request_uri: /stat.php?id=1585378&web_id=1585378
  • $uri: /stat.php
  • $document_uri: /stat.php (=$uri)

3、Upstream 使用域名(Hostname)

当 nginx 初始化并读取配置文件,它会使用 DNS 查找来解析域名,获取 ip 地址:

  • 如果域名跟 ip 的绑定是静态的,没关系。
  • 如果域名跟 ip 的绑定是动态的,那 nginx 就会报错(example.com could not be resolved.)

解决方案:使用 resolve

http {
 resolver 8.8.8.8; # 指定 DNS 地址
 # resolver 8.8.8.8 valid=30s; # valid 可选,默认使用响应的TTL值。
 upstream backend {
    server loadbalancer.east.elb.amazonaws.com resolve; # focus here
 }
}

4、Upstream 使用 Unix Domain Sockets

如果 upstream 指定的 server 在同一台机器上,可以使用 Unix Domain Sockets(UDS)

UDS 属于进程间通信IPC,Inter-Process Communication)的一种,仅在操作系统内核中完成,不需要经过网络协议栈(免去打包拆包等操作,因此性能更好),使用文件系统作为名称空间(不需要IP和Port)。

① 应用程序(以 Node.js 为例):

var http = require('http');

http.createServer(function(req, res) {
  console.log('received request');
  res.end('received request\n');
}).listen('/tmp/node_app.socket');    // focus here
// }).listen(80);

② nginx 配置:

upstream node_app {
    server unix:/tmp/node_app.socket;  # focus here
}

注意 /tmp/node_app.socket 的权限问题。

5、upstream 常见的 headers 转发

(1)默认行为

默认情况下,nginx 在代理请求时,会对 headers :

  • 重新定义 “Host” 和 “Connection” 标头
  • remove 值为空字符串的标头
  • 剩下的标头会继承
(2)如何修改 headers

通过 proxy_set_header

(3)常见的修改

① Host【推荐】

proxy_set_header Host $host;

因为默认的 nginx 会修改 host 值为自身,但作为一个好的代理,最好改写 host 为源值。

例如,在一些 SaaS 应用程序中通过 host 标识帐户很常见。


② X-Forwarded-XXX

这些字段是代理特有的字段,分别表示自身的 Host、IP 地址、协议类型(http/https)、端口。

注意,除了 X-Forwarded-For 是多个值(格式为 <client>, <proxy1>, <proxy2>, ……),其余的都是单个值。

  • X-Forwarded-Host
  • X-Forwarded-For(最常用,简称 XFF
  • X-Forwarded-Proto
  • X-Forwarded-Port

具体修改(主要是 X-Forwarded-For),下面会介绍。

补充:X-Forwarded-For 是一个 HTTP 扩展头部。HTTP/1.1(RFC 2616)协议并没有对它的定义,它最开始是由 Squid 这个缓存代理软件引入,用来表示 HTTP 请求端真实 IP。如今它已经成为事实上的标准,被各大 HTTP 代理、负载均衡等转发服务广泛使用,并被写入 RFC 7239(Forwarded HTTP Extension)标准之中。

6、获取客户端(用户)的真实 IP

(1)服务器端获取方式

以 Node.js(Express)为例:

这里我们用到第三方库 request-ip,原理为:

① 查找 HTTP Header (针对源目标的 IP 地址,未必真实(HTTP Header 的头可以随便修改))

按顺序查找:

X-Client-IP
X-Forwarded-For (Header may return multiple IP addresses in the format: "client IP, proxy 1 IP, proxy 2 IP", so we take the the first one.)
CF-Connecting-IP (Cloudflare)
Fastly-Client-Ip (Fastly CDN and Firebase hosting header when forwared to a cloud function)
True-Client-Ip (Akamai and Cloudflare)
X-Real-IP (Nginx proxy/FastCGI)
X-Cluster-Client-IP (Rackspace LB, Riverbed Stingray)
X-Forwarded, Forwarded-For and Forwarded (Variations of #2) 

② 查找 req 对象 (针对上一跳的 IP 地址(未必是源目标),但一定真实(因为基于 tcp 的可靠连接, ip 无法伪造))

按顺序查找:

req.connection.remoteAddress
req.socket.remoteAddress
req.connection.socket.remoteAddress # 会报错 Cannot read property,估计废弃了
req.info.remoteAddress # 会报错 Cannot read property,估计废弃了

③ 如果还是找不到,返回 null

不建议使用 express 自带的 req.ip,因为它只会取 X-Forwarded-For 的值返回数组。


④ 补充:

建议条件充足可以把都记录下来,方便后续使用或者人工排查。

(2)nginx 配置

nginx 这里要做好上一跳 IP 地址的记录(记录在 Header 中),否则服务器端获取的就是 nginx 的 IP 地址了。

① 方法1:使用 proxy_set_header 改写 header

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  • $remote_addr 为上一跳的 IP 地址。
  • $proxy_add_x_forwarded_for"X-Forwarded-For" + ”,“ + "$remote_addr",如果没有 "X-Forwarded-For",直接 = "$remote_addr"。

② 方法2:使用 nginx 的 http_realip 模块

编译安装的时候,add --with-http_realip_module

set_real_ip_from 192.168.1.0/24; #真实服务器上一级代理的IP地址或者IP段,可以写多行。
set_real_ip_from 192.168.2.1; 
# real_ip_header X-REAL-IP; 
real_ip_header X-Forwarded-For;  #从哪个header头检索出要的IP地址。
real_ip_recursive on; #递归的去除所配置中的可信IP。排除set_real_ip_from里面出现的IP

实测跟 Node.js app 搭配不起作用,不知道为什么?这里待写。

7、WebSockets

(1)HTTP Header 流程

步骤1:浏览器发送请求,申请升级为 WebSockets 协议。

Upgrade: websocket
Connection: Upgrade

步骤2:如果服务器支持 Websockets,它将以 HTTP 状态码 101 响应切换协议而不是正常的 200 响应,且返回一样的 Header。

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
(2)nginx 如何配置

nginx 对 WebSockets 开箱即用。

location /chat {
 proxy_pass http://node_app;
 proxy_http_version 1.1;
 proxy_set_header Upgrade $http_upgrade;  # focus here
 proxy_set_header Connection "upgrade";  # focus here
 proxy_set_header Host $host;
} 

proxy_http_version 被显式设置成 1.1,其实没有,默认还是 1.1。

六、负载均衡


负载平衡实际上是反向代理的一种。

1、HTTP 负载均衡

详细可看我之前的一篇:《nginx官方文档 之 http负载均衡 学习笔记》

2、TCP 负载均衡

  • nginx 开源版 1.9.0 支持 TCP 负载平衡。
  • 之前只能使用商业版的 nginx,或者别的竞品: LVS、Nginx、HAProxy。

    这些竞品中,作者推荐使用 nginx。从技术上讲,在负载方面,HAProxy 和 Varnish 可能比 nginx 快 5-10%。但是,nginx 简单,且适应面更广。AWS上 的 ELB 当然更简单,但功能也有限。

(1)写法
stream {
    upstream backend {
        server 127.0.0.1:8089; 
        server 127.0.0.2:1935;
        server 127.0.0.3:1935;  
    }
    server {
        listen 80;  
        proxy_pass backend;
    }
}

跟 HTTP 负载均衡写法的异同:

  • stream block 取代 http block(stream 表示 TCP/UDP 流量
  • upstream 写法不变
  • proxy_pass 属于 server 而不是 location,server 也不存在 location。
(2)优缺点

优点:性能好。直接转发原始TCP数据包,没有数据解析;如果是 HTTPS ,也免去了 SSL 层的损耗。

缺点:功能损失。例如无法通过 proxy_set_header 改写/注入 HTTP 标头


[拓展] 既然 proxy_set_header 无法用,那 TCP 代理怎么传递客户端的原始 IP 信息呢?

可以使用 proxy protocol

proxy protocol 是 HAProxy 的作者 Willy Tarreau 于 2010 年开发和设计的一个 Internet 协议,通过为 tcp 添加一个很小的头信息,来方便的传递客户端信息(协议栈、源IP、目的IP、源端口、目的端口等)。其本质是在三次握手结束后由代理在连接中插入了一个携带了原始连接四元组信息的数据包。

具体例子参考:https://docs.nginx.com/nginx/admin-guide/load-balancer/using-proxy-protocol/

proxy protocol 也可以用在 HTTP 负载均衡上。

3、邮件协议的负载均衡

# 邮件流量
mail{}

待写。

七、重写


1、介绍

rewrite 是 nginx 的重写模块,即ngx_http_rewrite_module

2、目的

  • 通知用户他们正在请求的资源现在位于其他位置
  • 控制处理流程
  • ……

3、涉及命令

  • returnrewrite

    之前介绍的 try_files 也可以部分做到

  • 重写模块同时也涉及了 breakifset 等其它命令

4、return

(1)介绍

语法:

return URL; # 外部重定向
return code URL;
return code [text];

可以将 return 指令放置在 server 或 location 中。

(2)demo
return https://baidu.com; # 默认 302
return 301 https://baidu.com; # 必须是支持重定向的响应码,比如 200 就不行
return 200 "okokok"; # 修改不了响应行文本,原因未知

5、rewrite

(1)介绍

语法:rewrite regex replacement [flag];

可以将 rewrite 指令放置在 server 或 location 中。

(2)参数

① regex 为正则表达式

② flag 取值如下:

flag break last redirect permanent
是否停下
重定向 不会 不会 内部重定向 外部重定向(临时,302) 外部重定向(永久,301)

注意,rewrite 后 url 就会变了,下一个 rewrite 识别的就是改过的 url

③ replacement 为重写的值

  • 如果 replacement 以协议头开头(如:http://,https:// 或 $scheme),则效果相当于 flag = redirect。
  • 如果想在 replacement 中抛弃之前路径的请求参数,可以在最后加了个 ?,如 rewrite ^/users/(.*)$ /show?user=$1?
(3)demo

以 flag = 无 为例:

location / {  
    rewrite ^/user/(.*)$  /show/$1;
    rewrite ^/show/(.*)$  /hide/$1;
} 

location /show {
    return 200;
}
location /hide {
    return 201;
}    

访问 https://example.com/user/a.jpg:

  • 1、/user/a.jpg 被 location / 捕获
  • 2、第一个 rewrite 把 /user/a.jpg 改写成 /show/a.jpg

    注意,这时的路径已经被改写成 /show/a.jpg 了。

  • 3、第二个 rewrite 把 /show/a.jpg 改写成 /hide/a.jpg
  • 4、下面没有命令了,执行内部重定向
  • 5、/hide/a.jpg 被 location /hide 捕获,返回 201

注:诸如 $1$2 这种变量,表示正则表达式匹配的第 N 个参数.

(4)死循环

某些情况下,rewrite 重定向后,可能又会执行 rewrite,然后循环往复,进入死循环。

为了避免这种情况,nginx 会在循环 10 次时之后,直接返回 500 异常

6、其他命令

(1)set —— 设置变量
set $var1 "hello world";
return 200 "response ok $var1";
(2)if —— 判断条件

if 比 rewrite 的正则匹配更灵活。

待写。

7、实例

注意:rewrite 因为有正则,所以比 return 更加影响性能(if 语句也如是),且可读性差,所以如果能用 return 就用 return。

(1)从旧网址重定向到新网址
server {
    listen 80;
    listen 443 ssl;
    server_name www.old-name.com old-name.com;

    return 301 $scheme://www.new-name.com$request_uri; 

    # 不推荐
    rewrite ^ $scheme://www.new-name.com$request_uri permanent;   
}
(2)强制所有请求使用 HTTPS
server {
    listen 80;
    server_name www.domain.com;

    return 301 https://www.domain.com$request_uri;

    # 不推荐
    if ($scheme != "https") {
        rewrite ^ https://www.mydomain.com$request_uri permanent;
    }
}
(3)添加/删​​除 www 前缀
# add 'www'
server {
    listen 80;
    listen 443 ssl;
    server_name domain.com;
    return 301 $scheme://www.domain.com$request_uri;
}

# remove 'www'
server {
    listen 80;
    listen 443 ssl;
    server_name www.domain.com;
    return 301 $scheme://domain.com$request_uri;
}
(4)兜底,把其它流量重定向到正确的域名 or 返回错误
server {
    listen 80 default_server;
    listen 443 ssl default_server;
    server_name _;

    return 301 $scheme://www.domain.com;
    # or
    return 444
}

八、调试


1、调试 nginx

用到 --with-debug ,待写。

2、如何调试 location

可以通过在不同 location 里添加 access_log 来调试。

3、如何调试 rewrite

  • 1、rewrite_log on; 开启 nginx 日志
  • 2、设置 error_log 的 level 是 notice

九、监控


监控指标:

2021-04-07-00-14-35

1、使用 http_stub_status_module 模块

待写

2、使用 Nginx Plus

待写

3、使用 openresty + lua

待写

4、使用 阿里云 日志服务

待写

十、实例


1、nginx 禁止 / 允许某个(些) IP 访问

使用到 ngx_http_access_module 模块

deny,相反的为 allow

deny 1.1.1.1; 
deny 8.0.0.0/8;
deny all
 
allow 1.1.1.1; 
allow 8.0.0.0/8;
allow all; # default 

原理:通过 $remote_addr 来判断。

在需要很多规则的情况下,最好使用 ngx_http_geo_module 模块。

结果:返回 403。

2、错误处理

error_page

(1)常见 HTTP (响应码)错误
  • 403 Forbidden
  • 404 Not Found
  • 500 Internal Server Error

    通常是 upstream server 返回的错误

  • 502 Bad Gateway

    通常是 nginx 无法到达 upstream server

  • 503 Service Unavailable

    通常是 nginx 因暂时超载或临时维护,无法处理HTTP请求

  • 504 gateway timeout

    通常是 upstream server 返回超时

(2)用法

error_page 可以位于 server block / location block

原理:通过内部重定向。(跟上面介绍的 index 指令 一样)

① 针对普通情况

基本:

# 指向错误页面
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;

# 重定向到外部 url
error_page 500 http://www.google.com;

# 重定向到 named location block
location / {
    error_page 404 = @fallback;
}
location @fallback {
    proxy_pass http://different_backend;
}

注1:因为 error_page 是通过内部重定向,所以你可以显示地写一个与之对应的 location (如 location /404.html)来干更多事。

注2:因为 error_page 是通过内部重定向,所以 access 日志会有两条记录。


高级:

# 更改HTTP状态码(不推荐,这样 nginx 日志记录也是)
error_page 500 = 200 /index.html

② 针对反向代理(使用 proxy_intercept_errors,error_page 才会生效)

location @rails {
    proxy_pass http://rails_app;
    proxy_intercept_errors on; # focus here
    error_page 404 /404.html;
}

十一、优化


NGINX是第一个可同时建立10,000个并发连接(即“C10K问题”)的Web服务器软件。

所以下面介绍的的优化,更多的还是为了实现高并发的目标。

这里介绍的只是简单的优化,离 C10K 还有点距离,但是经过这种一般优化后,峰值能保持在 1~3w 左右。(内存和 CPU 核心数不同,会有进一步优化空间)

1、Linux (内核)配置

(1)sysctl 的使用

sysctl 命令被用于在内核运行时动态地修改内核的运行参数,它包含一些TCP/IP堆栈和虚拟内存系统的高级选项。

查看配置:sysctl -a

临时添加配置(重启后会失效):sysctl -w net.ipv4.ip_forward=1

永久添加配置:修改配置文件 /etc/sysctl.conf,并重启使之生效 /sbin/sysctl -p

(2)net.core.somaxconn

net.core.somaxconn 参数表示 socket listen backlog(socket 的监听队列)上限。当一个请求尚未被处理或建立时,他会先进入 backlog 等着。所以当 server 处理请求较慢,以至于监听队列被填满后,新来的请求就会被拒绝。

所以你也可以把 somaxconn 理解成 能接收新的 TCP 数 or TCP 最大连接数。

默认值:128

什么时候需要设置:并发量高的时候(如在 error log 中发现 [73920] possible SYN flooding on port 80. Sending cookies. 的报错)

设置小了的危险:连接超时或重传

推荐值:根据你的当前流量来,如果害怕突发流量,可以设置最大(65535)。

2、nginx 配置优化

(1)连接

① 最大连接数

Nginx 是多进程模型,Worker 进程用于处理请求;

  • 单个 worker 进程的最大连接数:worker_connections
  • worker 进程的数量:worker_processes

注意:worker_connections 受 Linux 的进程最大打开文件数限制,需执行ulimit -n 65535 或修改相应配置文件。

所以:max_clients(Nginx 的最大连接数) = worker_connections x worker_processes

注意:Nginx 作为反向代理服务器时,max_clients(Nginx 的最大连接数) = worker_connections x worker_processes / 2,因为 Nginx 会同时建立 Client 的连接和后端 Web Server 的连接,即占用 2 个连接。


② 最大打开文件数

worker_rlimit_nofile即单个 worker 进程最大打开文件数,最好与 ulimit -n 的值保持一致。

worker_rlimit_nofile 65535;

③ 同一时刻接收多个连接

默认情况下,Worker 进程只会在一个时刻接收一个新的连接,multi_accept 为 on 可以实现在一个时刻内可以接收多个新的连接,提高处理效率。

events {
  multi_accept on; # 默认 off
}
(2)各种 timeouts

待写

(3)各种 buffer

buffer 即内存中处理,超过 buffer 指定的大小,则会被临时写入文件,这便影响了性能。

待写。

(4)禁用 access_log

比如针对静态资源,可以加上 access_log off;

(5)压缩

Gzip 的压缩页面需要浏览器和服务器双方都支持。

关于后来 google 推的 Brotli 压缩算法,对比 Gzip 可再减少20%,这个待写。

过程:

  • 浏览器发送请求头包含:Accept-Encoding: gzip, deflate, br
  • 服务器响应请求头包含:Content-Encoding: gzip

配置:

gzip on;                
gzip_min_length 1k;      # 设置允许压缩的最小字节(从header头的Content-Length中获取) 。当值为0时,所有都进行压缩。建议大于1k
gzip_buffers 4 16k;      # 设置gzip申请内存的大小,按块大小的倍数
gzip_http_version 1.1;   # 设置允许压缩的最低http版本
gzip_comp_level 5;       # 设置压缩等级,等级1-9,越低,压缩速度越快(越不占用 CPU)文件压缩比越小,反之相反
gzip_types text/plain application/x-javascript text/css application/xml text/javascript application/x-httpd-php application/javascript application/json;    # 设置需要压缩的MIME类型
gzip_vary on;            # 响应头 add:"Vary: Accept-Encoding"
 
gzip_proxied off;        # 下面单独介绍

解释:

1、gzip_comp_level 按需求来把,不然适用即可。通常压缩后的大小会减小到原来的 1/4 - 1/3。

2、gzip_types,默认为 text/html; 即只压缩 html,但推荐压缩 html 、js 、css 、xml 、shtml,不推荐压缩图片和视频(因为本身就压缩过了)
Nginx 判断文件类型不是分析文件内容,而是分析文件后缀(根据文件扩展名确定其MIME类型)

3、gzip_proxied,当 nginx 自己处在反向代理中:

  • off (不启用压缩)【默认】
  • expired (启用压缩,如果header头中包括"Expires"头信息)
  • no-cache (启用压缩,如果header头中包含"Cache-Control:no-cache")
  • no-store (启用压缩,如果header头中包含"Cache-Control:no-store")
  • private (启用压缩,如果header头中包含"Cache-Control:private")
  • no_last_modefied (启用压缩,如果header头中不包含"Last-Modified")
  • no_etag (启用压缩,如果header头中不包含"Etag"头信息)
  • auth (启用压缩,如果header头中包含"Authorization"头信息)
  • any (无条件启用压缩)

十二、使用 Lua 扩展 Nginx 功能


最先将 Nginx,Lua 组合到一起的是 OpenResty,它有一个ngx_lua模块,将Lua嵌入到了Nginx里面。

随后Tengine也包含了ngx_lua模块。至于二者的区别:OpenResty是Nginx的Bundle;而Tengine则是Nginx的Fork。

值得一提的是,OpenResty和Tengine均是国人自己创建的项目,前者主要由春哥和晓哲开发,后者主要由淘宝打理。

待写。

posted @ 2020-06-11 16:51  小蒋不素小蒋  阅读(1326)  评论(2编辑  收藏  举报

ICP证:沪ICP备20014317号