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.

170 lines
9.0 KiB
Markdown

2 years ago
# 45 | 不得不提的能力外延OpenResty常用的第三方库
你好,我是温铭。
对于开发语言和平台来讲,很多时候的学习,其实是对标准库和第三方库的学习,语法本身并不用花费很多的时间。这对 OpenResty 来说也是一样的,学完它自己的 API 和性能优化技巧后,就需要各种 lua-resty 库,来帮助我们把 OpenResty 的能力外延,以应用到更多的场景中去。
## 去哪里找 lua-resty 库?
和 PHP、Python、JavaScript 相比,当前 OpenResty 的标准库和第三方库还比较贫瘠,找出合适的 lua-resty 库还不是一件容易的事情。不过,这里仍然有两个推荐的渠道,可以帮你更快地找到它们。
我首先推荐的是由 Aapo 维护的 `awesome-resty` [仓库](https://github.com/bungle/awesome-resty),这个仓库分门别类地整理了和 OpenResty 相关的库,可以说是包罗万象,包括了 Nginx 的 C 模块、lua-resty 库、Web 框架、路由库、模板、测试框架等,是你寻找 OpenResty 资源的首选。
当然,如果你在 Aapo 的仓库中没有找到合适的库,那么还可以去 luarocks、opm和 GitHub 碰碰运气。有一些开源时间不长的、或者关注不多的库,可能就藏在其中。
在前面的课程中,我们已经接触了不少有用的库,比如 lua-resty-mlcache、lua-resty-traffic、lua-resty-shell 等。今天,在 OpenResty 性能优化部分的最后一节课,我们再来认识 3 个独具特色的周边库,它们都是由社区的开发者贡献的。
## ngx.var 的性能提升
首先让我们来看一个 C 模块:[lua-var-nginx-module](https://github.com/iresty/lua-var-nginx-module)。前面我曾经提到过,`ngx.var` 是一个性能损耗比较大的操作,在实际使用时,我们需要用 `ngx.ctx` 来做一层缓存。
那有没有什么方法,可以彻底解决 `ngx.var` 的性能问题呢?
这个 C 模块,就在这个方面做了一些尝试,效果也很显著,性能比起`ngx.var` 提升了 5 倍。它采用的是 FFI 的方式,所以,你需要在编译 OpenResty 的时候,先加上编译选项:
```
./configure --prefix=/opt/openresty \
--add-module=/path/to/lua-var-nginx-module
```
然后,使用 luarocks 的方式来安装 lua 库:
```
luarocks install lua-resty-ngxvar
```
这里调用的方法也很简单,只需要一行 `fetch` 函数的调用就可以了。它的效果完全等价于原有的 `ngx.var.remote_addr`,来获取到终端的 IP 地址:
```
content_by_lua_block {
local var = require("resty.ngxvar")
ngx.say(var.fetch("remote_addr"))
}
```
知道了这些基本操作后,你可能更好奇的是,这个模块到底是怎么做到性能大幅度提升的呢?还是那句老话,源码面前无秘密,就让我们来看看 `remote_addr` 这个变量在其中是如何获取的吧:
```
ngx_int_t
ngx_http_lua_var_ffi_remote_addr(ngx_http_request_t *r, ngx_str_t *remote_addr)
{
remote_addr->len = r->connection->addr_text.len;
remote_addr->data = r->connection->addr_text.data;
return NGX_OK;
}
```
阅读这段代码后,你会发现,这种 Lua FFI 的方式和 lua-resty-core 的做法如出一辙。它的优点很明显,使用 FFI 的方式来直接获取变量,绕过了 `ngx.var` 原有的查找逻辑;同时,缺点也很明显,那就是要为每一个希望获取的变量,都增加对应的 C 函数和 FFI 调用,这其实是一个体力活。
有人可能会问,我为什么会说这是体力活呢?上面的 C 代码看上去不是还挺有含量的吗?我们不妨来看看这几行代码的源头,它们出自 Nginx 代码中的 `src/http/ngx_http_variables.c`
```
static ngx_int_t
ngx_http_variable_remote_addr(ngx_http_request_t *r,
ngx_http_variable_value_t *v, uintptr_t data)
{
v->len = r->connection->addr_text.len;
v->valid = 1;
v->no_cacheable = 0;
v->not_found = 0;
v->data = r->connection->addr_text.data;
return NGX_OK;
}
```
看到源码后,谜底揭开了!`lua-var-nginx-module` 其实是 Nginx 变量代码的搬运工,并在外层做了 FFI 的封装,用这种方式达到了性能优化的目的。这其实也是一个很好的思路和优化方向。
这里我再多说几句,我们学习某个库或者某个工具,一定不要仅仅停留在操作使用的层面,还应该多问问为什么,多看看源码,在底层原理的层面上,我们才能学到更多的设计思想和解决思路。当然,我也非常鼓励你去贡献代码,以支持更多的 Nginx 变量。
## JSON Schema
下面我介绍的是一个 lua-resty 库:[lua-rapidjson](https://github.com/xpol/lua-rapidjson) 。它是对 `rapidjson` 这个腾讯开源的 JSON 库的封装,以性能见长。这里,我们着重介绍下它和 `cjson` 的不同之处,也就是支持 JSON Schema。
JSON Schema 是一个通用的标准,借助这个标准,我们就可以精确地描述接口中参数的格式,以及如何校验的问题。下面是一个简单的示例:
```
"stringArray": {
"type": "array",
"items": { "type": "string" },
"minItems": 1,
"uniqueItems": true
}
```
这段 JSON 准确地描述了 `stringArray` 这个参数的类型是字符串数组,并且数组不能为空,数组元素也不能重复。
而`lua-rapidjson`,则是可以让我们在 OpenResty 中来使用 JSON Schema这能给接口的校验带来极大的便利。举个例子比如对于前面介绍过的 limit count 限流接口,我们就可以用下面的 schema 来描述:
```
local schema = {
type = "object",
properties = {
count = {type = "integer", minimum = 0},
time_window = {type = "integer", minimum = 0},
key = {type = "string", enum = {"remote_addr", "server_addr"}},
rejected_code = {type = "integer", minimum = 200, maximum = 600},
},
additionalProperties = false,
required = {"count", "time_window", "key", "rejected_code"},
}
```
你会发现,这可以带来两个十分明显的收益:
* 对前端来说,前端可以直接复用这个 schema 描述,用于前端页面的开发和参数校验,而不用再去关心后端;
* 而对后端来说,后端直接使用 `lua-rapidjson` 的 schema 校验函数 `SchemaValidator` 就能完成接口合法性的判断,更是无须编写多余的代码。
## worker 间通信
最后,我要讲的是可以实现 OpenResty 中 worker 间通信的 [lua-resty](https://github.com/Kong/lua-resty-worker-events) 库。OpenResty 的 worker 之间,并没有机制可以直接通信,这显然会带来不少的问题。让我们设想这么一个场景:
> 一个 OpenResty 服务有 24 个 worker 进程,管理员通过 REST HTTP 接口更新了系统的某项配置,这时候只有一个 worker 收到了管理员的更新操作,并把结果写入了数据库,更新了共享字典和自己 worker 内的 lru 缓存。那么,其他 23 个 worker 怎么才能被通知去更新这项配置呢?
显然,多个 worker 之间需要一个通知的机制,才能完成上面的这个任务。在 OpenResty 自身不支持的情况下,我们就只能通过共享字典这个跨 worker 可以访问的空间,来曲线救国了。
`lua-resty-worker-events` 便是这个思路的具体实现。它在共享字典中维护了一个版本号,在有新消息需要发布的时候,给这个版本号加一,并把消息内容放到以版本号为 key 的字典中:
```
event_id, err = _dict:incr(KEY_LAST_ID, 1)
success, err = _dict:add(KEY_DATA .. tostring(event_id), json)
```
同时,在后台使用 `ngx.timer` 创建了一个默认间隔为 1 秒的 polling 循环,来不断地检测版本号是否有变化:
```
local event_id, err = get_event_id()
if event_id == _last_event then
return "done"
end
```
这样,一旦发现有新的事件通知需要处理时,就根据版本号从共享字典中获取消息内容:
```
while _last_event < event_id do
count = count + 1
_last_event = _last_event + 1
data, err = _dict:get(KEY_DATA..tostring(_last_event))
end
```
总的来说,虽然 `lua-resty-worker-events` 会有 1 秒钟的延时,但还是实现了 worker 之间的事件通知机制,瑕不掩瑜。
不过在一些实时性要求比较高的场景下比如消息推送OpenResty 缺少 worker 进程间直接通信的这个问题,就可能会给你带来一些困扰了。这一点目前没有更好的解决方案,如果你有好的想法,欢迎在 Github 或者 OpenResty 的邮件列表中来一起探讨。OpenResty 的很多功能都是由社区用户来驱动的,这样才能构造一个良性的生态循环。
## 写在最后
今天我们介绍的这三个库,都各具特色,也都为 OpenResty 的应用带来了更多的可能性。最后是一个互动话题,你是否发现过一些 OpenResty 周边有意思的库呢或者对于今天提到的这几个库你有什么发现或疑惑呢欢迎留言和我分享也欢迎你把这篇文章发给你身边的OpenResty使用者一起交流和进步。