开心娱乐最重要 發表於 2020-6-2 17:40:00

K8S 内部服务调用域名解析超时

<p>本文摘选自:https://zhuanlan.zhihu.com/p/145127061</p>
<h2>前言</h2>
<p>近期线上 k8s 时不时就会出现一些内部服务间的调用超时问题,通过日志可以得知超时的原因都是出现在<code>域名解析</code>上,并且都是 k8s 内部的域名解析超时,于是直接先将内部域名替换成 k8s service 的 IP,观察一段时间发现没有超时的情况发生了,但是由于使用 service IP 不是长久之计,所以还要去找解决办法。</p>
<h2>复现</h2>
<p>一开始运维同事在调用方 pod 中使用<code>ab</code>工具对目标服务进行了多次压测,并没有发现有超时的请求,我介入之后分析<code>ab</code>这类 http 压测工具应该都会有 dns 缓存,而我们主要是要测试 dns 服务的性能,于是直接动手撸了一个压测工具只做域名解析,代码如下:</p>
<div class="highlight">
<pre><code class="language-text">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(&amp;host, "host", "", "Resolve host")
    flag.IntVar(&amp;connections, "c", 100, "Connections")
    flag.Int64Var(&amp;duration, "d", 0, "Duration(s)")
    flag.Int64Var(&amp;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 &lt;- true
    }()
endD:
    for {
      select {
      case pool &lt;- nil:
            go func() {
                defer func() {
                  &lt;-pool
                }()
                resolver := &amp;net.Resolver{}
                now := time.Now()
                _, err := resolver.LookupIPAddr(context.Background(), host)
                use := time.Since(now).Nanoseconds() / int64(time.Millisecond)
                if min == 0 || use &lt; min {
                  min = use
                }
                if use &gt; max {
                  max = use
                }
                sum += use
                if limit &gt; 0 &amp;&amp; use &gt;= limit {
                  timeoutCount++
                }
                atomic.AddInt64(&amp;count, 1)
                if err != nil {
                  fmt.Println(err.Error())
                  atomic.AddInt64(&amp;errCount, 1)
                }
            }()
      case &lt;-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)
}</code></pre>
</div>
<p>编译好二进制程序直接丢到对应的 pod 容器中进行压测:</p>
<div class="highlight">
<pre><code class="language-text"># 200个并发,持续30秒
./dns -host {service}.{namespace} -c 200 -d 30</code></pre>
</div>
<p>这次可以发现最大耗时有<code>5s</code>多,多次测试结果都是类似:</p>
<p><img class="origin_image zh-lightbox-thumb lazy lazyload" alt="" width="774" data-caption="" data-size="normal" data-rawwidth="774" data-rawheight="73" data-original="https://pic1.zhimg.com/v2-57d09fbf454d7e2abbf20cbc9dd6e678_r.jpg" data-actualsrc="https://pic1.zhimg.com/v2-57d09fbf454d7e2abbf20cbc9dd6e678_b.png" data-lazy-status="ok" data-src="https://pic1.zhimg.com/80/v2-57d09fbf454d7e2abbf20cbc9dd6e678_720w.png"></p>
<p class="ztext-empty-paragraph">&nbsp;</p>
<p>而我们内部服务间 HTTP 调用的超时一般都是设置在<code>3s</code>左右,以此推断出与线上的超时情况应该是同一种情况,在并发高的情况下会出现部分域名解析超时而导致 HTTP 请求失败。</p>
<h2>原因</h2>
<p>起初一直以为是<code>coredns</code>的问题,于是找运维升级了下<code>coredns</code>版本再进行压测,发现问题还是存在,说明不是版本的问题,难道是<code>coredns</code>本身的性能就差导致的?想想也不太可能啊,才 200 的并发就顶不住了那性能也未免太弱了吧,结合之前的压测数据,平均响应都挺正常的(82ms),但是就有个别请求会延迟,而且都是 5 秒左右,所以就又带着<code>k8s dns 5s</code>的关键字去 google 搜了一下,这不搜不知道一搜吓一跳啊,原来是 k8s 里的一个大坑啊(其实和 k8s 没有太大的关系,只是 k8s 层面没有提供解决方案)。</p>
<h3>5s 超时原因</h3>
<p>linux 中<code>glibc</code>的 resolver 的缺省超时时间是 5s,而导致超时的原因是内核<code>conntrack</code>模块的 bug。</p>
<blockquote>Weave works 的工程师 Martynas Pumputis 对这个问题做了很详细的分析:<span class="invisible">https://www.<span class="visible">weave.works/blog/racy-c<span class="invisible">onntrack-and-dns-lookup-timeouts</span></span></span></blockquote>
<p>这里再引用下<span class="invisible">https://<span class="visible">imroc.io/posts/kubernet<span class="invisible">es/troubleshooting-with-kubernetes-network/</span></span></span>文章中的解释:</p>
<blockquote>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 的延时。</blockquote>
<h2>解决方案</h2>
<h3>方案(一):使用 TCP 协议发送 DNS 请求</h3>
<p>通过<code>resolv.conf</code>的<code>use-vc</code>选项来开启 TCP 协议</p>
<h3>测试</h3>
<ol>
<li>修改<code>/etc/resolv.conf</code>文件,在最后加入一行文本:<br>options use-vc</li>
<li>进行压测:<br># 200个并发,持续30秒,记录超过5s的请求个数 ./dns -host {service}.{namespace} -c 200 -d 30 -l 5000<br>结果如下:</li>

