openresty 执行阶段问题踩坑

背景

由于业务需求我们需要根据client_ip对部分接口做一个简单的 ab test,将部分流量导新的服务上。基于之前做 WAF 的经验,制定了大体方案:根据 client_ip 做 hash, 根据 hash 值来确定是请求新后端还是原来的后端服务。
在说明详细方案前,先说明一下现状。当前的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
location ~ ^/api/rest/v\d+/Login {
rewrite ^(.*)$ /loginServer$1 break;

proxy_http_version 1.1;
proxy_next_upstream error timeout invalid_header;
proxy_redirect off;

proxy_set_header Host $http_host;
proxy_set_header Clientip $http_clientip;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Connection "";

proxy_pass http://test.fanruo.net;
}

其中,/loginServer是当前服务路径前缀,/loginServerNew是新服务路径前缀。所以,具体需求可以描述为,10% 的用户使用/loginServerNew服务,90% 的用户使用/loginServer服务。

具体方案

  1. 根据字符串 client_ip 转成长整数 ip_num;
  2. 对 ip_num 按 100 取模,取最后两位 hash;
  3. 当 hash < 10 时走/loginServerNew服务, 设置 tag="New", 否则 tag=""
  4. 改造rewrite指令为 rewrite ^(.*)$ /loginServer$tag$1 break;

对应配置为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
location ~ ^/api/rest/v\d+/Login {
set $tag "";
access_by_lua_file conf/lua/abtest.lua;
rewrite ^(.*)$ /loginServer$tag$1 break;

proxy_http_version 1.1;
proxy_next_upstream error timeout invalid_header;
proxy_redirect off;

proxy_set_header Host $http_host;
proxy_set_header Clientip $http_clientip;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Connection "";

proxy_pass http://test.fanruo.net;
}

对应 lua 脚本(conf/lua/btest.lua):

1
2
3
4
5
6
7
8
9
local client_ip = ngx.var.http_clientip or ngx.var.remote_addr
local o1,o2,o3,o4 = client_ip:match("(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)" )
local ip_num = 2^24*o1 + 2^16*o2 + 2^8*o3 + o4
local hash = ip_num % 100

ngx.var.tag = ""
if hash > 10 then
ngx.var.tag = "New"
end

问题

按照上述实现,最终总是走原来的服务/loginServer。通过分析 tag 的值一直是 “”,因此,需求不能满足。

原因分析

通过分析,由于之前 WAF 是通过直接修改 proxy_pass 参数来完成的,而 proxy_pass 对应 content phase,该阶段在 access phase 之后,因此上面的配置没有问题。而本需求是在 rewrite phase,因此需要在该阶段之前修改对应变量 tag 只能在 set phase 来完成 nginx 变量修改操作。

解决方案

跟进上述分析,修改 nginx.conf 配置和 lua 脚本如下:

lua 脚本修改为:

1
2
3
4
5
6
7
8
9
10
11
local client_ip = ngx.var.http_clientip or ngx.var.remote_addr
local o1,o2,o3,o4 = client_ip:match("(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)" )
local ip_num = 2^24*o1 + 2^16*o2 + 2^8*o3 + o4
local hash = ip_num % 100

local tag = ""
if hash > 10 then
tag = "New"
end

return tag

nginx.conf 配置修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
location ~ ^/api/rest/v\d+/Login {
set_by_lua_file $tag conf/lua/abtest.lua;
rewrite ^(.*)$ /loginServer$tag$1 break;

proxy_http_version 1.1;
proxy_next_upstream error timeout invalid_header;
proxy_redirect off;

proxy_set_header Host $http_host;
proxy_set_header Clientip $http_clientip;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Connection "";

proxy_pass http://test.fanruo.net;
}