起因
最初在 nginx.conf 中调试时,当时希望 echo 出对应的变量值,并没有成功。起初认为是 nginx 安装时,并没有安装 echo 模块,然而事后发现实际用的是 openresty,openresty 在安装的时候,默认编译了 echo 模块,然而同样没有 echo 出对应的值,没有响应。特别是最近在 rewrite 命令之前希望 echo 出修改前的 uri 时,没有 echo 出来,然而在 rewrite 命令之后却 echo 出来了,于是乎更加疑惑。带着这些表面现象进行了深入的学习。
nginx.conf 是声明性
查找资料时,看到章亦春编写的 nginx 配置指令的执行顺序中提到的 "Nginx 的作者 Igor Sysoev 在公开场合曾不止一次地强调,Nginx 配置文件所使用的语言本质上是“声明性的”,而非“过程性的”( procedural)",顿时如醍醐灌顶。nginx 并不是按过程式执行的,执行的顺序并不是按 nginx.conf 的编写顺序。
nginx 主要执行阶段
nginx 的实际执行是阶段的,nginx 处理请求一共可划分为 11 个阶段,其中主要的是 rewrite 、access、content 三个阶段。其中 nginx 的不同模块分属不同阶段。各模块常见命令如下:
rewrite : ngx_rewrite 模块的 set、rewrite;ngx_set_misc 模块的 set_unescape_uri(可和nginx_rewrite 混合执行);ngx_lua 模块的 set_by_lua;ngx_headers_more 模块的 more_set_input_headers (总是 rewrite 阶段末尾);ngx_lua 模块的 rewrite_by_lua (rewrite 阶段末尾);
access : ngx_access 模块的 deny ;ngx_access 模块的 alllow(和 deny 命令一起时,谁在前就先执行谁,并跳过后者) ;ngx_lua 的 access_by_lua (access 阶段末尾)
content : ngx_echo 模块的 echo;ngx_lua 模块的 content_by_lua;ngx_proxy 模块的 proxy_pass;
执行顺序主要四点:
- 执行顺序为 rewrite > access > content ;
- 同一阶段的同一模块中的不同命令间的执行顺序是按书写顺序;
- 同一阶段大多数不同模块间的命令执行顺序是不确定的;
- 同一阶段部分不同模块间的命令也可按顺序执行,如 ngx_set_misc 模块和 ngx_rewrite 模块。
location ~ ^/(admin|api) {
set $auth_uri "http://127.0.0.1:8901/auth/verify"; # rewrite 阶段
access_by_lua_file $document_root/openresty/lua/authorization.lua; # access 阶段
# echo "original uri"; # content 阶段,无法 echo 出来。
rewrite ^/(.*)$ /public/index.php break; # rewrite 阶段
fastcgi_pass 127.0.0.1:9000; # content 阶段
fastcgi_index index.php; # contetn 阶段
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; # contetn 阶段
echo "after rewrite uri"; # content 阶段,能够 echo 出来,但 ngx_http_fastcgi_module 模块的命令没有执行。
include fastcgi_params;
}
从上诉注释的标明阶段可以看到,先执行的的是 rewrite 阶段的 set 和 rewrite,然后是 access 阶段的 access_by_lua_file,再次是 content 阶段的 fastcgi_pass 、fastcgi_index 、fastcgi_param。set > rewrite > access_by_lua_file > fastcgi_pass > fastcgi_index > fastcgi_param 。
因此将 ngx_echo 模块的 echo 命令放置在 rewrite 命令之前,并不代表在 rewrite 命令之前执行。fastcgi_past 和 echo 都属于 content 阶段,但分属不同模块,执行顺序不确定。分析本文最开始出现的情况。
location ~ ^/(admin|api) {
set $auth_uri "http://127.0.0.1:8901/auth/verify"; # rewrite 阶段
access_by_lua_file $document_root/openresty/lua/authorization.lua; # access 阶段
echo "original uri";
rewrite ^/(.*)$ /public/index.php break; # rewrite 阶段
fastcgi_pass 127.0.0.1:9000; # content 阶段
fastcgi_index index.php; # contetn 阶段
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; # contetn 阶段
echo "after rewrite uri";
include fastcgi_params;
}
首先,上述写法其目的是希望在 rewrite 命令前后,将 uri 打印出来,查看 rewrite 对 uri 的修改。上述两个错误:
- echo 命令属于 ngx_echo 模块,其执行阶段为 content,无法在 rewrite 命令之前 echo 出修改前的 uri;
- fastcgi_* 命令属于 ngx_http_fastcgi 模块,与 ngx_echo 为同 content 阶段执行的不同模块,执行顺序不确定。
上述写法在实际测试中 ngx_htttp_fastcgi 模块占优势,执行了。而当将 rewrite 之前的 echo 命令注释掉之后,ngx_echo 模块占优势,echo 出了修改后的 uri。上述两个模块,书写顺序在前的反而执行时不占优势,反而是后者成功执行,这不知道是不是规律,还有待验证。
rewrite 模块
rewrite 命令的功能为安装相关的正则表达式与字符串修改 uri。其语法为:
rewrite regex replacement flag
- regex 为正则表达式,可使用括号捕获,后续可根据位置应用捕获变量;
- replacement 为修改后的 uri;
- flag 为尾部标记。
尾部标记 flag 可由如下值:
- last —— 停止处理重写模块指令,搜索 location 与修改后的 uri 匹配;
- break —— 完成重写指令;
- redirect —— 返回 302 临时重定向;
- permanent —— 返回 301 永久重定向。
网上相关资料推荐利用 try_files 替换 rewrite 命令。
其语法为:
try_files file ... uri 或 try_files file ... = code
按顺序检查文件是否存在,返回第一个找到的文件。结尾的斜线表示为文件夹 —— $uri/。如果所有的文件都找不到,会进行一个内部重定向到最后一个参数。务必确认只有最后一个参数可以引起一个内部重定向,之前的参数只设置内部URI的指向。 最后一个参数是回退URI且必须存在,否则将会出现内部500错误。命名的location也可以使用在最后一个参数中。与rewrite指令不同,如果回退URI不是命名的location那么$args不会自动保留,如果你想保留$args,必须明确声明。
try_files $uri $uri/ /index.php?q=$uri&$args;
何为命令的 location,参照下例:
location / {
try_files $uri $uri/ @drupal;
}
location @drupal {
...
}
上例中 @drupal 就是命令的 location。
下面通过 nginx 和 laravel 的 结合比较 rewrite 和 try_files 的写法。
rewrite:
server {
...
root /path/to/laravel;
...
location / {
rewrite ^/(.*)$ /public/index.php break;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
...
}
try_files:
server {
...
root /path/to/laravel;
...
# 修改为 Laravel 转发规则
location / {
try_files $uri $uri/ /public/index.php?$query_string;
}
# PHP 支持
location ~ \.php$ {
try_files $uri /index.php =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/var/run/php5-fpm.sock; # 可以进行 http 的转发
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
从书写上来看,rewrite 命令视乎更简洁。
nginx 中 fastcgi 的使用
简要介绍几个概念:
- CGI 协议:CGI就是规定 Web Server 与相应解析器间通信时要传哪些数据、以什么样的格式传递给后方来处理相应请求的协议;
- FastCGI 协议:Fastcgi 是用来提高 CGI 程序性能的的一套实现手段。Fastcgi 会首先先启一个master,解析配置文件,初始化执行环境,然后再启动多个worker,动态管理 worker 的个数。worker 用于处理请求。
- FastCGI 进程管理器:实现了 FastCGI 的程序;
- CGI 程序:支持 CGI 协议的应用程序;
- Web Server:Web 服务器。
Web server 只是内容的分发者。对于静态文件, web server 在文件系统中直接寻找,发送给浏览器,而对于非静态文件,web server 寻找相应的解析器。对于非静态文件,处理过程如下:
以 nginx 处理 php文件举例,上图的关系变为:
在 fastcgi_params 中我们可以看到传递的参数 fastcgi_param REQUEST_URI $request_uri
。这个参数是用于告诉后方处理程序,这个请求的 uri 是多少。在上述 nginx.conf 中,利用 rewrite 命令改写 uri,其主要目的是给 $fastcgi_script_name 赋值。$fastcgi_script_name 默认是和 $uri 是一个值,修改 $uri 目的是定位至后方服务框架的入口文件进行处理。然而实际传递给后方程序的 uri 并没有修改,因为 $request_uri 是一个不可修改的文件。当希望自定义传递给后方程序的 uri 时,就得修改对应的语句为 fastcgi_param REQUEST_URI /you/want/uri
;。
参考:深入理解 FastCGI 协议以及在 PHP 中的实现
location 的执行顺序
语法:location [=||*|^~] /uri/ { … }
模式 | 含义 |
---|---|
location = /uri | = 表示精确匹配,只有完全匹配上才能生效 |
location ^~ /uri | ^~ 开头对URL路径进行前缀匹配,并且在正则之前。 |
location ~ pattern | 开头表示区分大小写的正则匹配 |
location ~* pattern | 开头表示不区分大小写的正则匹配 |
location /uri | 不带任何修饰符,也表示前缀匹配,但是在正则匹配之后 |
location / | 通用匹配,任何未匹配到其它location的请求都会匹配到,相当于switch中的default |
前缀匹配时,nginx 不对 url 做编码,因此请求 /static/20%/aa ,可以被规则 ^~ /static/ /aa 匹配到。(注意是空格)
多个 location 匹配顺序:
=(精确) > ^~(前缀) > ~(区分大小写正则) > ~*(不区分大小写正则) > /uri (前缀)> /(通用);
当有匹配成功时,停止匹配,按当前匹配规则处理请求。
【注意】:前缀匹配,如果有包含关系时,按最大匹配原则进行匹配。location /dir01 与 location /dir01/dir02,如有请求 http://localhost/dir01/dir02/file 将最终匹配到 location /dir01/dir02。
正则匹配按书写顺序进行匹配。
简单记忆:
=(精确) > ^~(前缀) > ~(区分大小写正则) > ~*(不区分大小写正则) > /uri (前缀)> /(通用)
;
前缀匹配要最大;正则匹配要最先
。