|
| 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