Linux域名解析中的IP地址选择“亲和性”问题
问题初现
最早发现问题时是在做一个测试,把某个域名解析到内网的几台机器上(10.0.0.9 和 10.0.0.119),结果用客户端(10.0.0.200 和 10.0.0.201,都由于特殊原因没有关掉 IPv6)去连接测试时发现,连接都跑到 10.0.0.119 上了,10.0.0.9 上几乎没有连接!
问题原因
网上 Google 了一下,据说是新的域名解析系统调用 getaddrinfo() 加入了对 rfc3484 的支持导致的,这种支持会将 DNS 服务器返回某个域名的多个 IP(顺序随机),按照一定的逻辑排序后再返回,这样的话由于客户端一般都会取第一个 IP,所以我们看到的结果就是某个客户端老连一个 IP。
至于 rfc3484 中涉及到的排序逻辑,我的理解大概是优先返回跟客户端“最近”的 IP 地址(具体算法肯定远比这个复杂,但大概就是这个意思,所以我把 rfc3484 引入的这个问题称之为“亲和性”问题)。
我们的客户端的 IP 是 10.0.0.200 和 10.0.0.201,显然离 10.0.0.119 比离 10.0.0.9 更“近”,所以,连接都跑 10.0.0.119 上去了,10.0.0.9 上几乎没有连接。
解决方法
- 强制使用老系统调用:gethostbyname
- 系统里干掉 IPv6
当然是第二种方法合适,因为这样我们不用动代码,而只需要配置下环境即可。
测试求证
本着打破沙锅问到底的精神,找了个简单程序调用 getaddrinfo,并跟进 glibc 的源代码里看到了 getaddrinfo 函数执行情况的细节。于是找了两台服务器(用作客户端):
- 10.0.0.3(IPv6 enabled)
- 10.0.0.233(IPv6 disabled)
在这两台机器上分别跑 gai.c 编出来的二进制文件,发现 IPv6 启用的那台(10.0.0.3)域名解析始终首先返回 10.0.0.9,而 IPv6 disabled 的那台(10.0.0.233)却一会儿首先返回 10.0.0.119,一会儿又首先返回 10.0.0.9。
附录
例程getaddrinfo.c
这个例程是网上翻出来的,连文件名、版权注释都没改。:)
1 | /* |
测试程序运行
安装软件
1 | debuginfo-install glibc-2.12-1.166.el6_7.3.x86_64; |
这里因为我的 glibc 版本是 2.12-1.166.el6_7.3
编译参数
1 | gcc -g -o g getaddrinfo.c; |
1 | break main |
gdb跟踪
发现在 glibc 源代码里,/usr/src/debug/glibc-2.12-2-gc4ccff1/sysdeps/posix/getaddrinfo.c 文件中第 2436 行和 2437 行:
1 | if (in6ai != NULL) |
这里的第 2437 行的函数 qsort 就是用来做排序的,前面的判断条件 in6ai != NULL 在有 IPv6 的环境里成立;反之在仅有 IPv4 的环境里不成立。这也就是干掉 IPv6 会直接规避掉这个大“坑”的直接原因。