openresty动态upstream实现

背景

公司现有应用的部署方式是所有的请求通过一个外网域名进行访问,在openresty内部通过location匹配来分离请求到不同的upstream。这样的配置方式带来的最大问题至少有两个:

  1. 配置膨胀。随着子应用接口的增加,location的配置会很复杂,不易阅读和新增配置。
  2. 每次新增和删除接口都需要修改所有openresty配置。这会带来额外的运维负担,而且会影响部分接口的稳定性。

那有没有一种方式可以动态的新增和删除需要跳转的uri呢? 这个就是下面我要提供的解决方案。

先来看一个示例配置,如下所示:

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
upstream www.a.com {
server 127.0.0.1:7001;
}

upstream www.b.com {
server 127.0.0.1:7002;
}

upstream www.abc.com {
server 127.0.0.1:7003;
}

server {
listen 80;
server_name www.abc.com;

charset utf-8;

access_log /data/logs/nginx/abc.access.log main;

location ~ (/a/hello.html)|(/a/world.html) {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://www.a.com;
}

location ~ (/b/hello.html)|(/b/world.html) {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://www.b.com;
}

location / {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://www.abc.com;
}
}

set_by_lua_file

该解决方案的原理是通过set_by_lua_file指令计算需要动态upstream的名称,然后在proxy_pass中引用该变量。具体请看如下的配置和lua代码:

1
2
3
4
5
### 生产环境需要开启:on.
lua_code_cache off;
lua_package_path '/path/to/lua/module/search/path';
lua_shared_dict rewrite_conf 10m;
init_by_lua_file init.lua;

init.lua 用来初始化全局配置。在这里我们用来读取动态配置的uri

init.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
local cjson = require "cjson"
local rewrite_conf = ngx.shared.rewrite_conf;

file = io.input("rewrite.json") -- 使用 io.input() 函数打开文件

local jsonCfg = ""
repeat
trueline = io.read() -- 逐行读取内容,文件结束时返回nil
trueif nil == line then
truetruebreak
trueend
truejsonCfg = jsonCfg..line
until (false)

io.close(file) -- 关闭文件

rewrite_json = cjson.decode(jsonCfg)

for k,v in pairs(rewrite_json) do
truerewrite_conf[k] = v
end

rewrite.json文件的内容如下所示:

rewrite.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
true"www.abc.com" : {
truetrue"rewrite_urls" : [
truetruetrue{
truetruetruetrue"uri" : "/a/hello.html",
truetruetruetrue"rewrite_upstream" : "www.a.com"
truetruetrue},
truetruetrue{
truetruetruetrue"uri" : "/b/hello.html",
truetruetruetrue"rewrite_upstream" : "www.b.com"
truetruetrue}
truetrue]
true}
}

下面就是我们简化后的location配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
server {
listen 80;
server_name www.abc.com;

charset utf-8;

access_log /data/logs/nginx/abc.access.log main;

location / {
## 动态计算upsteam的名字
set_by_lua_file $my_upstream set.lua;

proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://$my_upstream;
}
}
set.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-- 动态设置				
local cjson = require "cjson"

-- 执行 rewrite 判断
local rewrite_conf = ngx.shared.rewrite_conf
local host = ngx.var.http_host
local uri = ngx.var.uri

local default_upstream = host

if rewrite_conf[host] ~= nil then
truefor i, elem in ipairs(rewrite_conf[host]['rewrite_urls']) do
truetrueif uri == elem['uri'] then
truetruetruedefault_upstream = elem['rewrite_upstream']
truetruetruebreak
truetrueend
trueend
end

ngx.log(ngx.INFO, "default_upstream="..default_upstream)
return default_upstream

到此,整个方案就算完成了。但是还不够完美。因为没有实现动态的效果。

要实现动态的效果其实很简单,我这里有个建议:

  1. 通过将动态配置的uri信息,也就是rewrite.json的内容放置的Redis中。
  2. 提供一个管理API接口,用来读取Redis中的最新配置并更新ngx.shared.rewrite_conf的值。

当然了,如果配置更新了,但你不想主动调用该管理API接口。你也可以通过ngx.timer.at来设置定时任务来主动获取Redis的配置。

方案二 ngx.location.capture

先介绍一下指令ngx.location.capture的作用:

语法: res = ngx.location.capture(uri, options?)
上下文: rewrite_by_lua, access_by_lua, content_by_lua*
向 uri 发起一个同步非阻塞 Nginx 子请求。

Nginx 子请求是一种非常强有力的方式,它可以发起非阻塞的内部请求来访问目标location。目标 location 可以是配置文件中其它文件目录,或任何其它 nginx C 模块,包括 ngx_proxyngx_fastcgingx_memcngx_postgresngx_drizzle,甚至 ngx_lua 自身等等 。

需要注意的是,子请求只是模拟 HTTP 接口的形式,没有额外的 HTTP/TCP 流量,也没有 IPC (进程间通信) 调用。所有工作在内部高效地在 C 语言级别完成。

子请求与 HTTP 301/302 重定向指令 (通过 ngx.redirect) 完全不同,也与内部重定向 ((通过 ngx.exec) 完全不同。

在发起子请求前,用户程序应总是读取完整的 HTTP 请求体 (通过调用 ngx.req.read_body 或设置 lua_need_request_body 指令为 on).

该 API 方法(ngx.location.capture_multi 也一样)总是缓冲整个请求体到内存中。因此,当需要处理一个大的子请求响应,用户程序应使用 cosockets 进行流式处理,

实现方案

参考 用openresty实现动态upstream

该方法的实现可以做的灵活,功能也可以更强大。当然缺点是你需要做非常多的工作。例如:获取元素请求的参数,请求的HTTP方法类型,然后设置请求的URI请求参数,请求体(可以通过always_forward_body避免)等。

参考文档

lua-nginx-module

文章目录
  1. 1. 背景
  2. 2. set_by_lua_file
  3. 3. 方案二 ngx.location.capture
    1. 3.1. 实现方案
  4. 4. 参考文档
|