mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-16 06:03:45 +08:00
mod
This commit is contained in:
228
极客时间专栏/OpenResty从入门到实战/API篇/15 | OpenResty 和别的开发平台有什么不同?.md
Normal file
228
极客时间专栏/OpenResty从入门到实战/API篇/15 | OpenResty 和别的开发平台有什么不同?.md
Normal file
@@ -0,0 +1,228 @@
|
||||
<audio id="audio" title="15 | OpenResty 和别的开发平台有什么不同?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/9d/4c/9d7d6d7ab40c4c0794113ddff514804c.mp3"></audio>
|
||||
|
||||
你好,我是温铭。
|
||||
|
||||
上一模块中, 你已经学习了 OpenResty 的两个基石:NGINX 和 LuaJIT,相信你已经摩拳擦掌,准备开始学习 OpenResty 提供的 API 了吧?
|
||||
|
||||
不过,别着急,在这之前,你还需要再花一点儿时间,来熟悉下 OpenResty 的原理和基本概念。
|
||||
|
||||
## 原理
|
||||
|
||||
在前面的 LuaJIT 内容中,你已经见过下面这个架构图:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/14/f0/14ab2f0c81c170234ab739cb700a62f0.png" alt="">
|
||||
|
||||
这里我再详细解释一下。
|
||||
|
||||
OpenResty 的 master 和 worker 进程中,都包含一个 LuaJIT VM。在同一个进程内的所有协程,都会共享这个 VM,并在这个 VM 中运行 Lua 代码。
|
||||
|
||||
而在同一个时间点上,每个 worker 进程只能处理一个用户的请求,也就是只有一个协程在运行。看到这里,你可能会有一个疑问:NGINX 既然能够支持 C10K (上万并发),不是需要同时处理一万个请求吗?
|
||||
|
||||
当然不是,NGINX 实际上是通过 epoll 的事件驱动,来减少等待和空转,才尽可能地让 CPU 资源都用于处理用户的请求。毕竟,只有单个的请求被足够快地处理完,整体才能达到高性能的目的。如果采用的是多线程模式,让一个请求对应一个线程,那么在 C10K 的情况下,资源很容易就会被耗尽的。
|
||||
|
||||
在 OpenResty 层面,Lua 的协程会与 NGINX 的事件机制相互配合。如果 Lua 代码中出现类似查询 MySQL 数据库这样的 I/O 操作,就会先调用 Lua 协程的 yield 把自己挂起,然后在 NGINX 中注册回调;在 I/O 操作完成(也可能是超时或者出错)后,再由 NGINX 回调 resume 来唤醒 Lua 协程。这样就完成了 Lua 协程和 NGINX 事件驱动的配合,避免在 Lua 代码中写回调。
|
||||
|
||||
我们可以来看下面这张图,描述了这整个流程。其中,`lua_yield` 和 `lua_resume` 都属于 Lua 提供的 `lua_CFunction`。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fa/34/fae1008edb43c7476cf2f20da9928234.png" alt="">
|
||||
|
||||
另外一个方面,如果 Lua 代码中没有 I/O 或者 sleep 操作,比如全是密集的加解密运算,那么 Lua 协程就会一直占用 LuaJIT VM,直到处理完整个请求。
|
||||
|
||||
下面我提供了 `ngx.sleep` 的一段源码,可以帮你更清晰理解这一点。 这段代码位于 `ngx_http_lua_sleep.c` 中,你可以在 `lua-nginx-module` 项目的 [src 目录](https://github.com/openresty/lua-nginx-module/tree/master/src)中找到它。
|
||||
|
||||
在`ngx_http_lua_sleep.c` 中,我们可以看到 sleep 函数的具体实现。你需要先通过 C 函数 `ngx_http_lua_ngx_sleep`,来注册 `ngx.sleep` 这个 Lua API:
|
||||
|
||||
```
|
||||
void
|
||||
ngx_http_lua_inject_sleep_api(lua_State *L)
|
||||
{
|
||||
lua_pushcfunction(L, ngx_http_lua_ngx_sleep);
|
||||
lua_setfield(L, -2, "sleep");
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
下面便是 sleep 的主函数,这里我只摘取了几行主要的代码:
|
||||
|
||||
```
|
||||
static int ngx_http_lua_ngx_sleep(lua_State *L)
|
||||
{
|
||||
coctx->sleep.handler = ngx_http_lua_sleep_handler;
|
||||
ngx_add_timer(&coctx->sleep, (ngx_msec_t) delay);
|
||||
return lua_yield(L, 0);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
你可以看到:
|
||||
|
||||
- 这里先增加了 `ngx_http_lua_sleep_handler` 这个回调函数;
|
||||
- 然后调用 `ngx_add_timer` 这个 NGINX 提供的接口,向 NGINX 的事件循环中增加一个定时器;
|
||||
- 最后使用 `lua_yield` 把 Lua 协程挂起,把控制权交给 NGINX 的事件循环。
|
||||
|
||||
当 sleep 操作完成后, `ngx_http_lua_sleep_handler` 这个回调函数就被触发了。它里面调用了 `ngx_http_lua_sleep_resume`, 并最终使用 `lua_resume` 唤醒了 Lua 协程。更具体的调用过程,你可以自己去代码里面检索,这里我就不展开描述了。
|
||||
|
||||
`ngx.sleep` 只是最简单的一个示例,不过通过对它的剖析,你可以看出 `lua-nginx-module` 模块的基本原理。
|
||||
|
||||
## 基本概念
|
||||
|
||||
分析完原理之后,让我们一起温故而知新,回忆下 OpenResty 中**阶段**和**非阻塞**这两个重要的概念。
|
||||
|
||||
OpenResty 和 NGINX 一样,都有阶段的概念,并且每个阶段都有自己不同的作用:
|
||||
|
||||
- `set_by_lua`,用于设置变量;
|
||||
- `rewrite_by_lua`,用于转发、重定向等;
|
||||
- `access_by_lua`,用于准入、权限等;
|
||||
- `content_by_lua`,用于生成返回内容;
|
||||
- `header_filter_by_lua`,用于应答头过滤处理;
|
||||
- `body_filter_by_lua`,用于应答体过滤处理;
|
||||
- `log_by_lua`,用于日志记录。
|
||||
|
||||
当然,如果你的代码逻辑并不复杂,都放在 rewrite 或者 content 阶段执行,也是可以的。
|
||||
|
||||
不过需要注意,OpenResty 的 API 是有阶段使用限制的。每一个 API 都有一个与之对应的使用阶段列表,如果你超范围使用就会报错。这与其他的开发语言有很大的不同。
|
||||
|
||||
举个例子,这里我还是以 `ngx.sleep` 为例。通过查阅文档,我知道它只能用于下面列出的上下文中,并不包括 log 阶段:
|
||||
|
||||
```
|
||||
context: rewrite_by_lua*, access_by_lua*, content_by_lua*, ngx.timer.*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*_
|
||||
|
||||
```
|
||||
|
||||
而如果你不知道这一点,在它不支持的 log 阶段使用 sleep 的话:
|
||||
|
||||
```
|
||||
location / {
|
||||
log_by_lua_block {
|
||||
ngx.sleep(1)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在 NGINX 的错误日志中,就会出现 error 级别的提示:
|
||||
|
||||
```
|
||||
[error] 62666#0: *6 failed to run log_by_lua*: log_by_lua(nginx.conf:14):2: API disabled in the context of log_by_lua*
|
||||
stack traceback:
|
||||
[C]: in function 'sleep'
|
||||
|
||||
```
|
||||
|
||||
所以,在你使用 API 之前,一定记得要先查阅文档,确定其能否在代码的上下文中使用。
|
||||
|
||||
复习了阶段的概念后,我们再来回顾下非阻塞。首先明确一点,由 OpenResty 提供的所有 API,都是非阻塞的。
|
||||
|
||||
我继续以 sleep 1 秒这个需求为例来说明。如果你要在 Lua 中实现它,你需要这样做:
|
||||
|
||||
```
|
||||
function sleep(s)
|
||||
local ntime = os.time() + s
|
||||
repeat until os.time() > ntime
|
||||
end
|
||||
|
||||
```
|
||||
|
||||
因为标准 Lua 没有直接的 sleep 函数,所以这里我用一个循环,来不停地判断是否达到指定的时间。这个实现就是阻塞的,在 sleep 的这一秒钟时间内,Lua 正在做无用功,而其他需要处理的请求,只能在一边傻傻地等待。
|
||||
|
||||
不过,要是换成 `ngx.sleep(1)` 来实现的话,根据上面我们分析过的源码,在这一秒钟的时间内,OpenResty 依然可以去处理其他请求(比如 B 请求),当前请求(我们叫它 A 请求)的上下文会被保存起来,并由 NGINX 的事件机制来唤醒,再回到 A 请求,这样 CPU 就一直处于真正的工作状态。
|
||||
|
||||
## 变量和生命周期
|
||||
|
||||
除了这两个重要概念外,**变量的生命周期**,也是 OpenResty 开发中容易出错的地方。
|
||||
|
||||
前面说过,在 OpenResty 中,我推荐你把所有变量都声明为局部变量,并用 luacheck 和 lua-releng 这样的工具来检测全局变量。这其实对于模块来说也是一样的,比如下面这样的写法:
|
||||
|
||||
```
|
||||
local ngx_re = require "ngx.re"
|
||||
|
||||
```
|
||||
|
||||
其实,在 OpenResty 中,除了 `init_by_lua` 和 `init_worker_by_lua` 这两个阶段外,其余阶段都会设置一个隔离的全局变量表,以免在处理过程中污染了其他请求。即使在这两个可以定义全局变量的阶段,你也应该尽量避免去定义全局变量。
|
||||
|
||||
通常来说,试图用全局变量来解决的问题,其实更应该用模块的变量来解决,而且还会更加清晰。下面是一个模块中变量的示例:
|
||||
|
||||
```
|
||||
local _M = {}
|
||||
|
||||
_M.color = {
|
||||
red = 1,
|
||||
blue = 2,
|
||||
green = 3
|
||||
}
|
||||
|
||||
return _M
|
||||
|
||||
```
|
||||
|
||||
我在一个名为 hello.lua 的文件中定义了一个模块,模块包含了 color 这个 table。然后,我又在 nginx.conf 中增加了对应的配置:
|
||||
|
||||
```
|
||||
location / {
|
||||
content_by_lua_block {
|
||||
local hello = require "hello"
|
||||
ngx.say(hello.color.green)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这段配置会在 content 阶段中 require 这个模块,并把 green 的值作为 http 请求返回体打印出来。
|
||||
|
||||
你可能会好奇,模块变量为什么这么神奇呢?
|
||||
|
||||
实际上,在同一 worker 进程中,模块只会被加载一次;之后这个 worker 处理的所有请求,就可以共享模块中的数据了。我们说“全局”的数据很适合封装在模块内,是因为 OpenResty 的 worker 之间完全隔离,所以每个 worker 都会独立地对模块进行加载,而模块的数据也不能跨越 worker。
|
||||
|
||||
至于应该如何处理 worker 之间需要共享的数据,我会留到后面的章节来讲解,这里你先不必深究。
|
||||
|
||||
不过,这里也有一个很容易出错的地方,那就是**访问模块变量的时候,你最好保持只读,而不要尝试去修改,不然在高并发的情况下会出现 race**。这种 bug 依靠单元测试是无法发现的,它在线上偶尔会出现,并且很难定位。
|
||||
|
||||
举个例子,模块变量 green 当前的值是 3,而你在代码中做了加 1 的操作,那么现在 green 的值是 4 吗?不一定,它可能是 4,也可能是 5 或者是 6。因为在对模块变量进行写操作的时候,OpenResty 并不会加锁,这时就会产生竞争,模块变量的值就会被多个请求同时更新。
|
||||
|
||||
说完了全局变量、局部变量和模块变量,最后我们再来讲讲跨阶段的变量。
|
||||
|
||||
有些情况下,我们需要的是跨越阶段的、可以读写的变量。而像我们熟悉的 NGINX 中 `$host`、`$scheme` 等变量,虽然满足跨越阶段的条件,但却无法做到动态创建,你必须先在配置文件中定义才能使用它们。比如下面这样的写法:
|
||||
|
||||
```
|
||||
location /foo {
|
||||
set $my_var ; # 需要先创建 $my_var 变量
|
||||
content_by_lua_block {
|
||||
ngx.var.my_var = 123
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
OpenResty 提供了 `ngx.ctx`,来解决这类问题。它是一个 Lua table,可以用来存储基于请求的 Lua 数据,且生存周期与当前请求相同。我们来看下官方文档中的这个示例:
|
||||
|
||||
```
|
||||
location /test {
|
||||
rewrite_by_lua_block {
|
||||
ngx.ctx.foo = 76
|
||||
}
|
||||
access_by_lua_block {
|
||||
ngx.ctx.foo = ngx.ctx.foo + 3
|
||||
}
|
||||
content_by_lua_block {
|
||||
ngx.say(ngx.ctx.foo)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
你可以看到,我们定义了一个变量 `foo`,存放在 `ngx.ctx` 中。这个变量跨越了 rewrite、access 和 content 三个阶段,最终在 content 阶段打印出了值,并且是我们预期的 79。
|
||||
|
||||
当然,`ngx.ctx` 也有自己的局限性:
|
||||
|
||||
- 比如说,使用 `ngx.location.capture` 创建的子请求,会有自己独立的 `ngx.ctx` 数据,和父请求的 `ngx.ctx` 互不影响;
|
||||
- 再如,使用 `ngx.exec` 创建的内部重定向,会销毁原始请求的 `ngx.ctx`,重新生成空白的 `ngx.ctx`。
|
||||
|
||||
这两个局限,在官方文档中都有详细的[代码示例](https://github.com/openresty/lua-nginx-module#ngxctx),如果你有兴趣可以自行查阅。
|
||||
|
||||
## 写在最后
|
||||
|
||||
最后,我再多说几句。这节课,我们学习的是 OpenResty 的原理和几个重要的概念,不过,你并不需要背得滚瓜烂熟,毕竟,这些概念总是在和实际需求以及代码结合在一起时,才会变得有意义并生动起来。
|
||||
|
||||
不知道你是如何理解的呢?欢迎留言和我一起探讨,也欢迎你把这篇文章分享给你的同事、朋友,我们一起交流,一起进步。
|
||||
|
||||
|
||||
250
极客时间专栏/OpenResty从入门到实战/API篇/16 | 秒杀大多数开发问题的两个利器:文档和测试案例.md
Normal file
250
极客时间专栏/OpenResty从入门到实战/API篇/16 | 秒杀大多数开发问题的两个利器:文档和测试案例.md
Normal file
@@ -0,0 +1,250 @@
|
||||
<audio id="audio" title="16 | 秒杀大多数开发问题的两个利器:文档和测试案例" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/86/c9/868e7dd2ae529e45df6f8ebea9f081c9.mp3"></audio>
|
||||
|
||||
你好,我是温铭。在学习了 OpenResty 的原理和几个重要概念后,我们终于要开始 API 的学习了。
|
||||
|
||||
从我个人的经验来看,学习 OpenResty 的 API 是相对容易的,所以并没有占用本专栏太多的篇幅。你可以会疑惑:API 不是最常用、最重要的部分吗,为什么花的笔墨不多?
|
||||
|
||||
其实,这主要是出于两个方面的考虑。
|
||||
|
||||
第一,OpenResty 提供了非常详尽的文档。和很多其他的开发语言或者平台相比,OpenResty 除了会提供 API 的参数、返回值定义,还会提供完整的、可运行的代码示例,清楚地告诉你API 是如何处理各种边界条件的。
|
||||
|
||||
这种在 API 定义下面紧跟着示例代码和注意事项的做法,就是 OpenResty 文档的一贯风格。这样一来,在看完 API 描述后,你就可以立即在自己的环境下运行示例代码,并修改参数来和文档互相印证,加深记忆和理解。
|
||||
|
||||
第二,在文档之外,OpenResty还提供了高覆盖度的测试案例集。刚刚我提到过,OpenResty文档中提供了 API 的代码示例,但终究篇幅有限,多个 API 之间如何配合使用、各种异常情况下的报错和处理等,在文档中并没有呈现。
|
||||
|
||||
不过,不用担心,这些内容你大都可以在测试案例集里找到。
|
||||
|
||||
对于 OpenResty 的开发者来说,最好的 API 学习资料就是官方文档和测试案例,它们足够专业和友好。在这个前提下,如果我单纯地把文档翻译成中文再放在专栏中来讲,就没有太大意义了。
|
||||
|
||||
授人以鱼不如授之以渔,我更希望教给你的是通用的方法和经验。让我们用一个真实的例子来体验下,在 OpenResty 的开发中,如何让文档和测试案例集发挥更大的威力。
|
||||
|
||||
## shdict get API
|
||||
|
||||
shared dict(共享字典)是基于 NGINX 共享内存区的 Lua 字典对象,它可以跨多个 worker 来存取数据,一般用来存放限流、限速、缓存等数据。shared dict 相关的 API 有 20 多个,是 OpenResty 中最常用也是最重要的一组 API。
|
||||
|
||||
我们以最简单的 get 操作为例,你可以点开 [文档链接](https://github.com/openresty/lua-nginx-module/#ngxshareddictget) 做为对照。下面的最小化的代码示例,正是由官方文档改编而来:
|
||||
|
||||
```
|
||||
http {
|
||||
lua_shared_dict dogs 10m;
|
||||
server {
|
||||
location /demo {
|
||||
content_by_lua_block {
|
||||
local dogs = ngx.shared.dogs
|
||||
dogs:set("Jim", 8)
|
||||
local v = dogs:get("Jim")
|
||||
ngx.say(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
简单说明一下,在Lua 代码中使用 shared dict 之前,我们需要在 nginx.conf 中用 `lua_shared_dict` 指令增加一块内存空间,它的名字是 dogs,大小为 10M。修改完 nginx.conf后,你还需要重启进程,用浏览器或者 curl 访问才能看到结果。
|
||||
|
||||
这步骤看起来是不是有些繁琐呢?让我们用一种更直接的方式改造一下。你可以看到,使用 resty CLI 的这种方式,和在 nginx.conf 中嵌入代码的效果是一致的。
|
||||
|
||||
```
|
||||
$ resty --shdict 'dogs 10m' -e 'local dogs = ngx.shared.dogs
|
||||
dogs:set("Jim", 8)
|
||||
local v = dogs:get("Jim")
|
||||
ngx.say(v)
|
||||
'
|
||||
|
||||
```
|
||||
|
||||
你现在已经知道 nginx.conf 和 Lua 代码是如何配合的,也成功运行了 shared dict 的 set 和 get 方法。一般来说,大部分开发者也就此止步,不再深究了。
|
||||
|
||||
事实上,这里还是有几个值得注意的地方,比如:
|
||||
|
||||
1. 哪些阶段不能使用共享内存相关的 API 呢?
|
||||
1. 我们在示例代码中看到 get 函数只有一个返回值,那什么情况下会有多个返回值呢?
|
||||
1. get 函数的入参是什么类型?是否有长度限制?
|
||||
|
||||
不要小看这几个问题,窥一斑而见全豹,它们可以帮助我们更好的深入 OpenResty。接下来我就带你一一解读。
|
||||
|
||||
## 哪些阶段不能使用共享内存相关的 API ?
|
||||
|
||||
先来看第一个问题,答案很直接,文档中专门有一个 `context` (即上下文部分),里面列出了在什么环境下可以使用这个 API:
|
||||
|
||||
```
|
||||
context: set_by_lua*, rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, body_filter_by_lua*, log_by_lua*, ngx.timer.*, balancer_by_lua*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*, ssl_session_store_by_lua*
|
||||
|
||||
```
|
||||
|
||||
可以看出, `init` 和 `init_worker` 两个阶段不在其中,也就是说,共享内存的 get API 不能在这两个阶段使用。需要注意的是,每个共享内存的 API 可以使用的阶段并不完全相同,比如 set API 就可以在 `init` 阶段使用。
|
||||
|
||||
所以,千万不要想当然,还是那句话,使用时多翻阅文档。当然了,尽信书不如无书,OpenResty 的文档有时候也会出现错漏,这时候你就需要用实际的测试来验证了。
|
||||
|
||||
接下来,让我们修改下测试案例集,来确定下 `init` 阶段是否可以运行 shared dict 的 get API。
|
||||
|
||||
那该如何找到和共享内存相关的测试案例集呢?事实上,OpenResty 的测试案例都放在 `/t` 目录下,并且命名也是有规律的,即`自增数字-功能名.t`。搜索`shdict`,你可以找到 `043-shdict.t`,而这就是共享内存的测试案例集了,它里面有接近 100 个测试案例,包含各种正常和异常情况的测试。
|
||||
|
||||
我们来试着修改下第一个测试案例。
|
||||
|
||||
你可以把 content 阶段改为 init 阶段,并精简掉无关代码,看看 get 接口能否运行。这里我需要提醒一点,在现阶段,你不用非得搞明白测试案例是如何编写、组织和运行的,你只要知道它是在测试 get 接口就可以了:
|
||||
|
||||
```
|
||||
=== TEST 1: string key, int value
|
||||
--- http_config
|
||||
lua_shared_dict dogs 1m;
|
||||
--- config
|
||||
location = /test {
|
||||
init_by_lua '
|
||||
local dogs = ngx.shared.dogs
|
||||
local val = dogs:get("foo")
|
||||
ngx.say(val)
|
||||
';
|
||||
}
|
||||
--- request
|
||||
GET /test
|
||||
--- response_body
|
||||
32
|
||||
--- no_error_log
|
||||
[error]
|
||||
--- ONLY
|
||||
|
||||
```
|
||||
|
||||
你应该注意到了,在测试案例的最后,我加了 `--ONLY` 标记,这表示忽略其他所有测试案例,只运行这一个测试案例,以提高运行速度。后面在测试部分中,我会专门讲解各种各样的标记,你先记住这里就可以了。
|
||||
|
||||
修改完以后,我们用 prove 命令,就可以运行这个测试案例:
|
||||
|
||||
```
|
||||
$ prove t/043-shdict.t
|
||||
|
||||
```
|
||||
|
||||
然后,你会得到一个报错,这也就印证了文档中描述的阶段限制。
|
||||
|
||||
```
|
||||
nginx: [emerg] "init_by_lua" directive is not allowed here
|
||||
|
||||
```
|
||||
|
||||
## get 函数何时会有多个返回值?
|
||||
|
||||
我们再来看第二个问题,它可以从官方文档中总结出来。文档最开始就是这个接口的`syntax` 语法描述部分:
|
||||
|
||||
```
|
||||
value, flags = ngx.shared.DICT:get(key)
|
||||
|
||||
```
|
||||
|
||||
正常情况下,
|
||||
|
||||
- 第一个参数`value` 返回的是字典中 key 对应的值;但当 key 不存在或者过期时,`value` 的值为 nil。
|
||||
- 第二个参数 `flags` 就稍微复杂一些了,如果 set 接口设置了 flags,就返回,否则不返回。
|
||||
|
||||
一旦 API 调用出错,`value` 返回 nil,`flags` 返回具体的错误信息。
|
||||
|
||||
从文档总结的信息我们可以看出,`local v = dogs:get("Jim")` 这种只有一个接收参数的写法并不完善,因为它只覆盖了普通的使用场景,没有接收第二个参数,也没有做异常处理。我们可以把它修改为下面这样:
|
||||
|
||||
```
|
||||
local data, err = dogs:get("Jim")
|
||||
if data == nil and err then
|
||||
ngx.say("get not ok: ", err)
|
||||
return
|
||||
end
|
||||
|
||||
```
|
||||
|
||||
和第一个问题一样,我们可以到测试案例集里搜索一下,印证下我们对文档的理解:
|
||||
|
||||
```
|
||||
=== TEST 65: get nil key
|
||||
--- http_config
|
||||
lua_shared_dict dogs 1m;
|
||||
--- config
|
||||
location = /test {
|
||||
content_by_lua '
|
||||
local dogs = ngx.shared.dogs
|
||||
local ok, err = dogs:get(nil)
|
||||
if not ok then
|
||||
ngx.say("not ok: ", err)
|
||||
return
|
||||
end
|
||||
ngx.say("ok")
|
||||
';
|
||||
}
|
||||
--- request
|
||||
GET /test
|
||||
--- response_body
|
||||
not ok: nil key
|
||||
--- no_error_log
|
||||
[error]
|
||||
|
||||
```
|
||||
|
||||
在这个测试案例中,get 接口的入参为 nil,返回的 err 信息是 `nil key`。这一方面验证了我们对文档的分析是正确的,另一方面,也为第三个问题提供了部分答案——起码,get 的入参不能是 nil。
|
||||
|
||||
## get 函数的入参是什么类型?
|
||||
|
||||
至于第三个问题, get 的入参可以是什么类型的呢?我们按照老规矩先查看文档,不过很可惜,你会发现,文档里并没有注明 key 的合法类型有哪些。这时该怎么办呢?
|
||||
|
||||
别着急,至少我们知道 key 可以是字符串类型,并且不能为 nil。不知道你还记得 Lua 中的数据类型吗?除了字符串和 nil,还有数字、数组、布尔类型和函数。后面两个显然没有作为 key 的必要性,我们只需要验证前两个。不妨先去测试文件中搜索一下,是否有数字作为 key 的案例:
|
||||
|
||||
```
|
||||
=== TEST 4: number keys, string values
|
||||
|
||||
```
|
||||
|
||||
通过这个测试案例,你可以清楚看到,数字也可以作为 key ,内部会将数字转为字符串。那么数组呢?很遗憾,测试案例并没有覆盖到,我们需要自己动手试一下:
|
||||
|
||||
```
|
||||
$ resty --shdict 'dogs 10m' -e 'local dogs = ngx.shared.dogs
|
||||
dogs:get({})
|
||||
'
|
||||
|
||||
```
|
||||
|
||||
不出意料,果然报错了:
|
||||
|
||||
```
|
||||
ERROR: (command line -e):2: bad argument #1 to 'get' (string expected, got table)
|
||||
|
||||
```
|
||||
|
||||
综上,我们可以得出结论:get API 接受的 key 类型为字符串和数字。
|
||||
|
||||
那么入参 key 的长度是否有限制呢?这里其实也有一个对应的测试案例,我们一起来看一下:
|
||||
|
||||
```
|
||||
=== TEST 67: get a too-long key
|
||||
--- http_config
|
||||
lua_shared_dict dogs 1m;
|
||||
--- config
|
||||
location = /test {
|
||||
content_by_lua '
|
||||
local dogs = ngx.shared.dogs
|
||||
local ok, err = dogs:get(string.rep("a", 65536))
|
||||
if not ok then
|
||||
ngx.say("not ok: ", err)
|
||||
return
|
||||
end
|
||||
ngx.say("ok")
|
||||
';
|
||||
}
|
||||
--- request
|
||||
GET /test
|
||||
--- response_body
|
||||
not ok: key too long
|
||||
--- no_error_log
|
||||
[error]
|
||||
|
||||
```
|
||||
|
||||
很显然,字符串长度为 65536 的时候,就会被提示 key 太长了。你可以试下把长度改为 65535,虽然只少了1个字节,却不会再报错了。这就说明,key 的最大长度正是 65535。
|
||||
|
||||
## 写在最后
|
||||
|
||||
OpenResty 现在的官方文档只有英文版本,国内工程师在阅读时,难免会因为语言问题,抓不住重点,甚至误解其中的内容。但越是这样,越没有捷径可走,你更应该仔细地把文档从头到尾读完,并在有疑问时,结合测试案例集和自己的尝试,去确定出答案。这才是辅助我们学习 OpenResty 的正确途径。
|
||||
|
||||
最后,我想提醒一下,在 OpenResty 的 API 中,凡是返回值中带有错误信息的,都必须有变量来接收并做错误处理,否则前方一定会有坑等你跳进去。比如把出错的连接放入了连接池,或者在 API 调用失败的情况下继续后面的逻辑,总之一定让人叫苦不迭。
|
||||
|
||||
那么,你在写 OpenResty 代码的时候,如果遇到问题,一般是通过什么方式来解决的?是文档、邮件列表、QQ 群,还是其他渠道呢?
|
||||
|
||||
欢迎留言一起探讨,也欢迎你把这篇文章分享给你的同事、朋友,我们一起交流,一起进步。
|
||||
|
||||
|
||||
249
极客时间专栏/OpenResty从入门到实战/API篇/17 | 为什么能成为更好的Web服务器?动态处理请求和响应是关键.md
Normal file
249
极客时间专栏/OpenResty从入门到实战/API篇/17 | 为什么能成为更好的Web服务器?动态处理请求和响应是关键.md
Normal file
@@ -0,0 +1,249 @@
|
||||
<audio id="audio" title="17 | 为什么能成为更好的Web服务器?动态处理请求和响应是关键" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/48/6e/4896687fb4708633494c82751b8f1b6e.mp3"></audio>
|
||||
|
||||
你好,我是温铭。经过前面内容的铺垫后, 相信你已经对 OpenResty 的概念和如何学习它有了基本的认识。今天这节课,我们来看一下 OpenResty 如何处理终端请求和响应。
|
||||
|
||||
虽然 OpenResty 是基于 NGINX 的 Web 服务器,但它与 NGINX 却有本质的不同:NGINX 由静态的配置文件驱动,而 OpenResty 是由 Lua API 驱动的,所以能提供更多的灵活性和可编程性。
|
||||
|
||||
下面,就让我来带你领略 Lua API 带来的好处吧。
|
||||
|
||||
## API 分类
|
||||
|
||||
首先我们要知道,OpenResty 的 API 主要分为下面几个大类:
|
||||
|
||||
- 处理请求和响应;
|
||||
- SSL 相关;
|
||||
- shared dict;
|
||||
- cosocket;
|
||||
- 处理四层流量;
|
||||
- process 和 worker;
|
||||
- 获取 NGINX 变量和配置;
|
||||
- 字符串、时间、编解码等通用功能。
|
||||
|
||||
这里,我建议你同时打开 OpenResty 的 Lua API 文档,对照着其中的 [API 列表](https://github.com/openresty/lua-nginx-module/#nginx-api-for-lua) ,看看是否能和这个分类联系起来。
|
||||
|
||||
OpenResty 的 API 不仅仅存在于 lua-nginx-module 项目中,也存在于 lua-resty-core 项目中,比如 ngx.ssl、ngx.base64、ngx.errlog、ngx.process、ngx.re.split、ngx.resp.add_header、ngx.balancer、ngx.semaphore、ngx.ocsp 这些 API 。
|
||||
|
||||
而对于不在 lua-nginx-module 项目中的 API,你需要单独 require 才能使用。举个例子,比如你想使用 split 这个字符串分割函数,就需要按照下面的方法来调用:
|
||||
|
||||
```
|
||||
$ resty -e 'local ngx_re = require "ngx.re"
|
||||
local res, err = ngx_re.split("a,b,c,d", ",", nil, {pos = 5})
|
||||
print(res)
|
||||
'
|
||||
|
||||
```
|
||||
|
||||
当然,这可能会给你带来一个困惑:在 lua-nginx-module 项目中,明明有 ngx.re.sub、ngx.re.find 等好几个 ngx.re 开头的 API,为什么单单是 ngx.re.split 这个 API ,需要 require 后才能使用呢?
|
||||
|
||||
事实上,在前面 lua-resty-core 章节中,我们也提到过,OpenResty 新的 API 都是通过 FFI 的方式在 `lua-rety-core` 仓库中实现的,所以难免就会存在这种割裂感。自然,我也很期待lua-nginx-module 和 lua-resty-core 这两个项目以后可以合并,彻底解决此类问题。
|
||||
|
||||
## 请求
|
||||
|
||||
接下来,我们具体了解下OpenResty 是如何处理终端请求和响应的。先来看下处理请求的 API,不过,以 ngx.req 开头的 API 有 20 多个,该怎么下手呢?
|
||||
|
||||
我们知道,HTTP 请求报文由三部分组成:请求行、请求头和请求体,所以下面我就按照这三部分来对 API 做介绍。
|
||||
|
||||
### 请求行
|
||||
|
||||
首先是请求行,HTTP 的请求行中包含请求方法、URI 和 HTTP 协议版本。在 NGINX 中,你可以通过内置变量的方式,来获取其中的值;而在 OpenResty 中对应的则是 `ngx.var.*` 这个 API。我们来看两个例子。
|
||||
|
||||
- `$scheme` 这个内置变量,在 NGINX 中代表协议的名字,是 “http” 或者 “https”;而在 OpenResty 中,你可以通过 `ngx.var.scheme` 来返回同样的值。
|
||||
- `$request_method` 代表的是请求的方法,“GET”、“POST” 等;而在 OpenResty 中,你可以通过 `ngx.var. request_method` 来返回同样的值。
|
||||
|
||||
至于完整的 NGINX 内置变量列表,你可以访问 NGINX 的官方文档来获取:[http://nginx.org/en/docs/http/ngx_http_core_module.html#variables](http://nginx.org/en/docs/http/ngx_http_core_module.html#variables)。
|
||||
|
||||
那么问题就来了:既然可以通过`ngx.var.*` 这种返回变量值的方法,来得到请求行中的数据,为什么 OpenResty 还要单独提供针对请求行的 API 呢?
|
||||
|
||||
这其实是很多方面因素的综合考虑结果:
|
||||
|
||||
- 首先是对性能的考虑。`ngx.var` 的效率不高,不建议反复读取;
|
||||
- 也有对程序友好的考虑,`ngx.var` 返回的是字符串,而非 Lua 对象,遇到获取 args 这种可能返回多个值的情况,就不好处理了;
|
||||
- 另外是对灵活性的考虑,绝大部分的 `ngx.var` 是只读的,只有很少数的变量是可写的,比如 `$args` 和 `limit_rate`,可很多时候,我们会有修改 method、URI 和 args 的需求。
|
||||
|
||||
所以, OpenResty 提供了多个专门操作请求行的 API,它们可以对请求行进行改写,以便后续的重定向等操作。
|
||||
|
||||
我们先来看下,如何通过 API 来获取 HTTP 协议版本号。OpenResty 的 API `ngx.req.http_version` 和 NGINX 的 `$server_protocol` 变量的作用一样,都是返回 HTTP 协议的版本号。不过这个 API 的返回值是数字格式,而非字符串,可能的值是 2.0、1.0、1.1 和 0.9,如果结果不在这几个值的范围内,就会返回 nil。
|
||||
|
||||
再来看下获取请求行中的请求方法。刚才我们提到过,`ngx.req.get_method` 和 NGINX 的 `$request_method` 变量的作用、返回值一样,都是字符串格式的方法名。
|
||||
|
||||
但是,改写当前 HTTP 请求方法的 API,也就是 `ngx.req.set_method`,它接受的参数格式却并非字符串,而是内置的数字常量。比如,下面的代码,把请求方法改写为 POST:
|
||||
|
||||
```
|
||||
ngx.req.set_method(ngx.HTTP_POST)
|
||||
|
||||
```
|
||||
|
||||
为了验证 `ngx.HTTP_POST` 这个内置常量,确实是数字而非字符串,你可以打印出它的值,看输出是否为 8:
|
||||
|
||||
```
|
||||
$ resty -e 'print(ngx.HTTP_POST)'
|
||||
|
||||
```
|
||||
|
||||
这样一来,get 方法的返回值为字符串,而set 方法的输入值却是数字,就很容易让你在写代码的时候想当然了。如果是 set 时候传值混淆的情况还好,API 会崩溃报出 500 的错误;但如果是下面这种判断逻辑的代码:
|
||||
|
||||
```
|
||||
if (ngx.req.get_method() == ngx.HTTP_POST) then
|
||||
-- do something
|
||||
end
|
||||
|
||||
```
|
||||
|
||||
这种代码是可以正常运行的,不会报出任何错误,甚至在 code review 时也很难发现。不幸的是,我就犯过类似的错误,对此记忆犹新:当时已经经过了两轮 code review,还有不完整的测试案例尝试覆盖,然而,最终还是因为线上环境异常才追踪到了这里。
|
||||
|
||||
碰到这类情况,除了自己多小心,或者再多一层封装外,并没有什么有效的方法来解决。平常你在设计自己的业务 API 时,也可以多做一些这方面的考虑,尽量保持 get、set 方法的参数格式一致,即使这会牺牲一些性能。
|
||||
|
||||
另外,在改写请求行的方法中,还有 `ngx.req.set_uri` 和 `ngx.req.set_uri_args` 这两个 API,可以用来改写 uri 和 args。我们来看下这个 NGINX 配置:
|
||||
|
||||
```
|
||||
rewrite ^ /foo?a=3? break;
|
||||
|
||||
```
|
||||
|
||||
那么,如何用等价的 Lua API 来解决呢?答案就是下面这两行代码。
|
||||
|
||||
```
|
||||
ngx.req.set_uri_args("a=3")
|
||||
ngx.req.set_uri("/foo")
|
||||
|
||||
```
|
||||
|
||||
其实,如果你看过官方文档,就会发现 `ngx.req.set_uri` 还有第二个参数:jump,默认是 false。如果设置为 true,就等同于把 rewrite 指令的 flag 设置为 `last`,而非上面示例中的 `break`。
|
||||
|
||||
不过,我个人并不喜欢 rewrite 指令的 flag 配置,看不懂也记不住,远没有代码来的直观和好维护。
|
||||
|
||||
### 请求头
|
||||
|
||||
再来看下和请求头有关的 API。我们知道,HTTP 的请求头是 `key : value` 格式的,比如:
|
||||
|
||||
```
|
||||
Accept: text/css,*/*;q=0.1
|
||||
Accept-Encoding: gzip, deflate, br
|
||||
|
||||
```
|
||||
|
||||
在OpenResty 中,你可以使用 `ngx.req.get_headers` 来解析和获取请求头,返回值的类型则是 table:
|
||||
|
||||
```
|
||||
local h, err = ngx.req.get_headers()
|
||||
|
||||
if err == "truncated" then
|
||||
-- one can choose to ignore or reject the current request here
|
||||
end
|
||||
|
||||
for k, v in pairs(h) do
|
||||
...
|
||||
end
|
||||
|
||||
```
|
||||
|
||||
这里默认返回前 100 个 header,如果请求头超过了 100 个,就会返回 `truncated` 的错误信息,由开发者自己决定如何处理。你可能会好奇为什么会有这样的处理,这一点先留个悬念,在后面安全漏洞的章节中我会提到。
|
||||
|
||||
不过,需要注意的是,OpenResty 并没有提供获取某一个指定请求头的 API,也就是没有 `ngx.req.header['host']` 这种形式。如果你有这样的需求,那就需要借助 NGINX 的变量 `$http_xxx` 来实现了,那么在 OpenResty 中,就是 `ngx.var.http_xxx` 这样的获取方式。
|
||||
|
||||
看完了获取请求头,我们再来看看应该如何改写和删除请求头,这两种操作的 API 其实都很直观:
|
||||
|
||||
```
|
||||
ngx.req.set_header("Content-Type", "text/css")
|
||||
ngx.req.clear_header("Content-Type")
|
||||
|
||||
```
|
||||
|
||||
当然,官方文档中也提到了其他方法来删除请求头,比如把 header 的值设置为 nil等,但为了代码更加清晰的考虑,我还是推荐统一用 `clear_header` 来操作。
|
||||
|
||||
### 请求体
|
||||
|
||||
最后来看请求体。出于性能考虑,OpenResty 不会主动读取请求体的内容,除非你在 nginx.conf 中强制开启了 `lua_need_request_body` 指令。此外,对于比较大的请求体,OpenResty 会把内容保存在磁盘的临时文件中,所以读取请求体的完整流程是下面这样的:
|
||||
|
||||
```
|
||||
ngx.req.read_body()
|
||||
local data = ngx.req.get_body_data()
|
||||
if not data then
|
||||
local tmp_file = ngx.req.get_body_file()
|
||||
-- io.open(tmp_file)
|
||||
-- ...
|
||||
end
|
||||
|
||||
```
|
||||
|
||||
这段代码中有读取磁盘文件的 IO 阻塞操作。你应该根据实际情况来调整 `client_body_buffer_size` 配置的大小(64 位系统下默认是 16 KB),尽量减少阻塞的操作;你也可以把 `client_body_buffer_size` 和 `client_max_body_size` 配置成一样的,完全在内存中来处理,当然,这取决于你内存的大小和处理的并发请求数。
|
||||
|
||||
另外,请求体也可以被改写,`ngx.req.set_body_data` 和 `ngx.req.set_body_file` 这两个API,分别接受字符串和本地磁盘文件做为输入参数,来完成请求体的改写。不过,这类操作并不常见,你可以查看文档来获取更详细的内容。
|
||||
|
||||
## 响应
|
||||
|
||||
处理完请求后,我们就需要发送响应返回给客户端了。和请求报文一样,响应报文也由几个部分组成,即状态行、响应头和响应体。同样的,接下来我会按照这三部分来介绍相应的API。
|
||||
|
||||
### 状态行
|
||||
|
||||
状态行中,我们主要关注的是状态码。在默认情况下,返回的 HTTP 状态码是 200,也就是 OpenResty 中内置的常量 `ngx.HTTP_OK`。但在代码的世界中,处理异常情况的代码总是占比最多的。
|
||||
|
||||
如果你检测了请求报文,发现这是一个恶意的请求,那么你需要终止请求:
|
||||
|
||||
```
|
||||
ngx.exit(ngx.HTTP_BAD_REQUEST)
|
||||
|
||||
```
|
||||
|
||||
不过,OpenResty 的 HTTP 状态码中,有一个特别的常量:`ngx.OK`。当 `ngx.exit(ngx.OK)` 时,请求会退出当前处理阶段,进入下一个阶段,而不是直接返回给客户端。
|
||||
|
||||
当然,你也可以选择不退出,只使用 `ngx.status` 来改写状态码,比如下面这样的写法:
|
||||
|
||||
```
|
||||
ngx.status = ngx.HTTP_FORBIDDEN
|
||||
|
||||
```
|
||||
|
||||
如果你想了解更多的状态码常量,可以从[文档](https://github.com/openresty/lua-nginx-module/#http-status-constants)中查询到。
|
||||
|
||||
### 响应头
|
||||
|
||||
说到响应头,其实,你有两种方法来设置它。第一种是最简单的:
|
||||
|
||||
```
|
||||
ngx.header.content_type = 'text/plain'
|
||||
ngx.header["X-My-Header"] = 'blah blah'
|
||||
ngx.header["X-My-Header"] = nil -- 删除
|
||||
|
||||
```
|
||||
|
||||
这里的 ngx.header 保存了响应头的信息,可以读取、修改和删除。
|
||||
|
||||
第二种设置响应头的方法是 `ngx_resp.add_header` ,来自 lua-resty-core 仓库,它可以增加一个头信息,用下面的方法来调用:
|
||||
|
||||
```
|
||||
local ngx_resp = require "ngx.resp"
|
||||
ngx_resp.add_header("Foo", "bar")
|
||||
|
||||
```
|
||||
|
||||
与第一种方法的不同之处在于,add header 不会覆盖已经存在的同名字段。
|
||||
|
||||
### 响应体
|
||||
|
||||
最后看下响应体,在 OpenResty 中,你可以使用 `ngx.say` 和 `ngx.print` 来输出响应体:
|
||||
|
||||
```
|
||||
ngx.say('hello, world')
|
||||
|
||||
```
|
||||
|
||||
这两个 API 的功能是一致的,唯一的不同在于, `ngx.say` 会在最后多一个换行符。
|
||||
|
||||
为了避免字符串拼接的低效,`ngx.say / ngx.print` 不仅支持字符串作为参数,也支持数组格式:
|
||||
|
||||
```
|
||||
$ resty -e 'ngx.say({"hello", ", ", "world"})'
|
||||
hello, world
|
||||
|
||||
```
|
||||
|
||||
这样在 Lua 层面就跳过了字符串的拼接,把这个它不擅长的事情丢给了 C 函数去处理。
|
||||
|
||||
## 写在最后
|
||||
|
||||
到此,让我们回顾下今天的内容。我们按照请求报文和响应报文的内容,依次介绍了与之相关的 OpenResty API。你可以看得出来,和 NGINX 的指令相比,OpenResty API更加灵活和强大。
|
||||
|
||||
那么,在你处理 HTTP 请求时,OpenResty 提供的 Lua API 是否足够满足你的需求呢?欢迎留言一起探讨,也欢迎你把这篇文章分享给你的同事、朋友,我们一起交流,一起进步。
|
||||
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
<audio id="audio" title="18 | worker间的通信法宝:最重要的数据结构之shared dict" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b7/a8/b7a770648c696dd5b49fac1a887615a8.mp3"></audio>
|
||||
|
||||
你好,我是温铭。
|
||||
|
||||
前面我们讲过,在 Lua 中, table 是唯一的数据结构。与之对应的一个事实是,共享内存字典shared dict,是你在 OpenResty 编程中最为重要的数据结构。它不仅支持数据的存放和读取,还支持原子计数和队列操作。
|
||||
|
||||
基于 shared dict,你可以实现多个 worker 之间的缓存和通信,以及限流限速、流量统计等功能。你可以把 shared dict 当作简单的 Redis 来使用,只不过 shared dict 中的数据不能持久化,所以你存放在其中的数据,一定要考虑到丢失的情况。
|
||||
|
||||
## 数据共享的几种方式
|
||||
|
||||
在编写 OpenResty Lua 代码的过程中,你不可避免地会遇到,在一个请求的不同阶段、不同 worker 之间共享数据的情况,还可能需要在 Lua 和 C 代码之间共享数据。
|
||||
|
||||
所以,在正式介绍 shared dict 的 API 之前,先让我们了解一下,OpenResty 中常见的几种数据共享的方法;并学会根据实际情况,选择较为合适的数据共享方式。
|
||||
|
||||
**第一种是 Nginx 中的变量**。它可以在 Nginx C 模块之间共享数据,自然的,也可以在 C 模块和 OpenResty 提供的 `lua-nginx-module` 之间共享数据,比如下面这段代码:
|
||||
|
||||
```
|
||||
location /foo {
|
||||
set $my_var ''; # this line is required to create $my_var at config time
|
||||
content_by_lua_block {
|
||||
ngx.var.my_var = 123;
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
不过,使用 Nginx 变量这种方式来共享数据是比较慢的,因为它涉及到 hash 查找和内存分配。同时,这种方法有其局限性,只能用来存储字符串,不能支持复杂的 Lua 类型。
|
||||
|
||||
**第二种是`ngx.ctx`,可以在同一个请求的不同阶段之间共享数据**。它其实就是一个普通的 Lua 的 table,所以速度很快,还可以存储各种 Lua 的对象。它的生命周期是请求级别的,当一个请求结束的时候,`ngx.ctx` 也会跟着被销毁掉。
|
||||
|
||||
下面是一个典型的使用场景,我们用 `ngx.ctx` 来缓存 `Nginx 变量` 这种昂贵的调用,并在不同阶段都可以使用到它:
|
||||
|
||||
```
|
||||
location /test {
|
||||
rewrite_by_lua_block {
|
||||
ngx.ctx.host = ngx.var.host
|
||||
}
|
||||
access_by_lua_block {
|
||||
if (ngx.ctx.host == 'openresty.org') then
|
||||
ngx.ctx.host = 'test.com'
|
||||
end
|
||||
}
|
||||
content_by_lua_block {
|
||||
ngx.say(ngx.ctx.host)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这时,如果你使用 curl 访问的话:
|
||||
|
||||
```
|
||||
curl -i 127.0.0.1:8080/test -H 'host:openresty.org'
|
||||
|
||||
```
|
||||
|
||||
就会打印出 `test.com`,可以表明 `ngx.ctx` 的确是在不同阶段共享了数据。当然,你还可以自己动手修改上面的例子,保存 table 等更复杂的对象,而非简单的字符串,看看它是否满足你的预期。
|
||||
|
||||
不过,这里需要特别注意的是,正因为 `ngx.ctx` 的生命周期是请求级别的,所以它并不能在模块级别进行缓存。比如,我在 `foo.lua` 文件中这样使用就是错误的:
|
||||
|
||||
```
|
||||
local ngx_ctx = ngx.ctx
|
||||
|
||||
local function bar()
|
||||
ngx_ctx.host = 'test.com'
|
||||
end
|
||||
|
||||
```
|
||||
|
||||
我们应该在函数级别进行调用和缓存:
|
||||
|
||||
```
|
||||
local ngx = ngx
|
||||
|
||||
local function bar()
|
||||
ngx_ctx.host = 'test.com'
|
||||
end
|
||||
|
||||
```
|
||||
|
||||
`ngx.ctx` 还有很多的细节,后面的性能优化部分,我们再继续探讨。
|
||||
|
||||
接着往下看,**第三种方法是使用`模块级别的变量`,在同一个 worker 内的所有请求之间共享数据**。跟前面的 Nginx 变量和 `ngx.ctx` 不一样,这种方法有些不太好理解。不过别着急,概念抽象,代码先行,让我们先来看个例子,弄明白什么是 `模块级别的变量`:
|
||||
|
||||
```
|
||||
-- mydata.lua
|
||||
local _M = {}
|
||||
|
||||
local data = {
|
||||
dog = 3,
|
||||
cat = 4,
|
||||
pig = 5,
|
||||
}
|
||||
|
||||
function _M.get_age(name)
|
||||
return data[name]
|
||||
end
|
||||
|
||||
return _M
|
||||
|
||||
```
|
||||
|
||||
在 nginx.conf 的配置如下:
|
||||
|
||||
```
|
||||
location /lua {
|
||||
content_by_lua_block {
|
||||
local mydata = require "mydata"
|
||||
ngx.say(mydata.get_age("dog"))
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在这个示例中,`mydata` 就是一个模块,它只会被 worker 进程加载一次,之后,这个 worker 处理的所有请求,都会共享 `mydata` 模块的代码和数据。
|
||||
|
||||
自然,`mydata` 模块中的 `data` 这个变量,就是 `模块级别的变量`,它位于模块的 top level,也就是模块最开始的位置,所有函数都可以访问到它。
|
||||
|
||||
所以,你可以把需要在请求间共享的数据,放在模块的 top level 变量中。不过,需要特别注意的是,一般我们只用这种方式来保存**只读的数据**。如果涉及到写操作,你就要非常小心了,因为可能会有 **race condition**,这是**非常难以定位的 bug**。
|
||||
|
||||
我们可以通过下面这个最简化的例子来体会下:
|
||||
|
||||
```
|
||||
-- mydata.lua
|
||||
local _M = {}
|
||||
|
||||
local data = {
|
||||
dog = 3,
|
||||
cat = 4,
|
||||
pig = 5,
|
||||
}
|
||||
|
||||
function _M.incr_age(name)
|
||||
data[name] = data[name] + 1
|
||||
return data[name]
|
||||
end
|
||||
|
||||
return _M
|
||||
|
||||
```
|
||||
|
||||
在模块中,我们增加了 `incr_age` 这个函数,它会对 data 这个表的数据进行修改。
|
||||
|
||||
然后,在调用的代码中,我们增加了最关键的一行 `ngx.sleep(5)`,这个 sleep 是一个 yield 操作:
|
||||
|
||||
```
|
||||
location /lua {
|
||||
content_by_lua_block {
|
||||
local mydata = require "mydata"
|
||||
ngx.say(mydata. incr_age("dog"))
|
||||
ngx.sleep(5) -- yield API
|
||||
ngx.say(mydata. incr_age("dog"))
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
如果没有这行 sleep 代码(也可以是其他的非阻塞 IO 操作,比如访问 Redis 等),就不会有 yield 操作,也就不会产生竞争,那么,最后输出的数字就是顺序的。
|
||||
|
||||
但当我们加了这行代码后,哪怕只是在 sleep 的 5 秒钟内,也很可能就有其他请求调用了`mydata. incr_age` 函数,修改了变量的值,从而导致最后输出的数字不连续。要知道,在实际的代码中,逻辑不会这么简单,bug 的定位也一定会困难得多。
|
||||
|
||||
所以,除非你很确定这中间没有 yield 操作,不会把控制权交给 Nginx 事件循环,否则,我建议你还是保持对模块级别变量的只读。
|
||||
|
||||
**第四种,也是最后一种方法,用 shared dict 来共享数据,这些数据可以在多个 worker 之间共享。**
|
||||
|
||||
这种方法是基于红黑树实现的,性能很好,但也有自己的局限性——你必须事先在 Nginx 的配置文件中,声明共享内存的大小,并且这不能在运行期更改:
|
||||
|
||||
```
|
||||
lua_shared_dict dogs 10m;
|
||||
|
||||
```
|
||||
|
||||
shared dict 同样只能缓存字符串类型的数据,不支持复杂的 Lua 数据类型。这也就意味着,当我需要存放 table 等复杂的数据类型时,我将不得不使用 json 或者其他的方法,来序列化和反序列化,这自然会带来不小的性能损耗。
|
||||
|
||||
总之,还是那句话,这里并没有银弹,不存在一种完美的数据共享方式,你需要根据需求和场景,来组合多个方法来使用。
|
||||
|
||||
## 共享字典
|
||||
|
||||
上面数据共享的部分,我们花了很多的篇幅来学,有的人可能纳闷儿:它们看上去和 shared dict 没有直接关系,是不是有些文不对题呢?
|
||||
|
||||
事实并非如此,你可以自己想一下,为什么 OpenResty 中要有 shared dict 的存在呢?
|
||||
|
||||
回忆一下刚刚讲的几种方法,前面三种数据共享的范围都是在请求级别,或者单个 worker 级别。所以,在当前的 OpenResty 的实现中,只有 shared dict 可以完成 worker 间的数据共享,并借此实现 worker 之间的通信,这也是它存在的价值。
|
||||
|
||||
在我看来,明白一个技术为何存在,并弄清楚它和别的类似技术之间的差异和优势,远比你只会熟练调用它提供的 API 更为重要。这种技术视野,会给你带来一定程度的远见和洞察力,这也可以说是工程师和架构师的一个重要区别。
|
||||
|
||||
回到共享字典本身,它对外提供了 20多个 Lua API,不过所有的这些 API 都是原子操作,你不用担心多个 worker 和高并发的情况下的竞争问题。
|
||||
|
||||
这些 API 都有官方详细的[文档](https://github.com/openresty/lua-nginx-module#ngxshareddict),我就不再一一赘述了。这里我想再强调一下,任何技术课程的学习,都不能代替对官方文档的仔细研读。这些耗时的笨功夫,每个人都省不掉的。
|
||||
|
||||
继续看shared dict 的 API,这些 API可以分为下面三个大类,也就是字典读写类、队列操作类和管理类这三种。
|
||||
|
||||
### 字典读写类
|
||||
|
||||
首先来看字典读写类。在最初的版本中,只有字典读写类的 API,它们也是共享字典最常用的功能。下面是一个最简单的示例:
|
||||
|
||||
```
|
||||
$ resty --shdict='dogs 1m' -e 'local dict = ngx.shared.dogs
|
||||
dict:set("Tom", 56)
|
||||
print(dict:get("Tom"))'
|
||||
|
||||
```
|
||||
|
||||
除了 set 外,OpenResty 还提供了 `safe_set`、`add`、`safe_add`、`replace` 这四种写入的方法。这里`safe` 前缀的含义是,在内存占满的情况下,不根据 LRU 淘汰旧的数据,而是写入失败并返回 `no memory` 的错误信息。
|
||||
|
||||
除了 get 外,OpenResty 还提供了 `get_stale` 的读取数据的方法,相比 `get` 方法,它多了一个过期数据的返回值:
|
||||
|
||||
```
|
||||
value, flags, stale = ngx.shared.DICT:get_stale(key)
|
||||
|
||||
```
|
||||
|
||||
你还可以调用 `delete` 方法来删除指定的 key,它和 `set(key, nil)` 是等价的。
|
||||
|
||||
### 队列操作类
|
||||
|
||||
再来看队列操作,它是 OpenResty 后续新增的功能,提供了和 Redis 类似的接口。队列中的每一个元素,都用 `ngx_http_lua_shdict_list_node_t` 来描述:
|
||||
|
||||
```
|
||||
typedef struct {
|
||||
ngx_queue_t queue;
|
||||
uint32_t value_len;
|
||||
uint8_t value_type;
|
||||
u_char data[1];
|
||||
} ngx_http_lua_shdict_list_node_t;
|
||||
|
||||
```
|
||||
|
||||
我把这些队列操作 API 的 [PR](https://github.com/openresty/lua-nginx-module/pull/586/files) 贴在了文章中,如果你对此感兴趣,可以跟着文档、测试案例和源码,来分析具体的实现。
|
||||
|
||||
不过,下面这 5 个队列 API,在文档中并没有对应的代码示例,这里我简单介绍一下:
|
||||
|
||||
- lpush/rpush,表示在队列两端增加元素;
|
||||
- lpop/rpop,表示在队列两端弹出元素;
|
||||
- llen,表示返回队列的元素数量。
|
||||
|
||||
别忘了我们上节课讲过的另一个利器——测试案例。如果文档中没有,我们通常可以在测试案例中找到对应的代码。队列相关的测试,正是在 `145-shdict-list.t` 这个文件中:
|
||||
|
||||
```
|
||||
=== TEST 1: lpush & lpop
|
||||
--- http_config
|
||||
lua_shared_dict dogs 1m;
|
||||
--- config
|
||||
location = /test {
|
||||
content_by_lua_block {
|
||||
local dogs = ngx.shared.dogs
|
||||
|
||||
local len, err = dogs:lpush("foo", "bar")
|
||||
if len then
|
||||
ngx.say("push success")
|
||||
else
|
||||
ngx.say("push err: ", err)
|
||||
end
|
||||
|
||||
local val, err = dogs:llen("foo")
|
||||
ngx.say(val, " ", err)
|
||||
|
||||
local val, err = dogs:lpop("foo")
|
||||
ngx.say(val, " ", err)
|
||||
|
||||
local val, err = dogs:llen("foo")
|
||||
ngx.say(val, " ", err)
|
||||
|
||||
local val, err = dogs:lpop("foo")
|
||||
ngx.say(val, " ", err)
|
||||
}
|
||||
}
|
||||
--- request
|
||||
GET /test
|
||||
--- response_body
|
||||
push success
|
||||
1 nil
|
||||
bar nil
|
||||
0 nil
|
||||
nil nil
|
||||
--- no_error_log
|
||||
[error]
|
||||
|
||||
```
|
||||
|
||||
### 管理类
|
||||
|
||||
最后要说的管理类 API 也是后续新增的,属于社区呼声比较高的需求。其中,共享内存的使用情况就是最典型的例子。比如,用户申请了 100M 的空间作为 shared dict,那么这 100M 是否够用呢?里面存放了多少 key?具体是哪些 key 呢?这几个都是非常现实的问题。
|
||||
|
||||
对于这类问题,OpenResty 的官方态度,是希望用户使用火焰图来解决,即非侵入式,保持代码基的高效和整洁,而不是提供侵入式的 API 来直接返回结果。
|
||||
|
||||
但站在使用者友好角度来考虑,这些管理类 API 还是非常有必要的。毕竟开源项目是用来解决产品需求的,并不是展示技术本身的。所以,下面我们就来了解一下,这几个后续增加的管理类 API。
|
||||
|
||||
首先是 `get_keys(max_count?)`,它默认也只返回前 1024 个 key;如果你把 `max_count` 设置为 0,那就返回所有 key。
|
||||
|
||||
然后是 `capacity` 和 `free_space`,这两个 API 都属于 lua-resty-core 仓库,所以需要你 require 后才能使用:
|
||||
|
||||
```
|
||||
require "resty.core.shdict"
|
||||
|
||||
local cats = ngx.shared.cats
|
||||
local capacity_bytes = cats:capacity()
|
||||
local free_page_bytes = cats:free_space()
|
||||
|
||||
```
|
||||
|
||||
它们分别返回的,是共享内存的大小(也就是 `lua_shared_dict` 中配置的大小)和空闲页的字节数。因为 shared dict 是按照页来分配的,即使 `free_space` 返回为 0,在已经分配的页面中也可能存在空间,所以它的返回值并不能代表共享内存实际被占用的情况。
|
||||
|
||||
## 写在最后
|
||||
|
||||
在实际的开发中,我们经常会用到多级缓存,OpenResty 的官方项目中也有对缓存的封装。你能找出来是哪几个项目吗?或者你知道一些其他缓存封装的 lua-resty 库吗?
|
||||
|
||||
欢迎留言和我分享,也欢迎你把这篇文章分享给你的同事、朋友,我们一起交流,一起进步。
|
||||
|
||||
|
||||
187
极客时间专栏/OpenResty从入门到实战/API篇/19 | OpenResty 的核心和精髓:cosocket.md
Normal file
187
极客时间专栏/OpenResty从入门到实战/API篇/19 | OpenResty 的核心和精髓:cosocket.md
Normal file
@@ -0,0 +1,187 @@
|
||||
<audio id="audio" title="19 | OpenResty 的核心和精髓:cosocket" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b3/39/b33be3f616df6012d81f7605d30a5e39.mp3"></audio>
|
||||
|
||||
你好,我是温铭,今天我们来学习下 OpenResty 中的核心技术:cosocket。
|
||||
|
||||
其实在前面的课程中,我们就已经多次提到过它了,cosocket 是各种 `lua-resty-*` 非阻塞库的基础,没有 cosocket,开发者就无法用 Lua 来快速连接各种外部的网络服务。
|
||||
|
||||
在早期的 OpenResty 版本中,如果你想要去与 Redis、memcached 这些服务交互的话,需要使用 `redis2-nginx-module`、`redis-nginx-module` 和 `memc-nginx-module`这些 C 模块.这些模块至今仍然在 OpenResty 的发行包中。
|
||||
|
||||
不过,cosocket 功能加入以后,它们都已经被 `lua-resty-redis` 和 `lua-resty-memcached` 替代,基本上没人再去使用 C 模块连接外部服务了。
|
||||
|
||||
## 什么是 cosocket?
|
||||
|
||||
那究竟什么是cosocket 呢?事实上,cosocket是 OpenResty 中的专有名词,是把协程和网络套接字的英文拼在一起形成的,即 cosocket = coroutine + socket。所以,你可以把 cosocket 翻译为“协程套接字”。
|
||||
|
||||
cosocket 不仅需要 Lua 协程特性的支持,也需要 Nginx 中非常重要的事件机制的支持,这两者结合在一起,最终实现了非阻塞网络 I/O。另外,cosocket 支持 TCP、UDP 和 Unix Domain Socket。
|
||||
|
||||
如果我们在 OpenResty 中调用一个 cosocket 相关函数,内部实现便是下面这张图的样子:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/80/06/80d16e11d2750d6e4127445c126c9f06.png" alt="">
|
||||
|
||||
记性比较好的同学应该发现了,在前面 OpenResty 原理和基本概念的那节课里,我也用过这张图。从图中你可以看到,用户的 Lua 脚本每触发一个网络操作,都会有协程的 yield 以及 resume。
|
||||
|
||||
遇到网络 I/O 时,它会交出控制权(yield),把网络事件注册到 Nginx 监听列表中,并把权限交给 Nginx;当有 Nginx 事件达到触发条件时,便唤醒对应的协程继续处理(resume)。
|
||||
|
||||
OpenResty 正是以此为蓝图,封装实现 connect、send、receive 等操作,形成了我们如今见到的 cosocket API。下面,我就以处理 TCP 的 API 为例来介绍一下。处理 UDP 和 Unix Domain Socket ,与TCP 的接口基本是一样的。
|
||||
|
||||
## cosocket API 和指令简介
|
||||
|
||||
TCP 相关的 cosocket API 可以分为下面这几类。
|
||||
|
||||
- 创建对象:ngx.socket.tcp。
|
||||
- 设置超时:tcpsock:settimeout 和 tcpsock:settimeouts。
|
||||
- 建立连接:tcpsock:connect。
|
||||
- 发送数据:tcpsock:send。
|
||||
- 接受数据:tcpsock:receive、tcpsock:receiveany 和 tcpsock:receiveuntil。
|
||||
- 连接池:tcpsock:setkeepalive。
|
||||
- 关闭连接:tcpsock:close。
|
||||
|
||||
我们还要特别注意下,这些 API 可以使用的上下文:
|
||||
|
||||
```
|
||||
rewrite_by_lua*, access_by_lua*, content_by_lua*, ngx.timer.*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*_
|
||||
|
||||
```
|
||||
|
||||
这里我还要强调一点,归咎于 Nginx 内核的各种限制,cosocket API 在 `set_by_lua*`, `log_by_lua*`, `header_filter_by_lua*` 和 `body_filter_by_lua*` 中是无法使用的。而在 `init_by_lua*` 和 `init_worker_by_lua*` 中暂时也不能用,不过 Nginx 内核对这两个阶段并没有限制,后面可以增加对这它们的支持。
|
||||
|
||||
此外,与这些 API 相关的,还有 8 个 `lua_socket_` 开头的 Nginx 指令,我们简单来看一下。
|
||||
|
||||
- `lua_socket_connect_timeout`:连接超时,默认 60 秒。
|
||||
- `lua_socket_send_timeout`:发送超时,默认 60 秒。
|
||||
- `lua_socket_send_lowat`:发送阈值(low water),默认为 0。
|
||||
- `lua_socket_read_timeout`: 读取超时,默认 60 秒。
|
||||
- `lua_socket_buffer_size`:读取数据的缓存区大小,默认 4k/8k。
|
||||
- `lua_socket_pool_size`:连接池大小,默认 30。
|
||||
- `lua_socket_keepalive_timeout`:连接池 cosocket 对象的空闲时间,默认 60 秒。
|
||||
- `lua_socket_log_errors`:cosocket 发生错误时,是否记录日志,默认为 on。
|
||||
|
||||
这里你也可以看到,有些指令和 API 的功能一样的,比如设置超时时间和连接池大小等。不过,如果两者有冲突的话,API 的优先级高于指令,会覆盖指令设置的值。所以,一般来说,我们都推荐使用 API 来做设置,这样也会更加灵活。
|
||||
|
||||
接下来,我们一起来看一个具体的例子,弄明白到底如何使用这些 cosocket API。下面这段代码的功能很简单,是发送 TCP 请求到一个网站,并把返回的内容打印出来:
|
||||
|
||||
```
|
||||
$ resty -e 'local sock = ngx.socket.tcp()
|
||||
sock:settimeout(1000) -- one second timeout
|
||||
local ok, err = sock:connect("www.baidu.com", 80)
|
||||
if not ok then
|
||||
ngx.say("failed to connect: ", err)
|
||||
return
|
||||
end
|
||||
|
||||
local req_data = "GET / HTTP/1.1\r\nHost: www.baidu.com\r\n\r\n"
|
||||
local bytes, err = sock:send(req_data)
|
||||
if err then
|
||||
ngx.say("failed to send: ", err)
|
||||
return
|
||||
end
|
||||
|
||||
local data, err, partial = sock:receive()
|
||||
if err then
|
||||
ngx.say("failed to receive: ", err)
|
||||
return
|
||||
end
|
||||
|
||||
sock:close()
|
||||
ngx.say("response is: ", data)'
|
||||
|
||||
```
|
||||
|
||||
我们来具体分析下这段代码。
|
||||
|
||||
- 首先,通过 `ngx.socket.tcp()` ,创建 TCP 的 cosocket 对象,名字是 sock。
|
||||
- 然后,使用 `settimeout()` ,把超时时间设置为 1 秒。注意这里的超时没有区分 connect、receive,是统一的设置。
|
||||
- 接着,使用 `connect()` 去连接指定网站的 80 端口,如果失败就直接退出。
|
||||
- 连接成功的话,就使用 `send()` 来发送构造好的数据,如果发送失败就退出。
|
||||
- 发送数据成功的话,就使用 `receive()` 来接收网站返回的数据。这里 `receive()` 的默认参数值是 `*l`,也就是只返回第一行的数据;如果参数设置为了`*a`,就是持续接收数据,直到连接关闭;
|
||||
- 最后,调用 `close()` ,主动关闭 socket 连接。
|
||||
|
||||
你看,短短几步就可以完成,使用 cosocket API 来做网络通信,就是这么简单。不过,不能满足于此,接下来,我们对这个示例再做一些调整。
|
||||
|
||||
**第一个动作,对 socket 连接、发送和读取这三个动作,分别设置超时时间。**
|
||||
|
||||
我们刚刚用的`settimeout()` ,作用是把超时时间统一设置为一个值。如果要想分开设置,就需要使用 `settimeouts()` 函数,比如下面这样的写法:
|
||||
|
||||
```
|
||||
sock:settimeouts(1000, 2000, 3000)
|
||||
|
||||
```
|
||||
|
||||
这行代码表示连接超时为 1 秒,发送超时为 2 秒,读取超时为 3 秒。
|
||||
|
||||
在OpenResty 和 lua-resty 库中,大部分和时间相关的 API 的参数,都以毫秒为单位,但也有例外,需要你在调用的时候特别注意下。
|
||||
|
||||
**第二个动作,receive接收指定大小的内容。**
|
||||
|
||||
刚刚说了,`receive()` 接口可以接收一行数据,也可以持续接收数据。不过,如果你只想接收 10K 大小的数据,应该怎么设置呢?
|
||||
|
||||
这时,`receiveany()` 闪亮登场。它就是专为满足这种需求而设计的,一起来看下面这行代码:
|
||||
|
||||
```
|
||||
local data, err, partial = sock:receiveany(10240)
|
||||
|
||||
```
|
||||
|
||||
这段代码就表示,最多只接收 10K 的数据。
|
||||
|
||||
当然,关于receive,还有另一个很常见的用户需求,那就是一直获取数据,直到遇到指定字符串才停止。
|
||||
|
||||
`receiveuntil()` 专门用来解决这类问题,它不会像 `receive()` 和 `receiveany()` 一样返回字符串,而会返回一个迭代器。这样,你就可以在循环中调用它来分段读取匹配到的数据,当读取完毕时,就会返回 nil。下面就是一个例子:
|
||||
|
||||
```
|
||||
local reader = sock:receiveuntil("\r\n")
|
||||
|
||||
while true do
|
||||
local data, err, partial = reader(4)
|
||||
if not data then
|
||||
if err then
|
||||
ngx.say("failed to read the data stream: ", err)
|
||||
break
|
||||
end
|
||||
|
||||
ngx.say("read done")
|
||||
break
|
||||
end
|
||||
ngx.say("read chunk: [", data, "]")
|
||||
end
|
||||
|
||||
```
|
||||
|
||||
这段代码中的 `receiveuntil` 会返回 `\r\n` 之前的数据,并通过迭代器每次读取其中的 4 个字节,也就实现了我们想要的功能。
|
||||
|
||||
**第三个动作,不直接关闭 socket,而是放入连接池中。**
|
||||
|
||||
我们知道,没有连接池的话,每次请求进来都要新建一个连接,就会导致 cosocket 对象被频繁地创建和销毁,造成不必要的性能损耗。
|
||||
|
||||
为了避免这个问题,在你使用完一个 cosocket 后,可以调用 `setkeepalive()` 放到连接池中,比如下面这样的写法:
|
||||
|
||||
```
|
||||
local ok, err = sock:setkeepalive(2 * 1000, 100)
|
||||
if not ok then
|
||||
ngx.say("failed to set reusable: ", err)
|
||||
end
|
||||
|
||||
```
|
||||
|
||||
这段代码设置了连接的空闲时间为 2 秒,连接池的大小为 100。这样,在调用 `connect()` 函数时,就会优先从连接池中获取 cosocket 对象。
|
||||
|
||||
不过,关于连接池的使用,有两点需要我们注意一下。
|
||||
|
||||
- 第一,不能把发生错误的连接放入连接池,否则下次使用时,就会导致收发数据失败。这也是为什么我们需要判断每一个 API 调用是否成功的一个原因。
|
||||
- 第二,要搞清楚连接的数量。连接池是 worker 级别的,每个 worker 都有自己的连接池。所以,如果你有 10 个 worker,连接池大小设置为 30,那么对于后端的服务来讲,就等于有 300 个连接。
|
||||
|
||||
## 写在最后
|
||||
|
||||
总结一下,今天我们学习了cosocket 的基本概念,以及相关的指令和 API,并通过一个实际的例子,熟悉了TCP 相关的 API 应该如何使用。而UDP 和 Unix Domain Socket的使用类似于TCP,弄明白今天所学,你基本上都能迎刃而解了。
|
||||
|
||||
从中你应该也能感受到,cosocket 用起来还是比较容易上手的,而且用好它,你就可以去连接各种外部的服务了,可以说是给 OpenResty 插上了想象的翅膀。
|
||||
|
||||
最后,给你留两个作业题。
|
||||
|
||||
第一问,在今天的例子中,`tcpsock:send` 发送的是字符串,如果我们需要发送一个由字符串构成的 table,又该怎么处理呢?
|
||||
|
||||
第二问,你也看到了,cosocket 在很多阶段中不能使用,那么,你能否想到一些绕过的方式呢?
|
||||
|
||||
欢迎留言和我分享,也欢迎你把这篇文章分享给你的同事、朋友,我们一起交流,一起进步。
|
||||
|
||||
|
||||
200
极客时间专栏/OpenResty从入门到实战/API篇/20 | 超越 Web 服务器:特权进程和定时任务.md
Normal file
200
极客时间专栏/OpenResty从入门到实战/API篇/20 | 超越 Web 服务器:特权进程和定时任务.md
Normal file
@@ -0,0 +1,200 @@
|
||||
<audio id="audio" title="20 | 超越 Web 服务器:特权进程和定时任务" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/8e/38/8e18455afada7424d471e975ed42ff38.mp3"></audio>
|
||||
|
||||
你好,我是温铭。
|
||||
|
||||
前面我们介绍了 OpenResty API、共享字典缓存和 cosocket。它们实现的功能,都还在 Nginx 和 Web 服务器的范畴之内,算是提供了开发成本更低、更容易维护的一种实现,提供了可编程的 Web 服务器。
|
||||
|
||||
不过,OpenResty并不满足于此。我们今天就挑选几个,OpenResty 中超越 Web 服务器的功能来介绍一下。它们分别是定时任务、特权进程和非阻塞的 ngx.pipe。
|
||||
|
||||
## 定时任务
|
||||
|
||||
在 OpenResty 中,我们有时候需要在后台定期地执行某些任务,比如同步数据、清理日志等。如果让你来设计,你会怎么做呢?最容易想到的方法,便是对外提供一个 API 接口,在接口中完成这些任务;然后用系统的 crontab 定时调用 curl,来访问这个接口,进而曲线地实现这个需求。
|
||||
|
||||
不过,这样一来不仅会有割裂感,也会给运维带来更高的复杂度。所以, OpenResty 提供了 `ngx.timer` 来解决这类需求。你可以把`ngx.timer` ,看作是 OpenResty 模拟的客户端请求,用以触发对应的回调函数。
|
||||
|
||||
其实,OpenResty 的定时任务可以分为下面两种:
|
||||
|
||||
- `ngx.timer.at`,用来执行一次性的定时任务;
|
||||
- `ngx.time.every`,用来执行固定周期的定时任务。
|
||||
|
||||
还记得上节课最后我留下的思考题吗?问题是如何突破 `init_worker_by_lua` 中不能使用 cosocket 的限制,这个答案其实就是 `ngx.timer`。
|
||||
|
||||
下面这段代码,就是启动了一个延时为 0 的定时任务。它启动了回调函数 `handler`,并在这个函数中,用 cosocket 去访问一个网站:
|
||||
|
||||
```
|
||||
init_worker_by_lua_block {
|
||||
local function handler()
|
||||
local sock = ngx.socket.tcp()
|
||||
local ok, err = sock:connect(“www.baidu.com", 80)
|
||||
end
|
||||
|
||||
local ok, err = ngx.timer.at(0, handler)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这样,我们就绕过了 cosocket 在这个阶段不能使用的限制。
|
||||
|
||||
再回到这部分开头时我们提到的的用户需求,`ngx.timer.at` 并没有解决周期性运行这个需求,在上面的代码示例中,它是一个一次性的任务。
|
||||
|
||||
那么,又该如何做到周期性运行呢?表面上来看,基于 `ngx.timer.at` 这个API 的话,你有两个选择:
|
||||
|
||||
- 你可以在回调函数中,使用一个 while true 的死循环,执行完任务后 sleep 一段时间,自己来实现周期任务;
|
||||
- 你还可以在回调函数的最后,再创建另外一个新的 timer。
|
||||
|
||||
不过,在做出选择之前,有一点我们需要先明确下:timer 的本质是一个请求,虽然这个请求不是终端发起的;而对于请求来讲,在完成自己的任务后它就要退出,不能一直常驻,否则很容易造成各种资源的泄漏。
|
||||
|
||||
所以,第一种使用 while true 来自行实现周期任务的方案并不靠谱。第二种方案虽然是可行的,但递归地创建 timer ,并不容易让人理解。
|
||||
|
||||
那么,是否有更好的方案呢?其实,OpenResty 后面新增的 `ngx.time.every` API,就是专门为了解决这个问题而出现的,它是更加接近 crontab 的解决方案。
|
||||
|
||||
但美中不足的是,在启动了一个 timer 之后,你就再也没有机会来取消这个定时任务了,毕竟`ngx.timer.cancel` 还是一个 todo 的功能。
|
||||
|
||||
这时候,你就会面临一个问题:定时任务是在后台运行的,并且无法取消;如果定时任务的数量很多,就很容易耗尽系统资源。
|
||||
|
||||
所以,OpenResty 提供了 `lua_max_pending_timers` 和 `lua_max_running_timers` 这两个指令,来对其进行限制。前者代表等待执行的定时任务的最大值,后者代表当前正在运行的定时任务的最大值。
|
||||
|
||||
你也可以通过 Lua API,来获取当前等待执行和正在执行的定时任务的值,下面是两个示例:
|
||||
|
||||
```
|
||||
content_by_lua_block {
|
||||
ngx.timer.at(3, function() end)
|
||||
ngx.say(ngx.timer.pending_count())
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这段代码会打印出 1,表示有 1 个计划任务正在等待被执行。
|
||||
|
||||
```
|
||||
content_by_lua_block {
|
||||
ngx.timer.at(0.1, function() ngx.sleep(0.3) end)
|
||||
ngx.sleep(0.2)
|
||||
ngx.say(ngx.timer.running_count())
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这段代码会打印出 1,表示有 1 个计划任务正在运行中。
|
||||
|
||||
## 特权进程
|
||||
|
||||
接着来看特权进程。我们都知道 Nginx 主要分为 master 进程和 worker 进程,其中,真正处理用户请求的是 worker 进程。我们可以通过 `lua-resty-core` 中提供的 `process.type` API ,获取到进程的类型。比如,你可以用 `resty` 运行下面这个函数:
|
||||
|
||||
```
|
||||
$ resty -e 'local process = require "ngx.process"
|
||||
ngx.say("process type:", process.type())'
|
||||
|
||||
```
|
||||
|
||||
你会看到,它返回的结果不是 `worker`, 而是 `single`。这意味 `resty` 启动的 Nginx 只有 worker 进程,没有 master 进程。其实,事实也是如此。在 `resty` 的实现中,你可以看到,下面这样的一行配置, 关闭了 master 进程:
|
||||
|
||||
```
|
||||
master_process off;
|
||||
|
||||
```
|
||||
|
||||
而OpenResty 在 Nginx 的基础上进行了扩展,增加了特权进程:privileged agent。特权进程很特别:
|
||||
|
||||
- 它不监听任何端口,这就意味着不会对外提供任何服务;
|
||||
- 它拥有和 master 进程一样的权限,一般来说是 `root` 用户的权限,这就让它可以做很多 worker 进程不可能完成的任务;
|
||||
- 特权进程只能在 `init_by_lua` 上下文中开启;
|
||||
- 另外,特权进程只有运行在 `init_worker_by_lua` 上下文中才有意义,因为没有请求触发,也就不会走到`content`、`access` 等上下文去。
|
||||
|
||||
下面,我们来看一个开启特权进程的示例:
|
||||
|
||||
```
|
||||
init_by_lua_block {
|
||||
local process = require "ngx.process"
|
||||
|
||||
local ok, err = process.enable_privileged_agent()
|
||||
if not ok then
|
||||
ngx.log(ngx.ERR, "enables privileged agent failed error:", err)
|
||||
end
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
通过这段代码开启特权进程后,再去启动 OpenResty 服务,我们就可以看到,Nginx 的进程中多了特权进程的身影:
|
||||
|
||||
```
|
||||
nginx: master process
|
||||
nginx: worker process
|
||||
nginx: privileged agent process
|
||||
|
||||
```
|
||||
|
||||
不过,如果特权只在 `init_worker_by_lua` 阶段运行一次,显然不是一个好主意,那我们应该怎么来触发特权进程呢?
|
||||
|
||||
没错,答案就藏在刚刚讲过的知识里。既然它不监听端口,也就是不能被终端请求触发,那就只有使用我们刚才介绍的 `ngx.timer` ,来周期性地触发了:
|
||||
|
||||
```
|
||||
init_worker_by_lua_block {
|
||||
local process = require "ngx.process"
|
||||
|
||||
local function reload(premature)
|
||||
local f, err = io.open(ngx.config.prefix() .. "/logs/nginx.pid", "r")
|
||||
if not f then
|
||||
return
|
||||
end
|
||||
local pid = f:read()
|
||||
f:close()
|
||||
os.execute("kill -HUP " .. pid)
|
||||
end
|
||||
|
||||
if process.type() == "privileged agent" then
|
||||
local ok, err = ngx.timer.every(5, reload)
|
||||
if not ok then
|
||||
ngx.log(ngx.ERR, err)
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
上面这段代码,实现了每 5 秒给 master 进程发送 HUP 信号量的功能。自然,你也可以在此基础上实现更多有趣的功能,比如轮询数据库,看是否有特权进程的任务并执行。因为特权进程是 root 权限,这显然就有点儿“后门”程序的意味了。
|
||||
|
||||
## 非阻塞的 ngx.pipe
|
||||
|
||||
最后我们来看非阻塞的 ngx.pipe。刚刚讲过的这个代码示例中,我们使用了 Lua 的标准库,来执行外部命令行,把信号发送给了 master 进程:
|
||||
|
||||
```
|
||||
os.execute("kill -HUP " .. pid)
|
||||
|
||||
```
|
||||
|
||||
这种操作自然是会阻塞的。那么,在 OpenResty 中,是否有非阻塞的方法来调用外部程序呢?毕竟,要知道,如果你是把 OpenResty 当做一个完整的开发平台,而非 Web 服务器来使用的话,这就是你的刚需了。
|
||||
|
||||
为此,`lua-resty-shell` 库应运而生,使用它来调用命令行就是非阻塞的:
|
||||
|
||||
```
|
||||
$ resty -e 'local shell = require "resty.shell"
|
||||
local ok, stdout, stderr, reason, status =
|
||||
shell.run([[echo "hello, world"]])
|
||||
ngx.say(stdout)
|
||||
|
||||
```
|
||||
|
||||
这段代码可以算是 hello world 的另外一种写法了,它调用系统的 `echo` 命令来完成输出。类似的,你可以用 `resty.shell` ,来替代 Lua 中的 `os.execute` 调用。
|
||||
|
||||
我们知道,`lua-resty-shell` 的底层实现,依赖了 `lua-resty-core` 中的 [[ngx.pipe](https://github.com/openresty/lua-resty-core/blob/master/lib/ngx/pipe.md)] API,所以,这个使用 `lua-resty-shell` 打印出 `hello wrold` 的示例,改用 `ngx.pipe` ,可以写成下面这样:
|
||||
|
||||
```
|
||||
$ resty -e 'local ngx_pipe = require "ngx.pipe"
|
||||
local proc = ngx_pipe.spawn({"echo", "hello world"})
|
||||
local data, err = proc:stdout_read_line()
|
||||
ngx.say(data)'
|
||||
|
||||
```
|
||||
|
||||
这其实也就是 `lua-resty-shell` 底层的实现代码了。你可以去查看 `ngx.pipe` 的文档和测试案例,来获取更多的使用方法,这里我就不再赘述了。
|
||||
|
||||
## 写在最后
|
||||
|
||||
到此,今天的主要内容我就讲完了。从上面的几个功能,我们可以看出,OpenResty 在做一个更好用的 Nginx 的前提下,也在尝试往通用平台的方向上靠拢,希望开发者能够尽量统一技术栈,都用 OpenResty 来解决开发需求。这对于运维来说是相当友好的,因为只要部署一个 OpenResty 就可以了,维护成本更低。
|
||||
|
||||
最后,给你留一个思考题。由于可能会存在多个 Nginx worker,那么 timer 就会在每个 worker 中都运行一次,这在大多数场景下都是不能接受的。我们应该如何保证 timer 只能运行一次呢?
|
||||
|
||||
欢迎留言说说你的解决方法,也欢迎你把这篇文章分享给你的同事、朋友,我们一起交流,一起进步。
|
||||
|
||||
|
||||
197
极客时间专栏/OpenResty从入门到实战/API篇/21 | 带你玩转时间、正则表达式等常用API.md
Normal file
197
极客时间专栏/OpenResty从入门到实战/API篇/21 | 带你玩转时间、正则表达式等常用API.md
Normal file
@@ -0,0 +1,197 @@
|
||||
<audio id="audio" title="21 | 带你玩转时间、正则表达式等常用API" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3f/7b/3fbd10a52dad9e660748b05f114fde7b.mp3"></audio>
|
||||
|
||||
你好,我是温铭。在前面几节课中,你已经熟悉了不少OpenResty 中重要的 Lua API 了,今天我们再来了解下其他一些通用的 API,主要和正则表达式、时间、进程等相关。
|
||||
|
||||
## 正则
|
||||
|
||||
先来看下最常用,也是最重要的正则。在 OpenResty 中,我们应该一直使用 `ngx.re.*` 提供的一系列 API,来处理和正则表达式相关的逻辑,而不是用 Lua 自带的模式匹配。这不仅是出于性能方面的考虑,还因为Lua 自带的正则是自成体系的,并非 PCRE 规范,这对于绝大部分开发者来说都是徒增烦恼。
|
||||
|
||||
在前面的课程中,你已经多多少少接触过一些 `ngx.re.*` 的 API了,文档也写得非常详细,我就不再一一列举了。这里,我再单独强调两个内容。
|
||||
|
||||
### ngx.re.split
|
||||
|
||||
第一个是`ngx.re.split`。字符串切割是很常见的功能,OpenResty 也提供了对应的 API,但在社区的 QQ 交流群中,很多开发者都找不到这样的函数,只能选择自己手写。
|
||||
|
||||
为什么呢?其实, `ngx.re.split` 这个 API 并不在 lua-nginx-module 中,而是在 lua-resty-core 里面;并且它也不在 lua-resty-core 首页的文档中,而是在 `lua-resty-core/lib/ngx/re.md` 这个第三级目录的文档中出现的。多种原因,导致很多开发者完全不知道这个 API 的存在。
|
||||
|
||||
类似这种“藏在深闺无人识“的 API,还有我们前面提到过的 `ngx_resp.add_header`、`enable_privileged_agent` 等等。那么怎么来最快地解决这种问题呢?除了阅读 lua-resty-core 首页文档外,你还需要把 `lua-resty-core/lib/ngx/` 这个目录下的 `.md` 格式的文档也通读一遍才行。
|
||||
|
||||
我们前面夸了很多 OpenResty 文档做得好的地方,不过,这一点上,也就是在一个页面能够查询到完整的 API 列表,确实还有很大的改进空间。
|
||||
|
||||
### lua_regex_match_limit
|
||||
|
||||
第二个,我想介绍一下`lua_regex_match_limit`。我们之前并没有花专门的篇幅,来讲 OpenResty 提供的 Nginx 指令,因为大部分情况下我们使用默认值就足够了,它们也没有在运行时去修改的必要性。不过,我们今天要讲的这个,和正则表达式相关的`lua_regex_match_limit` 指令,却是一个例外。
|
||||
|
||||
我们知道,如果我使用的正则引擎是基于回溯的 NFA 来实现的,那么就有可能出现灾难性回溯(Catastrophic Backtracking),即正则在匹配的时候回溯过多,造成 CPU 100%,正常服务被阻塞。
|
||||
|
||||
一旦发生灾难性回溯,我们就需要用 gdb 分析 dump,或者 systemtap 分析线上环境才能定位,而且事先也不容易发现,因为只有特别的请求才会触发。这显然就给攻击者带来了可趁之机,ReDoS(RegEx Denial of Service)就是指的这类攻击。
|
||||
|
||||
如果你对如何自动化发现和彻底解决这个问题感兴趣,可以参考我之前在公众号写的一篇文章:[如何彻底避免正则表达式的灾难性回溯](https://mp.weixin.qq.com/s/K9d60kjDdFn6ZwIdsLjqOw)?
|
||||
|
||||
今天在这里,我主要给你介绍下,如何在 OpenResty 中简单有效地规避,也就是使用下面这行代码:
|
||||
|
||||
```
|
||||
lua_regex_match_limit 100000;
|
||||
|
||||
```
|
||||
|
||||
`lua_regex_match_limit` ,就是用来限制 PCRE 正则引擎的回溯次数的。这样,即使出现了灾难性回溯,后果也会被限制在一个范围内,不会导致你的 CPU 满载。
|
||||
|
||||
这里我简单说一下,这个指令的默认值是 0,也就是不做限制。如果你没有替换 OpenResty 自带的正则引擎,并且还涉及到了比较多的复杂的正则表达式,你可以考虑重新设置这个 Nginx 指令的值。
|
||||
|
||||
## 时间 API
|
||||
|
||||
接下来我们说说时间 API。OpenResty 提供了 10 个左右和时间相关的 API,从这个数量你也可见它的重要性。一般来说,最常用的时间 API就是 `ngx.now`,它可以打印出当前的时间戳,比如下面这行代码:
|
||||
|
||||
```
|
||||
resty -e 'ngx.say(ngx.now())'
|
||||
|
||||
```
|
||||
|
||||
从打印的结果可以看出,`ngx.now` 包括了小数部分,所以更加精准。而与之相关的 `ngx.time` 则只返回了整数部分的值。至于其他的 `ngx.localtime`、`ngx.utctime`、`ngx.cookie_time` 和 `ngx.http_time` ,主要是返回和处理时间的不同格式。具体用到的话,你可以查阅文档,本身并不难理解,我就没有必要专门来讲了。
|
||||
|
||||
不过,值得一提的是,**这些返回当前时间的 API,如果没有非阻塞网络 IO 操作来触发,便会一直返回缓存的值,而不是像我们想的那样,能够返回当前的实时时间**。可以看看下面这个示例代码:
|
||||
|
||||
```
|
||||
$ resty -e 'ngx.say(ngx.now())
|
||||
os.execute("sleep 1")
|
||||
ngx.say(ngx.now())'
|
||||
|
||||
```
|
||||
|
||||
在两次调用 `ngx.now` 之间,我们使用 Lua 的阻塞函数 sleep 了 1 秒钟,但从打印的结果来看,这两次返回的时间戳却是一模一样的。
|
||||
|
||||
那么,如果换成是非阻塞的 sleep 函数呢?比如下面这段新的代码:
|
||||
|
||||
```
|
||||
$ resty -e 'ngx.say(ngx.now())
|
||||
ngx.sleep(1)
|
||||
ngx.say(ngx.now())'
|
||||
|
||||
```
|
||||
|
||||
显然,它就会打印出不同的时间戳了。这里顺带引出了 `ngx.sleep` ,这个非阻塞的 sleep 函数。这个函数除了可以休眠指定的时间外,还有另外一个特别的用处。
|
||||
|
||||
举个例子,比如你有一段正在做密集运算的代码,需要花费比较多的时间,那么在这段时间内,这段代码对应的请求就会一直占用着 worker 和 CPU 资源,导致其他请求需要排队,无法得到及时的响应。这时,我们就可以在其中穿插 `ngx.sleep(0)`,使这段代码让出控制权,让其他请求也可以得到处理。
|
||||
|
||||
## worker 和进程 API
|
||||
|
||||
再来看worker 和进程相关的API。OpenResty 提供了 `ngx.worker.*` 和 `ngx.process.*` 这些 API, 来获取 worker 和进程相关的信息。其中,前者和 Nginx worker 进程有关,后者则是泛指所有的 Nginx 进程,不仅有 worker 进程,还有 master 进程和特权进程等等。
|
||||
|
||||
事实上,`ngx.worker.*` 由 lua-nginx-module 提供,而`ngx.process.*` 则是由 lua-resty-core 提供。还记得上节课我们留的作业题吗,如何保证在多 worker 的情况下,只启动一个 timer?其实,这就需要用到 `ngx.worker.id` 这个 API 了。你可以在启动 timer 之前,先做一个简单的判断:
|
||||
|
||||
```
|
||||
if ngx.worker.id == 0 then
|
||||
start_timer()
|
||||
end
|
||||
|
||||
```
|
||||
|
||||
这样,我们就能实现只启动一个 timer的目的了。这里注意,worker id 是从 0 开始返回的,这和 Lua 中数组下标从 1 开始并不相同,千万不要混淆了。
|
||||
|
||||
至于其他 worker 和 process 相关的 API,并没有什么特别需要注意的地方,就交给你自己去学习和练习了。
|
||||
|
||||
## 真值和空值
|
||||
|
||||
最后我们来看看,真值和空值的问题。在 OpenResty 中,真值与空值的判断,一直是个让人头痛、也比较混乱的点。
|
||||
|
||||
我们先看来下 Lua 中真值的定义:**除了 nil 和 false 之外,都是真值。**
|
||||
|
||||
所以,真值也就包括了:0、空字符串、空表等等。
|
||||
|
||||
再来看下 Lua 中的空值(nil),它是未定义的意思,比如你申明了一个变量,但还没有初始化,它的值就是 nil:
|
||||
|
||||
```
|
||||
$ resty -e 'local a
|
||||
ngx.say(type(a))'
|
||||
|
||||
```
|
||||
|
||||
而 nil 也是 Lua 中的一种数据类型。
|
||||
|
||||
明白了这两点后,我们现在就来具体看看,基于这两个定义,衍生出来的其他坑。
|
||||
|
||||
### ngx.null
|
||||
|
||||
第一个坑是`ngx.null`。因为 Lua 的 nil 无法作为 table 的 value,所以 OpenResty 引入了 `ngx.null`,作为 table 中的空值:
|
||||
|
||||
```
|
||||
$ resty -e 'print(ngx.null)'
|
||||
null
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
$ resty -e 'print(type(ngx.null))'
|
||||
userdata
|
||||
|
||||
```
|
||||
|
||||
从上面两段代码你可以看出,`ngx.null` 被打印出来是 null,而它的类型是 userdata。那么,可以把它当作假值吗?当然不行,事实上,`ngx.null` 的布尔值为真:
|
||||
|
||||
```
|
||||
$ resty -e 'if ngx.null then
|
||||
ngx.say("true")
|
||||
end'
|
||||
|
||||
```
|
||||
|
||||
所以,要谨记,**只有 nil 和 false 是假值**。如果你遗漏了这一点,就很容易踩坑,比如你在使用 lua-resty-redis 的时候,做了下面这个判断:
|
||||
|
||||
```
|
||||
local res, err = red:get("dog")
|
||||
if not res then
|
||||
res = res + "test"
|
||||
end
|
||||
|
||||
```
|
||||
|
||||
如果返回值 res 是 nil,就说明函数调用失败了;如果 res 是 ngx.null,就说明 redis 中不存在 `dog` 这个key。那么,在 `dog` 这个 key 不存在的情况下,这段代码就 500 崩溃了。
|
||||
|
||||
### cdata:NULL
|
||||
|
||||
第二个坑是`cdata:NULL`。当你通过 LuaJIT FFI 接口去调用 C 函数,而这个函数返回一个 NULL 指针,那么你就会遇到另外一种空值,即`cdata:NULL` 。
|
||||
|
||||
```
|
||||
$ resty -e 'local ffi = require "ffi"
|
||||
local cdata_null = ffi.new("void*", nil)
|
||||
if cdata_null then
|
||||
ngx.say("true")
|
||||
end'
|
||||
|
||||
```
|
||||
|
||||
和 `ngx.null` 一样,`cdata:NULL` 也是真值。但更让人匪夷所思的是,下面这段代码,会打印出 true,也就是说`cdata:NULL` 是和 `nil` 相等的:
|
||||
|
||||
```
|
||||
$ resty -e 'local ffi = require "ffi"
|
||||
local cdata_null = ffi.new("void*", nil)
|
||||
ngx.say(cdata_null == nil)'
|
||||
|
||||
```
|
||||
|
||||
那么我们应该如何处理 `ngx.null` 和 `cdata:NULL` 呢?显然,让应用层来关心这些闹心事儿是不现实的,最好是做一个二层封装,不要让调用者知道这些细节即可。
|
||||
|
||||
### cjson.null
|
||||
|
||||
最后,我们再来看下 cjson 中出现的空值。cjson 库会把 json 中的 NULL,解码为 Lua 的 `lightuserdata`,并用 `cjson.null` 来表示:
|
||||
|
||||
```
|
||||
$ resty -e 'local cjson = require "cjson"
|
||||
local data = cjson.encode(nil)
|
||||
local decode_null = cjson.decode(data)
|
||||
ngx.say(decode_null == cjson.null)'
|
||||
|
||||
```
|
||||
|
||||
Lua 中的 nil,被 json encode 和 decode 一圈儿之后,就变成了 `cjson.null`。你可以想得到,它引入的原因和 `ngx.null` 是一样的,因为 nil 无法在 table 中作为 value。
|
||||
|
||||
到现在为止,看了这么多 OpenResty 中的空值,不知道你蒙圈儿了没?不要慌张,这部分内容多看几遍,自己梳理一下,就不至于晕头转向分不清了。当然,你以后在写类似 `if not foo then` 的时候,就要多想想,这个条件到底能不能成立了。
|
||||
|
||||
## 写在最后
|
||||
|
||||
学完今天这节课后,OpenResty 中常用的 Lua API 我们就都介绍过了,不知道你是否都清楚了呢?
|
||||
|
||||
最后,留一个思考题给你:在 `ngx.now` 的示例中,为什么在没有 yield 操作的时候,它的值不会修改呢?欢迎留言分享你的看法,也欢迎你把这篇文章分享出去,我们一起交流,一起进步。
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
|
||||
<video poster="https://static001.geekbang.org/resource/image/2b/04/2b19372f8c88bb89c799382bb4767504.jpg" preload="none" controls=""><source src="https://media001.geekbang.org/customerTrans/fe4a99b62946f2c31c2095c167b26f9c/55ac88a8-16ce81d8277-0000-0000-01d-dbacd.mp4" type="video/mp4"><source src="https://media001.geekbang.org/e63152456c494c38a25ef45b274c9610/f2a99770955f4dff8f68622573c395ca-37963b86cd6511e18c03ec2809815ddb-sd.m3u8" type="application/x-mpegURL"><source src="https://media001.geekbang.org/e63152456c494c38a25ef45b274c9610/f2a99770955f4dff8f68622573c395ca-fbd7fdcd1c8e947dad21fe0f44731b04-hd.m3u8" type="application/x-mpegURL"></video>
|
||||
|
||||
你好,我是温铭。
|
||||
|
||||
今天的内容,我同样会以视频的形式来讲解。老规矩,在你进行视频学习之前,我想先问你这么几个问题:
|
||||
|
||||
- 你在使用 OpenResty 的时候,是否注意到有 API 存在安全隐患呢?
|
||||
- 在安全和性能之间,如何去平衡它们的关系呢?
|
||||
|
||||
这几个问题,也是今天视频课要解决的核心内容,希望你可以先自己思考一下,并带着问题来学习今天的视频内容。
|
||||
|
||||
同时,我会给出相应的文字介绍,方便你在听完视频内容后,及时总结与复习。下面是今天这节课的文字介绍部分。
|
||||
|
||||
## 今日核心
|
||||
|
||||
安全,是一个永恒的话题,不管你是写开发业务代码,还是做底层的架构,都离不开安全方面的考虑。
|
||||
|
||||
CVE-2018-9230 是与 OpenResty 相关的一个安全漏洞,但它并非 OpenResty 自身的安全漏洞。这听起来是不是有些拗口呢?没关系,接下来让我们具体看下,攻击者是如何构造请求的。
|
||||
|
||||
OpenResty 中的 `ngx.req.get_uri_args`、`ngx.req.get_post_args` 和 `ngx.req.get_headers`接口,默认只返回前 100 个参数。如果 WAF 的开发者没有注意到这个细节,就会被参数溢出的方式攻击。攻击者可以填入 100 个无用参数,把 payload 放在第 101 个参数中,借此绕过 WAF 的检测。
|
||||
|
||||
那么,应该如何处理这个 CVE 呢?
|
||||
|
||||
显然,OpenResty 的维护者需要考虑到向下兼容、不引入更多安全风险和不影响性能这么几个因素,并要在其中做出一个平衡的选择。
|
||||
|
||||
最终,OpenResty 维护者选择新增一个 err 的返回值来解决这个问题。如果输入参数超过 100 个,err 的提示信息就是 truncated。这样一来,这些 API 的调用者就必须要处理错误信息,自行判断拒绝请求还是放行。
|
||||
|
||||
其实,归根到底,安全是一种平衡。究竟是选择基于规则的黑名单方式,还是选择基于身份的白名单方式,抑或是两种方式兼用,都取决于你的实际业务场景。
|
||||
|
||||
## 课件参考
|
||||
|
||||
今天的课件已经上传到了我的GitHub上,你可以自己下载学习。
|
||||
|
||||
链接如下:[https://github.com/iresty/geektime-slides](https://github.com/iresty/geektime-slides)
|
||||
|
||||
如果有不清楚的地方,你可以在留言区提问,另也可以在留言区分享你的学习心得。期待与你的对话,也欢迎你把这篇文章分享给你的同事、朋友,我们一起交流、一起进步。
|
||||
@@ -0,0 +1,31 @@
|
||||
|
||||
<video poster="https://static001.geekbang.org/resource/image/65/c2/6565cf0a87645948ef66c547192db3c2.jpg" preload="none" controls=""><source src="https://media001.geekbang.org/customerTrans/fe4a99b62946f2c31c2095c167b26f9c/3d0d7df0-16ce81ba96a-0000-0000-01d-dbacd.mp4" type="video/mp4"><source src="https://media001.geekbang.org/ed7db4a5658b4ed59f225967841c31f8/e7f659c5c1864bb3bde42a4c200eab4b-d5090411129213bc3a3a2d388e0daa0b-sd.m3u8" type="application/x-mpegURL"><source src="https://media001.geekbang.org/ed7db4a5658b4ed59f225967841c31f8/e7f659c5c1864bb3bde42a4c200eab4b-3d9bf901ccefc4abb20c7469df8134d4-hd.m3u8" type="application/x-mpegURL"></video>
|
||||
|
||||
你好,我是温铭。
|
||||
|
||||
今天的内容,我同样会以视频的形式来讲解。老规矩,在你进行视频学习之前,先问你这么几个问题:
|
||||
|
||||
- 面对多个相同功能的 lua-resty 库,我们应该从哪些方面来选择?
|
||||
- 如何来组织一个 lua-resty 的结构?
|
||||
|
||||
这几个问题,也是今天视频课要解决的核心内容,希望你可以先自己思考一下,并带着问题来学习今天的视频内容。
|
||||
|
||||
同时,我会给出相应的文字介绍,方便你在听完视频内容后,及时总结与复习。下面是今天这节课的文字介绍部分。
|
||||
|
||||
## 今日核心
|
||||
|
||||
前面我们介绍过的 lua-resty 库都是官方自带的,但在 HTTP client 这个最常用的库上,官方并没有。这时候,我们就得自己来选择一个优秀的第三方库了。
|
||||
|
||||
那么,如何在众多的 lua-resty HTTP client 中,选择一个最好、最适合自己的第三方库呢?
|
||||
|
||||
这时候,你就需要综合考虑活跃度、作者、测试覆盖度、接口封装等各方面的因素了。我最后选择的是 lua-resty-requests([https://github.com/tokers/lua-resty-requests](https://github.com/tokers/lua-resty-requests)),它是由又拍云的工程师 tokers 贡献的,我个人很喜欢它的接口风格,也推荐给你。
|
||||
|
||||
在视频中我会从最简单的 get 接口入手,结合文档、测试案例和源码,来逐步展开。你可以看到一个优秀的 lua-resty 库是如何编写的,有哪些可以借鉴的地方。
|
||||
|
||||
## 课件参考
|
||||
|
||||
今天的课件已经上传到了我的GitHub上,你可以自己下载学习。
|
||||
|
||||
链接如下:[https://github.com/iresty/geektime-slides](https://github.com/iresty/geektime-slides)
|
||||
|
||||
如果有不清楚的地方,你可以在留言区提问,另也可以在留言区分享你的学习心得。期待与你的对话,也欢迎你把这篇文章分享给你的同事、朋友,我们一起交流、一起进步。
|
||||
263
极客时间专栏/OpenResty从入门到实战/API篇/24 | 实战:处理四层流量,实现Memcached Server.md
Normal file
263
极客时间专栏/OpenResty从入门到实战/API篇/24 | 实战:处理四层流量,实现Memcached Server.md
Normal file
@@ -0,0 +1,263 @@
|
||||
<audio id="audio" title="24 | 实战:处理四层流量,实现Memcached Server" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3f/f5/3f538d45054560e043ee575907fb5df5.mp3"></audio>
|
||||
|
||||
你好,我是温铭。
|
||||
|
||||
在前面几节课中,我们介绍了不少处理请求的 Lua API ,不过它们都是和七层相关的。除此之外,OpenResty 其实还提供了 `stream-lua-nginx-module` 模块来处理四层的流量。它提供的指令和 API ,与 `lua-nginx-module` 基本一致。
|
||||
|
||||
今天,我就带你一起用 OpenResty 来实现一个 memcached server,而且大概只需要 100 多行代码就可以完成。在这个小的实战中,我们会用到不少前面学过的内容,也会带入一些后面测试和性能优化章节的内容。
|
||||
|
||||
所以,我希望你能够明确一点,我们这节课的重点,不在于你必须读懂每一行代码的具体作用,而是你要从需求、测试、开发等角度,把 OpenResty 如何从零开发一个项目的全貌了然于心。
|
||||
|
||||
## 原始需求和技术方案
|
||||
|
||||
在开发之前,我们都需要明白需求是什么,到底是用来解决什么问题的,否则就会在迷失在技术选择中。比如看到我们今天的主题,你就应该先反问一下自己,为什么要实现一个 memcached server 呢?直接安装一个原版的 memcached 或者 redis 不就行了吗?
|
||||
|
||||
我们知道,HTTPS 流量逐渐成为主流,但一些比较老的浏览器并不支持 session ticket,那么我们就需要在服务端把 session ID 存下来。如果本地存储空间不够,就需要一个集群进行存放,而这个数据又是可以丢弃的,所以选用 memcached 就比较合适。
|
||||
|
||||
这时候,直接引入 memcached ,应该是最简单直接的方案。但出于以下几个方面的考虑,我还是选择使用 OpenResty 来造一个轮子:
|
||||
|
||||
- 第一,直接引入会多引入一个进程,增加部署和维护成本;
|
||||
- 第二,这个需求足够简单,只需要 get 和 set 操作,并且支持过期即可;
|
||||
- 第三,OpenResty 有 stream 模块,可以很快地实现这个需求。
|
||||
|
||||
既然要实现 memcached server,我们就需要先弄明白它的协议。memcached 的协议可以支持 TCP 和 UDP,这里我选择 TCP,下面是 get 和 set 命令的具体协议:
|
||||
|
||||
```
|
||||
Get
|
||||
根据 key 获取 value
|
||||
Telnet command: get <key>*\r\n
|
||||
|
||||
示例:
|
||||
get key
|
||||
VALUE key 0 4 data END
|
||||
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
Set
|
||||
存储键值对到 memcached 中
|
||||
Telnet command:set <key> <flags> <exptime> <bytes> [noreply]\r\n<value>\r\n
|
||||
|
||||
示例:
|
||||
set key 0 900 4 data
|
||||
STORED
|
||||
|
||||
```
|
||||
|
||||
除了 get 和 set 外,我们还需要知道 memcached 的协议的“错误处理”是怎么样做的。“错误处理”对于服务端的程序是非常重要的,我们在编写程序时,除了要处理正常的请求,也要考虑到各种异常。比如下面这样的场景:
|
||||
|
||||
- memcached 发送了一个get、set 之外的请求,我要怎么处理呢?
|
||||
- 服务端出错,我要给 memcached 的客户端一个什么样的反馈呢?
|
||||
|
||||
同时,我们希望写出能够兼容 memcached 的客户端程序。这样,使用者就不用区分这是 memcached 官方的版本,还是 OpenResty 实现的版本了。
|
||||
|
||||
下面这张图出自memcached 的文档,描述了出错的时候,应该返回什么内容和具体的格式,你可以用做参考:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/37/b0/3767ed0047e34aabaa7bf7d568438ab0.png" alt="">
|
||||
|
||||
现在,再来确定下技术方案。我们知道,OpenResty 的 shared dict 可以跨各个 worker 来使用,把数据放在 shared dict 里面,和放在 memcached 里面非常类似——它们都支持 get 和 set 操作,并且在进程重启后数据就丢失了。所以,使用 shared dict 来模拟 memcached 是非常合适的,它们的原理和行为都是一致的。
|
||||
|
||||
## 测试驱动开发
|
||||
|
||||
接下来就要开始动工了。不过,基于测试驱动开发的思想,在写具体的代码之前,让我们先来构造一个最简单的测试案例。这里我们不用 `test::nginx` 框架,毕竟它的上手难度也不低,我们不妨先用熟悉的 `resty` 来手动测试下:
|
||||
|
||||
```
|
||||
$ resty -e 'local memcached = require "resty.memcached"
|
||||
local memc, err = memcached:new()
|
||||
|
||||
memc:set_timeout(1000) -- 1 sec
|
||||
local ok, err = memc:connect("127.0.0.1", 11212)
|
||||
local ok, err = memc:set("dog", 32)
|
||||
if not ok then
|
||||
ngx.say("failed to set dog: ", err)
|
||||
return
|
||||
end
|
||||
|
||||
local res, flags, err = memc:get("dog")
|
||||
ngx.say("dog: ", res)'
|
||||
|
||||
```
|
||||
|
||||
这段测试代码,使用 `lua-rety-memcached` 客户端库发起 connect 和 set 操作,并假设 memcached 的服务端监听本机的 11212 端口。
|
||||
|
||||
看起来应该没有问题了吧。你可以在自己的机器上执行一下这段代码,不出意外的话,会返回 `failed to set dog: closed` 这样的错误提示,因为此时服务并没有启动。
|
||||
|
||||
到现在为止,你的技术方案就已经明确了,那就是使用 stream 模块来接收和发送数据,同时使用 shared dict 来存储数据。
|
||||
|
||||
衡量需求是否完成的指标也很明确,那就是跑通上面这段代码,并把 dog 的实际值给打印出来。
|
||||
|
||||
## 搭建框架
|
||||
|
||||
那还等什么,开始动手写代码吧!
|
||||
|
||||
我个人的习惯,是先搭建一个最小的可以运行的代码框架,然后再逐步地去填充代码。这样的好处是,在编码过程中,你可以给自己设置很多小目标;而且在完成一个小目标后,测试案例也会给你正反馈。
|
||||
|
||||
让我们先来设置好 Nginx 的配置文件,因为stream 和 shared dict 要在其中预设。下面是我设置的配置文件:
|
||||
|
||||
```
|
||||
stream {
|
||||
lua_shared_dict memcached 100m;
|
||||
lua_package_path 'lib/?.lua;;';
|
||||
server {
|
||||
listen 11212;
|
||||
content_by_lua_block {
|
||||
local m = require("resty.memcached.server")
|
||||
m.run()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
你可以看到,这段配置文件中有几个关键的信息:
|
||||
|
||||
- 首先,代码运行在 Nginx 的 stream 上下文中,而非 HTTP 上下文中,并且监听了 11212 端口;
|
||||
- 其次,shared dict 的名字为 memcached,大小是 100M,这些在运行期是不可以修改的;
|
||||
- 另外,代码所在目录为 `lib/resty/memcached`, 文件名为 `server.lua`, 入口函数为 `run()`,这些信息你都可以从`lua_package_path` 和 `content_by_lua_block` 中找到。
|
||||
|
||||
接着,就该搭建代码框架了。你可以自己先动手试试,然后我们一起来看下我的框架代码:
|
||||
|
||||
```
|
||||
local new_tab = require "table.new"
|
||||
local str_sub = string.sub
|
||||
local re_find = ngx.re.find
|
||||
local mc_shdict = ngx.shared.memcached
|
||||
|
||||
local _M = { _VERSION = '0.01' }
|
||||
|
||||
local function parse_args(s, start)
|
||||
end
|
||||
|
||||
function _M.get(tcpsock, keys)
|
||||
end
|
||||
|
||||
function _M.set(tcpsock, res)
|
||||
end
|
||||
|
||||
function _M.run()
|
||||
local tcpsock = assert(ngx.req.socket(true))
|
||||
|
||||
while true do
|
||||
tcpsock:settimeout(60000) -- 60 seconds
|
||||
local data, err = tcpsock:receive("*l")
|
||||
|
||||
local command, args
|
||||
if data then
|
||||
local from, to, err = re_find(data, [[(\S+)]], "jo")
|
||||
if from then
|
||||
command = str_sub(data, from, to)
|
||||
args = parse_args(data, to + 1)
|
||||
end
|
||||
end
|
||||
|
||||
if args then
|
||||
local args_len = #args
|
||||
if command == 'get' and args_len > 0 then
|
||||
_M.get(tcpsock, args)
|
||||
elseif command == "set" and args_len == 4 then
|
||||
_M.set(tcpsock, args)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return _M
|
||||
|
||||
```
|
||||
|
||||
这段代码,便实现了入口函数 `run()` 的主要逻辑。虽然我还没有做异常处理,依赖的 `parse_args`、`get` 和 `set` 也都是空函数,但这个框架已经完整表达了memcached server 的逻辑。
|
||||
|
||||
## 填充代码
|
||||
|
||||
接下来,让我们按照代码的执行顺序,逐个实现这几个空函数。
|
||||
|
||||
首先,我们可以根据 memcached [的协议](https://github.com/memcached/memcached/blob/master/doc/protocol.txt)[文档](https://github.com/memcached/memcached/blob/master/doc/protocol.txt),解析 memcached 命令的参数:
|
||||
|
||||
```
|
||||
local function parse_args(s, start)
|
||||
local arr = {}
|
||||
|
||||
while true do
|
||||
local from, to = re_find(s, [[\S+]], "jo", {pos = start})
|
||||
if not from then
|
||||
break
|
||||
end
|
||||
|
||||
table.insert(arr, str_sub(s, from, to))
|
||||
|
||||
start = to + 1
|
||||
end
|
||||
|
||||
return arr
|
||||
end
|
||||
|
||||
```
|
||||
|
||||
这里,我的建议是,先用最直观的方式来实现一个版本,不用考虑任何性能的优化。毕竟,完成总是比完美更重要,而且,基于完成的逐步优化才可以趋近完美。
|
||||
|
||||
接下来,我们就来实现下 `get` 函数。它可以一次查询多个键,所以下面代码中我用了一个 for 循环:
|
||||
|
||||
```
|
||||
function _M.get(tcpsock, keys)
|
||||
local reply = ""
|
||||
|
||||
for i = 1, #keys do
|
||||
local key = keys[i]
|
||||
local value, flags = mc_shdict:get(key)
|
||||
if value then
|
||||
local flags = flags or 0
|
||||
reply = reply .. "VALUE" .. key .. " " .. flags .. " " .. #value .. "\r\n" .. value .. "\r\n"
|
||||
end
|
||||
end
|
||||
reply = reply .. "END\r\n"
|
||||
|
||||
tcpsock:settimeout(1000) -- one second timeout
|
||||
local bytes, err = tcpsock:send(reply)
|
||||
end
|
||||
|
||||
```
|
||||
|
||||
其实,这里最核心的代码只有一行:`local value, flags = mc_shdict:get(key)`,也就是从 shared dict 中查询到数据;至于其余的代码,都在按照 memcached 的协议拼接字符串,并最终 send 到客户端。
|
||||
|
||||
最后,我们再来看下 `set` 函数。它将接收到的参数转换为 shared dict API 的格式,把数据储存了起来;并在出错的时候,按照 memcached 的协议做出处理:
|
||||
|
||||
```
|
||||
function _M.set(tcpsock, res)
|
||||
local reply = ""
|
||||
|
||||
local key = res[1]
|
||||
local flags = res[2]
|
||||
local exptime = res[3]
|
||||
local bytes = res[4]
|
||||
|
||||
local value, err = tcpsock:receive(tonumber(bytes) + 2)
|
||||
|
||||
if str_sub(value, -2, -1) == "\r\n" then
|
||||
local succ, err, forcible = mc_shdict:set(key, str_sub(value, 1, bytes), exptime, flags)
|
||||
if succ then
|
||||
reply = reply .. “STORED\r\n"
|
||||
else
|
||||
reply = reply .. "SERVER_ERROR " .. err .. “\r\n”
|
||||
end
|
||||
else
|
||||
reply = reply .. "ERROR\r\n"
|
||||
end
|
||||
|
||||
tcpsock:settimeout(1000) -- one second timeout
|
||||
local bytes, err = tcpsock:send(reply)
|
||||
end
|
||||
|
||||
```
|
||||
|
||||
另外,在填充上面这几个函数的过程中,你可以用测试案例来做检验,并用 `ngx.log` 来做 debug。比较遗憾的是,OpenResty 中并没有断点调试的工具,所以我们都是使用 `ngx.say` 和 `ngx.log` 来调试的,在这方面可以说是还处于刀耕火种的时代。
|
||||
|
||||
## 写在最后
|
||||
|
||||
这个实战项目到现在就接近尾声了,最后,我想留一个动手作业。你可以把上面 memcached server 的实现代码,完整地运行起来,并通过测试案例吗?
|
||||
|
||||
今天的作业题估计要花费你不少的精力了,不过,这还是一个原始的版本,还没有错误处理、性能优化和自动化测试,这些就要放在后面继续完善了。我也希望通过后面内容的学习,你最终能够完成一个完善的版本。
|
||||
|
||||
如果对于今天的讲解或者自己的实践有什么疑惑,欢迎你留言和我讨论。也欢迎你把这篇文章转发给你的同事朋友,我们一起实战,一起进步。
|
||||
|
||||
|
||||
142
极客时间专栏/OpenResty从入门到实战/API篇/25 | 答疑(二):特权进程的权限到底是什么?.md
Normal file
142
极客时间专栏/OpenResty从入门到实战/API篇/25 | 答疑(二):特权进程的权限到底是什么?.md
Normal file
@@ -0,0 +1,142 @@
|
||||
<audio id="audio" title="25 | 答疑(二):特权进程的权限到底是什么?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/00/b0/00e033e4519ef941c92485d925cd3db0.mp3"></audio>
|
||||
|
||||
你好,我是温铭。
|
||||
|
||||
专栏更新到现在,OpenResty第二版块 OpenResty API 篇,我们就已经学完了。恭喜你没有掉队,仍然在积极学习和实践操作,并且热情地留下了你的思考。
|
||||
|
||||
很多留言提出的问题很有价值,大部分我都已经在App里回复过,一些手机上不方便回复的或者比较典型、有趣的问题,我专门摘了出来,作为今天的答疑内容,集中回复。另一方面,也是为了保证所有人都不漏掉任何一个重点。
|
||||
|
||||
下面我们来看今天的这 6 个问题。
|
||||
|
||||
## 第一问,特权进程的权限
|
||||
|
||||
Q:我想请问下,特权进程是怎么回事,如果启动 OpenResty 的本身就是普通用户,如何获取root权限呢?另外,老师可以介绍下,特权进程的使用场景有哪些吗?
|
||||
|
||||
A:其实,特权进程的权限和 master 进程的权限保持一样。如果你用普通用户身份启动 OpenResty,那么 master 就是普通用户的权限,这时候特权进程也就没有什么特权了。
|
||||
|
||||
这一点应该还是很好理解的,普通用户启动的进程,无论如何也不会有 root 权限。
|
||||
|
||||
至于特权进程的使用场景,我们一般用特权进程来处理的是清理日志、重启 OpenResty 自身等需要高权限的任务。你需要注意的是,不要把 worker 进程的任务交给特权进程来处理。这并非因为特权进程不能做到,而是其存在安全隐患。
|
||||
|
||||
我见到过一个开发者,他把定时器的任务都交给了特权进程来处理。他为什么这么做呢?因为特权进程只有一个,这样 timer 就不会重复启动。
|
||||
|
||||
是不是觉得这看上去很聪明呀,不用 worker.id 这种笨方法就做到了。但是,别忘了,如果定时器的任务和用户的输入有关,这不就等于留了一个后门吗?显然是非常危险的。
|
||||
|
||||
## 第二问,阶段和调试
|
||||
|
||||
Q:老师,是不是无论在哪个阶段运行`ngx.say('hello')`,OpenResty都会在执行完本阶段的剩余代码后,直接响应给客户端,而不会继续执行其他阶段了呢?我测试出来是这样的。
|
||||
|
||||
A:事实上并非如此,我们可以来看下它的执行阶段的[顺序图](https://github.com/moonbingbing/openresty-best-practices/blob/master/images/openresty_phases.png):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/71/bf/71b24c95f042f0bf79ac34211e2dd0bf.png" alt="">
|
||||
|
||||
你可以做个测试,先在 content 里面 `ngx.say`;然后,在 log 或者 body filter 阶段使用 `ngx.log` 来打印下日志试试。
|
||||
|
||||
在专栏中,我并没有专门提到在 OpenResty 中做代码调试的问题,这也是开发者经常困惑的地方,我正好顺着这个问题在答疑中聊一下。
|
||||
|
||||
其实,OpenResty 中的代码调试,并没有断点这些高级功能(相应有一些付费的插件,但我并没有使用过),只能用 `ngx.say` 和`ngx.log` 来看输出。我知道的开发者,包括 OpenResty 的作者和贡献者们,都是这样来做 debug 的。所以,你需要有强有力的测试案例和调试日志来作为保证。
|
||||
|
||||
## 第三问,ngx.exit 和动手实验
|
||||
|
||||
Q:老师,文中的这句话,“OpenResty 的 HTTP 状态码中,有一个特别的常量:`ngx.OK`。当 `ngx.exit(ngx.OK)` 时,请求会退出当前处理阶段,进入下一个阶段,而不是直接返回给客户端。”
|
||||
|
||||
我记得,`ngx.OK`应该不能算是HTTP状态码,它对应的值是0。我的理解是:
|
||||
|
||||
- `ngx.exit(ngx.OK)`、`ngx.exit(ngx.ERROR)`和`ngx.exit(ngx.DECLINED)`时,请求会退出当前处理阶段,进入下一个阶段;
|
||||
- 而当`ngx.exit(ngx.HTTP_*)`以`ngx.HTTP_*`的各种HTTP状态码作为参数时,会直接响应给客户端。
|
||||
|
||||
不知道这样想对不对呢?
|
||||
|
||||
A:关于你的第一个问题,`ngx.ok` 确实不是http状态码,它是 OpenResty 中的一个常量,值是0。
|
||||
|
||||
至于第二个问题,`ngx.exit` 的官方文档其实正好可以解答:
|
||||
|
||||
```
|
||||
When status >= 200 (i.e., ngx.HTTP_OK and above), it will interrupt the execution of the current request and return status code to nginx.
|
||||
|
||||
When status == 0 (i.e., ngx.OK), it will only quit the current phase handler (or the content handler if the content_by_lua* directive is used) and continue to run later phases (if any) for the current request.
|
||||
|
||||
```
|
||||
|
||||
不过,文档里并没有提到, OpenResty对于`ngx.exit(ngx.ERROR)`和`ngx.exit(ngx.DECLINED)`是如何处理的,我们可以自己来做个测试,比如下面这样:
|
||||
|
||||
```
|
||||
location /lua {
|
||||
rewrite_by_lua "ngx.exit(ngx.ERROR)";
|
||||
echo hello;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
显然,访问这个 location,你可以看到 http 响应码为空,响应体也是空,并没有进入下一个执行阶段。
|
||||
|
||||
其实,还是那句话,在 OpenResty 的学习过程中,随着你逐步深入,一定会在某个阶段发现,文档和测试案例都无法回答你的问题。这时候,就需要你自己构建测试案例来验证你的想法了。你可以手动测试,也可以添加在 `test::nginx` 搭建的测试案例集里面。
|
||||
|
||||
## 第四问,变量和竞争
|
||||
|
||||
Q:老师,你好,我有下面几个问题想请教一下。
|
||||
|
||||
1. 前面讲过,`ngx.var`变量的作用域在nginx C和lua-nginx-module模块之间。这个我不太理解,从请求的角度来看,是指一个工作进程中的单个请求吗?
|
||||
1. 我的理解是,在我们操作模块内的变量时,如果两个操作之间有阻塞操作,可能会出现竞争。那么,如果两个操作之间没有阻塞操作,恰好CPU时间到了后,当前进程进入就绪队列,这样可能产生竞争吗?
|
||||
|
||||
A:我们依次来看这几个问题。
|
||||
|
||||
第一,关于`ngx.var` 变量的问题,你的理解是正确的。实际上,`ngx.var` 的生命周期和请求一致,请求结束它也就消失了。但它的优势,是数据可以在 C 模块和 Lua 代码中传递。这是其他几种方式都无法做到的。
|
||||
|
||||
第二,关于变量竞争的问题,其实,只要两个操作之间有 `yield 操作`,就可能出现竞争,而不是阻塞操作;有阻塞操作时是不会出现竞争的。换句话说,只要你不把主动权交给 Nginx 的事件循环,就不会有竞争。
|
||||
|
||||
## 第五问,共享字典操作是否需要加锁呢?
|
||||
|
||||
Q:老师,如果多个worker并发存储数据,是不是需要加锁呢?比如下面这个例子:
|
||||
|
||||
```
|
||||
resty --shdict 'dogs 10m' -e 'local dogs = ngx.shared.dogs
|
||||
local lock= ngx.xxxx.lock
|
||||
lock.lock()
|
||||
dogs:set("Jim", 8)
|
||||
lock.unlock()
|
||||
local v = dogs:get("Jim")
|
||||
ngx.say(v)
|
||||
'
|
||||
|
||||
```
|
||||
|
||||
A:其实这里不用你自己加锁,共享字典(shared dict)的操作都是原子性的,不管是 get 还是 set。这种类似加锁的处理,OpenResty已经帮你考虑到了。
|
||||
|
||||
## 第六问,OpenResty 中如何更新时间?
|
||||
|
||||
Q:`ngx.now()`取时间,是发生在resume函数恢复堆栈阶段吗?
|
||||
|
||||
A:Nginx 是以性能优先作为设计理念的,它会把时间缓存下来。这一点,我们从 `ngx.now` 的源码中就可以得到印证:
|
||||
|
||||
```
|
||||
static int
|
||||
ngx_http_lua_ngx_now(lua_State *L)
|
||||
{
|
||||
ngx_time_t *tp;
|
||||
|
||||
tp = ngx_timeofday();
|
||||
|
||||
lua_pushnumber(L, (lua_Number) (tp->sec + tp->msec / 1000.0L));
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
可以看出,`ngx.now()`这个获取当前时间函数的背后,隐藏的其实是 Nginx 的 `ngx_timeofday` 函数。而`ngx_timeofday` 函数,其实是一个宏定义:
|
||||
|
||||
```
|
||||
#define ngx_timeofday() (ngx_time_t *) ngx_cached_time
|
||||
|
||||
```
|
||||
|
||||
这里`ngx_cached_time` 的值,只在函数 `ngx_time_update` 中会更新。
|
||||
|
||||
所以,这个问题就简化成了, `ngx_time_update`什么时候会被调用?如果你在 Nginx 的源码中去跟踪它的话,就会发现, `ngx_time_update` 的调用都出现在事件循环中,这个问题也就明白了吧。
|
||||
|
||||
通过这个问题你应该也能发现,开源项目的好处就是,你可以根据蛛丝马迹,在源码中寻找答案,颇有一种破案的感觉。
|
||||
|
||||
今天主要解答这几个问题。最后,欢迎你继续在留言区写下你的疑问,我会持续不断地解答。希望可以通过交流和答疑,帮你把所学转化为所得。也欢迎你把这篇文章转发出去,我们一起交流、一起进步。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user