Nginx map 变量高级用法实战:缓存策略、爬虫限流与日志字段的配置化

在 Nginx 的配置体系里,map 指令是一个被严重低估的功能。很多人写 Nginx 配置,遇到「不同来源给不同策略」的需求时,第一反应是复制粘贴多个 location 块,或者干脆开一堆 if 判断,结果配置越写越长、越改越乱。其实 Nginx 早就提供了一个干净的解法:用 map 把输入变量映射成另一个变量,然后在需要的地方引用这个新变量即可。本文用一个真实的个人站场景,把 map 的语法、常见用法和坑位讲透。

一、map 到底解决了什么问题

先看一个反例。假设你的网站需要做到:访问静态资源走 30 天缓存,访问后台不加缓存,API 接口加短缓存,其他默认 1 天缓存。如果不用 map,很多人会写成:

location ~* \.(jpg|png|css|js)$ {
    add_header Cache-Control "public, max-age=2592000";
}
location /admin/ {
    add_header Cache-Control "no-store";
}
location /api/ {
    add_header Cache-Control "public, max-age=300";
}
# 还有默认分支...

location 匹配是有优先级的(精确 > ^~ 前缀 > 正则 > 普通前缀),一旦规则变多,排查「这个请求到底走了哪个 location、加了什么头」就变得极其痛苦。而用 map,可以直接根据请求 URI 生成一个缓存策略变量:

map $uri $cache_control {
    default                        "public, max-age=86400";
    ~*\.(jpg|jpeg|png|gif|webp)$   "public, max-age=2592000";
    ~*\.(css|js|woff2?)$           "public, max-age=31536000";
    ~*/admin/                      "no-store, no-cache";
    ~*/api/                        "public, max-age=300";
}

然后在 server 或 location 里统一引用:

add_header Cache-Control $cache_control always;

配置量减少了,逻辑也一目了然。这就是 map 的价值:把「判断条件」和「执行动作」解耦。判断逻辑集中在一个地方,执行动作只有一处,修改时不用担心漏改某条分支。

二、map 的语法与作用域

map 必须写在 http 块里,不能放在 server 或 location 内部。基本语法:

map $source_variable $result_variable {
    default             值;
    精确匹配字符串       值;
    ~正则表达式          值;
    ~*忽略大小写的正则    值;
    hostnames;           # 可选:启用主机名通配匹配
    include 文件路径;     # 可选:从外部文件引入映射
}

几个关键规则:

  • 源变量可以是任意内置变量,如 $uri、$request_uri、$host、$http_user_agent、$http_referer、$args、$remote_addr、$server_name。
  • 结果变量必须是自己新起的名字,不能是已存在的内置变量名,否则 Nginx 启动时会报 "variable is duplicated"。
  • 匹配顺序:先看精确匹配,再按配置文件中出现的先后顺序逐条尝试正则,第一条匹配即返回,不再继续。
  • default 不是必须的,但强烈建议显式写上,否则匹配失败时结果变量为空字符串,容易出现意料之外的响应头。
  • map 的结果可以引用其他变量,也可以用 $1$2 捕获正则的分组。

三、实战场景一:多域名与多语言的跳转

假设你的博客同时维护中文站、英文站和移动端子域,想根据访问的主机名自动切换不同的静态资源目录或者做 301 跳转:

map $host $site_lang {
    default          zh;
    www.example.com  zh;
    en.example.com   en;
    m.example.com    zh;
}

map $site_lang $lang_root {
    zh  "/data/www/zh";
    en  "/data/www/en";
}

server {
    listen 80;
    server_name www.example.com en.example.com m.example.com;
    root $lang_root/current;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }
}

这样新增一个语言站,只需要在 map 里加两行,不需要动 server 块。这种写法在多站点架构里非常常见。

四、实战场景二:按 UA 或 Referer 做差异化处理

个人站长大都有被爬虫或采集器骚扰的经历。与其写一堆 if,不如用 map 打标签:

map $http_user_agent $is_bad_bot {
    default                 0;
    ~*(ahrefs|semrush|mj12bot|dotbot)   1;
    ~*(bytespider|petalbot)             1;
    ""                                  1;   # 空 UA 也视为可疑
}

map $is_bad_bot $limit_key {
    1   $binary_remote_addr;
    0   "";
}

之后配合 limit_req 把可疑来源限速到一个极低的值(甚至直接 return 403):

limit_req_zone $limit_key zone=bad_bot:10m rate=1r/m;

server {
    listen 443 ssl;
    server_name www.example.com;

    location / {
        limit_req zone=bad_bot burst=2 nodelay;
        limit_req_status 429;
        # ...正常业务配置
    }
}

