Nginx map 模块实战:把 if 判断换成查找表,按域名分流、UA 识别与白名单限流一次讲清

为什么你的 Nginx 配置里该有个 map 块

很多个人站长的 Nginx 配置长这样:一堆 if 嵌套、一堆重复的 location、再加上几个 rewrite 规则硬编码在一起。能跑,但特别难维护——你想加个新的域名、加个新的 UA 判断、加个新的 IP 白名单,就得在几十行配置里翻半天,改错一个分号全站 502。

Nginx 里有个被严重低估的模块叫 ngx_http_map_module,它提供的 map 指令能把"输入 → 输出"的映射关系从流程控制里抽出来,变成一张纯数据表。配置立刻从"逻辑代码"变成"配置数据",可读性、可维护性、甚至性能都会好一个档次。这篇文章用一个真实的个人站场景,把 map 讲透。

map 的本质:一张在启动时编译好的查找表

先看最小语法:

map $变量名 $新变量名 {
    匹配值1   结果1;
    匹配值2   结果2;
    default   兜底结果;
}

它必须写在 http 块里(这是最容易搞错的一点,放到 server 块里会直接报错)。Nginx 在加载配置时就把它编译成一张哈希表或前缀树,运行期每个请求只做一次 O(1) 的查表,没有任何解释执行开销。所以它比一连串 if 判断既快又干净。

原始变量($变量名)可以是 Nginx 内置的任意变量,比如 $http_user_agent、$host、$remote_addr、$http_referer、$request_uri。结果变量则是你自定义的名字,之后可以在 server、location、甚至 log_format 里引用。

场景一:多个域名收口到一个站,但按域名分流

个人站长常见情况:你手里有三四个域名,主域名跑主站,其他域名想 301 跳到主域名,但其中一个老域名需要临时保留独立内容。用 map 可以做到干干净净:

map $host $redirect_target {
    default                    "";
    "old.example.com"          "";
    "shop.example.com"         "https://www.example.com/shop";
    "blog.example.com"         "https://www.example.com/blog";
}

然后在一个统一的 server 块里做判断:

server {
    listen 443 ssl http2;
    server_name old.example.com shop.example.com blog.example.com;

    if ($redirect_target != "") {
        return 301 $redirect_target$request_uri;
    }
    # old.example.com 的 redirect_target 是空串,会走下面的正常逻辑
    root /var/www/legacy;
    location / { try_files $uri $uri/ /index.php?$query_string; }
}

整块配置里只有一条 if,其余全靠数据表。以后新增一个待跳转的域名,只要在 map 里加一行,不用碰 server 块。

场景二:按 UA 给爬虫和真实用户走不同缓存

个人站最怕被采集站、AI 爬虫刷爆带宽,但又不想误伤搜索引擎。用 map 把 UA 分成几档,非常实用:

map $http_user_agent $is_bot {
    default                                    0;
    ~*(googlebot|bingbot|baiduspider)          1;   # 允许的正规爬虫
    ~*(ahrefs|semrush|mj12bot|dotbot)          2;   # 采集型,限速
    ~*(python-requests|curl|wget|scrapy)       3;   # 脚本,直接拒绝
}

注意两点:一是正则匹配要带 ~* 前缀(表示大小写不敏感),二是 map 的匹配顺序是"精确匹配优先、然后按配置里出现的先后顺序匹配第一个命中的正则",所以把更具体的规则放前面。之后就能在 location 里分流:

location / {
    if ($is_bot = 3) { return 403; }
    if ($is_bot = 2) {
        limit_req zone=slowburst burst=5 nodelay;
    }
    # is_bot = 1 的正规爬虫不加限制
    proxy_pass http://backend;
}

场景三:把 map 和限流 zone 结合,做灰度放量

这是 map 最被低估的用法。你可能只想对被封 IP 段做限速,普通用户完全不受影响。先用 geo 或 map 算出一个"是否受限"的标志,再把这个标志拼进 limit_req 的键:

geo $trusted {
    default        0;
    10.0.0.0/8     1;
    192.168.0.0/16 1;
}

map $trusted $limit_key {
    0   $binary_remote_addr;   # 非白名单:按 IP 限
    1   "";                    # 白名单:空键 = 不限
}

limit_req_zone $limit_key zone=perip:10m rate=20r/s;

server {
    location /api/ {
        limit_req zone=perip burst=40 nodelay;
        proxy_pass http://backend;
    }
}

关键技巧:当限流键的值为空字符串时,Nginx 会跳过这个请求的限流计数。这比写 if 判断白名单再决定要不要限流要优雅得多,而且没有 if 的性能损耗。

几个必须记住的坑

  • 位置错误:map 只能放 http 块,放进 server 或 location 会报 "map" directive is not allowed here。
  • 变量求值时机:map 的结果在用到它的时候才求值(惰性),所以不用担心"每个请求都算一遍"的性能问题。
  • 字符串要加引号:匹配值里如果含空格、特殊字符或 {},务必用双引号包起来,否则 Nginx 解析会出错。
  • default 建议显式写:不写 default 时,未匹配的情况结果为空串,某些指令(比如 return)遇到空串会出意外,养成写 default 的习惯。
  • hostnames 关键字:如果 map 的键是域名,可以加 hostnames; 选项做后缀匹配,但会牺牲一点哈希查找性能,键特别多时慎用。

验证配置是否生效

改完配置先 nginx -t 测语法,然后 nginx -s reload。想确认 map 真的按预期工作,可以临时把结果变量打进日志:

log_format debug '$remote_addr host=$host bot=$is_bot lim=$limit_key';
access_log /var/log/nginx/debug.log debug;

用不同的 UA 或 Host 请求几次,看日志里变量值对不对,确认无误后再删掉调试日志。这比反复重启、靠猜要可靠得多。

说到底,map 的价值不在于它能做别人做不了的事,而在于它把"判断逻辑"从"控制流程"里解耦出来。配置写久了你会越来越清楚:能变成数据表的判断,就不要写成 if。这是从"能跑就行"到"可维护"之间最省力的一步。

Last modification:October 4th, 2026 at 09:23 pm

Leave a Comment