1 Nginx简介
Web服务器市场份额
Nginx [engine x] 最初由 Lgor Sysoev 编写。根据 Netcraft 的数据,到2020年9月,Nginx 服务或代理了25.76%站点,市场份额占到了约34.03%。
Nginx 被广泛用作:
· HTTP服务器
· 反向代理服务器
· 邮件代理服务器
· 通用的TCP/UDP代理
2 Nginx架构
Nginx服务由一个Master进程和多个Worker进程构成。Master进程的主要职责是读取、解析配置文件,并维护工作进程。工作进程则负责实际的请求处理。
为了高效的在Worker之间分发请求,Nginx引入了依赖于操作系统的、高效的事件驱动模型。Worker进程的数量常常根据CPU核心数设置。这种模式的好处是每个worker进程都相互独立,无需添加锁,减少锁的开销。采用独立的进程并且不相互影响,一个进程进程退出并不影响其他的进程。一个worker进程的异常退出只会影响当前worker的上的请求,不影响其他worker的请求,降低了风险。
3 分Nginx处理http请求的11个阶段
Nginx 的11个执行阶段以及对应的http模块
Nginx 的11个执行阶段的枚举类可以参考nginx源码中的ngx_http_core_module.h 中ngx_http_phases枚举
typedef enum {NGX_HTTP_POST_READ_PHASE = 0,NGX_HTTP_SERVER_REWRITE_PHASE,NGX_HTTP_FIND_CONFIG_PHASE,NGX_HTTP_REWRITE_PHASE,NGX_HTTP_POST_REWRITE_PHASE,NGX_HTTP_PREACCESS_PHASE,NGX_HTTP_ACCESS_PHASE,NGX_HTTP_POST_ACCESS_PHASE,NGX_HTTP_PRECONTENT_PHASE,NGX_HTTP_CONTENT_PHASE,NGX_HTTP_LOG_PHASE
} ngx_http_phases;
模块介绍可以参考Modules reference
11个执行阶段的各个模块的执行流程图:
3.1 post_read 阶段介绍
Nginx的第一个阶段post_read 阶段是在正式处理请求之前工作的。在这个阶段刚刚获取了请求头的信息,还没有进行任何处理。我们可以拿到当前请求的原始数据,比如当前请求的真实IP。
在http协议中有两种方式获取用户IP:
· X-Forwardex-For 是用来传递 IP 的,这个头部会把经过的节点 IP 都记录下来。
· X-Real-IP:可以记录用户真实的 IP 地址,只能有一个。
3.1.1 ngx_http_realip_module 模块介绍
post_read涉及到的模块 ngx_http_realip_module 当前模块不会自动编译进Nginx中所以需要手动编译进入nginx源码文件夹中执行下列操作
./configure --with-http_realip_module
· 配置示例
set_real_ip_from 192.168.1.0/24;
set_real_ip_from 192.168.2.1;
set_real_ip_from 2001:0db8 :: / 32;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
· 内嵌变量
$realip_remote_addr #保留原始客户地址
$realip_remote_port #保留原始客户端端口
· 模块指令
当前模块有三个指令 set_real_ip_from、real_ip_header、real_ip_recursive
· set_real_ip_from
句法: set_real_ip_from address | CIDR | unix:;
默认: -
内容: http,server,location
指定可信的地址,只有从该地址建立的连接,获取的 realip 才是可信的
· real_ip_header
句法: real_ip_header field | X-Real-IP | X-Forwarded-For | proxy_protocol;
默认: real_ip_header X-Real-IP;
内容: http,server,location
指定从哪个头部取真实的 IP 地址,默认从 X-Real-IP 中取,如果设置从 X-Forwarded-For 中取,会先从最后一个 IP 开始取
· real_ip_recursive
句法: real_ip_recursive on | off;
默认: real_ip_recursive off;
内容: http,server,location
环回地址,默认关闭,打开的时候,如果 X-Forwarded-For 最后一个地址与客户端地址相同,会过滤掉该地址
· 实战 realIp 模块
修改nginx.conf 配置文件内容如下:
server {listen 80;server_name abbila.com;set_real_ip_from 10.211.55.2;real_ip_recursive off;#real_ip_recursive on;real_ip_header X-Forwarded-For;location / {return 200 "client real Ip is:$remote_addr \n";}
」
上面的配置中设置了可信Ip为10.211.55.2,real_ip_recursive是关闭状态的,real_ip_header 从X-Forwarded-For获取。测试结果:
curl -H "X-Forwarded-For: 1.1.1.1,22.22.22.22,10.211.55.2" abbila.com
client real Ip is:10.211.55.2
然后把real_ip_recursive 打开 内容如下:
server {listen 80;server_name abbila.com;set_real_ip_from 10.211.55.2;#real_ip_recursive off;real_ip_recursive on;real_ip_header X-Forwarded-For;location / {return 200 "client real Ip is:$remote_addr \n";}
」
测试结果
curl -H "X-Forwarded-For: 1.1.1.1,22.22.22.22,10.211.55.2" abbila.com
client real Ip is:22.22.22.22
通过上面两个实验可以看出来,如果real_ip_recursive关闭的话,获取的realIp为X-Forwarded-For的最后一个IP如果是打开状态的话那就把X-Forwarded-For中与可信IP重复的IP过滤掉然后取剩下的最后一个IP为realip。如果使用 X-Forwarded-For 获取 realip 的话,real_ip_recursive 需要打开。并且,realip 依赖于 set_real_ip_from 设置的可信地址。那么有人可能就会问了,那直接用 X-Real-IP 来选取真实的 IP 地址不就好了。这是可以的,但是 X-Real-IP 是 Nginx 独有的,如果客户端与服务器之间还有其他非 Nginx 软件实现的代理,就会造成取不到 X-Real-IP 头部,所以这个要根据实际情况来定。
3.2 rewrite 阶段
3.2.1 ngx_http_rewrite_model 模块介绍
rewrite 阶段设计到两个部分
· NGX_HTTP_REWRITE_PHASE
· NGX_HTTP_SERVER_REWRITE_PHASE
当前模块有 break、if、return、rewrite、rewrite_log、set、uninitialized_variable_warn 指令
· break 指令
句法: break;
默认: —
内容: server,location,if
停止处理当前ngx_http_rewrite_module指令集 。
· if 指令
句法: if (condition) { ... }
默认: —
内容: server, location
如果condition为true则执行{}中的内容。
condition可以为以下的一种或者几种:
· 变量名;如果变量的值为空字符串或“ 0”,则为false;否则为false。
· 使用“ =”和“ !=”运算符将变量与字符串进行比较。
· 使用“ ~”(区分大小写的匹配)和“ ~*”(区分大小写的匹配)运算符将变量与正则表达式进行匹配。正则表达式可以包含捕获。
这些捕获可用于以后在$1…$9变量中重用。负运算符“ !~”和“ !~*”也可用。如果正则表达式包含“ }”或“ ;”字符,则整个表达式应用单引号或双引号引起来。
· 使用“ -f”和“ !-f”运算符检查文件是否存在;
· 使用“ -d”和“ !-d”运算符检查目录是否存在;
· 使用“ -e”和“ !-e”运算符检查文件,目录或符号链接是否存在;
· 使用“ -x”和“ !-x”运算符检查可执行文件。
· return 指令
句法: return code [text];return code URL;return URL;
默认: —
内容: server,location,if
状态码可以包含以下几种:
· Nginx 自定义
· 444:立刻关闭连接,用户收不到响应
· HTTP 1.0 标准:
· 301:永久重定向
· 302:临时重定向,禁止被缓存
· HTTP 1.1 标准:
· 303:临时重定向,允许改变方法,禁止被缓存
· 307:临时重定向,不允许改变方法,禁止被缓存
· 308:永久重定向,不允许改变方法
也可以
return 200 "the status code is 200";
可以返回对应的 error_page 可以参考:
http://nginx.org/en/docs/http/ngx_http_core_module.html#error_page
· rewrite 指令
句法: rewrite regex replacement [flag];
默认: —
内容: server,location,if
可选flag参数可以是以下之一:
· last
停止处理当前ngx_http_rewrite_module指令集, 并开始搜索与更改后的URI相匹配的新位置;
· break
ngx_http_rewrite_module与break指令一样, 停止处理当前的指令集 ;
· redirect
返回带有302代码的临时重定向;如果替换字符串不是以“ http://”,“ https://”或“ $scheme” 开头,则使用
· permanent
返回带有301代码的永久重定向。
· rewrite_log 指令
句法: rewrite_log on | off;
默认: rewrite_log off;
内容: http,server,location,if
如果打开当前指令之后,会把 rewrite 的日志写入 logs/rewrite_error.log 日志文件中,
· set 指令
句法: set $variable value;
默认: —
内容: server,location,if
可以为variable 设置对应的value值。该value可以包含文本,变量,他们的组合。
· uninitialized_variable_warn 指令
句法: uninitialized_variable_warn on | off;
默认: uninitialized_variable_warn on;
内容: http,server,location,if
控制是否记录有关未初始化变量的警告。比如有些值没有找到等等。
· 实战一下rewrite模块:
nginx.conf 配置文件如下:
server {listen 80;server_name abbila.com;location /break/ {rewrite ^/break/(.*) /test/$1 break;}location /last/ {rewrite_log on;rewrite ^/last/(.*) /test/$1 last;}location /test/ {return 200 "test page";}
}
测试下last:
curl abbila.com/last/
test page%
在测试下break:
curl abbila.com/break/ <html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.18.0</center>
</body>
</html>
通过测试可以看到break是跳过当前请求的rewrite阶段,并继续执行本请求的其他阶段,last与break最大的不同是,last会重新发起一个新请求,并重新匹配location,所以对于/last,重新匹配请求以后会匹配到/test/,所以最终对应的content阶段的输出是test page;然后看一下日志error.log
2020/10/16 15:51:25 [error] 29625#0: *26 "/usr/local/webserver/nginx/html/test/index.html" is not found (2: No such file or directory), client: 10.211.55.2, server: abbila.com, request: "GET /break/ HTTP/1.1", host: "abbila.com"
2020/10/16 15:51:36 [notice] 29625#0: *27 "^/last/(.*)" matches "/last/", client: 10.211.55.2, server: abbila.com, request: "GET /last/ HTTP/1.1", host: "abbila.com"
2020/10/16 15:51:36 [notice] 29625#0: *27 rewritten data: "/test/", args: "", client: 10.211.55.2, server: abbila.com, request: "GET /last/ HTTP/1.1", host: "abbila.com"
因为在break里面没有打开rewrite_log 在last中设置了rewrite_log 可以看到里面有rewrite日志信息,注意要把error.log的级别调成notice级别。
error_log logs/error.log notice;
3.3 preaccess 阶段
我们经常会遇到一个问题,就是如何限制每个客户端的并发连接数?如何限制访问频率?当前阶段设计到两个模块 ngx_http_limit_conn_module ngx_http_limit_req_module这两个模块都是默认编译到nginx中的可以使用–without-http_limit_req_module --without-http_limit_conn_module 移除。
3.3.1 http_limit_req_module 和 http_limit_conn_module 模块
req模块有四个指令 limit_req、limit_req_log_level、limit_req_status、limit_req_zone
与指对应的conn也存在四个指令limit_conn、limit_conn_log_level、limit_conn_status、limit_conn_zone
· limit_req 指令
句法: limit_req zone=name [burst=number] [nodelay | delay=number];
默认: —
内容: http, server, location
定义共享内存(包括大小),以及 key 关键字和限制速率 rate 单位为 r/s 或者 r/m(每分钟或者每秒处理多少个请求)
· limit_req_log_level指令
句法: limit_req_log_level info | notice | warn | error;
默认: limit_req_log_level error;
内容: http,server,location
如果超过了设置的阀值则把日记的记录级别修改为对应的级别。
· limit_req_status 指令
句法: limit_req_status code;
默认: limit_req_status 503;
内容: http,server,location
为拒绝的服务请求设置对应点状态码。
· limit_req_zone 指令
句法: limit_req_zone key zone=name:size rate=rate [sync];
默认: —
内容: http
设置共享内存区域的参数,该参数将保留各种键的状态。特别是,状态存储当前的过多请求数。该key可以包含文本,变量,他们的组合。具有空键值的请求不予考虑。
可以看到上面各个模块的执行顺序。当两个同事设置时,limit_req 在 limit_conn 处理之前,因此是 limit_req 会生效
如果不添加 nodelay,请求会等待,直到能够处理请求;添加 nodelay,在不超出 burst 的限制的情况下会立刻处理并返回,超出限制则会返回 limit_req_status状态。
· 实战:限流模块
nginx.conf 配置如下
limit_conn_zone $binary_remote_addr zone=addr:10m; #申请共享内存
server {listen 80;server_name abbila.com;location / {limit_conn_status 500;limit_conn_log_level warn;limit_rate 50;limit_conn addr 1;#limit_req zone=one burst=3 nodelay;#limit_req zone=one; }
}
通过curl 访问
curl abbila.com/last/
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.18.0</center>
</body>
</html>
因为设置的limit_rate 是50个字节每秒,所以返回会特别慢,限制发生时设置的status是500所以同时发送两个请求会返回
curl abbila.com/last/
<html>
<head><title>500 Internal Server Error</title></head>
<body>
<center><h1>500 Internal Server Error</h1></center>
<hr><center>nginx/1.18.0</center>
</body>
</html>
3.4. access 阶段
当前模块涉及到ngx_http_access_module模块、ngx_http_auth_basic_module 、ngx_http_auth_request_module
3.4.1 ngx_http_access_module 模块
当前模块默认编译到nginx中,可以使用–without-http_access_module
当前模块有两个指令allow、deny
· allow 指令
句法: allow address | CIDR | unix: | all;
默认: —
内容: http,server,location,limit_except
允许访问指定的网络或地址。如果unix:指定了特殊值,则允许访问所有UNIX域套接字。
· deny 指令
句法: deny address | CIDR | unix: | all;
默认: —
内容: http,server,location,limit_except
拒绝访问指定的网络或地址。如果unix:指定了特殊值,则允许访问所有UNIX域套接字。
3.4.2 ngx_http_auth_basic_module 模块
当前模块默认编译进nginx中,可使用 --without-http_auth_basic_module 指令移除
该模块允许通过使用“ HTTP基本身份验证”协议验证用户名和密码来限制对资源的访问。当开启这这个模块后,如果通过浏览器访问url时
会返回"401 Unauthorized" ,浏览器会返回一个用户名密码的对话框。
该模块有两个指令auth_basic、auth_basic_user_file
· auth_basic 指令
句法: auth_basic string | off;
默认: auth_basic off;
内容: http,server,location,limit_except
· auth_basic_user_file
句法: auth_basic_user_file file;
默认: —
内容: http,server,location,limit_except
这里面会用到 htpasswd(依赖httpd-tools安装包,如果自己试验可自行安装),这个工具可以用来生成密码文件,而 auth_basic_user_file 就依赖这个密码文件。
使用方法:
htpasswd -c file -b user password
· 实战:鉴权模块
nginx.conf 配置文件内容,通过上面命令生成 abbila.pass 文件。
server {listen 80;server_name abbila.com;location / {auth_basic "close the web site";auth_basic_user_file abbila.pass;}
|
然后通过浏览器访问nginx 地址就可以看到:
然后输入账号密码就可以正常访问页面了。
3.4.3 ngx_http_auth_request_module 模块
当前模块没有编译到nginx中,可使用 --with-http_auth_basic_module 添加到nginx中。该模块可以将客户端输入的用户名、密码 username:password 通过 Base64 编码后写入 Request Headers 中。例如:abbila:abbila -> Authorization: Basic YWJiaWxhMTphYmJpbGE= 然后通过第三方程序解码后跟数据库中用户名、密码进行比较,Nginx 服务器通过 header 的返回状态判断是否认证通过。当前模块设计两个指令auth_request、auth_request_set
· auth_request 指令
句法: auth_request uri | off;
默认: auth_request off;
内容: http,server,location
如果子请求返回2xx响应代码,则允许访问。如果返回401或403,则使用相应的错误代码拒绝访问。子请求返回的其他响应代码都被视为错误。
· auth_request_set 指令
句法: auth_request_set $variable value;
默认: —
内容: http,server,location
授权成功后可以把赋值一些变量,可以包括鉴权请求的变量。
配置示例:
location /private/ {auth_request /auth;...
}location = /auth {proxy_pass https://auth.server.com/HttpBasicAuthenticate;#认证服务地址proxy_pass_request_body off;proxy_set_header Content-Length "";proxy_set_header X-Original-URI $request_uri;
}
根据认证服务地址返回的状态码来判断是否可以访问 /private/ 的请求。