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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/*
* getaddrinfo.c - Simple example of using getaddrinfo(3) function.
*
* Michal Ludvig <[email protected]> (c) 2002, 2003
* http://www.logix.cz/michal/devel/
*
* License: public domain.
*/

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

int
lookup_host (const char *host)
{
struct addrinfo hints, *res;
int errcode;
char addrstr[100];
void *ptr;

memset (&hints, 0, sizeof (hints));
hints.ai_family = PF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags |= AI_CANONNAME;

errcode = getaddrinfo (host, NULL, &hints, &res);
if (errcode != 0)
{
perror ("getaddrinfo");
return -1;
}

printf ("Host: %s\n", host);
while (res)
{
inet_ntop (res->ai_family, res->ai_addr->sa_data, addrstr, 100);

switch (res->ai_family)
{
case AF_INET:
ptr = &((struct sockaddr_in *) res->ai_addr)->sin_addr;
break;
case AF_INET6:
ptr = &((struct sockaddr_in6 *) res->ai_addr)->sin6_addr;
break;
}
inet_ntop (res->ai_family, ptr, addrstr, 100);
printf ("IPv%d address: %s (%s)\n", res->ai_family == PF_INET6 ? 6 : 4,
addrstr, res->ai_canonname);
res = res->ai_next;
}

return 0;
}

int
main (int argc, char *argv[])
{
if (argc < 2)
exit (1);
return lookup_host (argv[1]);
}

测试程序运行

安装软件

1
debuginfo-install glibc-2.12-1.166.el6_7.3.x86_64;

这里因为我的 glibc 版本是 2.12-1.166.el6_7.3

编译参数

1
2
3
gcc -g -o g getaddrinfo.c;
# 运行,gdb 调试
gdb ./g
1
2
3
break main
run www.a.shifen.com
# 再一步步跟吧

gdb跟踪

发现在 glibc 源代码里,/usr/src/debug/glibc-2.12-2-gc4ccff1/sysdeps/posix/getaddrinfo.c 文件中第 2436 行和 2437 行:

1
2
if (in6ai != NULL)
qsort (in6ai, in6ailen, sizeof (*in6ai), in6aicmp);

这里的第 2437 行的函数 qsort 就是用来做排序的,前面的判断条件 in6ai != NULL 在有 IPv6 的环境里成立;反之在仅有 IPv4 的环境里不成立。这也就是干掉 IPv6 会直接规避掉这个大“坑”的直接原因。