</ol>
<p><img class="origin_image zh-lightbox-thumb lazy lazyload" alt="" width="855" data-caption="" data-size="normal" data-rawwidth="855" data-rawheight="188" data-original="https://pic2.zhimg.com/v2-a196bf0fddaf6c7bde64a3b2246a9279_r.jpg" data-actualsrc="https://pic2.zhimg.com/v2-a196bf0fddaf6c7bde64a3b2246a9279_b.jpg" data-lazy-status="ok" data-src="https://pic2.zhimg.com/80/v2-a196bf0fddaf6c7bde64a3b2246a9279_720w.jpg"></p>
<p class="ztext-empty-paragraph">&nbsp;</p>
<h3>结论</h3>
<p>确实没有出现<code>5s</code>的超时问题了,但是部分请求耗时还是比较高,在<code>4s</code>左右,而且平均耗时比 UPD 协议的还高,效果并不好。</p>
<h3>方案(二):避免相同五元组 DNS 请求的并发</h3>
<p>通过<code>resolv.conf</code>的<code>single-request-reopen</code>和<code>single-request</code>选项来避免:</p>
<ul>
<li>single-request-reopen (glibc&gt;=2.9) 发送 A 类型请求和 AAAA 类型请求使用不同的源端口。这样两个请求在 conntrack 表中不占用同一个表项,从而避免冲突。</li>
<li>single-request (glibc&gt;=2.10) 避免并发,改为串行发送 A 类型和 AAAA 类型请求,没有了并发,从而也避免了冲突。</li>

</ul>
<h3>测试 single-request-reopen</h3>
<ol>
<li>修改<code>/etc/resolv.conf</code>文件,在最后加入一行文本:<br>options single-request-reopen</li>
<li>进行压测:<br># 200个并发,持续30秒,记录超过5s的请求个数 ./dns -host {service}.{namespace} -c 200 -d 30 -l 5000<br>结果如下:</li>

</ol>
<p><img class="origin_image zh-lightbox-thumb lazy lazyload" alt="" width="863" data-caption="" data-size="normal" data-rawwidth="863" data-rawheight="167" data-original="https://pic1.zhimg.com/v2-757e18fee4c6b6af118a2939c27001fc_r.jpg" data-actualsrc="https://pic1.zhimg.com/v2-757e18fee4c6b6af118a2939c27001fc_b.jpg" data-lazy-status="ok" data-src="https://pic1.zhimg.com/80/v2-757e18fee4c6b6af118a2939c27001fc_720w.jpg"></p>
<p class="ztext-empty-paragraph">&nbsp;</p>
<h3>测试 single-request</h3>
<ol>
<li>修改<code>/etc/resolv.conf</code>文件,在最后加入一行文本:<br>options single-request</li>
<li>进行压测:<br># 200个并发,持续30秒,记录超过5s的请求个数 ./dns -host {service}.{namespace} -c 200 -d 30 -l 5000<br>结果如下:</li>

