k8s dns解析

集群内域名解析原理

Kubernetes 集群节点上 kubelet 有--cluster-dns=${dns-service-ip} 和 
--cluster-domain=${default-local-domain} 两个 dns 相关参数,
分别被用来设置集群DNS服务器的IP地址和主域名后缀。

查看集群 default 命名空间下 dnsPolicy:ClusterFirst模式的 Pod 内的 DNS 域名解析配置文件 /etc/resolv.conf 内容:

nameserver 172.24.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

各参数描述如下:

nameserver: 定义 DNS 服务器的 IP 地址。

search: 设置域名的查找后缀规则,查找配置越多,说明域名解析查找匹配次数越多。
集群匹配有 default.svc.cluster.local、svc.cluster.local、cluster.local 3个后缀,

option: 定义域名解析配置文件选项,支持多个KV值。

例如该参数设置成ndots:5,说明如果访问的域名字符串内的点字符数量超过ndots值,则认为是完整域名,
并被直接解析;如果不足ndots值,则追加search段后缀再进行查询。

根据上述文件配置,在 Pod 内尝试解析:

1、同命名空间下服务,如 kubernetes:
添加一次 search 域,发送kubernetes.default.svc.cluster.local. 一次 ipv4 域名解析请求
到172.24.0.10 进行解析即可。

2、跨命名空间下的服务,如 kube-dns.kue-system:
添加两次 search 域,发送 kube-dns.kue-system.default.svc.cluster.local. 和
kube-dns.kue-system.svc.cluster.local. 两次 ipv4 域名解析请求到
172.24.0.10 才能解析出正确结果。

3、集群外服务,如 aliyun.com:
添加三次 search 域,发送 aliyun.com.default.svc.cluster.local.、
aliyun.com.svc.cluster.local.、aliyun.com.cluster.local. 和 aliyun.com 四次
ipv4 域名解析请求到 172.24.0.10 才能解析出正确结果。

Pod dnsPolicy

Kubernetes 集群中支持通过 dnsPolicy 字段为每个 Pod 配置不同的 DNS 策略。
目前支持四种策略:

1、None

表示空的DNS设置,这种方式一般用于想要自定义 DNS 配置的场景,而且,
往往需要和 dnsConfig 配合一起使用达到自定义 DNS 的目的。

2、Default

Default 的方式不一定是使用宿主机的方式,这种说法并不准确。
这种方式其实是让 kubelet 来决定使用何种 DNS 策略。
而 kubelet 默认的方式,就是使用宿主机的 /etc/resolv.conf

kubelet 是可以灵活来配置使用什么文件来进行DNS策略的,我们完全可以使用 kubelet 的参数:
–resolv-conf=/etc/resolv.conf 来决定你的DNS解析文件地址。

3、ClusterFirst

这种方式,表示 POD 内的 DNS 使用集群中配置的 DNS 服务,简单来说,就是使用 Kubernetes 中
 kubedns 或 coredns 服务进行域名解析。如果解析不成功,才会使用宿主机的 DNS 配置进行解析。

4、ClusterFirstWithHostNet

在某些场景下,我们的 POD 是用 HOST 模式启动的(HOST模式,是共享宿主机网络的),
一旦用 HOST 模式,表示这个 POD 中的所有容器,
都要使用宿主机的 /etc/resolv.conf 配置进行DNS查询,但如果你想使用了 HOST 模式,
还继续使用 Kubernetes 的DNS服务,那就将 dnsPolicy 设置为 ClusterFirstWithHostNet。

CoreDNS

CoreDNS 目前是 Kubernetes 标准的服务发现组件,dnsPolicy: ClusterFirst 模式的 Pod 会使用 CoreDNS 来解析集群内外部域名。

在 Kubernetes 中,服务发现有几种方式:

①:基于环境变量的方式
②:基于内部域名的方式

DNS 如何解析,依赖容器内 resolv 文件的配置

cat /etc/resolv.conf

nameserver 10.233.0.3
search default.svc.cluster.local svc.cluster.local cluster.local

这个文件中,配置的 DNS Server,一般就是 K8S 中,kubedns 的 Service 的 ClusterIP

[root@node4 user1]# kubectl get svc -n kube-system
NAME                   TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)         AGE
kube-dns               ClusterIP   10.233.0.3      <none>        53/UDP,53/TCP   270d
kubernetes-dashboard   ClusterIP   10.233.22.223   <none>        443/TCP         124d

