• 【博客496】k8s dns解析与服务发现原理


    k8s dns解析

    集群内域名解析原理

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

    查看集群 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
    
    • 1
    • 2
    • 3

    各参数描述如下:

    nameserver: 定义 DNS 服务器的 IP 地址。
    
    search: 设置域名的查找后缀规则,查找配置越多,说明域名解析查找匹配次数越多。
    集群匹配有 default.svc.cluster.local、svc.cluster.local、cluster.local 3个后缀,
    
    option: 定义域名解析配置文件选项,支持多个KV值。
    
    例如该参数设置成ndots:5,说明如果访问的域名字符串内的点字符数量超过ndots值,则认为是完整域名,
    并被直接解析;如果不足ndots值,则追加search段后缀再进行查询。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    根据上述文件配置,在 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 才能解析出正确结果。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    Pod dnsPolicy

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

    1、None

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

    2、Default

    Default 的方式不一定是使用宿主机的方式,这种说法并不准确。
    这种方式其实是让 kubelet 来决定使用何种 DNS 策略。
    而 kubelet 默认的方式,就是使用宿主机的 /etc/resolv.conf
    
    kubelet 是可以灵活来配置使用什么文件来进行DNS策略的,我们完全可以使用 kubelet 的参数:
    –resolv-conf=/etc/resolv.conf 来决定你的DNS解析文件地址。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3、ClusterFirst

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

    4、ClusterFirstWithHostNet

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

    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
    
    • 1
    • 2
    • 3
    • 4

    这个文件中,配置的 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              53/UDP,53/TCP   270d
    kubernetes-dashboard   ClusterIP   10.233.22.223           443/TCP         124d
    
    • 1
    • 2
    • 3
    • 4

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

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

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

    curl b
    
    • 1

    必须得有一个 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 ,直到找到为止。
    
    • 1
    • 2
    • 3

    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解析。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    访问外部域名走 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 域。
    
    • 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

    如何优化 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请求。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    优化方式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"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

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

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

    1、kubernetes Service 环境变量

    Kubernetes为每个Service资源生成包括以下形式的环境变量在内的一系列环境变量,
    在同一名称空间中创建的Pod对象都会自动拥有这些变量。
    
    {SVCNAME}_SERVICE_HOST
    {SVCNAME}_SERVICE_PORT
    
    如果SVCNAME中使用了连接线,那么Kubernetes会在定义为环境变量时将其转换为下划线。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    2、Docker Link 形式的环境变量

    Docker Link原理:https://www.jianshu.com/p/21d66ca6115e

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

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

    基于环境变量的服务发现其功能简单、易用,但存在一定的局限,例如,仅有那些与创建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在创建的时候就确定下来的。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    源码剖析:

    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)
            }
        }
        ...
        ...
    }
    
    • 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
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78

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

    docker inspect f580cc012e09 | grep res
            "ResolvConfPath": "/var/lib/docker/containers/010287003ba360003b0b7ec48b63240f084669de6771cd158ed4c287f7a1ac75/resolv.conf",
    
    • 1
    • 2
  • 相关阅读:
    1558. 得到目标数组的最少函数调用次数
    DVWA之SQL注入
    (附源码)计算机毕业设计ssm《Java EE开发技术》课程学习网站
    kali——tcpdump的使用
    OpenCV 4.10 发布
    分享一款嵌入式开源按键框架代码工程MultiButton
    Rocket MQ发送消息报错: service not available now
    零基础HTML教程(31)--HTML5多媒体
    2.2.3 vim操作合集
    【C++并发编程】(一)线程管理
  • 原文地址:https://blog.csdn.net/qq_43684922/article/details/126896262