</ol>
<p><img class="origin_image zh-lightbox-thumb lazy lazyload" alt="" width="784" data-caption="" data-size="normal" data-rawwidth="784" data-rawheight="165" data-original="https://pic1.zhimg.com/v2-103d4ba8c48d23b8b0edffe81ac4a194_r.jpg" data-actualsrc="https://pic1.zhimg.com/v2-103d4ba8c48d23b8b0edffe81ac4a194_b.jpg" data-lazy-status="ok" data-src="https://pic1.zhimg.com/80/v2-103d4ba8c48d23b8b0edffe81ac4a194_720w.jpg"></p>
<h3>结论</h3>
<p>通过压测结果可以看到<code>single-request-reopen</code>和<code>single-request</code>选项确实可以显著的降低域名解析耗时。</p>
<h3>关于方案(一)和方案(二)的实施步骤和缺点</h3>
<h3>实施步骤</h3>
<p>其实就是要给容器的<code>/etc/resolv.conf</code>文件添加选项,目前有两个方案比较合适:</p>
<ol>
<li>通过修改 pod 的 postStart hook 来设置</li>

</ol>
<div class="highlight">
<pre><code class="language-text">lifecycle:
postStart:
    exec:
      command:
      - /bin/sh
      - -c
      - "/bin/echo 'options single-request-reopen' &gt;&gt; /etc/resolv.conf"</code></pre>
</div>
<ol>
<li>通过修改 pod 的 template.spec.dnsConfig 来设置</li>
</ol>
<div class="highlight">
<pre><code class="language-text">template:
spec:
    dnsConfig:
      options:
      - name: single-request-reopen</code></pre>
</div>
<blockquote><code>注</code>: 需要 k8s 版本&gt;=1.9</blockquote>
<h3>缺点</h3>
<p>不支持<code>alpine</code>基础镜像的容器,因为<code>apline</code>底层使用的<code>musl libc</code>库并不支持这些 resolv.conf 选项,所以如果使用<code>alpine</code>基础镜像构建的应用,还是无法规避超时的问题。</p>
<h3>方案(三):本地 DNS 缓存</h3>
<p>其实 k8s 官方也意识到了这个问题比较常见,给出了 coredns 以 cache 模式作为 daemonset 部署的解决方案:&nbsp;<span class="invisible">https://<span class="visible">github.com/kubernetes/k<span class="invisible">ubernetes/tree/master/cluster/addons/dns/nodelocaldns</span></span></span></p>
<p>大概原理就是:</p>
<blockquote>本地 DNS 缓存以 DaemonSet 方式在每个节点部署一个使用 hostNetwork 的 Pod,创建一个网卡绑上本地 DNS 的 IP,本机的 Pod 的 DNS 请求路由到本地 DNS,然后取缓存或者继续使用 TCP 请求上游集群 DNS 解析 (由于使用 TCP,同一个 socket 只会做一遍三次握手,不存在并发创建 conntrack 表项,也就不会有 conntrack 冲突)</blockquote>
<h3>部署</h3>
<ol>
<li>获取当前<code>kube-dns service</code>的 clusterIP</li>
</ol>
<div class="highlight">
<pre><code class="language-text"># kubectl -n kube-system get svc kube-dns -o jsonpath="{.spec.clusterIP}"
10.96.0.10</code></pre>
</div>
<ol>
<li>下载官方提供的 yaml 模板进行关键字替换</li>
</ol>
<div class="highlight">
<pre><code class="language-text">wget -O nodelocaldns.yaml "https://github.com/kubernetes/kubernetes/raw/master/cluster/addons/dns/nodelocaldns/nodelocaldns.yaml" &amp;&amp; \
sed -i 's/__PILLAR__DNS__SERVER__/10.96.0.10/g' nodelocaldns.yaml &amp;&amp; \
sed -i 's/__PILLAR__LOCAL__DNS__/169.254.20.10/g' nodelocaldns.yaml &amp;&amp; \
sed -i 's/__PILLAR__DNS__DOMAIN__/cluster.local/g' nodelocaldns.yaml &amp;&amp; \
sed -i 's/__PILLAR__CLUSTER__DNS__/10.96.0.10/g' nodelocaldns.yaml &amp;&amp; \
sed -i 's/__PILLAR__UPSTREAM__SERVERS__/\/etc\/resolv.conf/g' nodelocaldns.yaml</code></pre>
</div>
<ol>
<li>最终 yaml 文件如下:</li>
</ol>
<div class="highlight">
<pre><code class="language-text"># Copyright 2018 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

