1. 问题现象与排查思路拆解1.1 报错信息的完整解读先把这条报错原封不动地摆出来nginx[openresty]:connect() to [xxxx:yyyy:zzzz:50::49]:443 failed (101: Network is unreachable)这条日志来自 OpenResty本质是 Nginx 加了一堆 Lua 模块的发行版出现在error.log里。拆开看几个关键信息connect()这是 Nginx 作为反向代理或 upstream 客户端时主动向后方服务发起 TCP 连接的系统调用。[xxxx:yyyy:zzzz:50::49]:443目标地址是一个 IPv6 地址端口 443说明后端服务监听在 HTTPS 上。failed (101: Network is unreachable)错误码 101 对应的是系统级的ENETUNREACH意思是内核在路由表里找不到一条能到达这个 IPv6 地址的路径。很多人第一反应是后端挂了但Network is unreachable和Connection refused、Connection timed out是两码事。Connection refused说明包到了对方机器但端口没开Connection timed out说明包发出去了但没回应而Network is unreachable是包根本没发出去内核在本地就把它拦下来了。这个区别非常关键它直接把排查方向从后端服务拉回到本机网络配置。1.2 为什么 IPv6 场景下这个问题特别常见IPv4 时代只要机器有默认网关基本不会出现Network is unreachable因为默认路由兜底了。但 IPv6 不一样IPv6 的路由机制有几个特点让这个问题高发第一IPv6 没有传统意义上的默认路由自动配置那么无脑。虽然 RARouter Advertisement可以下发默认路由但很多机房环境、容器环境、云主机环境里IPv6 是手动配的或者只配了地址没配路由。第二IPv6 地址段分配和路由是绑定的。你给网卡配了xxxx:yyyy:zzzz:50::49/64这样的地址内核只会自动生成一条xxxx:yyyy:zzzz:50::/64的直连路由。如果目标地址不在这个 /64 段内又没有默认路由内核直接返回ENETUNREACH。第三容器和虚拟化环境里IPv6 的转发、路由、邻居发现经常是半配置状态。Docker 默认不开 IPv6K8s 的 IPv6 支持也需要显式开启中间任何一环没配对就会出现这种地址看着对就是连不上的情况。1.3 排查的整体思路遇到这个报错我一般的排查顺序是这样的从下往上、从本机到远端确认本机 IPv6 是否启用sysctl net.ipv6.conf.all.disable_ipv6如果是 1那一切免谈。确认本机是否有可用的 IPv6 地址ip -6 addr show看目标网卡上有没有全局地址scope global。确认路由表里有没有到达目标的路由ip -6 route show重点看有没有默认路由default via ...或者目标地址所在段的路由。确认目标地址是否真的可达ping6或ping -6但注意 ICMPv6 通不代表 TCP 通反之亦然。确认 Nginx/OpenResty 的 upstream 配置是不是解析到了 IPv6 地址是不是配置了resolver是不是用了变量导致运行时解析。确认中间链路如果是容器看容器的网络命名空间如果是云主机看安全组和路由表。这个顺序的核心逻辑是先排除本机问题再排除配置问题最后才怀疑链路问题。因为Network is unreachable九成以上是本机路由或地址配置的问题很少是远端的问题。提示不要一上来就ping目标地址。如果本机连 IPv6 地址都没有ping6会直接报connect: Network is unreachable这个报错和 Nginx 的报错是同一个根源但很多人会误以为是目标不通从而跑偏方向。2. 核心原因逐层解析与实操验证2.1 本机 IPv6 栈被禁用这是最底层也最容易被忽略的原因。有些系统镜像、容器基础镜像、或者安全加固脚本会默认把 IPv6 关掉。关掉之后内核里根本没有 IPv6 协议栈任何 IPv6 操作都会返回ENETUNREACH。验证方法# 查看全局 IPv6 是否禁用 sysctl net.ipv6.conf.all.disable_ipv6 sysctl net.ipv6.conf.default.disable_ipv6 # 查看具体网卡的 IPv6 是否禁用 sysctl net.ipv6.conf.eth0.disable_ipv6如果返回1说明被禁用了。临时开启sysctl -w net.ipv6.conf.all.disable_ipv60 sysctl -w net.ipv6.conf.default.disable_ipv60 sysctl -w net.ipv6.conf.eth0.disable_ipv60永久生效需要写进/etc/sysctl.conf或/etc/sysctl.d/下的配置文件。这里有个坑all和default的关系。default是新建网卡时的默认值all是全局开关但all并不覆盖已经存在的网卡配置。所以如果你只改了all没改具体网卡可能还是不生效。稳妥的做法是三个都改。注意在某些容器环境里/proc/sys/net/ipv6是只读的你改不了。这种情况下需要在容器启动参数里开启 IPv6而不是在容器内部改。2.2 网卡没有全局 IPv6 地址IPv6 地址分几种类型链路本地地址fe80::/10、全局地址2000::/3等、唯一本地地址fc00::/7。链路本地地址每块网卡自动就有但它只能在同一个二层网络内通信不能路由。如果你的网卡只有fe80::开头的地址那访问任何非直连的 IPv6 地址都会Network is unreachable。验证方法ip -6 addr show输出里重点看有没有scope global的地址。比如2: eth0: BROADCAST,MULTICAST,UP,LOWER_UP mtu 1500 inet6 xxxx:yyyy:zzzz:50::100/64 scope global valid_lft forever preferred_lft forever inet6 fe80::1234:5678:9abc:def0/64 scope link valid_lft forever preferred_lft forever如果只有scope link没有scope global那就是地址没配上。配置方法取决于你的环境云主机一般在控制台配物理机用ip addr add或改网络配置文件容器看编排配置。这里有个细节valid_lft和preferred_lft。如果地址是 DHCPv6 或 RA 下发的会有租期。租期到了没续上地址会变成deprecated甚至消失这时候也会出现Network is unreachable。所以如果问题是间歇性出现要重点查这个。2.3 路由表缺少到达目标的路由这是最直接的原因。内核在发包前会查路由表IPv6 的路由查找逻辑是先查有没有精确匹配目标地址的路由。再查有没有匹配目标网段的路由。最后查有没有默认路由default。都没有返回ENETUNREACH。验证方法ip -6 route show正常应该有类似这样的输出xxxx:yyyy:zzzz:50::/64 dev eth0 proto kernel metric 256 pref medium fe80::/64 dev eth0 proto kernel metric 256 pref medium default via xxxx:yyyy:zzzz:50::1 dev eth0 metric 1024 pref medium如果缺少default via ...且目标地址不在直连段内就会报错。添加默认路由ip -6 route add default via xxxx:yyyy:zzzz:50::1 dev eth0这里的xxxx:yyyy:zzzz:50::1是网关地址必须是直连段内的地址否则内核会报Nexthop has invalid gateway。提示IPv6 的默认路由和 IPv4 一样也是通过网关转发的。但 IPv6 的网关发现依赖 NDPNeighbor Discovery Protocol如果网关的 MAC 地址解析不出来路由存在也没用。可以用ip -6 neigh show查看邻居表。2.4 Nginx/OpenResty 的 upstream 解析问题前面三个都是系统层面的这一层是应用层面的。OpenResty 在配置 upstream 时如果用的是域名且配置了resolver那么解析结果可能是 IPv6 地址。如果系统 IPv6 没配好就会报这个错。典型配置location /api/ { resolver 8.8.8.8 valid30s ipv6on; set $backend api.example.com; proxy_pass https://$backend; }这里ipv6on会让解析器优先返回 AAAA 记录。如果后端域名同时有 A 和 AAAA 记录而你的机器 IPv6 不通就会连到 IPv6 地址然后失败。解决办法有两个方向一是修好 IPv6二是让 Nginx 只用 IPv4。后者可以resolver 8.8.8.8 valid30s ipv6off;或者在 upstream 里直接写 IPv4 地址避免解析。还有一种情况是 upstream 里写死了 IPv6 地址upstream backend { server [xxxx:yyyy:zzzz:50::49]:443; }这种就是硬编码系统 IPv6 不通就必然失败。2.5 容器与命名空间隔离如果 OpenResty 跑在容器里问题会更复杂。容器的网络命名空间是独立的宿主机有 IPv6 不代表容器有。Docker 默认的 bridge 网络不支持 IPv6需要显式配置{ ipv6: true, fixed-cidr-v6: xxxx:yyyy:zzzz:60::/64 }K8s 里则需要 CNI 插件支持 IPv6且 Pod 的ipFamilyPolicy要配对。验证容器内 IPv6 状态# 进入容器 docker exec -it container sh # 查看地址 ip -6 addr show # 查看路由 ip -6 route show如果容器内只有lo的::1那说明容器完全没有 IPv6 网络需要从编排层面解决。3. 完整排查与修复流程3.1 第一步确认问题边界先做几个快速判断确定问题出在哪一层。# 1. 本机 IPv6 是否启用 cat /proc/sys/net/ipv6/conf/all/disable_ipv6 # 2. 本机是否有全局 IPv6 地址 ip -6 addr show scope global # 3. 是否有默认路由 ip -6 route show default # 4. 尝试连接目标 curl -6 -v https://[xxxx:yyyy:zzzz:50::49]:443/ --connect-timeout 5如果第 1 步返回 1问题在协议栈如果第 2 步为空问题在地址配置如果第 3 步为空问题在路由如果前三步都正常但第 4 步失败问题可能在链路或远端。3.2 第二步修复本机配置假设确认是本机问题按下面的顺序修启用 IPv6 协议栈cat /etc/sysctl.d/99-ipv6.conf EOF net.ipv6.conf.all.disable_ipv6 0 net.ipv6.conf.default.disable_ipv6 0 EOF sysctl -p /etc/sysctl.d/99-ipv6.conf配置 IPv6 地址以 eth0 为例ip addr add xxxx:yyyy:zzzz:50::100/64 dev eth0 ip link set eth0 up配置默认路由ip -6 route add default via xxxx:yyyy:zzzz:50::1 dev eth0验证连通性ping -6 -c 3 xxxx:yyyy:zzzz:50::1 ping -6 -c 3 xxxx:yyyy:zzzz:50::49如果网关通但目标不通说明本机到网关这一段 OK问题在网关之后。如果网关都不通检查网线、VLAN、交换机配置。3.3 第三步调整 OpenResty 配置系统层修好后还要确认 OpenResty 的配置不会再把请求导向不可达的地址。方案一修好 IPv6保留双栈。这是最理想的配置不用改系统层解决即可。方案二强制走 IPv4。如果 IPv6 短期内修不好可以在 Nginx 层禁用 IPv6 解析resolver 8.8.8.8 valid30s ipv6off;或者在 upstream 里只写 IPv4upstream backend { server 10.0.0.49:443; }方案三配置 fallback。OpenResty 支持proxy_next_upstream可以在一个 upstream 里同时配 IPv4 和 IPv6让 Nginx 自动切换upstream backend { server 10.0.0.49:443 max_fails2 fail_timeout10s; server [xxxx:yyyy:zzzz:50::49]:443 max_fails2 fail_timeout10s; }但要注意proxy_next_upstream默认只在error和timeout时切换Network is unreachable属于error会触发切换。不过如果两个都不可达还是会报错。3.4 第四步容器环境特殊处理如果 OpenResty 在容器里修复要分两层宿主机层和容器层。宿主机层确保宿主机 IPv6 正常且 Docker/K8s 配置了 IPv6。Docker 的/etc/docker/daemon.json{ ipv6: true, fixed-cidr-v6: xxxx:yyyy:zzzz:60::/64, experimental: true, ip6tables: true }改完重启 Dockersystemctl restart docker容器层启动容器时指定 IPv6 网络或者用--network host让容器共享宿主机网络命名空间简单粗暴但有效。docker run --network host openresty:latestK8s 里则需要在 Pod spec 里指定spec: ipFamilyPolicy: PreferDualStack ipFamilies: - IPv6 - IPv43.5 第五步验证与回归修完后要做完整验证# 1. 系统层 ip -6 addr show scope global ip -6 route show default ping -6 -c 3 gateway ping -6 -c 3 target # 2. 应用层 curl -6 -v https://[xxxx:yyyy:zzzz:50::49]:443/ --connect-timeout 5 # 3. Nginx 层 nginx -t nginx -s reload tail -f /usr/local/openresty/nginx/logs/error.log观察 error.log 里是否还有Network is unreachable。如果还有看目标地址是不是变了可能是 DNS 解析到了另一个 IPv6 地址。4. 常见问题速查与避坑经验4.1 常见问题速查表现象可能原因快速验证解决方向所有 IPv6 请求都失败协议栈被禁用sysctl net.ipv6.conf.all.disable_ipv6开启 IPv6只有跨网段失败缺默认路由ip -6 route show default添加默认路由间歇性失败地址租期到期ip -6 addr show看valid_lft续租或配静态地址容器内失败宿主机正常容器无 IPv6docker exec ... ip -6 addr配置容器 IPv6域名解析后失败解析到 AAAA 记录dig AAAA domain禁用 IPv6 解析或修 IPv6网关通目标不通中间链路问题traceroute6 target查中间设备改了配置不生效配置未持久化重启后复现写入配置文件4.2 避坑经验一不要忽略scope和valid_lft很多人看ip -6 addr show只看到有地址就以为 OK但没注意scope和valid_lft。scope link的地址不能用于跨网段通信valid_lft到期的地址会被内核移除。我遇到过一次地址是 RA 下发的租期 30 分钟但 RA 服务不稳定导致地址每隔一段时间就消失Nginx 就间歇性报错。后来改成静态地址才稳定。4.3 避坑经验二ping6通不代表 TCP 通ICMPv6 和 TCP 走的是不同的协议号防火墙可能放行 ICMPv6 但拦截 TCP。反过来有些环境禁 ICMPv6 但 TCP 正常。所以ping6只能作为参考最终验证要用curl或nc。# 测试 TCP 443 是否可达 nc -6 -zv xxxx:yyyy:zzzz:50::49 4434.4 避坑经验三Nginx 的resolver缓存OpenResty 的resolver有缓存默认valid时间可能很长。如果 DNS 记录变了Nginx 可能还在用旧的 IPv6 地址。这时候要么等缓存过期要么 reload Nginx。可以在配置里把valid设短一点resolver 8.8.8.8 valid10s ipv6off;但valid太短会增加 DNS 查询压力10 到 30 秒是比较平衡的值。4.5 避坑经验四容器里改 sysctl 的坑在容器里执行sysctl -w net.ipv6.conf.all.disable_ipv60经常会报Read-only file system。这是因为/proc/sys在容器里默认是只读挂载的。解决办法是在docker run时加--sysctldocker run --sysctl net.ipv6.conf.all.disable_ipv60 openresty:latest或者在 K8s 的 Pod spec 里securityContext: sysctls: - name: net.ipv6.conf.all.disable_ipv6 value: 0但注意不是所有 sysctl 都允许在容器里改net.ipv6.conf.all.disable_ipv6属于safe级别的一般可以。4.6 避坑经验五日志里的地址可能是解析后的Nginx 的 error.log 里显示的地址是解析后的实际连接地址。如果你配置的是域名日志里显示 IPv6 地址说明 DNS 解析到了 AAAA 记录。这时候不要只盯着这个地址查要回到域名层面看解析结果dig AAAA api.example.com dig A api.example.com如果 AAAA 记录指向的地址本身就有问题比如已经下线那修本机 IPv6 也没用得改 DNS 或改 Nginx 配置。4.7 避坑经验六IPv6 的 MTU 问题虽然Network is unreachable一般不是 MTU 问题但 IPv6 的 MTU 最小是 1280且不支持中间设备分片。如果链路 MTU 小于 1280会出现各种奇怪的问题。可以用ping -6 -M do -s 1232测试ping -6 -M do -s 1232 -c 3 xxxx:yyyy:zzzz:50::49如果大包不通小包通就是 MTU 问题需要调整网卡 MTU 或路径 MTU 发现。4.8 避坑经验七双栈环境的优先级Linux 默认的gai.conf决定了getaddrinfo返回地址的顺序。默认情况下 IPv6 优先。如果 IPv6 不稳定但 IPv4 正常可以调整优先级# /etc/gai.conf precedence ::ffff:0:0/96 100这行配置让 IPv4 映射地址优先。但这是系统级改动影响所有应用要谨慎。5. 长期稳定性建议与监控5.1 配置持久化临时命令ip addr add和ip route add重启就没了。生产环境必须写进配置文件。不同发行版位置不同Debian/Ubuntu/etc/network/interfaces或/etc/netplan/RHEL/CentOS/etc/sysconfig/network-scripts/ifcfg-eth0通用/etc/systemd/network/下的.network文件Netplan 示例network: version: 2 ethernets: eth0: addresses: - xxxx:yyyy:zzzz:50::100/64 routes: - to: default via: xxxx:yyyy:zzzz:50::1 accept-ra: false5.2 监控 IPv6 状态IPv6 的问题往往是间歇性的等用户报障就晚了。建议加监控# 每分钟检查一次默认路由是否存在 */1 * * * * /sbin/ip -6 route show default | grep -q default || echo IPv6 default route missing | logger或者用更专业的监控工具采集ip -6 route和ip -6 addr的输出做趋势分析。5.3 Nginx 层的兜底即使系统层修好了Nginx 层也可以加一些兜底配置。比如proxy_connect_timeout 5s; proxy_next_upstream error timeout http_502 http_503;这样即使某个地址不可达也能快速切换到下一个。5.4 定期演练IPv6 的问题不像 IPv4 那么直观很多运维人员不熟悉。建议定期做故障演练手动删掉默认路由看监控是否告警看 Nginx 是否按预期 fallback。演练过的故障才叫故障没演练过的叫事故。我个人在实际操作中的体会是Network is unreachable这个报错看着吓人但排查路径其实很清晰先看协议栈再看地址再看路由最后看应用配置。九成的问题在前三步就能定位。真正麻烦的是间歇性的问题那就要盯valid_lft、盯 RA、盯 DNS 缓存。IPv6 的坑比 IPv4 多但一旦配好稳定性其实更好因为地址空间大不容易冲突。最后再分享一个小技巧如果实在搞不定 IPv6又不想改架构可以在 Nginx 前面加一层只监听 IPv4 的代理把 IPv6 的问题隔离在系统层应用层只走 IPv4这样至少业务不受影响。