最近买了台新服务器,想着直接把旧服务器上的1Panel整个迁移过来,结果OpenResty容器怎么都启动不起来,折腾了好久才发现是个很隐蔽的问题,记录一下排查过程

问题现象

从旧服务器把1Panel的配置快照搬到新服务器后,OpenResty容器状态显示异常,查看日志报错如下:

1
[emerg] 1#1: could not build map_hash, you should increase map_hash_bucket_size: 32

看到这个报错一开始以为是map规则太多了,但检查配置发现stream分流里就一条规则:

1
2
3
4
map $ssl_preread_server_name $backend_name {
video.sample.com reality_backend;
default web_backend;
}

解决方法

遇到这个报错,需要在stream {}块中调大哈希桶大小。正确配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
stream {
map_hash_bucket_size 64;

map $ssl_preread_server_name $backend_name {
video.sample.com reality_backend;
default web_backend;
}

upstream reality_backend {
server 127.0.0.1:7443;
}

upstream web_backend {
server 127.0.0.1:8443;
}

server {
listen 443;
ssl_preread on;
proxy_pass $backend_name;
}
}

深挖原因

为什么一条规则需要64字节?

map_hash相关的两个参数含义不同:

  • map_hash_max_size:控制规则数量
  • map_hash_bucket_size:控制单条规则占用的内存大小

在64位系统中,单条规则实际占用内存包括:

  • 域名字符串本身(video.sample.com约16字节)
  • 指针和内部标记(约10字节)
  • 内存对齐(对齐到8的倍数)
  • 桶结束标记NULL指针(8字节)

算下来至少需要40字节,32字节的桶确实装不下

为什么旧服务器不报错?

更诡异的是,旧服务器上同样的配置(即使不设置map_hash_bucket_size)运行完全正常。这让我怀疑是不是系统层面有差异。

尝试查看CPU缓存行大小:

1
getconf LEVEL1_DCACHE_LINESIZE

两台服务器返回都是64,看起来没区别。但继续查看CPU型号信息:

1
cat /proc/cpuinfo | grep "model name" | uniq

这时候真相大白:

  • 旧服务器:AMD EPYC 7452 32-Core Processor
  • 新服务器:QEMU Virtual CPU version 2.5+

原来问题出在虚拟化策略上。Nginx会通过CPUID指令获取CPU型号,然后根据内置的CPU数据库来决定缓存行大小:

  • 旧服务器的云服务商使用了CPU直通(Host-Passthrough),Nginx识别到真实的AMD EPYC处理器,自动使用64字节缓存行,所以即使配置写32也会被自动对齐到64
  • 新服务器使用的是QEMU虚拟CPU,Nginx无法识别型号,为了保守起见fallback到32字节,导致真的就只有32字节可用

总结

  1. 在stream块中使用map时,需要在同一块中配置map_hash_bucket_size参数
  2. 如果使用了较长的域名或后端名称,建议显式设置map_hash_bucket_size 64或更大,不要依赖自动对齐
  3. 不同云服务商的虚拟化策略可能导致Nginx底层行为不一致,迁移配置时需要注意
  4. Docker容器内的环境可能和宿主机不同,原本正常的配置在容器里可能需要调整