apiVersion: v1
kind: ServiceAccount
metadata:
name: node-local-dns
namespace: kube-system
labels:
    kubernetes.io/cluster-service: "true"
    addonmanager.kubernetes.io/mode: Reconcile
---
apiVersion: v1
kind: Service
metadata:
name: kube-dns-upstream
namespace: kube-system
labels:
    k8s-app: kube-dns
    kubernetes.io/cluster-service: "true"
    addonmanager.kubernetes.io/mode: Reconcile
    kubernetes.io/name: "KubeDNSUpstream"
spec:
ports:
    - name: dns
      port: 53
      protocol: UDP
      targetPort: 53
    - name: dns-tcp
      port: 53
      protocol: TCP
      targetPort: 53
selector:
    k8s-app: kube-dns
---
apiVersion: v1
kind: ConfigMap
metadata:
name: node-local-dns
namespace: kube-system
labels:
    addonmanager.kubernetes.io/mode: Reconcile
data:
Corefile: |
    cluster.local:53 {
      errors
      cache {
                success 9984 30
                denial 9984 5
      }
      reload
      loop
      bind 169.254.20.10 10.96.0.10
      forward . 10.96.0.10 {
                force_tcp
      }
      prometheus :9253
      health 169.254.20.10:8080
      }
    in-addr.arpa:53 {
      errors
      cache 30
      reload
      loop
      bind 169.254.20.10 10.96.0.10
      forward . 10.96.0.10 {
                force_tcp
      }
      prometheus :9253
      }
    ip6.arpa:53 {
      errors
      cache 30
      reload
      loop
      bind 169.254.20.10 10.96.0.10
      forward . 10.96.0.10 {
                force_tcp
      }
      prometheus :9253
      }
    .:53 {
      errors
      cache 30
      reload
      loop
      bind 169.254.20.10 10.96.0.10
      forward . /etc/resolv.conf {
                force_tcp
      }
      prometheus :9253
      }
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-local-dns
namespace: kube-system
labels:
    k8s-app: node-local-dns
    kubernetes.io/cluster-service: "true"
    addonmanager.kubernetes.io/mode: Reconcile
spec:
updateStrategy:
    rollingUpdate:
      maxUnavailable: 10%
selector:
    matchLabels:
      k8s-app: node-local-dns
template:
    metadata:
      labels:
      k8s-app: node-local-dns
    spec:
      priorityClassName: system-node-critical
      serviceAccountName: node-local-dns
      hostNetwork: true
      dnsPolicy: Default # Don't use cluster DNS.
      tolerations:
      - key: "CriticalAddonsOnly"
          operator: "Exists"
      containers:
      - name: node-cache
          image: k8s.gcr.io/k8s-dns-node-cache:1.15.7
          resources:
            requests:
            cpu: 25m
            memory: 5Mi
          args:
            [
            "-localip",
            "169.254.20.10,10.96.0.10",
            "-conf",
            "/etc/Corefile",
            "-upstreamsvc",
            "kube-dns-upstream",
            ]
          securityContext:
            privileged: true
          ports:
            - containerPort: 53
            name: dns
            protocol: UDP
            - containerPort: 53
            name: dns-tcp
            protocol: TCP
            - containerPort: 9253
            name: metrics
            protocol: TCP
          livenessProbe:
            httpGet:
            host: 169.254.20.10
            path: /health
            port: 8080
            initialDelaySeconds: 60
            timeoutSeconds: 5
          volumeMounts:
            - mountPath: /run/xtables.lock
            name: xtables-lock
            readOnly: false
            - name: config-volume
            mountPath: /etc/coredns
            - name: kube-dns-config
            mountPath: /etc/kube-dns
      volumes:
      - name: xtables-lock
          hostPath:
            path: /run/xtables.lock
            type: FileOrCreate
      - name: kube-dns-config
          configMap:
            name: kube-dns
            optional: true
      - name: config-volume
          configMap:
            name: node-local-dns
            items:
            - key: Corefile
                path: Corefile.base</code></pre>
