kubernetes容器中域名解析优化

多余的DNS查询

一些需要解析外部DNS域名的应用,当运行在容器中时,如果我们在容器的network namespace中对dns报文(udp port 53)进行抓包,可能会发现在正确解析之前,还经过了若干次多余的尝试。

下面是我在容器中 ping google.com
,同时在容器的network namespace中抓到的包。

sudo nsenter -t 3885 -n tcpdump -i eth0 udp port 53
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
10:09:11.917900 IP 10.244.2.202.38697 > 10.96.0.10.domain: 11858+ A? google.com.default.svc.cluster.local. (54)
10:09:11.918847 IP 10.96.0.10.domain > 10.244.2.202.38697: 11858 NXDomain*- 0/1/0 (147)
10:09:11.922468 IP 10.244.2.202.38697 > 10.96.0.10.domain: 15573+ AAAA? google.com.default.svc.cluster.local. (54)
10:09:11.923001 IP 10.96.0.10.domain > 10.244.2.202.38697: 15573 NXDomain*- 0/1/0 (147)
10:09:11.923248 IP 10.244.2.202.43230 > 10.96.0.10.domain: 62042+ A? google.com.svc.cluster.local. (46)
10:09:11.923828 IP 10.96.0.10.domain > 10.244.2.202.43230: 62042 NXDomain*- 0/1/0 (139)
10:09:11.924005 IP 10.244.2.202.43230 > 10.96.0.10.domain: 54769+ AAAA? google.com.svc.cluster.local. (46)
10:09:11.924494 IP 10.96.0.10.domain > 10.244.2.202.43230: 54769 NXDomain*- 0/1/0 (139)
10:09:11.924704 IP 10.244.2.202.36252 > 10.96.0.10.domain: 20727+ A? google.com.cluster.local. (42)
10:09:11.925154 IP 10.96.0.10.domain > 10.244.2.202.36252: 20727 NXDomain*- 0/1/0 (135)
10:09:11.925316 IP 10.244.2.202.36252 > 10.96.0.10.domain: 13066+ AAAA? google.com.cluster.local. (42)
10:09:11.925758 IP 10.96.0.10.domain > 10.244.2.202.36252: 13066 NXDomain*- 0/1/0 (135)
10:09:11.925929 IP 10.244.2.202.35582 > 10.96.0.10.domain: 38821+ A? google.com.lan. (32)
10:09:11.927244 IP 10.244.2.202.35582 > 10.96.0.10.domain: 4430+ AAAA? google.com.lan. (32)
10:09:11.927416 IP 10.96.0.10.domain > 10.244.2.202.35582: 38821 NXDomain 0/0/0 (32)
10:09:11.928600 IP 10.96.0.10.domain > 10.244.2.202.35582: 4430 NXDomain 0/0/0 (32)
10:09:11.928839 IP 10.244.2.202.45290 > 10.96.0.10.domain: 45577+ A? google.com. (28)
10:09:11.929129 IP 10.244.2.202.45290 > 10.96.0.10.domain: 37586+ AAAA? google.com. (28)
10:09:11.929303 IP 10.96.0.10.domain > 10.244.2.202.45290: 45577 1/0/0 A 172.217.160.78 (54)
10:09:11.929541 IP 10.96.0.10.domain > 10.244.2.202.45290: 37586 1/0/0 AAAA 2404:6800:4008:801::200e (66)

可以看到,在最后(倒数第3、4行)正确解析之前,先是依次查询了下面几个域名,并且均查询了IPv4和IPv6:

google.com.default.svc.cluster.local.
google.com.svc.cluster.local.
google.com.cluster.local.
google.com.lan.

但是这8次查询都失败了,因为并不存在这样的域名。

kubernetes的容器域名解析

要想解释上面的现象,需要先从kubernetes的容器域名解析开始讲。

kubernetes上运行的容器,其域名解析和一般的Linux一样,都是根据 /etc/resolv.conf
文件。如下是容器中该文件的内容。

nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local lan
options ndots:5

nameserver即为kubernetes集群中,kube-dns的svc IP,集群中容器的nameserver均设置为kube-dns。

kubectl get svc -n kube-system
NAME             TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)                  AGE
kube-dns         ClusterIP   10.96.0.10              53/UDP,53/TCP,9153/TCP   236d

那么 search
ndots
是做什么的呢?

search和ndots

在解释search和ndots之前,需要先了解一个概念:FQDN(Fully qualified domain name)。

FQDN是完整域名,一般来说,域名最终以 .
结束表示是FQDN,例如 google.com.
是FQDN,但 google.com
不是。
对FQDN,操作系统会直接查询DNS server。那么非FQDN呢?这里就要用到search和ndots了。

ndots
表示的是域名中必须出现的 .
的个数,如果域名中的 .
的个数不小于 ndots
,则该域名为一个FQDN,操作系统会直接查询;如果域名中的 .
的个数小于 ndots
,操作系统会在 search
搜索域中进行查询。

例如上面的例子, ndots
为5,查询的域名 google.com
不以 .
结尾,且 .
的个数少于5,因此操作系统会依此在 default.svc.cluster.local svc.cluster.local cluster.local lan
四个域中进行了搜索,其中前面3个搜索域是由kubernetes注入的,最后的 lan
是操作系统默认的搜索域。

ndots
默认值为1,也就是说,只要域名中有一个 .
,操作系统就会认为是绝对域名,直接查询。

ndots
上限为15。

ndots:n
    Sets a threshold for the number of dots which must appear in a name given to res_query(3) (see resolver(3)) before an initial absolute query will be made.  The default  for  n  is  1, meaning  that  if  there  are  any  dots  in a name, the name will be tried first as an absolute name before any search list elements are appended to it.  The value for this option is silently capped to 15.

kubernetes为什么使用搜索域

为什么呢?先来看看代码。

var (
    // The default dns opt strings.
    defaultDNSOptions = []string{"ndots:5"}
)
func (c *Configurer) generateSearchesForDNSClusterFirst(hostSearch []string, pod *v1.Pod) []string {
    if c.ClusterDomain == "" {
        return hostSearch
    }

    nsSvcDomain := fmt.Sprintf("%s.svc.%s", pod.Namespace, c.ClusterDomain)
    svcDomain := fmt.Sprintf("svc.%s", c.ClusterDomain)
    clusterSearch := []string{nsSvcDomain, svcDomain, c.ClusterDomain}

    return omitDuplicates(append(clusterSearch, hostSearch...))
}
func (c *Configurer) GetPodDNS(pod *v1.Pod) (*runtimeapi.DNSConfig, error) {
    ...
    case podDNSCluster:
        if len(c.clusterDNS) != 0 {
            dnsConfig.Servers = []string{}
            for _, ip := range c.clusterDNS {
                dnsConfig.Servers = append(dnsConfig.Servers, ip.String())
            }
            dnsConfig.Searches = c.generateSearchesForDNSClusterFirst(dnsConfig.Searches, pod)
            dnsConfig.Options = defaultDNSOptions
            break
        }
    ...
    if utilfeature.DefaultFeatureGate.Enabled(features.CustomPodDNS) && pod.Spec.DNSConfig != nil {
        dnsConfig = appendDNSConfig(dnsConfig, pod.Spec.DNSConfig)
    }
}

kubernetes搜索域

从函数 generateSearchesForDNSClusterFirst
中可见,搜索域有三个:nsSvcDomain、svcDomain、clusterDomain。
kubernetes之所以要设置搜索域,目的是为了方便用户访问service。

例如,default namespace下的Pod a,如果访问同namespace下的service b,直接使用 b 就可以访问了,而这个功能,就是通过nsSvcDomain搜索域 default.svc.cluster.local
完成的。

类似的,对于不同namespace下的service,可以用 ${service name}.${namespace name}
来访问,是通过svcDomain搜索域完成的。

clusterDomain设计的目的,是为了方便同域中非kubernetes上的域名访问,例如设置kubernetes的domain为 ieevee.com
,那么对于 s.ieevee.com
域名,直接使用 s
来访问就可以了,当然前提是当前namespace中没有一个叫做 s
的svc。(是的。搜索域是有优先级的)

ndots默认值

ndots
默认值是写死的,5。
为什么是5呢?

thockin在 issue 33554
中做了解释,概况来说:

  1. kubernetes需要支持同namespace下service快速访问,例如 name
    ,因此 ndots>=1,对应搜索域 $namespace.svc.$zone
  2. kubernetes需要支持跨namespace下service快速访问,例如 kubernetes.default
    ,因此 ndots>=2,对应搜索域 svc.$zone
  3. kubernetes需要支持同namespace、跨namespace下,非service名称的快速访问,例如 name.namespace.svc
    ,因此 ndots>=3,对应搜索域 $zone
  4. kubernetes需要支持statefulset中的每个pod的访问,例如 mysql-0.mysql.default.svc
    ,因此 ndots>=4
  5. kubernetes需要支持SRV records( _$port._$proto.$service.$namespace.svc.$zone
    ),因此 ndots>=5

不过呢,如果你的使用情况并不像上面这么复杂,这个值可能并不适合你。

比如说,我们只会用到同namespace下(形如 a
)、跨namespace下的service访问(形如 a.b
),因此,ndots默认值为2更合适,但该值是写死在代码中的,不支持定制化,但可以通过下面的方法修改。

ndots修改

ndots是可以被修改的,可以通过 pod.Spec.DNSConfig
改写。

apiVersion: v1
kind: Pod
metadata:
  namespace: default
  name: dns-example
spec:
  containers:
    - name: test
      image: nginx
  dnsConfig:
    options:
      - name: ndots
        value: "2"

通过上面的修改,我们再来看容器中的DNS报文,就只有下面几条了。

sudo nsenter -t 3885 -n tcpdump -i eth0 udp port 53
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
10:30:35.917282 IP 10.244.2.202.39480 > 10.96.0.10.domain: 60870+ A? google.com. (28)
10:30:35.919194 IP 10.244.2.202.39480 > 10.96.0.10.domain: 9908+ AAAA? google.com. (28)
10:30:35.927047 IP 10.96.0.10.domain > 10.244.2.202.39480: 60870 1/0/0 A 216.58.200.238 (54)
10:30:35.929089 IP 10.96.0.10.domain > 10.244.2.202.39480: 9908 1/0/0 AAAA 2404:6800:4008:801::200e (66)

这对于应用的性能会有一定的提升,具体可以参见Ref。

dns cache

我们观察一些python类型的容器应用,会发现它们会发出非常多的DNS请求,几乎每次涉及到域名都需要发出DNS请求;但是对于一些Java应用,会发现它们发出的DNS请求非常规律,一般是每30秒一个。
这是因为,不同语言对于dns cache的处理不同了。
目前来看,只有Java做了dns cache。从JDK 1.6开始,Java默认会对DNS做缓存,主要是以下两个配置:

  • networkaddress.cache.ttl:域名解析成功后,DNS缓存时间,默认是30秒
  • networkaddress.cache.negative.ttl:域名解析失败后,冷却时间,默认是10秒

其他语言可以支持,但都需要一定hack。

总结

本文从一个多余的DNS查询现象开始,介绍了FQDN和DNS的搜索域,回答了kubernetes为什么需要搜索域这个问题,并且提出了一个解决多余DNS查询的方案,以及介绍了不同语言对于dns cache的处理情况。
Ref: