NGINX Plus R13 引入了面向 HTTP 流量的键值(key-value)存储功能,NGINX Plus R14 又将这一功能扩展到了 TCP/UDP(流)流量。该功能提供了一个 API 来动态维护键值,这些键值可用作 NGINX Plus 配置的一部分,并且无需重新加载配置。该功能有许多潜在的用例,我相信我们的客户会找到各种利用它的方法。
本文描述了一种用例,即动态地更改使用 Split Clients 模块进行 A/B 测试的方式。
键值(Key-Value)存储
NGINX Plus API 可用于维护一组允许 NGINX Plus 在运行时访问的键值对。我们来看这样一个用例,假如您想要保存一个客户端 IP 地址的拒绝列表,不允许该列表中的 IP 地址访问您的网站(或特定 URL)。
这里的key就是客户端 IP 地址,该地址在 $remote_addr
变量中捕获。而value是一个名为 $denylist_status
的变量,该变量设置为 1
,表示客户端 IP 地址已被加入拒接列表,设置为 0
则表示未被加入。
配置步骤如下:
- 创建共享内存区,以存储键值对(
keyval_zone
指令) - 为共享内存区命名
- 指定要为其分配的最大内存量
- 指定一个状态文件来存储条目,以便它们在 NGINX Plus 重启过程中维持不变(此为可选项)
我们之前已经为状态文件创建了 /etc/nginx/state_files 目录,并且可由运行了 NGINX worker 进程(由配置中其他位置的 user
指令定义)的未授权用户写入。此处,我们在 keyval_zone
指令中添加了 state
参数,以创建用于存储键值对的文件 denylist.json:
keyval_zone zone=denylist:64k
state=/etc/nginx/state_files/denylist.json;
在 NGINX Plus R16 及后续更新的版本中,我们可以利用两个额外的键值功能:
- 将
timeout
参数添加到keyval_zone
指令,为键值存储中的条目设置过期时间。例如,要拒绝地址两个小时,则添加timeout=2h
。 - 通过将
sync
参数添加到keyval_zone
指令,跨 NGINX Plus 实例集群同步键值存储。在这种情况下也必须添加timeout
参数。
因此,在我们的示例中,为了使用被拒绝两小时的 IP 地址的同步键值存储,指令将变为:
keyval_zone zone=denylist:64k timeout=2h sync
state=/etc/nginx/state_files/denylist.json;
有关键值存储同步设置的详细说明,请参见《NGINX Plus 管理指南》。
接下来,我们添加 keyval
指令来定义键值对。我们将 key 指定为客户端 IP 地址 ($remote_addr
),并将 value 分配给 $denylist_status
变量:
keyval $remote_addr $denylist_status zone=denylist;
如要在键值存储中创建键值对,请使用 HTTP POST
请求。例如:
# curl -iX POST -d '{"10.11.12.13":1}' http://localhost/api/3/http/keyvals/denylist
如要在现有键值对中修改 value ,请使用 HTTP PATCH
请求。例如:
# curl -iX PATCH -d '{"10.11.12.13":0}' http://localhost/api/3/http/keyvals/denylist
如要删除键值对,请使用 HTTP PATCH
请求将值设为 null
。例如:
# curl -iX PATCH -d '{"10.11.12.13":null}' http://localhost/api/3/http/keyvals/denylist
使用 Split Clients 进行 A/B 测试
Split Clients 模块允许您根据所选的请求特征在上游组之间分割入站流量。您将分割定义为被转发到不同上游组的入站流量的百分比。一个常见的用例是测试应用新版本,具体操作方法是向新版本发送一小部分流量,然后将剩余部分流量发送给当前版本。在我们的示例中,我们将 5% 的流量发送到新版本 appversion2 的上游组,剩余 (95%) 流量则发送到当前版本 appversion1。
我们根据请求中的客户端 IP 地址分配流量,因此我们将 split_clients
指令中的第一个参数设置为 NGINX 变量 $remote_addr
。我们将第二个参数中的变量 $upstream
设置为上游组的名称。
以下是基本配置:
split_clients $remote_addr $upstream {
5% appversion2;
* appversion1;
}
upstream appversion1 {
# ...
}
upstream appversion2 {
# ...
}
server {
listen 80;
location / {
proxy_pass http://$upstream;
}
}
搭配使用键值(Key-Value)存储和 Split Clients
在 NGINX Plus R13 之前,如要修改分割的比例,就必须编辑配置文件并重新加载配置。而借助键值存储,您只需更改键值对中存储的百分比,分割比例也会相应地更改,而无需重新加载配置。
在上一节中用例的基础上,假设我们决定让 NGINX Plus 支持将以下比例的流量发送到 appversion2
:0%、5%、10%、25%、50% 和 100%。我们还希望根据 Host
标头(在 NGINX 变量 $host
中捕获)进行分割。以下 NGINX PLUS 配置实现了此功能。
首先,我们设置键值存储:
keyval_zone zone=split:64k state=/etc/nginx/state_files/split.json;
keyval $host $split_level zone=split;
如初始用例中所述,在实际部署中,基于请求特征(例如客户端 IP 地址,$remote_addr
)进行分割是可行的。但在使用 curl
等工具的简单测试中,所有请求都来自单个 IP 地址,因此没有需要注意的分割。
我们在测试中基于一个更随机的值 $request_id
进行分割。为了便于将配置从测试转换到生产,我们在 server
块中创建了一个新变量 —— $client_ip
,并在测试环境中将其设置为 $request_id
,在生产环境中将其设置为 $remote_addr
。然后,我们设置 split_clients
配置。
每个分割比例的变量(split0
指 0%,split5
指 5%,依此类推)都在单独的 split_clients
指令中设置:
split_clients $client_ip $split0 {
* appversion1;
}
split_clients $client_ip $split5 {
5% appversion2;
* appversion1;
}
split_clients $client_ip $split10 {
10% appversion2;
* appversion1;
}
split_clients $client_ip $split25 {
25% appversion2;
* appversion1;
}
split_clients $client_ip $split50 {
50% appversion2;
* appversion1;
}
split_clients $client_ip $split100 {
* appversion2;
}
现在,键值存储和 split_clients
已经配置好了,接下来我们可以创建一个 map,将 $upstream
变量设置为适当的分割变量中指定的上游组:
map $split_level $upstream {
0 $split0;
5 $split5;
10 $split10;
25 $split25;
50 $split50;
100 $split100;
default $split0;
}
最后,我们配置好了上游组和虚拟服务器的其他部分。请注意,我们还配置了用于键值存储和实时活动监控仪表盘的 NGINX Plus API。这是 NGINX Plus R14 中的新状态仪表盘:
upstream appversion1 {
zone appversion1 64k;
server 192.168.50.100;
server 192.168.50.101;
}
upstream appversion2 {
zone appversion2 64k;
server 192.168.50.102;
server 192.168.50.103;
}
server {
listen 80;
status_zone test;
#set $client_ip $remote_addr; # Production
set $client_ip $request_id; # For testing only
location / {
proxy_pass http://$upstream;
}
location /api {
api write=on;
# in production, directives restricting access
}
location = /dashboard.html {
root /usr/share/nginx/html;
}
}
借助此配置,现在我们可以通过将 API 请求发送到 NGINX Plus 并为主机名设置 $split_level
作为 value ,来控制流量在 appversion1
和 appversion2
上游组之间的分割。举例来说,以下两个请求可以发送到 NGINX Plus,那么 www.example.com 5% 的流量将发送到 appversion2
上游组,而 www2.example.com 25% 的流量将发送到 appversion2
上游组:
# curl -iX POST -d '{"www.example.com":5}' http://localhost/api/3/http/keyvals/split
# curl -iX POST -d '{"www2.example.com":25}' http://localhost/api/3/http/keyvals/split
将 www.example.com 的 value 更改为 10
:
# curl -iX PATCH -d '{"www.example.com":10}' http://localhost/api/3/http/keyvals/split
清除 value:
# curl -iX PATCH -d '{"www.example.com":null}' http://localhost/api/3/http/keyvals/split
在每个请求之后,NGINX Plus 将立即开始使用新的分割value 。
以下是完整的配置文件:
# Set up a key‑value store to specify the percentage to send to each # upstream group based on the 'Host' header. keyval_zone zone=split:64k state=/etc/nginx/state_files/split.json; keyval $host $split_level zone=split; split_clients $client_ip $split0 { * appversion1; } split_clients $client_ip $split5 { 5% appversion2; * appversion1; } split_clients $client_ip $split10 { 10% appversion2; * appversion1; } split_clients $client_ip $split25 { 25% appversion2; * appversion1; } split_clients $client_ip $split50 { 50% appversion2; * appversion1; } split_clients $client_ip $split100 { * appversion2; } map $split_level $upstream { 0 $split0; 5 $split5; 10 $split10; 25 $split25; 50 $split50; 100 $split100; default $split0; } upstream appversion1 { zone appversion1 64k; server 192.168.50.100; server 192.168.50.101; } upstream appversion2 { zone appversion2 64k; server 192.168.50.102; server 192.168.50.103; } server { listen 80; status_zone test; # In each 'split_clients' block above, '$client_ip' controls which # application receives each request. For a production application, we set it # to '$remote_addr' (the client IP address). But when testing from just one # client, '$remote_addr' is always the same; to get some randomness, we set # it to '$request_id' instead. #set $client_ip $remote_addr; # Production set $client_ip $request_id; # Testing only location / { proxy_pass http://$upstream; } # Configure the NGINX Plus API and dashboard. For production, add directives # to restrict access to the API, for example 'allow' and 'deny'. location /api { api write=on; # in production, directives restricting access } location = /dashboard.html { root /usr/share/nginx/html; } }
结语
键值存储的用例有很多,本文只是冰山一角。您还可以使用类似的方法进行速率限制、带宽限制或连接限制。
如果您还没有用过 NGINX Plus,欢迎体验 30 天免费试用版。