</div>
<p>通过 yaml 可以看到几个细节:</p>
<ol>
<li>部署类型是使用的<code>DaemonSet</code>,即在每个 k8s node 节点上运行一个 dns 服务</li>
<li><code>hostNetwork</code>属性为<code>true</code>,即直接使用 node 物理机的网卡进行端口绑定,这样在此 node 节点中的 pod 可以直接访问 dns 服务,不通过 service 进行转发,也就不会有 DNAT</li>
<li><code>dnsPolicy</code>属性为<code>Default</code>,不使用 cluster DNS,在解析外网域名时直接使用本地的 DNS 设置</li>
<li>绑定在 node 节点<code>169.254.20.10</code>和<code>10.96.0.10</code>IP 上,这样节点下面的 pod 只需要将 dns 设置为<code>169.254.20.10</code>即可直接访问宿主机上的 dns 服务。</li>
</ol>
<h3>测试</h3>
<ol>
<li>修改<code>/etc/resolv.conf</code>文件中的 nameserver:<br>nameserver 169.254.20.10</li>
<li>进行压测:<br># 200个并发,持续30秒,记录超过5s的请求个数 ./dns -host {service}.{namespace} -c 200 -d 30 -l 5000<br>结果如下:</li>

</ol>
<p><img class="origin_image zh-lightbox-thumb lazy lazyload" alt="" width="849" data-caption="" data-size="normal" data-rawwidth="849" data-rawheight="171" data-original="https://pic4.zhimg.com/v2-d5d71bf21b8398afdbdebc85342776db_r.jpg" data-actualsrc="https://pic4.zhimg.com/v2-d5d71bf21b8398afdbdebc85342776db_b.jpg" data-lazy-status="ok" data-src="https://pic4.zhimg.com/80/v2-d5d71bf21b8398afdbdebc85342776db_720w.jpg"></p>
<p class="ztext-empty-paragraph">&nbsp;</p>
<h3>结论</h3>
<p>通过压测发现并没有解决超时的问题,按理说没有<code>conntrack</code>冲突应该表现出的情况与方案(二)类似才对,也可能是我使用的姿势不对,不过虽然这个问题还存在,但是通过<code>DaemonSet</code>将 dns 请求压力分散到各个 node 节点,也可以有效的缓解域名解析超时问题。</p>
<h3>实施</h3>
<ul>
<li>方案(一):通过修改 pod 的 template.spec.dnsConfig 来设置,并将<code>dnsPolicy</code>设置为<code>None</code></li>

</ul>
<div class="highlight">
<pre><code class="language-text">template:
spec:
    dnsConfig:
      nameservers:
      - 169.254.20.10
      searches:
      - public.svc.cluster.local
      - svc.cluster.local
      - cluster.local
      options:
      - name: ndots
      value: "5"
    dnsPolicy: None</code></pre>
</div>
<ul>
<li>方案(二):修改默认的<code>cluster-dns</code>,在 node 节点上将<code>/etc/systemd/system/kubelet.service.d/10-kubeadm.conf</code>文件中的<code>--cluster-dns</code>参数值修改为<code>169.254.20.10</code>,然后重启<code>kubelet</code></li>
</ul>
<div class="highlight">
<pre><code class="language-text">systemctl restart kubelet</code></pre>
</div>
<blockquote><code>注</code>:配置文件路径也可能是<code>/etc/kubernetes/kubelet</code></blockquote>
<h2>最终解决方案</h2>
<p>最后还是决定使用<code>方案(二)+方案(三)</code>配合使用,来最大程度的优化此问题,并且将线上所有的基础镜像都替换为非<code>apline</code>的镜像版本,至此问题基本解决,也希望 K8S 官方能早日将此功能直接集成进去。</p><br><br>
来源:https://www.cnblogs.com/qinghe123/p/13032601.html
頁: [1]
查看完整版本: K8S 内部服务调用域名解析超时