IPv4 和 IPv6 并存的时候,应用先连哪个?IPv6 连不通为什么会让整个网页卡几十秒?Happy Eyeballs 的 250 毫秒是怎么回事?这篇用 netlab 给 IPv6 加延迟、用一个真实的 IPv6 黑洞地址,实测顺序回退与 Happy Eyeballs 的差别。
net.inet6.ip6.v6only: 0(0 = 默认双栈) IPV6_V6ONLY=0:连 ::1 成功,服务端看到的对端地址是 ::1;连 127.0.0.1 成功,服务端看到的对端地址是 ::ffff:127.0.0.1 IPV6_V6ONLY=1:连 ::1 成功,服务端看到的对端地址是 ::1;连 127.0.0.1 失败:ConnectionRefusedError: Connection refused IPv4 映射地址:::ffff:127.0.0.1 → Python 的 .ipv4_mapped = 127.0.0.1;一个 AF_INET6 的监听套接字因此可以同时服务两种协议
监听 :: 的 AF_INET6 套接字默认同时收 IPv4 与 IPv6,IPv4 客户端以 ::ffff:a.b.c.d 的映射地址出现;IPV6_V6ONLY=1 则只收 IPv6。
getaddrinfo("localhost") 默认:IPv6 ::1 → IPv4 127.0.0.1
getaddrinfo("localhost") AI_ADDRCONFIG:IPv6 ::1 → IPv4 127.0.0.1
按 RFC 6724 的默认策略,IPv6 优先;所以连 localhost 时应用先试 ::1,失败(被拒绝)再试 127.0.0.1getaddrinfo 按 RFC 6724 把 IPv6 排在前面,应用依次尝试。被拒绝时瞬间回退,而地址挂住时每一项都要等到超时。
没有延迟时(基线):顺序连接(IPv6 先)用时中位 0.10 ms;Happy Eyeballs 用时中位 0.12 ms,5 次里 IPv6 赢了 5 次 IPv6 加了 400 ms 单向延迟:顺序连接(IPv6 先,等它连上才算):用时中位 801 ms,每次最终连上的是IPv6 Happy Eyeballs(250 ms 后启动 IPv4):用时中位 255 ms,每次最终连上的是IPv4(5 次里 IPv4 赢了 5 次) Happy Eyeballs 不是放弃 IPv6:基线里 IPv6 本来就快,它赢了(IPv4 的尝试没来得及启动或被放弃);只有 IPv6 太慢才让 IPv4 去抢
往返 800 ms 的 IPv6 让顺序回退花 801 ms;Happy Eyeballs 在 IPv6 起步 250 ms 后并行启动 IPv4,用时 255 ms。IPv6 正常时 IPv6 照样赢。
happy_eyeballs_delay=None(asyncio 默认,关闭):等待上限 5 秒,结果 [(5.0, 'TimeoutError'), (5.0, 'TimeoutError')]——一直卡在 IPv6 黑洞上,根本没尝试 IPv4(超时 = 5 秒的人为上限) happy_eyeballs_delay=0.25:[(254, '127.0.0.1'), (253, '127.0.0.1'), (252, '127.0.0.1')](毫秒,连上的对端地址)——约 250 ms 后启动 IPv4,立刻成功
目标是不存在的链路本地地址时,SYN 发不出去。asyncio 关闭 Happy Eyeballs 卡死,打开 0.25 就在 254 ms 内连上 IPv4。
happy_eyeballs_delay=0.05 → 连上用时中位 52 ms(额外开销 1.8 ms) happy_eyeballs_delay=0.1 → 连上用时中位 102 ms(额外开销 1.9 ms) happy_eyeballs_delay=0.25 → 连上用时中位 252 ms(额外开销 2.5 ms) happy_eyeballs_delay=0.5 → 连上用时中位 502 ms(额外开销 1.7 ms) happy_eyeballs_delay=1.0 → 连上用时中位 1004 ms(额外开销 3.8 ms) RFC 8305 的建议:先解析 AAAA 再等 A(解析延迟 50 ms)、连接尝试延迟 250 ms(可根据历史 RTT 在 100 ms 到 2 s 间调整);asyncio 的建议值同样是 0.25
延迟值就是黑洞时用户多等的时间上限(实测连上用时 = 延迟 + 2~4 ms)。RFC 8305 建议 250 ms,可在 100 ms 到 2 s 之间调整。