所以,所有域名的解析,其实都要经过 kubedns 的虚拟IP 10.233.0.3 进行解析,不论是 Kubernetes 内部域名还是外部的域名。

Kubernetes 中,域名的全称,必须是 service-name.namespace.svc.cluster.local 这种模式,服务名,就是Kubernetes中 Service 的名称

所以,当我们执行下面的命令时:

curl b

必须得有一个 Service 名称为 b,这是前提。
在容器内,会根据 /etc/resolve.conf 进行解析流程。选择 nameserver 10.233.0.3 进行解析,
然后,用字符串 “b”,依次带入 /etc/resolve.conf 中的 search 域,进行DNS查找,分别是:

// search 内容类似如下(不同的pod,第一个域会有所不同)
search default.svc.cluster.local svc.cluster.local cluster.local
b.default.svc.cluster.local -> b.svc.cluster.local -> b.cluster.local ,直到找到为止。

curl b,要比 curl b.default 效率高?

答案是肯定的,因为 curl b.default,多经过了一次 DNS 查询。
当执行 curl b.default,也就使用了带有命名空间的内部域名时,容器的第一个 DNS 请求是
// b.default + default.svc.cluster.local
b.default.default.svc.cluster.local

当请求不到 DNS 结果时,使用
// b.default + svc.cluster.local
b.default.svc.cluster.local
进行请求,此时才可以得到正确的DNS解析。

访问外部域名走 search 域吗

在 Kubernetes 中,其实 /etc/resolv.conf 这个文件,并不止包含 nameserver 和 search 域,
还包含了非常重要的一项:ndots。

[root@xxxx-67f54c6dff-h4zxq /]# cat /etc/resolv.conf 
nameserver 10.233.0.3
search cicd.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

ndots:5,表示:如果查询的域名包含的点“.”,不到5个,那么进行DNS查找,将使用非完全限定名称
(或者叫绝对域名),如果你查询的域名包含点数大于等于5,那么DNS查询,默认会使用绝对域名进行查询。

域名中点数少于5个的情况:
// 对域名 a.b.c.d.ccccc 进行DNS解析请求 
[root@xxxxx-67f54c6dff-h4zxq /]# nslookup  a.b.c.d.ccccc 172.22.121.65 
Server:         172.22.121.65
Address:        172.22.121.65#53

** server can't find a.b.c.d.ccccc: NXDOMAIN

// 抓包数据如下:
18:08:11.013497 IP 172.20.92.100.33387 > node011094.domain: 28844+ A? a.b.c.d.ccccc.cicd.svc.cluster.local. (54)
18:08:11.014337 IP 172.20.92.100.33952 > node011094.domain: 57782+ A? a.b.c.d.ccccc.svc.cluster.local. (49)
18:08:11.015079 IP 172.20.92.100.45984 > node011094.domain: 55144+ A? a.b.c.d.ccccc.cluster.local. (45)
18:08:11.015747 IP 172.20.92.100.54589 > node011094.domain: 22860+ A? a.b.c.d.ccccc. (31)
18:08:11.015970 IP node011094.36383 > 192.168.x.x.domain: 22860+ A? a.b.c.d.ccccc. (31)

// 结论:
// 点数少于5个,先走search域,最后将其视为绝对域名进行查询

域名中点数>=5个的情况:
// 对域名 a.b.c.d.e.ccccc 进行DNS解析请求
[root@xxxxx-67f54c6dff-h4zxq /]# nslookup  a.b.c.d.e.ccccc 172.22.121.65 
Server:         172.22.121.65
Address:        172.22.121.65#53

** server can't find a.b.c.d.e.ccccc: NXDOMAIN

// 抓包数据如下:
18:10:14.514595 IP 172.20.92.100.34423 > node011094.domain: 61170+ A? a.b.c.d.e.ccccc. (33)
18:10:14.514856 IP node011094.58522 > 192.168.x.x.domain: 61170+ A? a.b.c.d.e.ccccc. (33)
18:10:14.515880 IP 172.20.92.100.49328 > node011094.domain: 267+ A? a.b.c.d.e.ccccc.cicd.svc.cluster.local. (56)
18:10:14.516678 IP 172.20.92.100.35651 > node011094.domain: 54181+ A? a.b.c.d.e.ccccc.svc.cluster.local. (51)
18:10:14.517356 IP 172.20.92.100.33259 > node011094.domain: 53022+ A? a.b.c.d.e.ccccc.cluster.local. (47)

// 结论:
// 点数>=5个,直接视为绝对域名进行查找,只有当查询不到的时候,才继续走 search 域。

如何优化 DNS 请求浪费的情况

优化方式1:使用全限定域名

其实最直接,最有效的优化方式,就是使用 “fully qualified name”,
简单来说,使用“完全限定域名”(也叫绝对域名)

即:你访问的域名,必须要以 “.” 为后缀,这样就会避免走 search 域进行匹配,我们抓包再试一次:

// 注意:youku.com 后边有一个点 .
nslookup  youku.com. 172.22.121.65
在DNS服务容器上抓到的包如下:

16:57:07.628112 IP 172.20.92.100.36772 > nodexxxx.domain: 46851+ [1au] A? youku.com. (38)
16:57:07.628339 IP nodexxxx.47350 > 192.168.x.x.domain: 46851+ [1au] A? youku.com. (38)
并没有多余的DNS请求。

优化方式2:具体应用配置特定的 ndots

在 Kubernetes 中,默认设置了 ndots 值为5,是因为,Kubernetes 认为,内部域名,最长为5,
要保证内部域名的请求,优先走集群内部的DNS,而不是将内部域名的DNS解析请求,有打到外网的机会,
Kubernetes 设置 ndots 为5是一个比较合理的行为。

如果你需要定制这个长度,最好是为自己的业务,单独配置 ndots 即可:
apiVersion: v1
kind: Pod
metadata:
  namespace: default
  name: dns-example
spec:
  containers:
    - name: test
      image: nginx
  dnsConfig:
    options:
      - name: ndots
        value: "1"

k8s服务发现方式以及原理:环境变量

在 Node 上新创建一个 Pod 时,kubelet 会为每个 Pod(容器)添加一组环境变量,其中就包括当前系统中已经存在的 Service 的 IP 地址和端口号。

1、kubernetes Service 环境变量

Kubernetes为每个Service资源生成包括以下形式的环境变量在内的一系列环境变量,
在同一名称空间中创建的Pod对象都会自动拥有这些变量。

{SVCNAME}_SERVICE_HOST
{SVCNAME}_SERVICE_PORT

如果SVCNAME中使用了连接线,那么Kubernetes会在定义为环境变量时将其转换为下划线。

2、Docker Link 形式的环境变量

 

Docker使用--link选项实现容器连接时所设置的环境变量形式,
在创建Pod对象时,Kubernetes也会将与此形式兼容的一系列环境变量注入Pod对象中。

例如,在Service资源myapp-svc创建后创建的Pod对象中查看可用的环境变量,
其中以MYAPP_SVC_SERVICE开头的表示Kubernetes Service环境变量,
名称中不包含“SERVICE”字符串的环境变量为Docker Link形式的环境变量:

 / # env | grep -i myapp
 MYAPP_SVC_PORT_80_TCP_ADDR=10.98.57.156
 MYAPP_SVC_PORT_80_TCP_PORT=80
 HOSTNAME=myapp-deploy-5cbd66595b-2lhds
 MYAPP_SVC_PORT_80_TCP_PROTO=tcp
 MYAPP_SVC_PORT_80_TCP=tcp://10.98.57.156:80
 MYAPP_SVC_SERVICE_HOST=10.98.57.156
 MYAPP_SVC_SERVICE_PORT=80
 MYAPP_SVC_PORT=tcp://10.98.57.156:80

基于环境变量的服务发现的特点:

基于环境变量的服务发现其功能简单、易用,但存在一定的局限,例如,仅有那些与创建Pod对象在同一名称空间中且事先存在的Service对象的信息才会以环境变量的形式注入,那些处于非同一名称空间,或者是在Pod资源创建之后才创建的Service对象的相关环境变量则不会被添加。幸而,基于DNS的发现机制并不存在此类限制。

k8s服务发现方式以及原理:dns配置注入

kubelet启动Pod的时候,会将DNS配置注入到Pod中,其实就是kubelet 会为每个 Pod 重写此文件
DNS 查询可以使用 Pod 中的 /etc/resolv.conf 展开。

当pod调度到节点上之后,kubelet会来给pod配置具体的resolv.conf内容:

1 kubelet会先创建并运行pod的sandbox,然后获取到sandbox的ResolvConfPath
(/var/lib/docker/containers/xxxxxxx/resolv.conf),
接下来,把dns policy的具体内容写到sandbox的ResolvConfPath(直接覆盖写)。

2 kubelet继续创建同一个pod中的其他container,并且使用相同的ResolvConfPath
(同一个pod的所有容器的ResolvConfPath在宿主机上的真实源是同一个)。

所以,可以看到,pod内的resolv.conf是pod在创建的时候就确定下来的。

源码剖析:

func (ds *dockerService) RunPodSandbox(ctx context.Context, r *runtimeapi.RunPodSandboxRequest) (*runtimeapi.RunPodSandboxResponse, error) {
    config := r.GetConfig()

    // Step 1: Pull the image for the sandbox.
    image := defaultSandboxImage
    podSandboxImage := ds.podSandboxImage
    if len(podSandboxImage) != 0 {
        image = podSandboxImage
    }

    // NOTE: To use a custom sandbox image in a private repository, users need to configure the nodes with credentials properly.
    // see: http://kubernetes.io/docs/user-guide/images/#configuring-nodes-to-authenticate-to-a-private-repository
    // Only pull sandbox image when it's not present - v1.PullIfNotPresent.
    if err := ensureSandboxImageExists(ds.client, image); err != nil {
        return nil, err
    }

    // Step 2: Create the sandbox container.
    if r.GetRuntimeHandler() != "" && r.GetRuntimeHandler() != runtimeName {
        return nil, fmt.Errorf("RuntimeHandler %q not supported", r.GetRuntimeHandler())
    }
    createConfig, err := ds.makeSandboxDockerConfig(config, image)
    if err != nil {
        return nil, fmt.Errorf("failed to make sandbox docker config for pod %q: %v", config.Metadata.Name, err)
    }
    createResp, err := ds.client.CreateContainer(*createConfig)
    if err != nil {
        createResp, err = recoverFromCreationConflictIfNeeded(ds.client, *createConfig, err)
    }

    if err != nil || createResp == nil {
        return nil, fmt.Errorf("failed to create a sandbox for pod %q: %v", config.Metadata.Name, err)
    }
    resp := &runtimeapi.RunPodSandboxResponse{PodSandboxId: createResp.ID}

    ds.setNetworkReady(createResp.ID, false)
    defer func(e *error) {
        // Set networking ready depending on the error return of
        // the parent function
        if *e == nil {
            ds.setNetworkReady(createResp.ID, true)
        }
    }(&err)

    // Step 3: Create Sandbox Checkpoint.
    if err = ds.checkpointManager.CreateCheckpoint(createResp.ID, constructPodSandboxCheckpoint(config)); err != nil {
        return nil, err
    }

    // Step 4: Start the sandbox container.
    // Assume kubelet's garbage collector would remove the sandbox later, if
    // startContainer failed.
    err = ds.client.StartContainer(createResp.ID)
    if err != nil {
        return nil, fmt.Errorf("failed to start sandbox container for pod %q: %v", config.Metadata.Name, err)
    }

    // Rewrite resolv.conf file generated by docker.
    // NOTE: cluster dns settings aren't passed anymore to docker api in all cases,
    // not only for pods with host network: the resolver conf will be overwritten
    // after sandbox creation to override docker's behaviour. This resolv.conf
    // file is shared by all containers of the same pod, and needs to be modified
    // only once per pod.
    if dnsConfig := config.GetDnsConfig(); dnsConfig != nil {
        containerInfo, err := ds.client.InspectContainer(createResp.ID)
        if err != nil {
            return nil, fmt.Errorf("failed to inspect sandbox container for pod %q: %v", config.Metadata.Name, err)
        }
        // 重写容器的dns解析配置
        // containerInfo.ResolvConfPath这里为容器目录在宿主机上对应的路径
        // 比如:/var/lib/docker/containers/xxxx/resolv.conf"
        if err := rewriteResolvFile(containerInfo.ResolvConfPath, dnsConfig.Servers, dnsConfig.Searches, dnsConfig.Options); err != nil {
            return nil, fmt.Errorf("rewrite resolv.conf failed for pod %q: %v", config.Metadata.Name, err)
        }
    }
    ...
    ...
}

kubelet读取容器的resolv.conf,其实是读取了容器的这个变量:

docker inspect f580cc012e09 | grep res
        "ResolvConfPath": "/var/lib/docker/containers/010287003ba360003b0b7ec48b63240f084669de6771cd158ed4c287f7a1ac75/resolv.conf",