Author: haoransun
Wechat: SHR—97
学习来源:极客时间-Nginx核心知识100讲,本人购买课程后依据视频讲解汇总成个人见解。
前言
除HTTP过滤模块 和 只提供变量的Nginx模块之外,所有的HTTP模块必须从Nginx定义好的11个阶段进行请求处理。每一个HTTP模块何时生效,有没有机会生效,都要看一个请求究竟处理到哪一个阶段。Nginx是如何定义这11个处理阶段的呢?
HTTP请求处理时的11个阶段
post_read:read到Header内容,刚读完HTTP头部,没有做任何加工之前的原始数据。涉及到 realip模块。
server_rewrite、rewrite:涉及到rewrite模块.
find_config:Nginx框架会做,其实是在做location的匹配。
post_rewrite:即 rewrite之后,需要做的一些工作
Access有关的三个模块:确认访问权限的。(能不能访问)
preaccess:在access之前做一些处理。
access:auth_basic(用户名密码),access(访问IP),auth_request(第三方授权等)
post_access:在access之后做一些处理。
content有关的
precontent:在处理content之前做一些处理。
content:诸如一些方向代理等都是在这个阶段生效的。
log:打印access日志的
所有的请求都是由上到下一个阶段一个阶段按序执行。在debug时可以清楚地看到。
11个阶段的顺序处理
当一个HTTP请求进入到Nginx这11个阶段时,由于每一个阶段都可能有0-n个HTTP模块,如果某一个模块不再把HTTP请求向下传递,那么后面的模块是不会执行的。同一阶段中的多个模块,也不是每个模块都有机会执行到的,可能会有前面的模块把请求直接传递给下一个阶段的模块去处理。下面看一看HTTP模块顺序以及他们的处理流程。
每一个蓝色的模块都属于某一个阶段,这些模块是有序的。
char *ngx_module_name[]
顺序处理
顺序如何确定呢?可以去看 ngx_modules.c,即configure执行时,会用 with添加模块,这些都会出现在 ngx_module_name[]数组中,这些模块出现的顺序非常关键。
如 limit_req 与 limit_conn,二者同属于preaccess阶段,在数组中则是 limit_conn 先出现,limit_req后出现,但是对应于请求的处理时它们是相反的。一个HTTP请求,会先被 limit_req处理,再被limit_conn处理,假设这两个同时生效去阻止一个请求时,假设这两个返回值也不同,limit_req返回值是没有机会得到执行的,他已经先于limit_conn将请求结果返回给用户。
灰色的是Nginx框架执行的,其他的第三方HTTP模块没有机会在此运行。
非顺序处理
有些则是不会顺序执行的。如 access阶段,当某一个access模块满足,可以直接跳到 try_files模块。当content阶段index模块执行时有时会直接跳到log模块执行。
postread阶段:获取真实客户端地址的realip模块
它可以发现用户的真实IP地址,为后续模块的限速、限流等等功能提供了前提。
如何拿到真实的用户IP地址?
TCP连接有一个四元组,根据一条连接的Source_IP就能够判断出用户的IP地址了,但是网络中存在许多反向代理,这又导致反向代理后与上游服务器又建立了一个新的TCP连接。因此上游服务器想通过TCP中的Source_IP获取用户原始IP地址,是不可能的。
举例:
在家里上网时,家里的路由器可能分配了一个内网IP 192.168.0.x,当通过运营商(电信可能给分配了一个公网的IP:115.204.33.1)去访问某一个网站时,先命中到它的CDN,这个网站使用CDN加速(如图片等),这个CDN如果还没有把我所访问的资源缓存时,它可能要去回源,又建立了一条新的连接,回源过程中可能进入到一个反向代理中(如服务器买在阿里云,可能会用阿里云的SLB),这个SLB又会去建立一个新的连接,到我购买的服务器的Nginx,因此,Nginx如果仅通过拿地址的话,只能拿到反向代理的IP地址(2.2.2.2),反向代理之前的CDN的地址是1.1.1.1,其实我们要拿到的是用户的公网地址115.204.33.1., 如果要做限速、并发连接控制,肯定是基于这个公网IP进行的。
现在拿到的remote_addr是2.2.2.2,想要的是115.204.33.1,如何做到呢?
通过 2、3即可做到。
HTTP头部中有 X-Forwarded-For用来传递IP,如CDN的IP地址是1.1.1.1,他又建立了一个新的到反向代理的连接,这个方向代理服务器收到的Header中,可能会存在 X-Forwarded-for 与 X-Real-IP,这个是CDN添加的。
X-Forwarded-For 与 X-Real-IP不同,X-Real-IP永远都是一个用户真实IP地址,而X-Forwarded-For则是累加的。如上图中反向代理到Nginx的连接中,加上了CDN的IP地址
拿到用户真实IP地址如何使用?
基于变量来解耦使用。根据我们在realip模块中配置的指令,realip模块会把从 X-Forwarded-For、X-Real-IP中获取到的用户真实IP地址去覆盖 binary_remote_addr、remote_addr这两个变量的值。而这两个变量原来指向的是直接与Nginx连接的客户端地址。
realip模块
realip模块的指令
real_ip_recursive:环回地址,默认是关闭的,当它打开时,他会将X-Forwarded-For中,最后的那个地址如果是和客户端地址相同,就会赔pass掉,去取上一个地址。
例子:
1 | 需要自己添加realip.con配置,并且include到nginx.conf中 |
此处的server_name 用的是 realip_.taohui.tech;
因为当前所在的机器是 116.62.160.193,所以这个测试是不会跨服务器的,本机访问,所以将本机设置为可信地址(set_real_ip_from 116.62.160.193;)没有用它的默认配置( real_ip_header X-Real-IP;),而是重新作了配置(real_ip_header X-Forwarded-For;)环回地址用了默认 off ,对于这样的请求,返回 remote_addr 的地址。
1 | curl -H 'X-Forwarded-For: 1.1.1.1,116.62.160.193' realip taohui.tech |
返回的 116.62.160.193
如果开启了环回地址 即为 on,Nginx做一次 realod,再次访问,因为我们最后一个地址是本机地址,出发了环回地址被pass掉,发现变为上一个对端地址 1.1.1.1
小结
以上介绍了 post_read阶段中的realip模块,因为它处于的阶段,可以拿到没有加工过的X-Forwarded-For或X-Real-IP中的用户地址,因为后续的很多模块会去修改 X-Forwarded-For中头部的值。
rewrite阶段:rewrite模块
rewrite模块中的return指令会在 server_rewrite 与 rewrite阶段都会生效,生效后,后续的HTTP模块的其他阶段是没有机会得到执行的。
rewrite模块:return指令
444 表示Nginx立即关闭连接,不再向客户端返回任何内容。
rewrite模块:return指令与error_page指令
return示例
1 | 新建return.conf,并include进nginx.conf |
root html/ 即我们访问 location 下的某个资源时,会去html下去找资源是否存在。如果文件没有找到,会生成一个404错误码,正常会这样返回,但这里注释掉,并且定义了一个 error_page 404 /403.html;即当看到404时给他重新定向到 403.html页面。
访问时故意找一个不存在的资源
此时,解开 return 404 的注释,再次访问, error_page是没有机会得到执行的。
再如:在server中加入了一个 return 405;
此时会执行谁呢? 在11个阶段中不难发现, server配置项中的return 是在 server_rewrite中的,location中的return是在 rewrite中的,肯定是 server_rewrite中的return先执行,而 location中的return是没有机会执行的。即肯定返回405
rewrite模块:rewrite指令重写URL
rewrite指令示例(一)
1 | 新建配置文件,将它 include到nginx.conf中 |
首先访问 first/3.txt
在second中间 break注释放开,会有什么不一样呢?
rewrite指令示例(二)
1 | 新建配置文件,将它 include到nginx.conf中 |
访问第一个,因为指定了 permanent(永久重定向),返回301
访问第二个,因为指令了 redirect(临时重定向),返回302
访问第三个,因为什么都没有指定,但前面又有一个 http、https等,会返回302
访问第四个,虽然前面有 http、https,但最后指定了 permanent,会返回301
rewrite_log指令
默认是不会开启的,需要显示开启,打开后,刚刚访问过的所有重定向的URL都会在指定的 logs/rewrite_error.log中出现。
1 | vim rewrite_error.log |
rewrite模块:if指令-条件判断
if指令可以让我们判断请求中的变量的值是否满足某个条件,再去决定由哪一个配置块执行,再根据这些配置块调用相应的模块去解析请求。(逻辑判断)
rewrite模块的if指令
if指令的条件表达式
简单示例
find_config阶段
当我们在server块下的rewrite系列指令执行完毕后,开始根据用户请求中的URL去location中对应的URL正则表达式进行匹配。这一步【匹配完成后,就确定了由哪一个location对这个请求进行处理。
处理请求的 location 指令块
merge_slashes可以去合并URL里的斜杠,两个斜杠在一起时,默认打开该配置项,会合并成一个。只有当URL中用到base64编码等等规则时,才需要关闭。
location匹配规则:仅匹配URI,忽略参数
问题
location匹配顺序
1 | 新建配置文件,将它 include到nginx.conf中 |
访问Test1,精确匹配
访问Test1/,虽然有多个匹配,但是前缀字符串中遵循最长匹配的规则,所以匹配到了 Test1/,并且匹配上后,禁止后续正则表达式的匹配。
访问/Test1/Test2 ,/Test1/Test2 与 * /Test1/(\w+)$ 都匹配上了,但由于没有使用 ^禁止正则表达式匹配,所以匹配的是带有正则表达式的最长匹配。
访问/Test1/Test2/ ,因为正则没有匹配上,所以使用最长字符串匹配
小结
以上介绍了 location的匹配规则,对于URI的请求,到底是由哪一个location下的指令去执行,就十分了然了,同时也知道了当location数量非常多时,怎样通过 禁止正则表达式匹配、使用=精确匹配等等方式对非常频繁发起的请求来减少它们做location匹配的次数。
preaccess阶段
对连接做限制的limit_conn模块
问题:如何限制每个客户端的并发连接数?
limit_conn指令
演示
1 | 创建文件并且include到 nginx.conf中 |
上述配置文件定义了一个 10M 的共享内存,共享内存中使用 binary_remote_addr,这是一个二进制格式的IP地址(IPV4协议下只有4个字节,效率较高)。
定义了向用户返回的错误码是500(默认是503)
将 log_level调成了 warn(默认是error)
limit_conn_addr 1; 即限制了并发连接数为1(只为演示效果,当有两个客户端同时访问时,就会返回500)
limit_rate 50; 为了更好的演示,又加上了该配置项,即限制向用户返回的速度,每秒钟只返回50个字节,比较容易出现限制并发连接的场景。
在一个shell中访问,回复速度非常慢
在另一个shell中也访问,会回复500错误码
在 myerror.log中也可以看到
小结
当Nginx作为资源服务器为用户提供服务时,限制用户能够同时发起的并发连接数,是一个很常用的功能。Nginx默认编辑进去的 ngx_http_limit_conn_module模块提供了这样的功能。设计好Key是关键。
对请求做限制的limit_req模块
问题:如何限制每个客户端的每秒处理请求数?
leaky bucket算法
对于突发性流量,前两秒12Mbps,总共24M,2-7s没有流量,7-10为2Mbps,共6M,前10秒总共30M。
使用了该算法后,可以限制为3Mbps,前10秒总共 30M。
可以比喻为一个水龙头,向盆里流动的是突发性流量,而盆向下流的则是恒速流量。
当盆burst满的时候,立刻向用户返回503错误码。
当盆burst没有满的时候,但向下速率已经达到最大化的时,水滴就会存在盆里,即用户的响应会变慢,请求并不会被拒绝。
limit_req指令
问题
演示
1 | 创建文件并且include到 nginx.conf中 |
当没有加 burst 与 nodelay时,结果会是怎样?同时注释掉 limit_rate 这样可以快速返回内容。每分钟两条
curl limit.haoran.tech ,看到结果
再次访问
将 burst的注释解开会是什么样的呢?
访问3次都可以看到结果,访问第4次时,会有503错误码。
现在将限制连接与限制请求同时打开。看下效果。返回500(限制连接生效),返回503(限制请求生效),
每分钟只能处理2个请求,所以第3次访问时,limit_req生效,但其实 第二次访问时 limit_conn同样生效了。返回的还是503,而不是500,这是因为 limit_req模块是在limit_conn模块之前生效的,limit_req已经向用户拒绝了,limit_conn就没有机会得到执行了。
access阶段
对IP做限制的 access 模块
access模块 可以控制那些IP可以访问某些URL,那些不可以访问。
问题:如何限制那些IP地址的访问权限?
对用户名-密码做限制的 auth_ basic 模块
auth_basic模块的指令
生成密码文件
1 | yum install -y httpd-tools |
上述密码文件中的密码做了一个简单的base64编码。
在 nginx.conf配置文件中指定有关配置
浏览器访问 access.taohui.tech;会发现需要输入用户名-密码
当我们提供一个非常简单的页面时,如go-access,想对他做一个安全保护,auth_basic是一个不错的做法。
使用第三方做权限控制的 auth_request 模块
在生产环境中,往往会有一个动态Web服务器或者相应的一些应用服务器,它们提供更复杂的用户名-密码权限验证,这个时候可以通过访问Nginx的资源池先将这个请求传递给应用服务器上,根据应用服务器返回的结果再判断这个请求资源能不能继续执行,那么Nginx的access阶段有一个模块为 auth_request模块,他就可以完成这样的功能。
统一的用户权限验证系统
演示
当访问 / 时 通过 auth_request 生成子请求,会去访问这个URL test_auth,而这个URL通过 proxy_pass反向代理到本机的另一个Nginx服务器(监听端口为8090),他提供的URL为 auth_upstream。成功后,因为有一个默认的配置 root html/(即使注释了也会正常显示html下的 index页面),如果被拒绝,就会返回 8090 这台机器的错误码。
8090这台nginx的内容如下(成功的时候):
访问:
将上游的返回值改为403:
禁用缓存后,再次访问:
access阶段的 satisfy 指令
前面提到了 access 阶段的3个模块,那这三个模块任意一个模块拒绝了用户的请求,用户请求就无法执行了呢?其实并不是这样的,那他们是否严格的按照顺序往下执行呢?同样不是这样的。
因为 Nginx的HTTP框架中提供了一个 satisfy指令,允许我们改变模块的执行顺序。
限制所有access阶段模块的 satisfy指令
即一个 access模块,有三种处理结果:
忽略,即没有任何配置,直接跳到下一个access模块
放行(allow),先判断satisfy开关,如果配置为 all(表示必须所有的access模块都同意放行这个请求才可以通过),所以继续执行下一个access模块;如果配置为 any(即不用再去考虑后续的access模块是否同意,直接跳到下一个 post_access阶段执行)
拒绝(deny),同样判断satisfy开关,如果配置为 all(直接拒绝请求),不再向下执行。如果是 any,虽然当前这个模块拒绝了,但也会后续模块会同意放行,所以继续执行下一个access模块
问题
1:肯定不会生效,因为return指令的生效期是 server_rewrite 与 rewrite阶段,二者都领先于 access,access是没有机会得到执行的。
2:肯定有影响,即如果 access阶段已经拒绝了,则auth_basic是没有机会输入用户名-密码的。
3:可以访问到,配置了 satisfy any
4:提到之前,仍然可以访问,因为模块间的顺序ok就行了,配置指令间的顺序无关紧要
5:将 deny all 改为 allow all,没有机会输入,因为配置的 satisfy all,任意的模块同意就可以了,allow all是 access模块的,它先于auth_basic模块执行的,它已经同意了,则auth_basic是没有机会输入用户名-密码的。
precontent阶段
按序访问资源的 try_files 模块
对于反向代理的场景十分有用,Nginx先尝试去获取磁盘上的文件内容,如果没有再反向代理到上游服务。
1 | 新建配置文件,并include到 nginx.conf文件中 |
访问 /first,如果系统在维护的话可能会有一个 /system/maintenance.html文件,如果这个文件找不到的话,我们就去找 uri(即 html下first有没有),同样没有,$uri/index.html、$uri.html同样都没有,这时使用了 @lasturl 符号 去访问 另一个 location @lasturl。在这个location中返回 200的状态码。
访问 /second,一样与一个去尝试,所有文件都找不到时,返回404.
实时拷贝流量 mirror 模块
mirror模块可以帮我们创造一份镜像流量,如生产环境中处理一些请求,这些请求可能需要把他们同步的拷贝一份到我的测试、开发环境中做处理。
即当请求到了Nginx后,可以生成一个子请求,这个子请求可以通过反向代理去访问我们的其他环境(测试环境等),对其他环境返回值不作处理。
举例
需要一个上游服务器
1 | 新建配置文件,并include到 nginx.conf文件中 |
收到一个请求时,会拷贝一份流量到 mirror 中去,/mirror收到后,会指定 internal(内部),将其方向代理到本机的10020端口上去。
访问8001
实时查看日志
再去看上游Nginx(10020)的日志,是否收到
content阶段
static模块 root 和 alias 指令
content阶段中 static模块 默认是在Nginx框架中的,是没有办法做移除的。
问题
1 | 新建配置文件,并include到 nginx.conf文件中 |
直接访问 root/,文件不存在
查看日志,在 html后又加上了刚刚 location中的root,因为有个 反斜杠,所有有添加了 index.html,这个文件其实是不存在的。
直接访问 root/1.txt,文件不存在
查看日志,它其实是在 html/first/1.txt 后面又添加了 /root/1.txt,即 html/first/1.txt/root/1.txt
直接访问 curl static.taohui.tech/alias/ ,他匹配到了 location /alias 会去访问 html下的index.html,所以应该访问首页
直接访问 curl static.taohui.tech/alias/1.txt ,不会添加完整路径,文件存在
static模块 3个变量
生成待访问文件的三个相关变量
1 | 新建配置文件,并include到 nginx.conf文件中 |
realpath 实际上是一个软链接,他指向了 first目录下,这个目录下有一个1.txt文件
在下图中可看到,返回3个路径,第一个是完整路径,后两个都是1.txt所在的目录,只不过 document_root 没有做软链接的替换,还是根据配置项拼接出来的,而 realpath_root 已经将 realpath 替换为真实 first目录。
static模块提供的其他功能
静态文件返回时的 content-type
当我们去读磁盘上的文件时,根据文件的扩展名做一次映射。types指令就是做这个事情的,为了加速,需要将 content-type 与 扩展名 做一次映射放入 Hash 表中。
default_type是在没有文件名时用来告诉用户这个content-type究竟怎样解析
未找到文件时的错误日志
static模块对url不以斜杠结尾却访问目录的做法
很多人使用 static 模块的 root/alias 指令将Nginx当做静态资源服务器时,很可能会发现,当我们去访问一个目录,但是在url结尾没有加上斜杠时,实际上Nginx会返回一个301的重定向,那么对于重定向中的内容,Nginx提供了3种不同的指令,去控制location这样的行为。
重定向跳转的域名
演示
1 | 新建配置文件,并且include到 nginx.conf配置文件中 |
在 server_name 中配置了两个域名,第一个是主域名。将 absolute_redireect off 开启(默认是on),root指向 html/ 下有一个 first文件夹。
先来访问 first文件夹,没有加反斜杠,此时应该获得一个301重定向
将 absolute_redirect off 注释掉。再次访问,发现 在 Location中将域名都添加了进去。
如果头部有一个 Host: aaa,那么就会将它替换掉掉Location中的localhost。
将 dirredirect.conf配置文件中的 server_name_in_redirect on开启后,再去访问,会发现Location中以主域名来绑定。
index 与 autoindex模块
在前面已经演示过,autoindex会以目录形式显示服务器上的资源。但有时在搭建的时候,会没有看到目录结构,看到的是一个文件的内容,这是因为** index 模块** 先于 autoindex 模块产生作用。
对访问/时的处理:content阶段的index模块
显示目录内容:content阶段的autoindex模块
autoindex 模块的指令
autoindex_exact_size on|off :当默认打开的格式(向用户返回的是html格式时才有效)是显式相对的路径。绝对路径:以字节来显示。相对路径:以K、M显示。
演示
1 | 新建配置文件,并且include到 nginx.conf配置文件中 |
监听了1个8080端口,以server_name指定的域名进行访问,默认没有修改index a.html(注释掉了)。当访问 / 时,会去找 index.html,在 alias指定的html下是有这个文件。
去访问 autoindex.taohui.tech:8080,得到的是index.html内容
因为 index 模块是没有办法从 Nginx中移除的,所以可以去修改 index指向的文件,将它指向一个不存在的 a.html文件(即将 index a.html 注释解开)
再次访问autoindex.taohui.tech:8080,是JSON格式返回这个目录
同理,将 autoindex_format json 改为 html
reload后,再次访问,因为是以相对路径,所以可以显示到K。
content阶段中有Alibaba提供的concat模块
concat模块可在一次请求中返回多个文件的内容,这对在Web页面中访问多个小文件来提升性能十分有用。(需要下载并且在 .configure 时编译进Nginx)
concat模块的指令
concat 开启或者关闭
concat_delimiter:String,如果服务器返回多个文件,通过指定的String分隔符进行分割
concat_types: MIME types,对那些文件的类型做合并
concat_unique:对某一种文件类型进行合并,还是对多个文件类型进行合并
concat_ignore_file_error:如果某个文件出现错误,是忽略它,返回其他文件的内容
concat_max_files:最多合并多少个文件,默认为10
看一下淘宝网的做法
可以看到他的大部分请求,都使用了 ??,后面添加了多个文件,后面用逗号隔开。
在响应中也可以看到
演示
1 |
|
在 concat.conf配置文件中,首先打开了这个功能,最多20个文件,类型是 text/plain,以 三个分号来分隔多个文件。
现在来访问,他回去 html/concat 路径下找 1.txt 与 2.txt,这两个文件是存在的,内容如下:
访问: