在之前的文章中,我们深入讨论了 Nginx Reload 这一难题,并探讨了可能的解决方案。我为此设计了一个通用的解决方案,名为 Utopia 框架,让开发者能够基于此框架,通过编写 Lua 脚本来满足其业务需求。
Utopia框架设计图
先说核心思路:
无需修改 Nginx 源代码。
无需更改 Nginx 任何配置指令。
使用 Lua 实现常见 Nginx 功能。
更新的是 Lua 对应的配置。
简而言之,通过 Lua 脚本重新掌控 Nginx 常见功能,如路由、响应头设置、上游服务器管理等等。甚至可以添加 Nginx 本身不具备>的功能,如 JWT 鉴权等。
实现方式
首先,我们需要使 Nginx 支持 Lua 开发,这就需要开发一个 Nginx Lua C 模块。通过这个模块,可以让 Lua 脚本在 Nginx 进程内>运行,开发者可以编写 Lua 脚本来控制 HTTP 请求。这正是 nginx-http-lua-module 所实现的功能。它用于与 Nginx 进行交互,提供诸如 r.uri、r.body、r.set_header(name, value) 等操作请求的方法,还内置了 ngx.json_encode()、ngx.json_decode() 等常用接口。
接下来是 Lua 部分,我们需要进行配置管理和配置使用。
配置管理的目的是将 Lua 配置更新到 Nginx 进程内,使运行在该进程中的 Lua 模块能够直接使用配置。由于我们目前尚未实现实时>刷新,因此通过定时器的机制来定期同步更新配置。
配置使用则是通过 Lua 脚本控制 HTTP 请求,例如 IP 黑名单、路由处理、响应头设置等。
为什么这样设计
现代软件通常通过 HTTP 接口来更新配置,这些配置可以存储在文件、共享内存或第三方服务中。但不管使用何种存储方式,只需要提供一种对外统一的 HTTP 接口。用户可以通过 curl 命令行工具或仪表板界面来更新配置。这即是配置管理的一部分。
一旦配置更新,由于 NGINX 采用多进程架构,为了性能考虑,应当确保每个进程都能快速访问这些配置数据。因此,配置应定期更新>到每个进程内。最理想的情况是能够实现秒级刷新,这完全是可能的。换句话说,配置应当在每个进程内生效。当 HTTP 请求到达各个 NGINX 进程时,Nginx 将执行 Lua 代码,Lua 代码将访问配置数据并进行相应的处理。这即是配置使用的部分。这一设计架构简洁清晰,与NGINX社区的风格相符。
如何设计配置
首先,配置采用 JSON 格式,与 RESTful 风格保持一致,同时也借鉴了我在 Nginx Unit 开发中的经验。
其次,对于 HTTP 服务器而言,最核心的部分就是路由。一切都始于路由,一切操作都基于路由发生。因此,我们需要设计一个易于使用、简洁高效的路由系统。
示例配置如下:
{
"routes": [
{
"match": {
"uri": "/baz",
"host": "example.com"
},
"action": {
"backend": "b1",
"set_headers": {
"X-Foo": "foo"
},
"ip_blacklist": [
"192.168.1.1",
"10.0.0.2",
"172.16.0.0/24"
]
}
}
],
"backends": {
"b1": {
"policy": "round_robin",
"servers": [
"127.0.0.1:8090",
"127.0.0.1:8082"
]
}
}
}
routes是个数组,包含多条route。每个route由match和action组成。
match定义了匹配规则,这跟nginx的location很像,但更灵活。
action定义了执行动作,可能是ip黑名单,头部响应设置,或者后端服务。
backends类似nginx的upstreams。
配置管理
我们会将所有lua代码放进utopia这样的目录下。
首先在nginx里提供更新配置的http接口:
server {
listen 8000;
location /config {
lua_script "require('utopia').config(r)";
}
location /timer {
lua_timer "require('utopia').timer()";
}
}
/config用于更新配置,使用如下:
curl -X POST -d@/path/conf.json http://127.0.0.1:8000/config
/timer用于定时同步配置,这个在将来可以优化掉,做到实时更新。
所有的Lua代码都运行在NGINX进程里,配置更新进进程里的Lua模块后,Lua代码就能正常使用配置,而不用做类似nginx reload这样的操作,以达到热加载的效果。
比如头部响应设置的配置如何生效的:
function _M.set_header_init(action, headers)
action.set_headers = headers;
end
配置使用
utopia框架会实现通用的功能,比如路由处理,这也是最核心的部分。任何请求都会先进入路由部分,根据配置里的match规则,找到对应的action。然后在处理部分就可以直接用action对应的数据,比如头部响应设置的代码如下:
local function set_header_handler(r, action)
for name, value in pairs(action.set_headers) do
r.set_header(name, value);
end
end
完整示例
我们以一个例子演示用这个框架怎么实现IP黑名单拦截的。
1. 制定配置语法,我们用"ip_blacklist"作为IP黑名单的配置,比如:
"ip_blacklist": ["ip", "ip", ...]
{
"routes": [
{
"match": {
"uri": "/baz"
},
"action": {
"ip_blacklist": [
"192.168.1.1",
"10.0.0.2",
"172.16.0.0/24"
]
}
}
]
}
2.配置验证
local function conf_vldt_ip_blacklist(value)
for _, ip in ipairs(value) do
if (type(ip) ~= "string") then
return "ip blacklist value must be a string"
end
end
return false;
end
local route_action_members = {
ip_blacklist = {
type = CONF.ARRAY,
validator = conf_vldt_ip_blacklist,
},
...
};
3. 配置生效
if (action['ip_blacklist']) then
route.action.ip_blacklist = action['ip_blacklist'];
end
4. 配置使用
local function ip_blacklist_handler(r, action)
for _, ip in ipairs(action.ip_blacklist) do
if (r.client_ip == ip) then
r.exit(405);
end
end
end
如果没完全理解,等这个开源后体验下就很容易理解了。
关于性能:
我们的设计原则是用Lua只写业务逻辑,算法和数据结构等跟性能有关的,用C去实现,这些隐藏在nginx lua c模块里。举个例子,有些开发者用Lua实现json的解析,我们会将这种常见的功能放在 C模块里,然后提供接口来使用,比如ngx.json_encode,ngx.json_decode。这样性能很高了。
关于易用性:
nginx-http-lua-module是整个的基础,它的易用性决定了整个utopia框架的使用门槛。还是以json为例,我将引入ngx.json_encode, ngx.json_decode这样的接口,这样开发者不用编译安装和维护cjson这样的第三方库。
因此我们的目标是提供高性能和易用的框架,以另一种方式解决nginx reload问题。
未来展望
我们要实现nginx-http-lua-module必要的功能:
请求对象:r.uri, r.method, r.body, r.echo(), r.exit().
nginx对象:ngx.json_encode, ngx.json_decode().
https://github.com/hongzhidao/nginx-http-lua-module
接着我们要实现lua utopia框架:
配置验证、路由功能、并且以ip黑名单作为示例
最后实现常用功能支持进来,头部响应设置,upstreams管理等。
这样开发者可以不用重造常用轮子。
预告后面文章:开发内置JSON功能,分享有关原创能力的东西。
关注公众号,不错过这个框架的创作过程。(公众号:程序员洪志道)