作者
:春哥
读者
:锅巴GG
Nginx的配置文件使用的就是一门微型的编程语言
许多真实世界里的Nginx配置文件其实就是一个一个的小程序。
很久之前就拜读过春哥的很多文章,以及Nginx的相关博文,大部分内容都是来自持续关注的OpenResty内容,但是本教程确实是基础中的基础。
由于最近工作的关系,需要系统的学习掌握Nginx架构特性以及利用lua-nginx-module进行二次开发,不仅开始重读相关的资料,而重读的时候,尤其以此教程入手获益匪浅,故记录之。
本笔记只按自己的理解记录,和原书差异较大,有问题请参阅原书。
Nginx配置指令的11个阶段
- 重要的事情放前面
我的理解,应该要掌握的内容重点是在:
- Ngnix配置文件的自身生命式特性
- 变量的作用域
- 变量书写顺序依赖阶段和模块实现
谈谈变量
在 Nginx 配置中,变量只能存放一种类型的值,因为也只存在一种类型的值,那就是字符串。
# 使用了标准ngx_rewrite模块的set配置指令对变量$a进行了赋值操作
set $a "hello world";
# “变量插值”(variable interpolation)
set $b "$a, $a";
#使用特别的记法来消除歧义,当引用的变量名之后紧跟着变量名的构成字符时(比如后跟字母、数字以及下划线)
set $first "hello ";
echo "${first}world";
set 指令(以及 geo 指令)不仅有赋值的功能,它还有创建 Nginx 变量的副作用,即当作为赋值对象的变量尚不存在时,它会自动创建该变量。
Nginx 变量的创建和赋值操作发生在全然不同的时间阶段。Nginx 变量的创建只能发生在 Nginx 配置加载的时候,或者说 Nginx 启动的时候;而赋值操作则只会发生在请求实际处理的时候。这意味着不创建而直接使用变量会导致启动失败,同时也意味着我们无法在请求处理时动态地创建新的 Nginx 变量。
- Nginx 变量一旦创建,其变量名的可见范围就是整个 Nginx 配置,甚至可以跨越不同虚拟主机的 server 配置块。
- Nginx 变量名的可见范围虽然是整个配置,但每个请求都有所有变量的独立副本,或者说都有各变量用来存放值的容器的独立副本,彼此互不干扰。
Nginx 变量的生命期是不可能跨越请求边界的。
Nginx 变量的一个常见误区是认为变量容器的生命期,是与 location
配置块绑定的,其实不然:
server {
listen 8080;
location /foo {
set $a hello;
#使用第三方模块 [ngx_echo]提供的 [echo_exec]配置指令
#发起到 location /bar的“内部跳转”
echo_exec /bar;
} `
location /bar {
echo "a = [$a]";
}
}
一个请求在其处理过程中,即使经历多个不同的 location
配置块,它使用的还是同一套 Nginx 变量的副本。
标准 ngx_rewrite 模块的 rewrite 配置指令其实也可以发起“内部跳转”,例如上面那个例子用 rewrite 配置指令可以改写成下面这样的形式:
server {
listen 8080;
location /foo {
set $a hello;
rewrite ^ /bar;
}
location /bar {
echo "a = [$a]";
}
}
- 效果和使用 echo_exec 是完全相同的。
- Nginx 变量值容器的生命期是与当前正在处理的请求绑定的,而与 location无关。
通过 set 指令隐式创建的 Nginx 变量。这些变量称为“用户自定义变量”,或者“用户变量”。既然有“用户自定义变量”,自然也就有由 Nginx 核心和各个 Nginx 模块提供的“预定义变量”,或者说“内建变量”(builtin variables)。
- 内建变量
Nginx 内建变量最常见的用途就是获取关于请求或响应的各种信息。例如由 ngx_http_core 模块提供的内建变量 $uri,可以用来获取当前请求的 URI(经过解码,并且不含请求参数),而 $request_uri 则用来获取请求最原始的 URI (未经解码,并且包含请求参数)。
另一个特别常用的内建变量其实并不是单独一个变量,而是有无限多变种的一群变量,即名字以 arg_开头的所有变量,我们估且称之为 $arg_XXX 变量群。一个例子是$arg_name,这个变量的值是当前请求名为 name
的 URI 参数的值,而且还是未解码的原始形式的值。
1. Nginx 会在匹配参数名之前,自动把原始请求中的参数名调整为全部小写的形式。
2. 尝试改写另外一些只读的内建变量,比如$arg_XXX变量,在某些 Nginx 的版本中甚至可能导致进程崩溃。
有一些内建变量是支持改写的,其中一个例子是 $args. 这个变量在读取时返回当前请求的 URL 参数串(即请求 URL 中问号后面的部分,如果有的话),而在赋值时可以直接修改参数串。这里的 $args 变量和 $arg_XXX 一样,也不再使用属于自己的存放值的容器。当我们读取 $args 时,Nginx 会执行一小段代码,从 Nginx 核心中专门存放当前 URL 参数串的位置去读取数据;而当我们改写 $args 时,Nginx 会执行另一小段代码,对相同位置进行改写。Nginx 的其他部分在需要当前 URL 参数串的时候,都会从那个位置去读数据,所以我们对 $args 的修改会影响到所有部分的功能。
与面向对象编程中的“存取器”概念相对应,Nginx 变量也是支持绑定“存取处理程序”的。Nginx 模块在创建变量时,可以选择是否为变量分配存放值的容器,以及是否自己提供与读写操作相对应的“存取处理程序”。
不是所有的 Nginx 变量都拥有存放值的容器。拥有值容器的变量在 Nginx 核心中被称为“被索引的”(indexed);反之,则被称为“未索引的”(non-indexed)。
在设置了“取处理程序”的情况下,Nginx 变量也可以选择将其值容器用作缓存,这样在多次读取变量的时候,就只需要调用“取处理程序”计算一次。
Nginx 模块可以为其创建的变量选择使用值容器,作为其“取处理程序”计算结果的缓存。显然, ngx_map 模块认为变量间的映射计算足够昂贵,需要自动将因变量的计算结果缓存下来,这样在当前请求的处理过程中如果再次读取这个因变量,Nginx 就可以直接返回缓存住的结果,而不再调用该变量的“取处理程序”再行计算了。
- 注意缓存和惰性求值以及主动求值
只在实际使用对象时才计算对象值的技术,在计算领域被称为“惰性求值”(lazy evaluation)。提供“惰性求值” 语义的编程语言并不多见,最经典的例子便是 Haskell. 与之相对的便是“主动求值” (eager evaluation)。
Nginx 变量的值只有一种类型,那就是字符串,但是变量也有可能压根就不存在有意义的值。没有值的变量也有两种特殊的值:一种是“不合法”(invalid),另一种是“没找到”(not found)。
- 输出特殊值“找不到”的效果和空字符串是相同的。因为这一回是 Nginx 的“变量插值”引擎自动把“找不到”给忽略了。那么我们究竟应当如何捕捉到“找不到”这种特殊值的踪影呢?
通过第三方模块 ngx_lua,我们可以轻松地在 Lua 代码中做到这一点。请看下面这个例子:
location /test {
content_by_lua '
if ngx.var.arg_name == nil then
ngx.say("name: missing")
else
ngx.say("name: [", ngx.var.arg_name, "]")
end
';
}
* ngx_lua
[ngx_lua](http://wiki.nginx.org/HttpLuaModule) 模块将 Lua 语言解释器(或者 [LuaJIT](http://luajit.org/luajit.html) 即时编译器)嵌入到了 Nginx 核心中,从而可以让用户在 Nginx 核心中直接运行 Lua 语言编写的程序。我们可以选择在 Nginx 不同的请求处理阶段插入我们的 Lua 代码。这些 Lua 代码既可以直接内联在 Nginx 配置文件中,也可以单独放置在外部 .lua 源码文件(或者 Lua 字节码文件)里,然后在 Nginx 配置文件中引用这些文件的路径。
虽然反复指出 Nginx 变量只有字符串这一种数据类型,但这并不能阻止像 ngx_array_var 这样的第三方模块让 Nginx 变量也能存放数组类型的值。
下面就是这样的一个例子:
location /test {
array_split "," $arg_names to=$array;
array_map "[$array_it]" $array;
array_join " " $array to=$res;
echo $res;
}
这个例子中使用了 ngx_array_var 模块的 array_split、 array_map 和 array_join 这三条配置指令,其含义很接近 Perl 语言中的内建函数 split、map 和 join(当然,其他脚本语言也有类似的等价物)。
我们来看看访问 /test 接口的结果:
$ curl 'http://localhost:8080/test?names=Tom,Jim,Bob
[Tom] [Jim] [Bob]
使用 ngx_array_var 模块可以很方便地处理这样具有不定个数的组成元素的输入数据,例如此例中的 names URL 参数值就是由不定个数的逗号分隔的名字所组成。不过,这种类型的复杂任务通过 ngx_lua 来做通常会更灵活而且更容易维护。
----
### 谈谈顺序
> 因为要理解11个阶段的顺序,需要做大量的实验,所以特此附上我测试用的完整样例,供大家学习参考。如果你不能直接说出每个location直接调用会返回的结果,不妨重新看看教程。
nginx.conf
worker_processes 1;
error_log logs/error.log;
events {
worker_connections 1024;
}
http {
map $args $foo {
default 0;
debug 1;
}
server {
listen 8084;
# set_real_ip_from 127.0.0.1;
# real_ip_header X-My-IP;
location /test {
set $addr $remote_addr;
echo "from: $addr";
}
location / {
}
}
server {
listen 8083;
location / {
root /Users/gosber/sloth;
}
}
server {
listen 8082;
location / {
root /Users/gosber/sloth;
autoindex on; # 故意写在index前面,如果存在index.html,优先级应该index高
index index.html;
}
location /index.html {
set $a 32;
echo "a = $a";
}
location /test {
set $args "foo=1&bar=2";
proxy_pass http://127.0.0.1:8081/args;
}
location /main {
#curl --data helo 'localhost:8082/main'
set $var main;
echo_location /foo;
echo_location /bar;
echo "main: $var";
echo "main method: $request_method";
echo "main method(父子请求独立): $echo_request_method";
echo_location /sub;
# curl --data helo --cookie user=gosber 'localhost:8082/main?name='
content_by_lua '
if ngx.var.arg_name == nil then
ngx.say("name: missing")
else
ngx.say("name: [", ngx.var.arg_name, "]")
end
if ngx.var.cookie_user == nil then
ngx.say("cookie user: missing")
else
ngx.say("cookie user: [", ngx.var.cookie_user, "]")
end
';
}
location /foo {
set $var foo;
echo "foo: $var";
}
location /bar {
set $var bar;
echo "bar: $var";
}
location /sub {
echo "sub method: $request_method";
echo "sub method(父子请求独立): $echo_request_method";
}
location /test1 {
array_split "," $arg_names to=$array;
array_map "[$array_it]" $array;
array_join " " $array to=$res;
echo $res;
}
location /test2 {
set $a 32;
set $b 56;
set_by_lua $c "return ngx.var.a + ngx.var.b";
set $quation "$a + $b = $c";
echo $quation;
}
location /test3 {
set $a 1;
rewrite_by_lua "ngx.var.a = ngx.var.a + 1";
# 第三方模块 ngx_lua 提供的 rewrite_by_lua 配置指令也和 more_set_input_headers 一样运行在 rewrite 阶段的末尾。我们来验证一下
set $a 56;
echo $a;
}
location /hello {
allow 127.0.0.1;
deny all;
echo "hello world";
}
location /hellolua {
access_by_lua '
if ngx.var.remote_addr == "127.0.0.1" then
return
end
ngx.exit(403)
';
echo "hello world";
}
location /shunxu {
# 完全反写,不会影响执行顺序
# content phase
echo "age = $age";
# access phase
deny 10.32.168.49;
access_by_lua "ngx.var.age = ngx.var.age * 3";
# rewrite phase
set $age 1;
rewrite_by_lua "ngx.var.age = ngx.var.age + 1";
}
location /echo {
echo_before_body "before...";
proxy_pass http://127.0.0.1:8082/echofoo;
echo_after_body "after...";
}
location /echofoo {
echo "contents to be proxied";
}
}
server {
listen 8081;
location /test1 {
set $orig_foo $foo;
set $args debug;
echo "original foo: $orig_foo";
echo "foo: $foo";
}
location /args {
echo "args: $args";
}
location /t {
echo "uri = $uri";
echo "request_uri = $request_uri";
echo "name1 = $arg_name1";# Nginx 会在匹配参数名之前,自动把原始请求中的参数名调整为全部小写的形式,可以接收Name,Name等
set_unescape_uri $name $arg_name;
set_unescape_uri $class $arg_class;
echo "name: $name";
echo "class: $class";
}
location / {
default_type text/html;
echo "d=[$d]";
content_by_lua '
ngx.say("hello, world $d
")
';
}
location /test {
set $a 测试;
# echo "This is a dollar sign: $a";
set $b foo;
echo_exec /bar;
}
location /bar {
set $c ccc;
set $d ddd;
echo "b = [$b] d=[$d]";
}
location /c {
set $c myccc;
echo "my c and d c=[$c] d=[$d]"; #变量全局可见,所以声明一次,全局可用,但是赋值不能跨越边界,除非是内部跳转可以传递
}
location /d {
rewrite ^ /c;
}
}
}
* index.html和hello.html内容
```bash
gosber@freedamadeMBP ~/sloth cat index.html
just index.html
gosber@freedamadeMBP ~/sloth cat hello.html
just hello.html
好吧,技术书籍并不适合写书评或者笔记,因为细节特别的多。
尤其投入的学习之后,基本仍不住想要动手coding的冲动,根本不想再写笔记。下次一定注意,不再写技术学习的笔记了。 ——锅巴GG
想加入更多乐读创业社的活动,请访问网站→ http://ledu.club
或关注微信公众号选取: