极客时间已完结课程限时免费阅读

16 | 秒杀大多数开发问题的两个利器:文档和测试案例

16 | 秒杀大多数开发问题的两个利器:文档和测试案例-极客时间

16 | 秒杀大多数开发问题的两个利器:文档和测试案例

讲述:温铭

时长10:50大小8.68M

你好,我是温铭。在学习了 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 操作为例,你可以点开 文档链接 做为对照。下面的最小化的代码示例,正是由官方文档改编而来:
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 方法。一般来说,大部分开发者也就此止步,不再深究了。
事实上,这里还是有几个值得注意的地方,比如:
哪些阶段不能使用共享内存相关的 API 呢?
我们在示例代码中看到 get 函数只有一个返回值,那什么情况下会有多个返回值呢?
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*
可以看出, initinit_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 群,还是其他渠道呢?
欢迎留言一起探讨,也欢迎你把这篇文章分享给你的同事、朋友,我们一起交流,一起进步。
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 4

提建议

上一篇
15 | OpenResty 和别的开发平台有什么不同?
下一篇
17 | 为什么能成为更好的Web服务器?动态处理请求和响应是关键
unpreview
 写留言

精选留言(14)

  • helloworld
    2019-07-02
    驱动测试案例: 1. 安装相关模块 sudo yum install cpan -y sudo cpan YAML sudo cpan Test::Nginx 2. test 测试案例 git clone [email protected]:openresty/lua-nginx-module.git cd lua-nginx-module/ prove t/043-shdict.t
    展开
    20
  • helloworld
    2019-07-02
    老师,指出文章中可能的一处错误:"哪些阶段不能使用共享内存相关的 API?" 这个例子举的貌似不太对。 restydoc -s ngx.shared 可知共享内存的相关API是支持init_by_lua和init_worker_by_lua阶段的,你在043-shdict.t的第一个测试案例中将content_by_lua修改为init_by_lua,执行测试报错,说明的是init_by_lua指令不能在location里,而不是说明共享内存API的问题。通过restydoc -s init_by_lua 查看,可知,init_by_lua指令的上下文只能是http。
    展开

    作者回复: 是的,确实写错了,抱歉,我去修改下。多谢指正。

    14
  • HelloBug
    2019-07-02
    温铭老师,你好~ 看了https://openresty.gitbooks.io/programming-openresty/content/testing里面的test-nginx文档。今天看到的部分有一个疑问,在RunningTest一节讲一个test file里的test block默认是shuffle方式随机运行的,以便在不同的执行顺序下可能出现的bug被遗漏。然后在PreparingTest一节里讲到,每个test block都会生成一个nginx.conf,生成nginx进程执行,执行下一个test block时,会关闭之前进程,重新启动新的进程。这样打乱test block和不打乱不就没有什么区别了吗?
    展开

    作者回复: 这是个好问题,我和你的感觉是一样的。我在自己的项目中都是用 no_shuffle,严格按照顺序来执行。 这是因为测试案例可能会依赖外部的持久化存储,比如 redis、memcached、postgres 等,那么这个时候随机运行测试就可能出错。 所以,我也不太理解为什么默认要用shuffle的方式运行。 我能想到的一个原因,可能是在把 TEST_NGINX_FORCE_RESTART_ON_TEST 设置为 0 的时候,也就是在配置文件不变化的时候不重启 nginx,这个时候随机就有意义了。 另外,如果TEST_NGINX_FORCE_RESTART_ON_TEST设置为 0, 并且 TEST_NGINX_BENCHMARK 设置为 1,那么就会在不重启和大压力的情况下去做随机测试。 test::nginx 有很多种组合的模式,我觉得有些设计的过于灵活和复杂了,个人见解,欢迎交流。 补充一句哈,还有 TEST_NGINX_USE_HUP 这个环境变量,可以保证测试案例间的 shared dict 不被清空。

    共 2 条评论
    6
  • 刘丹
    2019-07-01
    请问OpenResty的测试环境要怎样搭建?运行prove t1.t 命令出错: t1.t .. Can't locate Test/Nginx/Socket/Lua.pm in @INC (@INC contains: /usr/local/lib64/perl5 /usr/local/share/perl5 /usr/lib64/perl5/vendor_perl /usr/share/perl5/vendor_perl /usr/lib64/perl5 /usr/share/perl5 .) at t1.t line 2. BEGIN failed--compilation aborted at t1.t line 2. t1.t .. Dubious, test returned 2 (wstat 512, 0x200) No subtests run Test Summary Report ------------------- t1.t (Wstat: 512 Tests: 0 Failed: 0) Non-zero exit status: 2 Parse errors: No plan found in TAP output Files=1, Tests=0, 0 wallclock secs ( 0.02 usr + 0.00 sys = 0.02 CPU) Result: FAIL
    展开
    共 3 条评论
    1
  • 张仕华
    2019-07-01
    赞赞赞
    1
  • nil
    2022-05-05
    ```shell resty --shdict 'dogs 10m' \ -e 'local dogs = ngx.shared.dogs local v, err = dogs:get({}) if v == nil and err then ngx.say("not ok: ", err) else ngx.say("got: ", v) end' # got: ``` 没有报错,是不是现在 get API 接受 table 类型的入参了
    展开
  • nobia
    2020-12-31
    resty --shdict 'dogs 10m' -e 'local dogs = ngx.shared.dogs dogs:get({}) ' 运行这个没有报错 resty -V resty 0.27 nginx version: openresty/1.19.3.1 built by gcc 8.3.1 20191121 (Red Hat 8.3.1-5) (GCC) built with OpenSSL 1.1.1h 22 Sep 2020 (running with OpenSSL 1.1.1i 8 Dec 2020)
    展开
  • 纳兰容若
    2020-10-22
    麻烦问一下 安装完Test::Nginx 运行时提示:Bailout called. Further testing stopped: Failed to get the version of the Nginx in PATH
    共 1 条评论
  • scaat
    2019-12-02
    resty --shdict 'dogs 10m' -e 'local dogs = ngx.shared.dogs dogs:get({}) ' 在我的 nginx version: openresty/1.15.8.1 中,并没有出现报错。 请问老师,这是新版本 get 函数支持 key 的类型为数组了?还是我遗漏了什么?还望老师能指点迷津。
  • J.Smile
    2019-07-04
    呦呦切克闹
  • 😑
    2019-07-02
    温老师,如果多个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) ' 那么以后的课程中会讲解到op的锁吗?出现死锁怎么处理呢?
    展开

    作者回复: 不用自己加锁哈,shared dict 的操作都是原子性的,这种类似加锁的处理已经帮你考虑到了。

    共 2 条评论
  • 六维
    2019-07-02
    lua_shared_dict dogs 10m; 这一句应该放在哪里呢?按文中描述,放在www/conf/nginx.conf,通过http://www.chrono.com/demo访问,出现错误提示:2019/07/02 11:16:12 [error] 11184#3284: *3 lua entry thread aborted: runtime error: content_by_lua(http.conf:32):2: attempt to index global 'dogs' (a nil value) stack traceback: coroutine 0: content_by_lua(http.conf:32): in main chunk, client: 127.0.0.1, server: localhost, request: "GET /demo HTTP/1.1", host: "www.chrono.com"
    展开

    作者回复: lua_shared_dict dogs 10m 放在 nginx.conf 中是对的。你在代码中是如何引用 dogs 这个共享字典的呢?

    共 3 条评论
  • HelloBug
    2019-07-02
    通常通过本地调试、文档、网上搜索、邮件列表这些方式解决~
    共 2 条评论
  • manatee
    2019-07-01
    想请问一下老师讲openresty的官方文档是指哪里的内容呢?是github中每个模块readme的部分吗

    作者回复: 是的