You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

249 lines
11 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 16 | 秒杀大多数开发问题的两个利器:文档和测试案例
你好,我是温铭。在学习了 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 呢?
2. 我们在示例代码中看到 get 函数只有一个返回值,那什么情况下会有多个返回值呢?
3. 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 群,还是其他渠道呢?
欢迎留言一起探讨,也欢迎你把这篇文章分享给你的同事、朋友,我们一起交流,一起进步。