注意这里的一个技巧:$limit_key 在正常访客时是空字符串。当 limit_req 的 key 为空时,Nginx 会跳过限流(空 key 不计数),所以正常用户完全不受影响,只有匹配到爬虫 UA 的请求才会被限速。这种「用空 key 关闭限流」的模式是 Nginx 里非常经典的写法。

同理,防止盗链也可以用 map 来区分「站内引用」和「外部引用」:

map $http_referer $bad_referer {
    default                 0;
    ""                      1;   # 直接访问(无 Referer)
    ~*example\.com          0;   # 站内来源放行
    ~*(baidu|google|bing)   0;   # 搜索引擎放行
    ~*                     1;    # 其他来源视为盗链
}

五、实战场景三:灰度发布与流量切分

个人站长做改版时,最怕新版本上线直接全量翻车。用 map 配合 Nginx 的 cookie 或 IP 可以做简单的灰度:

map $cookie_canary $backend {
    default        "app_v1";
    "1"            "app_v2";
}

upstream app_v1 { server 127.0.0.1:9001; }
upstream app_v2 { server 127.0.0.1:9002; }

server {
    location / {
        proxy_pass http://$backend;
    }
}

把 upstream 名字也做成变量,然后 proxy_pass 里引用变量。这样你自己访问时带上一个 canary=1 的 cookie,就会命中新版本;普通访客继续走老版本。验证没问题后,把 default 改成 app_v2 即完成全量切换。整个过程不需要改任何业务代码。

需要注意的是,当 proxy_pass 的值包含变量时,Nginx 不会在启动时做 upstream 名字的静态校验,而是在每次请求时动态解析,所以 upstream 名字写错的话启动不会报错,只会在访问时报 502,排查时要留意这一点。

六、实战场景四:日志字段的清洗与统一

日志分析时经常要对原始字段做归一化。比如你想统计访问来源,但 Referer 里带着各种参数,直接用会非常杂乱。可以先用 map 归一:

map $http_referer $ref_group {
    default                 "other";
    ~*baidu\.com            "baidu";
    ~*google\.              "google";
    ~*bing\.com             "bing";
    ~*weixin|wechat         "wechat";
    ""                      "direct";
}

log_format main '$remote_addr - [$time_local] "$request" '
                '$status $body_bytes_sent ref_group=$ref_group';

access_log /var/log/nginx/access.log main;

这样日志里直接多了一个干净的 ref_group 字段,后面用 awk 或者 GoAccess 分析时就不用再写复杂的正则匹配了。对个人站长做流量结构分析非常实用。

七、实战场景五:按请求参数做 A/B 分流与地域化处理

个人站长做内容站时,经常会遇到「同一篇文章,PC 端和移动端要展示不同结构」的需求。传统的做法是在模板里用 PHP 判断 UA,但其实 Nginx 层面用 map 就能提前把流量分好,减轻后端压力:

map $http_user_agent $device_class {
    default          "pc";
    ~*iphone         "mobile";
    ~*android        "mobile";
    ~*ipad           "tablet";
    ~*(micromessenger|qqbrowser) "wechat";
}

map $device_class $template_variant {
    pc       "desktop";
    tablet   "desktop";
    mobile   "mobile";
    wechat   "wechat";
}

然后把这个变量通过 fastcgi_param 传给 PHP,模板层直接读这个参数选择布局即可。这样做的收益是可观的:UA 判断的正则匹配由 Nginx 的 C 代码完成,比 PHP 里跑正则快得多,而且逻辑集中在配置文件里,换主题、换框架都不用重写这套判断。

同样的思路还能用于地域化。如果你接了 GeoIP 模块,可以用 $geoip_country_code 做映射,给不同国家的访客展示不同语言的首页入口:

map $geoip_country_code $landing_page {
    default  "/";
    CN       "/";
    US       "/en/";
    JP       "/ja/";
    KR       "/ko/";
}

没有 GeoIP 模块的情况下,也可以用 $http_accept_language 做简化版的语言判断,虽然精度不如 IP 库,但对个人站来说足够用了。

八、实战场景六:维护模式与紧急开关

网站改版、数据库迁移时需要一个「一键进入维护页」的开关。很多人的做法是手动改配置文件再 reload,但更优雅的方式是让 map 读一个文件或者读一个 cookie:

map $cookie_maint_mode $maint_flag {
    default  "0";
    "on"     "1";
}

map $remote_addr $is_admin_ip {
    default  0;
    1.2.3.4  1;   # 你自己的固定 IP
}

