Skip to content

Commit 88596c2

Browse files
authored
Merge pull request #2 from tokers/lua_code_cache
add chapters/012-ngx_lua_cache.md
2 parents 53e214d + d2818e9 commit 88596c2

File tree

1 file changed

+290
-0
lines changed

1 file changed

+290
-0
lines changed

chapters/012-ngx_lua_cache.md

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
> 以下代码均出自 lua-nginx-module v0.10.7 版本
2+
3+
> 这篇文章主要深入介绍 `lua-nginx-module` 是怎么对 Lua 代码进行加载和缓存
4+
>
5+
`ngx_http_lua_cache_loadbuffer ` 这个函数是提供给诸如 `rewrite_by_lua``access_by_lua` 等指令的回调方法在 load 对应的 Lua code 的时候使用的,这个函数也在 [rewrite_by_lua](003-ngx_lua_rewrite_by_lua.md) 这一节里简单讲过,本节主要介绍 `ngx_http_lua_cache_load_code``ngx_http_lua_clfactory_loadbuffer``ngx_http_lua_cache_store_code` 这三个函数
6+
7+
### 缓存状态机
8+
9+
首先得来回顾下 `ngx_http_lua_cache_loadbuffer ` 这个函数的缓存状态机
10+
11+
- 调用 ngx_http_lua_cache_load_code,判断当前的 Lua chunk 有没有缓存,得到返回码,如果返回码为 NGX_OK,跳到第二步;如果返回码是 NGX_ERROR,跳到第三步;否则跳到第四步
12+
- 从缓存中拿到 Lua chunk 且被压入到栈,返回 NGX_OK
13+
- 出错,返回 NGX_ERROR
14+
- 缓存 Miss,从原生的 Lua 代码加载,然后压栈,如果出错,记录错误日志然后返回 NGX_ERROR;否则返回 NGX_OK
15+
16+
缓存是万金油!!!
17+
我们总是期望从缓存里拿到加载好的 Lua chunk 而不是笨拙地从原生代码转换成 chunk
18+
19+
> 插一句,参加过 ACM 的同学应该会觉得这个东西有没有很像记忆化搜索呢 :)
20+
21+
### ngx_http_lua_cache_load_code
22+
23+
这个函数的功能就是字面意思,尝试从缓存里取出 Lua chunk
24+
25+
```c
26+
static ngx_int_t
27+
ngx_http_lua_cache_load_code(ngx_log_t *log, lua_State *L,
28+
const char *key)
29+
{
30+
int rc;
31+
u_char *err;
32+
33+
/* get code cache table */
34+
lua_pushlightuserdata(L, &ngx_http_lua_code_cache_key);
35+
lua_rawget(L, LUA_REGISTRYINDEX); /* sp++ */
36+
37+
dd("Code cache table to load: %p", lua_topointer(L, -1));
38+
39+
if (!lua_istable(L, -1)) {
40+
dd("Error: code cache table to load did not exist!!");
41+
return NGX_ERROR;
42+
}
43+
44+
lua_getfield(L, -1, key); /* sp++ */
45+
46+
if (lua_isfunction(L, -1)) {
47+
/* call closure factory to gen new closure */
48+
rc = lua_pcall(L, 0, 1, 0);
49+
if (rc == 0) {
50+
/* remove cache table from stack, leave code chunk at
51+
* top of stack */
52+
lua_remove(L, -2); /* sp-- */
53+
return NGX_OK;
54+
}
55+
56+
if (lua_isstring(L, -1)) {
57+
err = (u_char *) lua_tostring(L, -1);
58+
59+
} else {
60+
err = (u_char *) "unknown error";
61+
}
62+
63+
ngx_log_error(NGX_LOG_ERR, log, 0,
64+
"lua: failed to run factory at key \"%s\": %s",
65+
key, err);
66+
lua_pop(L, 2);
67+
return NGX_ERROR;
68+
}
69+
70+
dd("Value associated with given key in code cache table is not code "
71+
"chunk: stack top=%d, top value type=%s\n",
72+
lua_gettop(L), lua_typename(L, -1));
73+
74+
/* remove cache table and value from stack */
75+
lua_pop(L, 2); /* sp-=2 */
76+
77+
return NGX_DECLINED;
78+
}
79+
```
80+
81+
首先需要明确的一点是,`lua-nginx-module` 它把所有的 Lua chunk 存放在 Lua 提供的注册表中(Registry),通过某个键,来获取到专门存放 chunk 的 code table,而这个键就是 `ngx_http_lua_code_cache_key` 这个 char 类型的变量的地址(一个全局变量,存放在全局/静态存储区),显然它是独一无二的。然后再根据参数 key(见 [rewrite_by_lua](003-ngx_lua_rewrite_by_lua.md) 里的分析)来作为 code table 的键,把对应的 Lua chunk 拿出来并存在栈顶。然后需要判断这个 chunk 是否合法(是否是一个函数),如果合法,调用 lua_pcall 运行一次,这一步可能会令人迷惑,既然我们已经拿到了 Lua chunk,为什么要先运行一次呢?这里暂时先不解释,继续看下去就会明白了。
82+
83+
恩,假如顺利从缓存里拿到了 Lua chunk,就不需要再继续运行那个缓存状态机了,否则还得老老实实地从 Lua 代码加载
84+
85+
### ngx_http_lua_clfactory_loadbuffer
86+
87+
当 Cache Miss 的情况下,这个函数会在 `ngx_http_lua_cache_loadbuffer` 里调用
88+
89+
```c
90+
ngx_int_t
91+
ngx_http_lua_clfactory_loadbuffer(lua_State *L, const char *buff,
92+
size_t size, const char *name)
93+
{
94+
ngx_http_lua_clfactory_buffer_ctx_t ls;
95+
96+
ls.s = buff;
97+
ls.size = size;
98+
ls.sent_begin = 0;
99+
ls.sent_end = 0;
100+
101+
return lua_load(L, ngx_http_lua_clfactory_getS, &ls, name);
102+
}
103+
```
104+
105+
恩,有必要看下 `ngx_http_lua_clfactory_buffer_ctx_t` 这个结构体
106+
107+
```c
108+
typedef struct {
109+
int sent_begin;
110+
int sent_end;
111+
const char *s;
112+
size_t size;
113+
} ngx_http_lua_clfactory_buffer_ctx_t;
114+
```
115+
116+
- `sent_begin` 标记 lua_load 是否调用过 Reader 来获取新的 chunk 片
117+
- `sent_end` 标记 Lua chunk 已经读取完毕
118+
- `s` 指向的是要加载的整个 Lua chunk 串的首地址
119+
- `size` 则是这个 Lua chunk 串的大小
120+
121+
这个函数最终是调用了 lua_load,把 Reader 设置为函数 `ngx_http_lua_clfactory_getS`,参数是一个 `ngx_http_lua_clfactory_buffer_ctx_t ` 的变量,name 则是最终得到的 Lua chunk 的名字,也就是我们在 [rewrite_by_lua](003-ngx_lua_rewrite_by_lua.md) 里面分析过的 `chunkname` 这个串了。接着我们得来看看这个 Reader,也就是函数 `ngx_http_lua_clfactory_getS `
122+
123+
### ngx_http_lua_clfactory_getS
124+
125+
```c
126+
static const char *
127+
ngx_http_lua_clfactory_getS(lua_State *L, void *ud, size_t *size)
128+
{
129+
ngx_http_lua_clfactory_buffer_ctx_t *ls = ud;
130+
131+
if (ls->sent_begin == 0) {
132+
ls->sent_begin = 1;
133+
*size = CLFACTORY_BEGIN_SIZE;
134+
135+
return CLFACTORY_BEGIN_CODE;
136+
}
137+
138+
if (ls->size == 0) {
139+
if (ls->sent_end == 0) {
140+
ls->sent_end = 1;
141+
*size = CLFACTORY_END_SIZE;
142+
return CLFACTORY_END_CODE;
143+
}
144+
145+
return NULL;
146+
}
147+
148+
*size = ls->size;
149+
ls->size = 0;
150+
151+
return ls->s;
152+
}
153+
154+
155+
#define CLFACTORY_BEGIN_CODE "return function() "
156+
#define CLFACTORY_BEGIN_SIZE (sizeof(CLFACTORY_BEGIN_CODE) - 1)
157+
#define CLFACTORY_END_CODE "\nend"
158+
#define CLFACTORY_END_SIZE (sizeof(CLFACTORY_END_CODE) - 1)
159+
```
160+
161+
我们发现,当 lua_load 首次调用这个 Reader 的时候,得到的将是字符串 "return function()";当正式的 Lua chunk 读完以后,将会得到字符串 "\nend"。这样,实际上加载的 chunk 是一个返回函数的 Lua 语句:
162+
163+
```lua
164+
return function()
165+
...
166+
end
167+
```
168+
169+
看到这里,大家应该可以明白上面所说的,为什么要先调用加载好的 chunk 一次,因为只有调用一次以后,我们才能得到包装着 Lua 代码的函数(当时是编译好的 chunk 了);这么做是有原因的,毕竟 lua_pcall 运行的只能是函数而用户所写的 Lua 代码则是不可控的
170+
171+
到这里,代码也从 Lua code 里加载出来了,下面得把 Lua chunk 给缓存起来
172+
173+
```c
174+
static ngx_int_t
175+
ngx_http_lua_cache_store_code(lua_State *L, const char *key)
176+
{
177+
int rc;
178+
179+
/* get code cache table */
180+
lua_pushlightuserdata(L, &ngx_http_lua_code_cache_key);
181+
lua_rawget(L, LUA_REGISTRYINDEX);
182+
183+
dd("Code cache table to store: %p", lua_topointer(L, -1));
184+
185+
if (!lua_istable(L, -1)) {
186+
dd("Error: code cache table to load did not exist!!");
187+
return NGX_ERROR;
188+
}
189+
190+
lua_pushvalue(L, -2); /* closure cache closure */
191+
lua_setfield(L, -2, key); /* closure cache */
192+
193+
/* remove cache table, leave closure factory at top of stack */
194+
lua_pop(L, 1); /* closure */
195+
196+
/* call closure factory to generate new closure */
197+
rc = lua_pcall(L, 0, 1, 0);
198+
if (rc != 0) {
199+
dd("Error: failed to call closure factory!!");
200+
return NGX_ERROR;
201+
}
202+
203+
return NGX_OK;
204+
}
205+
```
206+
207+
流程我们应该比较熟悉了,从注册表里拿出 code table,然后把参数 key 作为键,将之前得到的 chunk 存到 code table,另外值得注意的是,我们还得调用一次这个 chunk(理由同上)
208+
209+
> 关于 Lua 栈的使用,可以参考 Lua 文档
210+
> 这里解释下该函数调用过程中,Lua 栈的变化
211+
212+
```c
213+
/*
214+
|----------|
215+
|stack peak|
216+
|----------|
217+
|Lua chunk |
218+
|----------|
219+
||
220+
|| lua_pushlightuserdata(L, &ngx_http_lua_code_cache_key);
221+
\/
222+
223+
|-------------------|
224+
| stack peak |
225+
|-------------------|
226+
| cache table key |
227+
|-------------------|
228+
| Lua chunk |
229+
|-------------------|
230+
||
231+
|| lua_rawget(L, LUA_REGISTRYINDEX);
232+
\/
233+
234+
|---------------|
235+
| stack peak |
236+
|---------------|
237+
| cache table |
238+
|---------------|
239+
| Lua chunk |
240+
|---------------|
241+
||
242+
|| lua_pushvalue(L, -2);
243+
\/
244+
245+
|---------------|
246+
| stack peak |
247+
|---------------|
248+
| Lua chunk |
249+
|---------------|
250+
| cache table |
251+
|---------------|
252+
| Lua chunk |
253+
|---------------|
254+
||
255+
|| lua_setfield(L, -2, key);
256+
\/
257+
258+
|---------------|
259+
| stack peak |
260+
|---------------|
261+
| cache table |
262+
|---------------|
263+
| Lua chunk |
264+
|---------------|
265+
||
266+
|| lua_pop(L, 1);
267+
\/
268+
269+
|---------------|
270+
| stack peak |
271+
|---------------|
272+
| Lua chunk |
273+
|---------------|
274+
275+
||
276+
|| rc = lua_pcall(L, 0, 1, 0);
277+
\/
278+
279+
|------------------------|
280+
| stack peak |
281+
|------------------------|
282+
| Lua chunk(function) |
283+
|------------------------|
284+
*/
285+
```
286+
287+
288+
### 总结
289+
290+
本文着重介绍了这个针对代码缓存的缓存状态机,无论何时何地,缓存始终是万金油,一次加载,之后每次都只要从缓存里拿出 Lua chunk 就行了,这大大减少了后续每个请求的处理时间

0 commit comments

Comments
 (0)