为什么你的 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。这是从"能跑就行"到"可维护"之间最省力的一步。