server {
    listen 443 ssl;
    server_name www.example.com;

    location / {
        # 维护模式下,非管理员 IP 返回 503
        if ($maint_flag = "1") {
            set $maint_trigger "${is_admin_ip}";
        }
        error_page 503 /maintenance.html;
        location = /maintenance.html {
            internal;
        }
        # ...正常业务
    }
}

需要说明的是,Nginx 官方并不推荐滥用 if("if is evil"),在 location 内部使用 if 时要格外小心它只在 rewrite 阶段生效的特性。对于维护模式这种低频需求,更稳妥的做法是直接用一个空文件做判断:

map -f$request_filename $maint_on {
}

或者干脆用 map 配合一个自定义 upstream——当维护开关打开时,把流量 proxy_pass 到一个静态的维护页服务。这种「把判断放在 map、把动作放在 upstream」的模式,比写 if 更可靠,也更符合 Nginx 的设计哲学。

九、实战场景七:请求大小与超时的差异化控制

不同接口对请求体大小的容忍度完全不同。上传接口需要几十 MB,普通 API 只需要几十 KB。放任不管的话,要么上传被拦,要么被恶意请求撑爆内存。用 map 精确控制:

map $request_uri $max_body {
    default                      "1m";
    ~*/api/upload                "64m";
    ~*/api/avatar                "8m";
    ~*/admin/import              "32m";
}

map $request_uri $proxy_timeout {
    default      "60s";
    ~*/api/export  "600s";
    ~*/api/report  "300s";
}

在 location 中引用:

client_max_body_size $max_body;
proxy_read_timeout   $proxy_timeout;

这样每个接口的上限都清清楚楚写在配置里,出问题时不用在一堆 location 之间来回找,也能有效防止某个接口被异常大的请求拖垮。这种写法在进行接口安全审计时特别有价值——审计员只需要看一个 map 块,就能知道全站所有接口的体积上限策略。

十、几个必须知道的坑

  • map 只在 http 块中生效,写在 server 或 location 里会报 "directive is not allowed here"。
  • 结果变量不要重名:如果你写 map $uri $request_uri,Nginx 启动会直接失败。变量名建议统一加前缀,比如 $m_cache_control
  • 正则匹配是「首次命中即返回」,且按书写顺序,不是按精确度。所以更具体的规则要写在更宽泛的规则前面,否则会被前面那条先捕获。
  • hostnames 关键字:如果要匹配 *.example.com 这种通配主机名,需要加上 hostnames; 否则通配符不生效。
  • 性能开销可忽略但非零:map 的正则匹配在每次请求都会执行。规则在几十条以内对性能几乎没有影响,但如果写了上百条复杂正则,建议压测确认。
  • include 让配置可维护:map 的条目可以直接用 include 从独立文件读取,非常适合把 IP 黑白名单、爬虫名单放在单独文件里,避免 nginx.conf 变成几千行。

关于 include,一个实用的做法是给爬虫名单单独建文件:

# /etc/nginx/conf.d/bad_bots.map
~*ahrefs     1;
~*semrush    1;
~*mj12bot    1;

然后在 map 中引入:

map $http_user_agent $is_bad_bot {
    default 0;
    include /etc/nginx/conf.d/bad_bots.map;
}

以后要加新的爬虫,只改这个名单文件,然后 nginx -t && nginx -s reload 即可。

十一、调试 map 是否生效

map 的匹配结果不像普通配置那样容易肉眼确认。最直接的验证方式是把结果写进响应头临时观察:

add_header X-Debug-Cache-Control $cache_control;
add_header X-Debug-Bad-Bot $is_bad_bot;

然后用 curl 验证:

curl -sI https://www.example.com/style.css | grep X-Debug
curl -sI -A "Bytespider" https://www.example.com/ | grep X-Debug

确认无误后记得把调试头删掉。把调试结果暴露在生产环境是不必要的安全隐患。

十二、小结

map 是 Nginx 配置从「堆砌」走向「结构化」的关键工具。它的核心思想是把条件和动作分离:所有判断逻辑收敛到 http 块里的 map 定义,业务配置里只引用变量。带来的直接好处是配置更短、更易读、改动风险更小。

对个人站长而言,最值得立刻上手的三个用法是:用 map 统一缓存策略、用 map 做爬虫标记与限流、用 map 清洗日志字段。这三个场景覆盖了日常运维的绝大部分需求,配置完毕后你会发现 Nginx 配置文件清爽了很多,排查问题时也终于能一眼看清请求走了哪条逻辑。理解了 map,你的 Nginx 配置水平基本就跨过了一个台阶。

Last modification:September 12th, 2026 at 08:00 am

Leave a Comment