K8S进入Pod排查DNS超时问题

K8S进入Pod排查DNS超时问题

  • 一、抓包分析
  • 二、k8s夺命的5秒DNS延迟
    • 原因
    • 解决方法
    • 场景测试
      • 参考脚本,只做域名解析,不使用dns缓存
      • mac本上编译
      • 压力测试
      • 结论

一、抓包分析

1、登录到node节点
2、docker ps|grep pod名,找到容器对应ID。
3、使用如下脚本进入pod 对应的namespace

# cat a.sh
#!/bin/bash
docker_id=$1
pid=`docker inspect --format "{{ .State.Pid}}" ${docker_id}`
nsenter -n -t${pid}

4、ifconfig查看下当前IP确认已进入到pod内
5、分析dns问题:tcpdump -i eth0 -nt -s 500 port domain
返回值内容解释

二、k8s夺命的5秒DNS延迟

参考
https://zhuanlan.zhihu.com/p/145127061
https://cloud.tencent.com/developer/article/1583706

原因

五元组:指源IP地址,源端口,目的IP地址,目的端口和传输层协议。

DNS client (glibc 或 musl libc) 会并发请求 A 和 AAAA 记录,跟 DNS Server 通信自然会先 connect (建立 fd),后面请求报文使用这个 fd 来发送,由于 UDP 是无状态协议, connect 时并不会发包,也就不会创建 conntrack 表项, 而并发请求的 A 和 AAAA 记录默认使用同一个 fd 发包,send 时各自发的包它们源 Port 相同(因为用的同一个 socket 发送),当并发发包时,两个包都还没有被插入 conntrack 表项,所以 netfilter 会为它们分别创建 conntrack 表项,而集群内请求 kube-dns 或 coredns 都是访问的 CLUSTER-IP,报文最终会被 DNAT 成一个 endpoint 的 POD IP,当两个包恰好又被 DNAT 成同一个 POD IP 时,它们的五元组就相同了,在最终插入的时候后面那个包就会被丢掉,如果 dns 的 pod 副本只有一个实例的情况就很容易发生(始终被 DNAT 成同一个 POD IP),现象就是 dns 请求超时,client 默认策略是等待 5s 自动重试,如果重试成功,我们看到的现象就是 dns 请求有 5s 的延时。

解决方法

避免相同五元组 DNS 请求的并发
通过resolv.conf的single-request-reopen和single-request选项来避免:

  • single-request-reopen (glibc>=2.9) 发送 A 类型请求和 AAAA 类型请求使用不同的源端口。这样两个请求在 conntrack 表中不占用同一个表项,从而避免冲突。

  • single-request (glibc>=2.10) 避免并发,改为串行发送 A 类型和 AAAA 类型请求,没有了并发,从而也避免了冲突。

给容器的/etc/resolv.conf文件添加选项:
通过修改 pod 的 postStart hook 来设置

lifecycle:
  postStart:
    exec:
      command:
        - /bin/sh
        - -c
        - "/bin/echo 'options single-request-reopen' >> /etc/resolv.conf"

场景测试

参考脚本,只做域名解析,不使用dns缓存

package main

import (
    "context"
    "flag"
    "fmt"
    "net"
    "sync/atomic"
    "time"
)

var host string
var connections int
var duration int64
var limit int64
var timeoutCount int64

func main() {
    // os.Args = append(os.Args, "-host", "www.baidu.com", "-c", "200", "-d", "30", "-l", "5000")

    flag.StringVar(&host, "host", "", "Resolve host")
    flag.IntVar(&connections, "c", 100, "Connections")
    flag.Int64Var(&duration, "d", 0, "Duration(s)")
    flag.Int64Var(&limit, "l", 0, "Limit(ms)")
    flag.Parse()

    var count int64 = 0
    var errCount int64 = 0
    pool := make(chan interface{}, connections)
    exit := make(chan bool)
    var (
        min int64 = 0
        max int64 = 0
        sum int64 = 0
    )

    go func() {
        time.Sleep(time.Second * time.Duration(duration))
        exit <- true
    }()
endD:
    for {
        select {
        case pool <- nil:
            go func() {
                defer func() {
                    <-pool
                }()
                resolver := &net.Resolver{}
                now := time.Now()
                _, err := resolver.LookupIPAddr(context.Background(), host)
                use := time.Since(now).Nanoseconds() / int64(time.Millisecond)
                if min == 0 || use < min {
                    min = use
                }
                if use > max {
                    max = use
                }
                sum += use
                if limit > 0 && use >= limit {
                    timeoutCount++
                }
                atomic.AddInt64(&count, 1)
                if err != nil {
                    fmt.Println(err.Error())
                    atomic.AddInt64(&errCount, 1)
                }
            }()
        case <-exit:
            break endD
        }
    }

    fmt.Printf("request count:%d\nerror count:%d\n", count, errCount)
    fmt.Printf("request time:min(%dms) max(%dms) avg(%dms) timeout(%dn)\n", min, max, sum/count, timeoutCount)
}

mac本上编译

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build 

压力测试

分别针对resolv.conf默认配置、添加single-request-reopen和single-request参数进行压测,根据压测结果使用single-request-reopen性能强于single-request(最大耗时也只有300ms左右),同时也没有丢包现象。

200个并发,持续30秒
./k8s-ab -host nacos-1.nacos-hs.default.svc.cluster.local -c 200 -d 30 -l 5000

结论

经过测试在Alpine Linux 3.8下支持resolv.conf配置参数

你可能感兴趣的:(K8S与容器,故障处理)