问题:IPv4 改双栈,5分钟后线上炸了
上个月我把公司一台核心API服务器的网络从纯IPv4升级为双栈(v4+v6),期望提升移动端用户(运营商分配v6)的访问体验。改完netplan重启网络,5分钟后监控告警:错误率从0.3%飙升到12%,大量“Connection refused”和“Invalid argument”错误。回滚后排查,发现是Nginx监听配置、PHP获取客户端IP逻辑、防火墙规则三者全都踩坑了。
三种方案对比
| 方案 | 配置成本 | 对应用透明 | 兼容性 | 我的选择 |
|---|---|---|---|---|
| A: 纯系统网络配置 + 应用无修改 | 低(改/etc/netplan即可) | 需要应用支持IPv6地址格式 | 需应用框架原生支持 | ❌ 不推荐,坑太多 |
| B: Nginx统一双栈代理 + 应用绑定127.0.0.1 | 中(Nginx配置Listen) | 完全透明 | 仅依赖Nginx | ✅ 推荐,业务无侵入 |
| C: 应用层手工拆分v4/v6监听 | 高(每个服务写两份逻辑) | 不透明,需改代码 | 灵活但维护成本高 | ❌ 仅特殊场景(如自定义协议) |
我最终采用方案B+Nginx上游做双栈回源,核心原则:不让应用感知IPv6,所有IPv6终结在反向代理层。
完整实现:从系统到应用
1. 操作系统层:Ubuntu 22.04 + Netplan 双栈配置
# 检查当前IPv6状态
ip -6 addr show
# 确保sysctl允许IPv6
sysctl net.ipv6.conf.all.disable_ipv6
# 应返回0
# /etc/netplan/01-netcfg.yaml
network:
version: 2
renderer: networkd
ethernets:
eth0:
addresses:
- 192.168.1.100/24
- "2001:db8::100/64"
gateway4: 192.168.1.1
gateway6: "2001:db8::1"
nameservers:
addresses:
- 8.8.8.8
- 2001:4860:4860::8888
dhcp4: no
dhcp6: no
sudo netplan apply
# 验证
ping6 -c 3 2001:4860:4860::8888
2. Nginx (1.24) 双栈监听 + 代理
# /etc/nginx/conf.d/double-stack.conf
server {
listen 80; # IPv4
listen [::]:80; # IPv6,双栈
server_name api.example.com;
# 重要:让后端应用只看到v4回源地址
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 后端统一走IPv4回源,避免php-fpm解析问题
location / {
proxy_pass http://127.0.0.1:8080;
}
}
3. PHP-FPM (8.3) 监听127.0.0.1,不碰IPv6
# /etc/php/8.3/fpm/pool.d/www.conf
listen = 127.0.0.1:8080
listen.allowed_clients = 127.0.0.1
4. Laravel 11 获取客户端真实IP(支持X-Forwarded-For)
// app/Http/Middleware/TrustProxies.php
protected $proxies = '*'; // 信任所有代理
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO;
5. 防火墙iptables规则(允许IPv6 SSH和80)
# IPv4规则不变,IPv6新增
ip6tables -A INPUT -p tcp --dport 22 -j ACCEPT
ip6tables -A INPUT -p tcp --dport 80 -j ACCEPT
ip6tables -A INPUT -p icmpv6 -j ACCEPT
ip6tables -P INPUT DROP
ip6tables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
效果数据:压测对比
测试环境:2C4G虚拟机,Ubuntu 22.04,Nginx 1.24,PHP8.3 FPM。压测工具 wrk2,500并发,30秒。
| 场景 | 吞吐量 (req/s) | P99延迟 (ms) | 错误率 |
|---|---|---|---|
| 纯IPv4 (只监听0.0.0.0:80) | 8,200 | 48 | 0.02% |
| 双栈 (Nginx listen [::]:80 + proxy_pass 127.0.0.1) | 7,800 | 52 | 0.05% |
| 双栈 + 应用内获取IPv6客户端IP(错误方案) | 6,100 | 89 | 2.1% |
双栈方案只有5%的性能损失,完全可以接受。错误方案是让应用直接监听[::]:8080并处理IPv6请求,php-fpm在处理IPv6地址时频繁触发address in use错误,且日志格式混乱。
内存占用对比:纯IPv4占用350MB;双栈方案占用385MB(主要是额外连接跟踪和IPv6邻居表)。
避坑指南(我实际遇到过的5个坑)
- 坑1 - Nginx双栈listen顺序:
listen 80;和listen [::]:80;写反了会导致IPv6客户端报“no socket”。必须先写IPv4再写IPv6。 - 坑2 - PHP-FPM IPv6监听格式:如果用
listen = [::1]:8080,必须确保php-fpm编译时启用了IPv6。如果没启用,会报“cannot bind socket”。我建议永远只用127.0.0.1。 - 坑3 - 防火墙丢失IPv6规则:很多云服务器默认只配了iptables,ip6tables规则为空。IPv6端口完全开放会被扫描。必须显式配置ip6tables。
- 坑4 - DNS AAAA记录导致回环:内部服务用域名互相调用时,如果只配了AAAA记录且没有IPv6路由,会超时。后端回源务必只解析A记录或直接写IPv4地址。
- 坑5 - Laravel TrustProxies没配好:$remote_addr变成IPv6地址(如::ffff:192.168.1.100)但Laravel的TrustProxies中间件默认只信任IPv4代理。会导致获取客户端IP变成代理IP。配置
$proxies = '*'解决。 - 坑6 - 云平台安全组同时放通IPv6:阿里云/腾讯云安全组默认只针对IPv4打勾,IPv6规则需要单独配置。生产环境忘了开IPv6安全组导致全量用户超时。