NGINX.COM
Web Server Load Balancing with NGINX Plus

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 ,来控制流量在 appversion1appversion2 上游组之间的分割。举例来说,以下两个请求可以发送到 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 天免费试用版

Hero image
Kubernetes:
从测试到生产

通过多种流量管理工具提升弹性、可视性和安全性

关于作者

Rick Nelson

Rick Nelson

方案工程区域副总裁

Rick Nelson is the Manager of Pre‑Sales, with over 30 years of experience in technical and leadership roles at a variety of technology companies, including Riverbed Technology. From virtualization to load balancing to accelerating application delivery, Rick brings deep technical expertise and a proven approach to maximizing customer success.

关于 F5 NGINX

F5, Inc. 是备受欢迎的开源软件 NGINX 背后的商业公司。我们为现代应用的开发和交付提供一整套技术。我们的联合解决方案弥合了 NetOps 和 DevOps 之间的横沟,提供从代码到用户的多云应用服务。访问 nginx-cn.net 了解更多相关信息。