本文简单介绍如何通过 lua 脚本和 ngx_shared_dict 在 nginx 中动态加载后端服务配置以及动态更新服务配置.
nginx
加载 lua 脚本
在 Nginx 中需要引入和加载 lua 脚本,从而在路由转发时运行 lua 脚本进行我们的逻辑。初始化代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
http {
lua_shared_dict endpoints_data 5m; #定义upstream共享内存空间
lua_shared_dict cache 1m; #定义计数共享空间
access_log nginx_access.log;
lua_package_path "/etc/nginx/lua/?.lua;;";
init_by_lua_block {
collectgarbage("collect")
local ok, res
# 加载脚本 configuration.lua
ok, res = pcall(require, "configuration")
if not ok then
error("require failed: " .. tostring(res))
else
configuration = res
end
}
# 执行脚本内初始化方法,这里为可选项,如果没有可初始化的代码部分 这里可以不要
init_worker_by_lua_block {
configuration.prepare()
}
}
|
执行 lua 脚本
如何在 Nginx 配置中执行 lua 脚本,从而实现一些特殊逻辑?这里给出一个简单的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
server {
# 执行最简单的 lua 脚本
location /hello {
default_type 'text/plain';
content_by_lua 'ngx.say("hello, lua")';
}
# 配置接口
# 这里是执行加载的 lua 脚本中方法
location /configuration {
client_max_body_size 5m;
client_body_buffer_size 1m;
proxy_buffering off;
content_by_lua_block {
configuration.call() # 调用 call() 方法
}
}
# 执行较为复杂的 lua 逻辑
location /lua {
default_type 'text/plain';
# 读取请求中的 path 参数 并从共享 dict 中查询这个值,
# 返回查询到的结果
content_by_lua '
local path = ngx.req.get_uri_args()["path"]
if path == nil then
ngx.say("path not found")
return
end
local data = ngx.shared.endpoints_data:get("/"..path)
if not data then
ngx.say("unkonw path")
return
end
ngx.say("paths: "..data)
';
}
}
|
lua
的语法相对简单好上手,实现一些简单的逻辑也很方便,非常值得学习。
完整配置
先给出 Nginx 的完整配置,里面包括动态配置后端服务列表和动态加载服务转发的逻辑,然后再给出 lua 部分详细实现的代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
|
user nginx;
worker_processes 1;
pid /var/run/nginx.pid;
error_log nginx_error.log;
events {
worker_connections 1024;
}
http {
lua_shared_dict endpoints_data 5m; #定义upstream共享内存空间
lua_shared_dict cache 1m; #定义计数共享空间
access_log nginx_access.log;
lua_package_path "/etc/nginx/lua/?.lua;;";
init_by_lua_block {
collectgarbage("collect")
local ok, res
ok, res = pcall(require, "configuration")
if not ok then
error("require failed: " .. tostring(res))
else
configuration = res
end
}
# 执行脚本内初始化方法,这里为可选项,如果没有可初始化的代码部分 这里可以不要
init_worker_by_lua_block {
configuration.prepare()
}
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
server {
# 执行最简单的 lua 脚本
location /hello {
default_type 'text/plain';
content_by_lua 'ngx.say("hello, lua")';
}
# 配置接口
# 这里是执行加载的 lua 脚本中方法
location /configuration {
client_max_body_size 5m;
client_body_buffer_size 1m;
proxy_buffering off;
content_by_lua_block {
configuration.call() # 调用 call() 方法
}
}
# 执行较为复杂的 lua 逻辑
location /lua {
default_type 'text/plain';
# 读取请求中的 path 参数 并从共享 dict 中查询这个值,
# 返回查询到的结果
content_by_lua '
local path = ngx.req.get_uri_args()["path"]
if path == nil then
ngx.say("path not found")
return
end
local data = ngx.shared.endpoints_data:get("/"..path)
if not data then
ngx.say("unkonw path")
return
end
ngx.say("paths: "..data)
';
}
# other path
location / {
set $load_ups "";
# 动态设置当前 upstream, 未找到返回404
rewrite_by_lua '
local ups = configuration.getEndpoints()
if ups ~= nil then
ngx.log(ngx.ERR,"got upstream", ups)
ngx.var.load_ups = ups
return
end
ngx.status = ngx.HTTP_NOT_FOUND
ngx.exit(ngx.status)
';
proxy_pass http://$load_ups$uri;
add_header X-Upstream $upstream_addr always; # 添加 backend ip
}
}
}
|
lua
定义变量
因为需要用到 shared_dict 特性,在 lua 和 Nginx 之间公用内存块 从而实现数据的同步共享,所以需要预定义一些变量。
1
2
3
4
5
6
7
8
|
-- 引入变量
local io = io
local ngx = ngx
local table = table
-- 当前包的对象,类似 go 语言的定义结构体 让给这个结构体实现方法
local _M = {}
-- 与 Nginx 共享的空间 可读写
local Endpoints = ngx.shared.endpoints_data
|
动态更新服务列表
服务列表是通过被调接口实现,即有别的服务区监听服务节点(endpoint)的变化,然后调用/configuration/backends
接口,被 Nginx 配置的 /configuration
规则命中后调用 configuration.call()
方法,我们看一下这个 call
方法的实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
-- call called by ngx
function _M.call()
-- 只处理 GET 和 POST
if ngx.var.request_method ~= "POST" and ngx.var.request_method ~= "GET" then
ngx.status = ngx.HTTP_BAD_REQUEST
ngx.print("Only POST and GET requests are allowed!")
return
end
-- 目前只处理后端服务的配置 所以判断路由
if ngx.var.request_uri == "/configuration/backends" then
-- 调用内部方法
handle_backends()
return
end
-- 非法请求 返回 404
ngx.status = ngx.HTTP_NOT_FOUND
ngx.print("Not found!")
end
|
多说一句,调用 /configuration/backends
时传参是在请求 body 里,格式为 json
所以需要引入第三方的 json 解析包。handle_backends
方法的实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
-- handle_backends .
local function handle_backends()
if ngx.var.request_method == "GET" then
ngx.status = ngx.HTTP_OK
-- 返回查询的服务列表
local path = ngx.req.get_uri_args()["path"]
ngx.print(Endpoints:get("path"))
return
end
-- 读取请求 body
local obj = fetch_request_body()
if not obj then
ngx.log(ngx.ERR, "dynamic-configuration: unable to read valid request body")
ngx.status = ngx.HTTP_BAD_REQUEST
return
end
-- 通过 第三方包 json 解析 body到 lua table
local rule, err = json.decode(obj)
if not rule then
ngx.log(ngx.ERR, "could not parse backends data: ", err)
return
end
ngx.log(ngx.ERR, "decoed rule", obj)
-- 清空共享空间
Endpoints:flush_all()
-- 遍历并写入
for _, new_rule in ipairs(rule.rules) do
-- 更新
-- 将数组合并
local succ, err1, forcible = Endpoints:set(new_rule.path, table.concat(new_rule.upstreams, ","))
ngx.log(ngx.ERR, "set result", succ, err1,forcible)
end
ngx.status = ngx.HTTP_CREATED
ngx.say("ok")
end
-- 读取请求 body 部分
local function fetch_request_body()
ngx.req.read_body()
local body = ngx.req.get_body_data()
if not body then
-- request body might've been written to tmp file if body > client_body_buffer_size
local file_name = ngx.req.get_body_file()
local file = io.open(file_name, "rb")
if not file then
return nil
end
body = file:read("*all")
file:close()
end
return body
end
|
请求 body 的 json 结构如下:
1
2
3
4
5
6
7
8
|
type NginxRuleConf struct {
Rules []struct{
Path string `json:"path"`
ServiceName string `json:"serviceName"`
Port int32 `json:"-"`
Upstreams []string `json:"upstreams"`
} `json:"rules"`
}
|
动态读取后端服务
上面已经通过接口的方式动态更新服务节点列表并写入到共享空间 endpoints_data
内,我们现在实现读取服务列表并选择其中一个节点进行接口转发。
代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
-- 轮顺的方式取节点
function _M.getEndpoints()
local cache = ngx.shared.cache
local path = ngx.var.request_uri
local eps = Endpoints:get(path)
if not eps then
return nil
end
local tab = split(eps,",")
local index = cache:get(path)
if index == nil or index > #tab then
index = 1
end
-- 加一
cache:set(path,index+1)
return tab[index]
end
|
结论
至此实现的效果是,可以动态配置多个后端服务和后端服务节点列表,外部服务请求 Nginx 时,会尝试从已有的服务中匹配转发,如果服务有多个节点则轮顺的方法去转发。如有服务信息发生变化,则通过调用 Nginx 中配置的 configuration
接口更新即可,无需修改 Nginx 配置。