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 作为树节点名,含义相同。重要的是理解 exactinclusive 字段分别承载精确和前缀两种匹配的配置。

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_matchnoregex 标志,最后被组织成一棵前缀树 + 一个正则链表。

3. 配置解析阶段(pre‑processing)

location 块的预处理发生在配置文件解析时,由 ngx_http_core_location 函数完成。该函数会调用 ngx_http_core_merge_loc_conf 等逻辑,但真正决定查找顺序的关键是 ngx_http_init_locationsngx_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(&regex_locations, q);
    } else {
        // 普通前缀匹配,归入普通前缀链
        ngx_queue_insert_tail(&ordinary_locations, q);
    }
}

这段代码在 src/http/ngx_http.c 中,清晰展示了 location 的分类:精确 =^~ 禁止正则、~ 正则、以及无修饰符的前缀。注意,同一个 location 块可能同时出现在 exactinclusive 中,如果配置是 = /pathexact 非空;如果是 ^~ /pathnoregex 为 1。

3.2 构造前缀查找树

ngx_http_init_static_location_trees 函数将分类后的 location 构建成一棵 BST。其核心思想是:将每个 locationname 作为 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 /,它们会被放入同一个树节点的 exactinclusive 成员。

3.3 正则 location 链表

正则 location 不会插入 BST,而是放进一个有序链表(ngx_http_core_loc_conf_t->regex_locations)。它们按照配置文件中的出现顺序排列,在请求处理阶段按顺序匹配,第一个匹配的生效。这个链表在 ngx_http_init_locations 末尾组织:

if (!ngx_queue_empty(&regex_locations)) {
    rclcf = ngx_http_insert_exact_location(...);  // 将链表头挂到 server 的 regex_locations
}

3.4 预处理后的内存结构总览

经过配置解析阶段,一个 server 块下的所有 location 最终被组织为一棵前缀查找树 + 一条正则 location 链表的静态内存结构。下图展示了这种结构的整体关系:

前缀树 BST

left

right

嵌套子树

right

节点: login

节点: v2/

server 级 loc_conf

regex_locations
(正则 location 链表头)

节点: /api/

节点: /

节点: /static/

exact: = /api/login 的配置

inclusive: /api/ 的配置(普通或 ^~)

~ \\.php$ 配置

~* \\.jpg$ 配置

...

  • 前缀树:每个树节点的 exact 指向对应精确匹配的 loc_confinclusive 指向前缀匹配(普通或 ^~)的配置;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, &regex_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 匹配的实际优先级如下:

  1. 精确匹配 = /uri:在树查找时,若 URI 完全等于某节点的 name,且该节点有 exact 配置,则直接使用。这是最高优先级,无需考虑前缀长度、正则。
  2. ^~ 前缀匹配:若在树中找到了最长匹配前缀节点,且该节点的配置设置了 noregex,则使用该配置,并跳过正则。
  3. 正则匹配 ~ / ~*:按配置文件中的顺序依次尝试,第一个匹配到的生效。
  4. 普通前缀匹配 /uri:如果以上都没匹配,就使用树查找得到的最长前缀匹配结果。

流程图

接收请求 URI

ngx_http_find_static_location
在静态前缀 BST 中查找

精确匹配?

使用 exact 配置,结束

有 noregex 标志?

使用当前 inclusive 配置,结束

保存当前前缀匹配结果

存在正则 location 链表?

遍历正则链表

正则匹配成功?

使用该正则 location,结束

使用保存的前缀匹配结果

返回最长前缀匹配配置

最终 location 配置

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 到达时,查找过程如下:

  1. 前缀树查找ngx_http_find_static_location 从根开始匹配。首先匹配 /app/ 节点,由于 uri 还有剩余部分(api/v1/users),且该节点 tree 非空,函数会将 uri 指针前移已匹配长度,然后进入子树查找。
  2. 子树查找:在子树中继续匹配,找到 api/ 节点。此时剩余部分为 v1/users,而 api/ 节点无子树,因此 last 记录为 api/ 节点并最终返回。
  3. 配置选择:返回的树节点中 inclusive 指向 /app/api/loc_conf,最终请求使用该配置(假设无正则覆盖或该节点 noregex 标记)。

下面用 Mermaid 流程图展示这一嵌套匹配过程:

请求 URI: /app/api/v1/users

前缀树根

匹配节点 /app/

node->tree != NULL ?

uri 指针前移:剩余 api/v1/users

进入子树:根为 admin/ 和 api/

匹配节点 api/

有子树且仍有剩余 ?

last = api/ 节点

返回 api/ 节点

使用 inclusive 配置(/app/api/)

如果嵌套层级更深,上述过程会递归进行,每次进入子树都相当于在较小范围内再做一次前缀匹配。但要注意,嵌套 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_locationngx_http_find_static_location 的源码剖析,我们彻底掌握了 = ^~ ~ 和普通前缀的查找顺序与实现原理。这种设计在保证灵活性的同时,也提供了高性能的前缀树查找机制(O(log N)),避免每次请求都遍历所有 location。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