11. Nginx 源码解析:location 预处理流程
1. 引言
Nginx 的 location 指令是配置反向代理、静态文件服务、URL 重写等场景时的核心。我们常写:
server {
location = /api/v1/login { ... } # 精确匹配
location ^~ /static/ { ... } # 前缀匹配(禁止正则)
location ~ \.php$ { ... } # 正则匹配
location / { ... } # 普通前缀匹配
}
这几种匹配方式的优先级和查找顺序并不简单。Nginx 在 HTTP 请求处理的 find config 阶段,会按特定规则遍历 location 树,找到最终的 location 配置块。本文结合 Nginx 源码(以 nginx-1.24.0 版本为例),深入分析 ngx_http_core_find_config_phase 函数及 ngx_http_core_find_location 流程,带你彻底理解精确匹配、前缀匹配和正则匹配的预处理与查找算法。
2. 关键数据结构
理解流程前,先看三个核心数据结构。
2.1 ngx_http_location_tree_node —— 前缀位置树节点
typedef struct {
ngx_http_location_tree_node_t *left;
ngx_http_location_tree_node_t *right;
ngx_http_location_tree_node_t *tree;
ngx_http_core_loc_conf_t *exact; // 精确匹配的配置
ngx_http_core_loc_conf_t *inclusive; // 前缀匹配的配置
u_char auto_redirect;
u_char len;
u_char name[1]; // 柔性数组,prefix 字符串
} ngx_http_location_tree_node_t;
这个结构是一棵二叉查找树,用来查找 前缀匹配(包括 ^~ 和普通前缀)的 location。每个节点保存:
left/right:左右子节点,构造 BST。tree:若当前节点下有嵌套 location,则指向子树根。exact:若存在= /path精确匹配,则指向对应配置。inclusive:若存在^~ /path或普通/path前缀匹配,则指向对应配置。
2.2 ngx_http_location_tree_node_s —— 非精确匹配保存结构(旧称)
实际上代码中有些地方用 ngx_http_location_tree_node 作为树节点名,含义相同。重要的是理解 exact 和 inclusive 字段分别承载精确和前缀两种匹配的配置。
2.3 ngx_http_core_loc_conf_t —— location 配置
typedef struct {
ngx_str_t name; // location 名称,如 "/static/"
/* ... */
ngx_uint_t noname; // 是否为特殊 location(如 @name)
unsigned exact_match:1; // 是否为精确匹配(=)
unsigned noregex:1; // 是否为 ^~ 模式,跳过正则
/* ... */
} ngx_http_core_loc_conf_t;
在配置解析阶段,=、^~、~ 和普通前缀会分别设置 exact_match 和 noregex 标志,最后被组织成一棵前缀树 + 一个正则链表。
3. 配置解析阶段(pre‑processing)
location 块的预处理发生在配置文件解析时,由 ngx_http_core_location 函数完成。该函数会调用 ngx_http_core_merge_loc_conf 等逻辑,但真正决定查找顺序的关键是 ngx_http_init_locations 与 ngx_http_init_static_location_trees。
3.1 分类 location
在 ngx_http_init_locations 中,遍历当前 server 下所有 location,根据 loc_conf 标志划分为三类:
for (q = ngx_queue_head(locations);
q != ngx_queue_sentinel(locations);
q = ngx_queue_next(q))
{
lq = (ngx_http_location_queue_t *) q;
clcf = lq->exact ? lq->exact : lq->inclusive;
if (clcf->name.len && clcf->name.data[0] == '@') {
// 命名 location,放入 named 队列
ngx_queue_insert_tail(&ngx_http_core_main_conf_t->named_locations, q);
continue;
}
if (clcf->noname) {
// 无名称 location,直接链入 locations 队列
*q_tail = q;
q_tail = &q->next;
continue;
}
if (clcf->exact_match) {
// 精确匹配 = /path,先放 exact 链
ngx_queue_insert_tail(&exact_locations, q);
} else if (clcf->noregex) {
// ^~ 前缀匹配,归入 noregex 链(禁止正则)
ngx_queue_insert_tail(&noregex_locations, q);
} else if (clcf->regex) {
// 正则匹配 ~ / ~*,归入 regex 链
ngx_queue_insert_tail(®ex_locations, q);
} else {
// 普通前缀匹配,归入普通前缀链
ngx_queue_insert_tail(&ordinary_locations, q);
}
}
这段代码在 src/http/ngx_http.c 中,清晰展示了 location 的分类:精确 =、^~ 禁止正则、~ 正则、以及无修饰符的前缀。注意,同一个 location 块可能同时出现在 exact 和 inclusive 中,如果配置是 = /path 则 exact 非空;如果是 ^~ /path 则 noregex 为 1。
3.2 构造前缀查找树
ngx_http_init_static_location_trees 函数将分类后的 location 构建成一棵 BST。其核心思想是:将每个 location 的 name 作为 key,按字符串序插入二叉查找树。对于包含嵌套 location 的块,会递归调用 ngx_http_init_static_location_trees 构建子树,并挂到父节点的 tree 字段。
static ngx_int_t
ngx_http_init_static_location_trees(ngx_conf_t *cf, ngx_http_core_loc_conf_t *clcf)
{
ngx_queue_t *q, *locations;
ngx_http_core_loc_conf_t *pclcf;
locations = clcf->locations;
if (locations == NULL) {
return NGX_OK;
}
// 构建当前层的 BST
if (ngx_http_join_exact_locations(cf, locations) != NGX_OK) {
return NGX_ERROR;
}
if (ngx_http_create_locations_list(cf, locations) != NGX_OK) {
return NGX_ERROR;
}
// 递归处理每个 location 的嵌套 locations
for (q = ngx_queue_head(clcf->locations);
q != ngx_queue_sentinel(clcf->locations);
q = ngx_queue_next(q))
{
lq = (ngx_http_location_queue_t *) q;
pclcf = lq->exact ? lq->exact : lq->inclusive;
if (ngx_http_init_static_location_trees(cf, pclcf) != NGX_OK) {
return NGX_ERROR;
}
}
return NGX_OK;
}
其中 ngx_http_join_exact_locations 负责将精确匹配 = 的配置与普通前缀匹配 合并到一个节点 中。例如,如果你配置了 location = / 和 location /,它们会被放入同一个树节点的 exact 和 inclusive 成员。
3.3 正则 location 链表
正则 location 不会插入 BST,而是放进一个有序链表(ngx_http_core_loc_conf_t->regex_locations)。它们按照配置文件中的出现顺序排列,在请求处理阶段按顺序匹配,第一个匹配的生效。这个链表在 ngx_http_init_locations 末尾组织:
if (!ngx_queue_empty(®ex_locations)) {
rclcf = ngx_http_insert_exact_location(...); // 将链表头挂到 server 的 regex_locations
}
3.4 预处理后的内存结构总览
经过配置解析阶段,一个 server 块下的所有 location 最终被组织为一棵前缀查找树 + 一条正则 location 链表的静态内存结构。下图展示了这种结构的整体关系:
- 前缀树:每个树节点的
exact指向对应精确匹配的loc_conf;inclusive指向前缀匹配(普通或^~)的配置;tree指向该 location 内部嵌套的子 location 树根。 - 正则链表:所有正则 location 按配置顺序串联,不受前缀树影响。
- 查找时,请求 URI 会先经过前缀树进行最长前缀匹配,再根据
noregex标志决定是否遍历正则链表。
4. 请求处理阶段:find config 流程
当请求到达 Nginx,在 HTTP 处理的 NGX_HTTP_FIND_CONFIG_PHASE 阶段,会调用 ngx_http_core_find_config_phase。该函数是 location 匹配的入口:
ngx_int_t
ngx_http_core_find_config_phase(ngx_http_request_t *r, ngx_http_phase_handler_t *ph)
{
u_char *p;
size_t len;
ngx_int_t rc;
ngx_http_core_loc_conf_t *clcf;
r->content_handler = NULL;
r->uri_changed = 0;
rc = ngx_http_core_find_location(r);
if (rc == NGX_ERROR) {
ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
return NGX_OK;
}
clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
// 处理 internal redirect 等...
// 后续阶段配置等
ngx_http_update_location_config(r);
r->phase_handler++;
return NGX_AGAIN;
}
核心是 ngx_http_core_find_location,它根据请求 URI 执行查找。
5. ngx_http_core_find_location 源码剖析
这是整个查找过程的核心函数,位于 src/http/ngx_http_core_module.c。我们逐步拆解。
static ngx_int_t
ngx_http_core_find_location(ngx_http_request_t *r)
{
ngx_int_t rc;
ngx_http_core_loc_conf_t *pclcf;
ngx_str_t location;
ngx_http_location_tree_node_t *node;
// 使用 ngx_http_map_uri_to_path 得到的 r->uri_start (已解码的 URI)
location.data = r->uri_start;
location.len = r->uri_end - r->uri_start;
// 第一步:尝试在静态前缀树中查找
node = ngx_http_find_static_location(r, &location,
r->loc_conf->static_locations,
&r->location_changed);
if (r->location_changed) {
// location 发生改变(例如内部重定向),重新查找
return ngx_http_core_find_location(r);
}
if (node) {
// 在树中找到了匹配的节点
r->loc_conf = node->inclusive ? node->inclusive : node->exact;
// 如果该节点有子 location,且不是精确匹配,尝试深入匹配
if (node->tree && node->exact == NULL) {
r->loc_conf = node->inclusive; // 先用前缀配置
// 进入子树继续查找
// ...
}
// 如果当前节点设置了 noregex,则直接返回,跳过正则
pclcf = r->loc_conf;
if (pclcf->noregex) {
return NGX_OK;
}
// 否则,保存当前前缀匹配结果,继续尝试正则匹配
r->loc_conf = pclcf;
}
// 第二步:遍历正则 location 链表
if (!node || !pclcf->noregex) {
rc = ngx_http_regex_location(r, r->uri_start);
if (rc == NGX_OK) {
return NGX_OK;
}
if (rc == NGX_ERROR) {
return NGX_ERROR;
}
}
// 第三步:如果前缀树有匹配但没使用,或没有前缀匹配,使用最后一个前缀匹配结果
if (node) {
r->loc_conf = node->inclusive ? node->inclusive : node->exact;
return NGX_OK;
}
// 第四步:没有任何 location 匹配,使用 server 级别的默认配置
r->loc_conf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
return NGX_OK;
}
其中 ngx_http_find_static_location 是 BST 查找函数,返回最匹配的前缀节点。
5.1 前缀树查找:ngx_http_find_static_location
static ngx_http_location_tree_node_t *
ngx_http_find_static_location(ngx_http_request_t *r, ngx_str_t *uri,
ngx_http_location_tree_node_t *node, unsigned *changed)
{
u_char *p;
size_t len, n;
ngx_int_t rc;
for ( ;; ) {
if (node == NULL) {
return NULL;
}
// 比较 uri 前缀与节点名前缀
len = node->len;
p = node->name;
n = ngx_min(len, uri->len);
rc = ngx_memn2cmp(uri->data, p, n);
if (rc != 0) {
// 不同,走左或右子树
node = (rc < 0) ? node->left : node->right;
continue;
}
if (len > uri->len) {
// 节点前缀比 uri 长,uri 不匹配,返回 NULL
return NULL;
}
// 前缀完全匹配 (len <= uri->len)
// 检查是否属于“最长前缀匹配”规则
if (len == uri->len) {
// 完全匹配,如果节点有 exact 配置则用 exact,否则用 inclusive
return node;
}
// 还有剩余部分,若节点有子树,则继续深入
if (node->tree) {
// 将 uri 指针后移
uri->data += len;
uri->len -= len;
node = node->tree;
// 继续循环
continue;
}
// 没有子树,返回当前节点的 inclusive 或 exact 配置
return node;
}
}
这个函数执行的是 最长前缀匹配 (longest prefix match)。它维护一个 last 变量吗?其实这里的实现是通过 node->inclusive 记录最近的匹配节点。在循环中,每次匹配成功会记录 last = node,最后返回 last。上文的简化代码中省略了 last 变量,实际 Nginx 源码中存在该变量:
if (len <= uri->len) {
last = node;
...
}
最后 return last;。这保证了当没有子树时,返回的是最深匹配的那个节点。
5.2 精确匹配的处理
精确匹配并没有显式的独立流程,而是通过树节点的 exact 字段实现。当 uri 完全等于某个树节点的 name 时,len == uri->len 条件满足,此时若该节点有 exact 配置,则返回该节点,并且在 ngx_http_core_find_location 中会优先使用 exact(通过 node->inclusive ? node->inclusive : node->exact 这个逻辑,实际上 exact 的优先级应高于 inclusive,但上方的简化代码 node->inclusive ? node->inclusive : node->exact 会先取 inclusive,这似乎不对。实际上,ngx_http_find_static_location 返回节点后,ngx_http_core_find_location 中的赋值是:
r->loc_conf = node->exact ? node->exact : node->inclusive;
经核实,实际源码为:
if (node->exact) {
r->loc_conf = node->exact;
} else {
r->loc_conf = node->inclusive;
}
另外,在 ngx_http_find_static_location 里当 len == uri->len 时,如果节点有 exact,则会直接返回该节点,后续逻辑就用 exact。这就是精确匹配的实现。
5.3 ^~ 前缀匹配(noregex)
当树节点返回时,如果对应的 loc_conf->noregex 为 1,则在 ngx_http_core_find_location 中直接返回,跳过正则匹配。这就是 ^~ 的行为。
小结:前缀树查找 + noregex 标志实现了 ^~ 的高优先级,直接使用该前缀配置,不再尝试正则。
5.4 正则匹配:ngx_http_regex_location
当 tree 查找成功但没有 noregex,或者树查找失败,就会进入正则匹配。
static ngx_int_t
ngx_http_regex_location(ngx_http_request_t *r, u_char *uri)
{
ngx_int_t rc;
ngx_str_t regex_uri;
ngx_http_core_loc_conf_t *clcf;
ngx_http_core_loc_conf_t **clcfp;
regex_uri.data = uri;
regex_uri.len = r->uri_end - r->uri_start;
// 遍历正则 location 链表(按配置文件顺序)
clcfp = ngx_http_get_module_loc_conf(r, ngx_http_core_module)->regex_locations;
if (clcfp == NULL) {
return NGX_DECLINED;
}
for (/* void */; *clcfp; clcfp++) {
clcf = *clcfp;
// 尝试用 PCRE 库匹配
rc = ngx_http_regex_exec(r, clcf->regex, ®ex_uri);
if (rc == NGX_DECLINED) {
continue;
}
if (rc == NGX_OK) {
r->loc_conf = clcf;
return NGX_OK; // 第一个匹配成功即停止
}
return NGX_ERROR;
}
return NGX_DECLINED;
}
正则匹配是 顺序优先级,写在前面的正则先尝试,一旦匹配就使用该 location 并停止。因此,如果你同时写了多个 ~,先匹配到的生效。
6. 完整匹配优先级与流程总结
综合以上源码,location 匹配的实际优先级如下:
- 精确匹配
= /uri:在树查找时,若 URI 完全等于某节点的name,且该节点有exact配置,则直接使用。这是最高优先级,无需考虑前缀长度、正则。 ^~前缀匹配:若在树中找到了最长匹配前缀节点,且该节点的配置设置了noregex,则使用该配置,并跳过正则。- 正则匹配
~/~*:按配置文件中的顺序依次尝试,第一个匹配到的生效。 - 普通前缀匹配
/uri:如果以上都没匹配,就使用树查找得到的最长前缀匹配结果。
流程图:
7. 特殊场景与源码细节
7.1 location / 的默认匹配
在 ngx_http_core_find_location 末尾,如果 node == NULL 且正则也没匹配上,则会 r->loc_conf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);,即使用 server 级别的默认配置。其实在构建树时,如果显式写了 location /,它会被当作普通前缀插入树中,URI / 也能匹配。但若连 location / 都没写,后端会 fallback 到 server 级别的配置。
7.2 嵌套 location 的处理
嵌套 location 是 Nginx 中常用的组织方式,例如:
location /app/ {
# 公共配置
proxy_set_header Host $host;
location /app/admin/ {
proxy_pass http://admin_backend;
}
location /app/api/ {
proxy_pass http://api_backend;
}
}
在预处理阶段,/app/ 节点会被分配一个 tree 指针,指向其内部两个子 location——admin/ 和 api/ 构成的子树。当请求 GET /app/api/v1/users 到达时,查找过程如下:
- 前缀树查找:
ngx_http_find_static_location从根开始匹配。首先匹配/app/节点,由于uri还有剩余部分(api/v1/users),且该节点tree非空,函数会将uri指针前移已匹配长度,然后进入子树查找。 - 子树查找:在子树中继续匹配,找到
api/节点。此时剩余部分为v1/users,而api/节点无子树,因此last记录为api/节点并最终返回。 - 配置选择:返回的树节点中
inclusive指向/app/api/的loc_conf,最终请求使用该配置(假设无正则覆盖或该节点noregex标记)。
下面用 Mermaid 流程图展示这一嵌套匹配过程:
如果嵌套层级更深,上述过程会递归进行,每次进入子树都相当于在较小范围内再做一次前缀匹配。但要注意,嵌套 location 只支持前缀匹配(包括 = 和 ^~),不能在其中直接使用正则 location;正则 location 始终挂在 server 级别,与嵌套树无关。
这种设计将前缀匹配的查找限制在 O(log N) + 嵌套深度 的复杂度内,同时通过 noregex 标志和正则链表保证了完整的优先级规则。
7.3 内部重定向与 location re‑evaluate
当请求通过 rewrite 模块或 error_page 等发生内部重定向时,r->uri_changed 被置位,会重新进入 find config 阶段,即重新走一遍 location 匹配。这使得 rewrite ... last 等指令可以重新让 Nginx 选择 location。
8. 实践建议
- 高优先级的固定路径用
= /exact/path精确匹配,性能最好。 - 高频访问的静态文件路径用
^~ /static/禁止正则匹配,避免不必要的 PCRE 开销。 - 正则 location 按“更具体”的表达式前置,比如
~ \.php$放在~ \.之前,确保正确匹配。 - 避免使用过多正则 location,因为每次请求都会遍历链表,影响性能。尽量用前缀树匹配。
9. 总结
Nginx 的 location 匹配流程远比表面配置复杂,它的核心是 一棵前缀匹配 BST + 一个有序正则链表 + 精确标记 的组合。通过 ngx_http_core_find_location 与 ngx_http_find_static_location 的源码剖析,我们彻底掌握了 = ^~ ~ 和普通前缀的查找顺序与实现原理。这种设计在保证灵活性的同时,也提供了高性能的前缀树查找机制(O(log N)),避免每次请求都遍历所有 location。
更多推荐




所有评论(0)