NGINX Gateway Fabric 是 Kubernetes Gateway API 规范的一个实现。它使用 NGINX 作为数据平面,可处理如 GatewayClass
、Gateway
、 ReferenceGrant
及 HTTPRoute
等 Gateway API 资源,把 NGINX 配置为 HTTP 负载均衡器,将在 Kubernetes 中运行的应用暴露给集群外部。
在本文中,我们将探讨 NGINX Gateway Fabric 如何使用 NGINX JavaScript 脚本语言(njs)来简化基于请求头、查询参数及方法的 HTTP 请求匹配的实现。
在深入探讨 NGINX JavaScript 之前,我们先来看看 NGINX Gateway Fabric 如何配置数据平面。
使用 Go 模板从 Gateway API 资源配置 NGINX
为了配置 NGINX 数据平面,我们基于在 Kubernetes 集群中创建的 Gateway API 资源生成配置文件。这些文件均从 Go 模板生成。为了生成这些文件,我们需要处理 Gateway API 资源,将其转换为代表 NGINX 配置的数据结构,然后通过将 NGINX 配置模板应用于 NGINX 数据结构来执行它们。NGINX 数据结构包含映射到 NGINX 指令的字段。
在大多数情况下,这种方法都非常有效。Gateway API 资源中的大多数字段均可轻松转换为 NGINX 指令。以流量精分为例。在 Gateway API 中,通过在 HTTPRouteRule 的 backendRefs
字段中列出多个 Service 及其权重来配置流量精分。
下面的配置片段将 50% 的流量精分到 service-v1
,并将其余 50% 的流量精分到 service-v2
:
backendRefs:
- name: service-v1
port: 80
weight: 50
- name: service-v2
port: 80
weight: 50
鉴于 NGINX HTTP 精分客户端模块原生支持流量精分,因此可直接使用模板将其转换为 NGINX 配置。
生成的配置如下所示:
split_clients $request_id $variant {
50% upstream-service-v1;
50% upstream-service-v2;
}
在流量精分等情形中,Go 模板是一套简单而强大的工具,可用于生成 NGINX 配置,以反映用户通过 Gateway API 资源配置的流量规则。
不过,我们发现,Gateway API 规范中定义的更复杂的路由规则无法使用 Go 模板轻松映射到 NGINX 指令,因此我们需要一种更高级别的语言来实现这些规则。于是,我们转而使用 NGINX JavaScript。
什么是 NGINX JavaScript?
NGINX JavaScript 是 NGINX 和 NGINX Plus 的通用脚本框架,作为 Stream 和 HTTP NGINX 模块实现。NGINX JavaScript 模块支持您使用 njs 代码扩展 NGINX 的配置语法。njs 代码是 JavaScript 语言的一个子集,是专为 NGINX 运行时量身打造的一种现代、快速、强大的高级脚本。与主要用于 Web 浏览器的标准 JavaScript 不同,njs 是一种服务器端语言。采用这种方法旨在满足服务器端代码执行的要求,并与 NGINX 的请求处理架构相集成。
njs 有许多用例(包括响应过滤、诊断日志记录及加入子请求),但本文主要探讨 NGINX Gateway Fabric 如何使用 njs 执行 HTTP 请求匹配。
HTTP 请求匹配
在深入了解 NGINX JavaScript 解决方案之前,我们先来谈谈如何实现 Gateway API 功能。
HTTP 请求匹配是根据某些条件(matches
)— 例如请求的请求头、查询参数和/或方法,匹配请求与路由规则的流程。Gateway API 允许您指定一组 HTTPRouteRules,这些 HTTPRouteRule 会根据规则中定义的匹配项将客户端请求发送至特定后端。
例如,如果您在 Kubernetes 上运行了两个版本的应用,但希望将请求头为 version:v2
的请求路由到应用的版本 2 并将其他所有请求路由到版本 1,则可执行以下路由规则:
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: v1-app
port: 80
- matches:
- path:
type: PathPrefix
value: /
headers:
- name: version
value: v2
backendRefs:
- name: v2-app
port: 80
现在,假设您还想将查询参数为 TEST=v2
的流量发送到应用的版本 2,则可添加另一个匹配该查询参数的规则:
- matches
- path:
type: PathPrefix
value: /coffee
queryParams:
- name: TEST
value: v2
以下是上述示例中定义的三个路由规则:
- 匹配路径为 / 的请求,并将其路由至后端 v1-app。
- 匹配路径为 / 且请求头为
version:v2
的请求,并将其路由至后端 v2-app。 - 匹配路径为 / 且查询参数为
TEST=v2
的请求,并将其路由至后端 v2-app。
NGINX Gateway Fabric 必须处理这些路由规则,并配置 NGINX 以相应地路由请求。在下一节中,我们将使用 NGINX JavaScript 来处理此路由。
NGINX JavaScript 解决方案
为了确定在定义匹配项后请求的路由目的地,我们使用 njs 编写了一个名为 redirect
的 location 处理函数。该函数可根据请求的请求头、参数及方法将请求重定向到内部 location 块。
下面我们来看看 NGINX Gateway Fabric 为上面定义的三个路由规则生成的 NGINX 配置。
注: 在本文中,该配置已简化。
# nginx.conf
load_module /usr/lib/nginx/modules/ngx_http_js_module.so; # 载入 NGINX JavaScript 模块
events {}
http {
js_import /usr/lib/nginx/modules/httpmatches.js; # 导入 njs 脚本
server {
listen 80;
location /_rule1 {
internal; # 与规则 1 相对应的内部 location 块
proxy_pass http://upstream-v1-app$request_uri;
}
location /_rule2{
internal; # 与规则 2 相对应的内部 location 块
proxy_pass http://upstream-v2-app$request_uri;
}
location /_rule3{
internal; # 与规则 3 相对应的内部 location 块
proxy_pass http://upstream-v2-app$request_uri;
}
location / {
# 该 location 块处理客户端对路径的请求 /
set $http_matches "[{\"redirectPath\":\"/_rule2\",\"headers\":[\"version:v2\"]},{\"redirectPath\":\"/_rule3\",\"params\":[\"TEST=v2\"]},{\"redirectPath\":\"/_rule1\",\"any\":true}]";
js_content httpmatches.redirect; # 执行重定向 njs 函数
}
}
}
js_import
指令用于指定包含 redirect 函数的文件,js_content
指令用于执行 redirect
函数。
redirect
函数取决于 http_matches
变量。http_matches
变量包含路由规则中定义的匹配项的 JSON 编码列表。JSON 匹配项包含所需的请求头、查询参数和方法及 redirectPath
(将请求重定向到匹配项的路径)。每个 redirectPath
必须对应一个内部 location 块。
下面我们来详细了解一下 http_matches
变量中的每个 JSON 匹配项(所示顺序与上述路由规则一致):
{"redirectPath":"/_rule1","any":true}
– “any” 布尔值表示所有请求均符合该规则,应重定向到路径为/_rule1
的内部 location 块。{"redirectPath":"/_rule2","headers"[“version:v2”]}
– 请求头为version:v2
的请求符合该规则,应重定向到路径为/_rule2
的内部 location 块。{"redirectPath":"/_rule3","params"[“TEST:v2”]}
– 查询参数为TEST=v2
的请求符合该规则,应重定向到路径为/_rule3
的内部 location 块。
关于 http_matches
变量,最后需要注意的一点是,匹配顺序很重要。redirect
函数将接受请求满足的第一个匹配项。NGINX Gateway Fabric 会根据 Gateway API 定义的算法对匹配项进行排序,以确保选择正确的匹配项。
现在,我们来看看用于 redirect
函数的 JavaScript 代码(点击此处,查看完整代码):
// httpmatches.js
function redirect(r) {
let matches;
try {
matches = extractMatchesFromRequest(r);
} catch (e) {
r.error(e.message);
r.return(HTTP_CODES.internalServerError);
return;
}
// Matches is a list of http matches in order of precedence.
// We will accept the first match that the request satisfies.
// If there's a match, redirect request to internal location block.
// If an exception occurs, return 500.
// If no matches are found, return 404.
let match;
try {
match = findWinningMatch(r, matches);
} catch (e) {
r.error(e.message);
r.return(HTTP_CODES.internalServerError);
return;
}
if (!match) {
r.return(HTTP_CODES.notFound);
return;
}
if (!match.redirectPath) {
r.error(
`cannot redirect the request; the match ${JSON.stringify(
match,
)} does not have a redirectPath set`,
);
r.return(HTTP_CODES.internalServerError);
return;
}
r.internalRedirect(match.redirectPath);
}
redirect
函数接受 NGINX HTTP 请求对象作为参数,并从中提取 http_matches
变量。然后,它通过将请求的属性(可在请求对象中找到)与匹配项列表进行对照来查找吻合的匹配项,并在内部将请求重定向到吻合匹配项的重定向路径。
为何使用 NGINX JavaScript?
虽然可使用 Go 模板实现 HTTP 请求匹配以生成 NGINX 配置,但与流量精分等简单用例相比,不易操作。与 split_clients
指令不同,在低级别 NGINX 配置中,没有原生方法来对照请求的属性与匹配项列表。
我们选择在 NGINX Gateway Fabric 中使用 njs 进行 HTTP 请求匹配,原因如下:
- 简单性 – 可轻松实现复杂的 HTTP 请求匹配,有助于增强代码可读性并提高开发效率。
- 调试 – 通过提供描述性错误消息简化了调试,从而加快了问题解决速度。
- 单元测试 – 可对代码进行全面单元测试,确保功能稳健可靠。
- 可扩展性 – 高级脚本特性支持轻松扩展和修改,无需进行复杂的手动配置变更即可满足不断演变的项目需求。
- 性能 – 专为 NGINX 而打造,具有出色性能。
后续步骤
如果您有兴趣使用 NGINX 数据平面实现 Gateway API,请访问我们 GitHub 上的 NGINX Gateway Fabric 项目,并通过以下方式参与其中:
- 以贡献者的身份加入项目
- 在实验室中试用实现
- 执行测试并提供反馈