乐趣区

关于后端:优雅太优雅了竟能如此顺滑攻破K8s疑难杂症

👉腾小云导读

作者常常帮忙用户解决各种 K8s 各类「疑难杂症」,积攒了丰盛教训。本文将分享几个网络相干问题的排查和解决思路,深入分析并开展相干常识,实用性较强。此外,本文几个状况是在应用 TKE 时遇到的。不同厂商的网络环境可能不一样,文中会对不同问题的网络环境进行阐明。欢送持续往下浏览。

👉看目录点珍藏,随时涨技术

1 跨 VPC 拜访 NodePort 常常超时

2 LB 压测 CPS 低

3 DNS 解析偶然 5S 延时

4 Pod 拜访另一个集群的 apiserver 有延时

5 DNS 解析异样

6 Pod 偶然存活查看失败

7 拜访 externalTrafficPolicy 为 Local 的 Service 对应 LB 有时超时

8 结语

在公众号后盾回复 「k8s」,收费浏览本文作者所著 kubernetes 实际指南书。

接下来,本篇将特地介绍 7 个 K8s 常见问题及其解决方案。心愿对你有帮忙。

01、跨 VPC 拜访 NodePort 常常超时

景象: 从 VPC a 拜访 VPC b 的 TKE 集群的某个节点的 NodePort,有时候失常,有时候会卡住直到超时。

起因怎么查?

当然是先抓包,抓 server 端 NodePort 的包,发现异常时 server 能收到 SYN,但没响应 ACK:

重复执行 netstat -s | grep LISTEN 发现 SYN 被抛弃数量一直减少:

剖析:

  • 两个 VPC 之间应用对等连贯买通的,CVM 之间通信应该就跟在一个内网一样能够互通。
  • 为什么同一 VPC 下拜访没问题,跨 VPC 有问题? 两者拜访的区别是什么?

再认真看下 client 所在环境,发现 client 是 VPC a 的 TKE 集群节点,捋一下:

  • client 在 VPC a 的 TKE 集群的节点
  • server 在 VPC b 的 TKE 集群的节点

因为 TKE 集群中有个叫 ip-masq-agent 的 daemonset,它会给 node 写 iptables 规定,默认 SNAT 目标 IP 是 VPC 之外的报文,所以 client 拜访 server 会做 SNAT,也就是这里跨 VPC 相比同 VPC 拜访 NodePort 多了一次 SNAT。

如果是因为多了一次 SNAT 导致的这个问题,应该跟内核参数无关。因为是 server 收到包没回包,所以应该是 server 所在 node 的内核参数问题。比照这个 node 和 一般 TKE node 的默认内核参数,发现这个 node net.ipv4.tcp\_tw\_recycle = 1,这个参数默认是敞开的。我跟用户沟通后发现,这个内核参数的确在做压测的时候调整过。

解释一下,TCP 被动敞开连贯的一方在发送最初一个 ACK 会进入 TIME\_AWAIT 状态,再期待 2 个 MSL 工夫后才会敞开(因为如果 server 没收到 client 第四次挥手确认报文,server 会重发第三次挥手 FIN 报文,所以 client 须要停留 2 MSL 的时长来解决可能会反复收到的报文段;同时期待 2 MSL 也能够让因为网络不通顺产生的滞留报文生效,防止新建设的连贯收到之前旧连贯的报文),理解更具体的过程请参考 TCP 四次挥手。

参数 tcp\_tw\_recycle 用于疾速回收 TIME\_AWAIT 连贯,通常在减少连贯并发能力的场景会开启,比方发动大量短连贯,疾速回收可防止 tw\_buckets 资源耗尽导致无奈建设新连贯 time wait bucket table overflow

查得 tcp\_tw\_recycle 有个坑,在 RFC1323 有段形容:

An additional mechanism could be added to the TCP, a per-host cache of the last timestamp received from any connection. This value could then be used in the PAWS mechanism to reject old duplicate segments from earlier incarnations of the connection, if the timestamp clock can be guaranteed to have ticked at least once since the old connection was open. This would require that the TIME-WAIT delay plus the RTT together must be at least one tick of the sender’s timestamp clock. Such an extension is not part of the proposal of this RFC.

大略意思是说:

TCP 有一种行为,能够缓存每个连贯最新的工夫戳,后续申请中如果工夫戳小于缓存的工夫戳,即视为有效,相应的数据包会被抛弃。

Linux 是否启用这种行为取决于 tcp\_timestampstcp\_tw\_recycle,因为 tcp\_timestamps 缺省开启,所以当 tcp\_tw\_recycle 被开启后,实际上这种行为就被激活了,当客户端或服务端以 NAT 形式构建的时候就可能呈现问题。

当多个客户端通过 NAT 形式联网并与服务端交互时,服务端看到的是同一个 IP。也就是说对服务端而言这些客户端实际上等同于一个,惋惜因为这些客户端的工夫戳可能存在差别。于是乎从服务端的视角看,便可能呈现工夫戳错乱的景象,进而间接导致工夫戳小的数据包被抛弃。

如果产生了此类问题,具体的体现通常是是客户端明明发送的 SYN,但服务端就是不响应 ACK。

回到问题上——client 所在节点上可能也会有其它 pod 拜访到 server 所在节点,而它们都被 SNAT 成了 client 所在节点的 NODE IP。但工夫戳存在差别,server 就会看到工夫戳错乱,因为开启了 tcp\_tw\_recycletcp\_timestamps 激活了上述行为,就丢掉了比缓存工夫戳小的报文,导致局部 SYN 被抛弃。这也解释了为什么之前抓包发现异常时 server 收到了 SYN,但没有响应 ACK,进而阐明为什么 client 的申请局部会卡住直到超时。

因为 tcp\_tw\_recycle 坑太多,在内核 4.12 之后已移除:

remove tcp\_tw\_recycle

02、LB 压测 CPS 低

景象: LoadBalancer 类型的 Service,间接压测 NodePort CPS 比拟高,但如果压测 LB CPS 就很低。

环境阐明: 用户应用的黑石 TKE,不是私有云 TKE,黑石的机器是物理机,LB 的实现也跟私有云不一样,但 LoadBalancer 类型的 Service 的实现同样也是 LB 绑定各节点的 NodePort,报文发到 LB 后转到节点的 NodePort,而后再路由到对应 pod,而测试在私有云 TKE 环境下没有这个问题。

  • client 抓包

    大量 SYN 重传

  • server 抓包

    抓 NodePort 的包,发现当 client SYN 重传时 server 能收到 SYN 包但没有响应。

又是 SYN 收到但没响应,难道又是开启 tcp\_tw\_recycle 导致的?查看节点的内核参数发现并没有开启,除了这个起因,还会有什么状况能导致被抛弃?

conntrack -S 看到 insert\_failed 数量在一直减少,也就是 conntrack 在插入很多新连贯的时候失败了,为什么会插入失败?什么状况下会插入失败?

  • 挖内核源码

netfilter conntrack 模块为每个连贯创立 conntrack 表项时,表项的创立和最终插入之间还有一段逻辑,没有加锁,是一种乐观锁的过程。conntrack 表项并发刚创立时五元组不抵触的话能够创立胜利,但两头通过 NAT 转换之后五元组就可能变成雷同,第一个能够插入胜利,前面的就会插入失败,因为曾经有雷同的表项存在。比方一个 SYN 曾经做了 NAT 然而还没到最终插入的时候,另一个 SYN 也在做 NAT,因为之前那个 SYN 还没插入,这个 SYN 做 NAT 的时候就认为这个五元组没有被占用,那么它 NAT 之后的五元组就可能跟那个还没插入的包雷同。

在这个问题里理论就是 netfilter 做 SNAT 时源端口选举抵触了,黑石 LB 会做 SNAT,SNAT 时应用了 16 个不同 IP 做源,然而短时间内源 Port 却是集中统一的,并发两个 SYN a 和 SYN b,被 LB SNAT 后源 IP 不同但源 Port 很可能雷同。

这里就假如两个报文被 LB SNAT 之后它们源 IP 不同源 Port 雷同,报文同时到了节点的 NodePort 会再次做 SNAT 再转发到对应的 Pod,当报文到了 NodePort 时,这时它们五元组不抵触,netfilter 为它们别离创立了 conntrack 表项,SYN a 被节点 SNAT 时默认行为是 从 port\_range 范畴的以后源 Port 作为起始地位开始循环遍历,选举出没有被占用的作为源 Port,因为这两个 SYN 源 Port 雷同,所以它们源 Port 选举的起始地位雷同。

当 SYN a 选出源 Port 但还没将 conntrack 表项插入时,netfilter 认为这个 Port 没被占用就很可能给 SYN b 也选了雷同的源 Port,这时它们五元组就雷同了。当 SYN a 的 conntrack 表项插入后再插入 SYN b 的 conntrack 表项时,发现曾经有雷同的记录就将 SYN b 的 conntrack 表项抛弃了。

  • 解决办法摸索

不应用源端口选举,在 iptables 的 MASQUERADE 规定如果加 –random-fully 这个 flag 能够让端口选举齐全随机,基本上能防止绝大多数的抵触,但也无奈齐全杜绝。最终决定开发 LB 间接绑 Pod IP,不基于 NodePort,从而防止 netfilter 的 SNAT 源端口抵触问题。

03、DNS 解析偶然 5S 延时

问题:仔细分析,理论跟之前黑石 TKE 压测 LB CPS 低的根因是同一个,都是因为 netfilter conntrack 模块的设计问题,只不过之前产生在 SNAT,这个产生在 DNAT。

这里我尝试用通俗的语言来总结下起因:

DNS client (glibc 或 musl libc) 会并发申请 A 和 AAAA 记录,跟 DNS Server 通信天然会先 connect (建设 fd),前面申请报文应用这个 fd 来发送。

因为 UDP 是无状态协定,connect 时并不会创立 conntrack 表项, 而并发申请的 A 和 AAAA 记录默认应用同一个 fd 发包,这时它们源 Port 雷同。当并发发包时,两个包都还没有被插入 conntrack 表项,所以 netfilter 会为它们别离创立 conntrack 表项,而集群内申请 kube-dns 或 coredns 都是拜访的 CLUSTER-IP,报文最终会被 DNAT 成一个 endpoint 的 POD IP。当两个包被 DNAT 成同一个 IP,最终它们的五元组就雷同了,在最终插入的时候前面那个包就会被丢掉。如果 dns 的 pod 正本只有一个实例的状况就很容易产生。景象就是 dns 申请超时,client 默认策略是期待 5s 主动重试。如果重试胜利,咱们看到的景象就是 dns 申请有 5s 的延时。

  • 解决方案一: 应用 TCP 发送 DNS 申请

如果应用 TCP 发 DNS 申请,connect 时就会插入 conntrack 表项,而并发的 A 和 AAAA 申请应用同一个 fd,所以只会有一次 connect,也就只会尝试创立一个 conntrack 表项,也就防止插入时抵触。

resolv.conf 能够加 options use-vc 强制 glibc 应用 TCP 协定发送 DNS query。上面是这个 man resolv.conf 中对于这个选项的阐明:

use-vc (since glibc 2.14)  
                     Sets RES_USEVC in _res.options. This option forces the  
                     use of TCP for DNS resolutions.
  • 解决方案二: 防止雷同五元组 DNS 申请的并发

resolv.conf 还有另外两个相干的参数:

single-request-reopen (since glibc 2.9):A 和 AAAA 申请应用不同的 socket 来发送,这样它们的源 Port 就不同,五元组也就不同,防止了应用同一个 conntrack 表项。

single-request (since glibc 2.10):A 和 AAAA 申请改成串行,没有并发,从而也防止了抵触。

man resolv.conf 中解释如下:

single-request-reopen (since glibc 2.9)  
                     Sets RES_SNGLKUPREOP in _res.options. The resolver  
                     uses the same socket for the A and AAAA requests. Some  
                     hardware mistakenly sends back only one reply. When  
                     that happens the client system will sit and wait for  
                     the second reply. Turning this option on changes this  
                     behavior so that if two requests from the same port are  
                     not handled correctly it will close the socket and open  
                     a new one before sending the second request.  
  
single-request (since glibc 2.10)  
                     Sets RES_SNGLKUP in _res.options. By default, glibc  
                     performs IPv4 and IPv6 lookups in parallel since  
                     version 2.9. Some appliance DNS servers cannot handle  
                     these queries properly and make the requests time out.  
                     This option disables the behavior and makes glibc  
                     perform the IPv6 and IPv4 requests sequentially (at the  
                     cost of some slowdown of the resolving process).

要给容器的 resolv.conf 加上 options 参数,最不便的是间接在 Pod Spec 外面的 dnsConfig 加。(k8s v1.9 及以上才反对)

spec:  
      dnsConfig:  
        options:  
          - name: single-request-reopen

加 options 还有其它一些办法:

第一,在容器的 ENTRYPOINT 或者 CMD 脚本中,执行 /bin/echo ‘options single-request-reopen’ >> /etc/resolv.conf

第二,在 postStart hook 里加。

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

第三,应用 MutatingAdmissionWebhook。这是 1.9 引入的 Controller,用于对一个指定的资源的操作之前,对这个资源进行变更。istio 的主动 sidecar 注入就是用这个性能来实现的,也能够通过 MutatingAdmissionWebhook 来主动给所有 Pod 注入 resolv.conf 文件,不过须要肯定的开发量。

  • 解决方案三: 应用本地 DNS 缓存

仔细观察能够看到后面两种计划是 glibc 反对的。而基于 alpine 的镜像底层库是 musl libc 不是 glibc,所以即便加了这些 options 也没用。

这种状况能够思考应用本地 DNS 缓存来解决。容器的 DNS 申请都发往本地的 DNS 缓存服务(dnsmasq, nscd 等),不须要走 DNAT,也不会产生 conntrack 抵触。另外还有个益处,就是防止 DNS 服务成为性能瓶颈。

应用本地 DNS 缓存有两种形式:

每个容器自带一个 DNS 缓存服务。

每个节点运行一个 DNS 缓存服务,所有容器都把本节点的 DNS 缓存作为本人的 nameserver。

从资源效率的角度来思考,举荐后一种形式。

04、Pod 拜访另一个集群的 apiserver 有延时

景象:集群 a 的 Pod 内通过 kubectl 拜访集群 b 的内网地址,偶然呈现延时的状况,但间接在宿主机上用同样的办法却没有这个问题。

提炼环境和景象精华如下:

首先,在 pod 内将另一个集群 apiserver 的 ip 写到了 hosts。因为 TKE apiserver 开启内网集群外内网拜访创立的内网 LB 临时没有反对主动绑内网 DNS 域名解析,所以集群外的内网拜访 apiserver 须要加 hosts。

其次,pod 内执行 kubectl 拜访另一个集群偶然提早 5s,有时甚至 10s。

察看到 5s 延时。感觉跟之前 conntrack 的丢包导致 dns 解析 5s 延时无关,然而加了 hosts 呀!怎么还去解析域名?

进入 pod netns 抓包。 执行 kubectl 时的确有 dns 解析,并且产生延时的时候 dns 申请没有响应而后做了重试。

看起来延时应该就是之前已知 conntrack 丢包导致 dns 5s 超时重试导致的。然而为什么会去解析域名?明明配了 hosts 啊,失常状况应该是优先查找 hosts,没找到才去申请 dns 呀,有什么配置能够管制查找程序?

实际上,/etc/nsswitch.conf 能够管制,但看有问题的 pod 里没有这个文件。而后察看到有问题的 pod 用的 alpine 镜像,试试其它镜像后发现只有基于 alpine 的镜像才会有这个问题。

进一步地,musl libc 并不会应用 /etc/nsswitch.conf,也就是说 alpine 镜像并没有实现用这个文件管制域名查找优先程序,瞥了一眼 musl libc 的 gethostbynamegetaddrinfo 的实现,看起来也没有读这个文件来管制查找程序,写死了先查 hosts,没找到再查 dns。

这么说,那还是该先查 hosts 再查 dns 呀,为什么这里抓包看到是先查的 dns? (如果是先查 hosts 就能命中查问,不会再发动 dns 申请)

拜访 apiserver 的 client 是 kubectl,用 go 写的,会不会是 go 程序解析域名时压根没调底层 c 库的 gethostbynamegetaddrinfo

进一步理解后,咱们发现:go runtime 用 go 实现了 glibc 的 getaddrinfo 的行为来解析域名,缩小了 c 库调用 (应该是思考到缩小 cgo 调用带来的的性能损耗)

issue 地址:

net: replicate DNS resolution behaviour of getaddrinfo(glibc) in the go dns resolver

翻源码验证下:

Unix 系的 OS 下,除了 openbsd,go runtime 会读取 /etc/nsswitch.conf (net/conf.go):

hostLookupOrder 函数决定域名解析程序的策略,Linux 下,如果没有 nsswitch.conf 文件就 dns 比 hosts 文件优先 (net/conf.go):

能够看到 hostLookupDNSFiles 的意思是 dns first (net/dnsclient\_unix.go):

所以尽管 alpine 用的 musl libc 不是 glibc,但 go 程序解析域名还是一样走的 glibc 的逻辑,而 alpine 没有 /etc/nsswitch.conf 文件,也就解释了为什么 kubectl 拜访 apiserver 先做 dns 解析,没解析到再查的 hosts,导致每次拜访都去申请 dns,恰好又碰到 conntrack 那个丢包问题导致 dns 5s 延时,在用户这里体现就是 pod 内用 kubectl 拜访 apiserver 偶然呈现 5s 延时,有时呈现 10s 是因为重试的那次 dns 申请刚好也遇到 conntrack 丢包导致延时又叠加了 5s。

解决方案:

换根底镜像,不必 alpine

挂载 nsswitch.conf 文件 (能够用 hostPath)

05、DNS 解析异样

景象: 有个用户反馈域名解析有时有问题,看报错是解析超时。

第一反馈,当然是看 coredns 的 log:

[ERROR] 2 loginspub.gaeamobile-inc.net.    
A: unreachable backend: read udp 172.16.0.230:43742->10.225.30.181:53: i/o timeout

这是上游 DNS 解析异样了。因为解析内部域名 coredns 默认会申请上游 DNS 来查问,这里的上游 DNS 默认是 coredns pod 所在宿主机的 resolv.conf 外面的 nameserver。coredns pod 的 dnsPolicy 为“Default”,也就是会将宿主机里的 resolv.conf 里的 nameserver 加到容器里的 resolv.conf, coredns 默认配置 proxy . /etc/resolv.conf

意思是非 service 域名会应用 coredns 容器中 resolv.conf 文件里的 nameserver 来解析。

超时的上游 DNS 10.225.30.181 并不是冀望的 nameserver。VPC 默认 DNS 应该是 180 结尾的。咱们看了 coredns 所在节点的 resolv.conf,发现的确多出了这个非冀望的 nameserver。最终,咱们跟用户确认,这个 DNS 不是用户本人加上去的。增加节点时这个 nameserver 自身就在 resolv.conf 中。

依据内部人员反馈,10.225.30.181 是广州一台年久失修将被撤裁的 DNS 物理网络,没有 VIP,撤掉就没有了。所以如果 coredns 用到了这台 DNS 解析时就可能 timeout。前面测试,某些 VPC 的集群的确会有这个 nameserver,奇了怪了,哪里冒出来的?

咱们,又试了下间接创立 CVM,不加进 TKE 节点发现没有这个 nameserver,只有一加进 TKE 节点就有了。

看起来是 TKE 的问题。将 CVM 增加到 TKE 集群会主动重装系统,初始化并加进集群成为 K8s 的 node,确认了初始化过程并不会写 resolv.conf,会不会是 TKE 的 OS 镜像问题?

咱们尝试搜一下除了 /etc/resolv.conf 之外哪里还有这个 nameserver 的 IP,最初发现 /etc/resolvconf/resolv.conf.d/base 这外面有。

看下 /etc/resolvconf/resolv.conf.d/base 的作用:

Ubuntu 的 /etc/resolv.conf 是动静生成的,每次重启都会将 /etc/resolvconf/resolv.conf.d/base 外面的内容加到 /etc/resolv.conf 里。

经确认,这个文件的确是 TKE 的 Ubuntu OS 镜像里自带的,可能公布 OS 镜像时不小心加进去的。

那为什么有些 VPC 的集群的节点 /etc/resolv.conf 外面没那个 IP 呢?它们的 OS 镜像里也都有那个文件那个 IP 呀。

咱们求教其余共事发现:

非 dhcp 子机,cvm 的 cloud-init 会笼罩 /etc/resolv.conf 来设置 dns;

dhcp 子机,cloud-init 不会设置,而是通过 dhcp 动静下发;

2018 年 4 月 之后创立的 VPC 就都是 dhcp 类型了的,比拟新的 VPC 都是 dhcp 类型的。

/etc/resolv.conf 一开始内容都蕴含 /etc/resolvconf/resolv.conf.d/base 的内容,也就是都有那个不冀望的 nameserver。但老的 VPC 因为不是 dhcp 类型,所以 cloud-init 会笼罩 /etc/resolv.conf,抹掉了不被冀望的 nameserver,而新创建的 VPC 都是 dhcp 类型,cloud-init 不会笼罩 /etc/resolv.conf,导致不被冀望的 nameserver 残留在了 /etc/resolv.conf,而 coredns pod 的 dnsPolicy 为“Default”,也就是会将宿主机的 /etc/resolv.conf 中的 nameserver 加到容器里,coredns 解析集群外的域名默认应用这些 nameserver 来解析,当用到那个将被撤裁的 nameserver 就可能 timeout。

能够从两个层面解决:

  • 长期解决:

    删掉 /etc/resolvconf/resolv.conf.d/base 重启

  • 长期解决:

    咱们从新制作 TKE Ubuntu OS 镜像而后公布更新

这下应该没问题了吧,然而用户反馈还是会偶然解析有问题,但景象不一样了,这次并不是 dns timeout。

用脚本跑测试仔细分析景象:

  • 申请 loginspub.gaeamobile-inc.net 时,偶然提醒域名无奈解析
  • 申请 accounts.google.com 时,偶然提醒连贯失败

进入 dns 解析偶然异样的容器的 netns 抓包:

  • dns 申请会并发申请 A 和 AAAA 记录
  • 测试脚本发申请打印序号,抓包而后 wireshark 剖析比照异样时申请序号偏移量,找到异样时的 dns 申请报文,发现异常时 A 和 AAAA 记录的申请 id 抵触,并且 AAAA 响应先返回

失常状况下 id 不会抵触,这里抵触了也就能解释这个 dns 解析异样的景象了:

  • loginspub.gaeamobile-inc.net 没有 AAAA (ipv6) 记录,它的响应先返回告知 client 不存在此记录,因为申请 id 跟 A 记录申请抵触,前面 A 记录响应返回了 client 发现 id 反复就疏忽了,而后认为这个域名无奈解析
  • accounts.google.com 有 AAAA 记录,响应先返回了,client 就拿这个记录去尝试申请,但以后容器环境不反对 ipv6,所以会连贯失败

那为什么 dns 申请 id 会抵触?

咱们持续察看发现,其它节点上的 pod 不会复现这个问题,有问题这个节点上也不是所有 pod 都有这个问题,只有基于 alpine 镜像的容器才有这个问题,在此节点新起一个测试的 alpine:latest 的容器也一样有这个问题。

为什么 alpine 镜像的容器在这个节点上有问题在其它节点上没问题?为什么其余镜像的容器都没问题?它们跟 alpine 的区别是什么?

咱们发现区别,alpine 应用的底层 c 库是 musl libc,其它镜像根本都是 glibc。翻 musl libc 源码,结构 dns 申请时,申请 id 的生成没加锁,而且跟以后工夫戳无关:

看正文,作者应该认为这样 id 根本不会抵触,事实证明,绝大多数状况的确不会抵触,在网上搜了很久没有搜到任何对于 musl libc 的 dns 申请 id 抵触的状况。这个看起来取决于硬件,可能在某种类型硬件的机器上运行,短时间内生成的 id 就可能抵触。尝试跟用户在雷同地区的集群,增加雷同配置雷同机型的节点,也复现了这个问题,但起初删除再增加时又不能复现了,看起来前面新建的 cvm 又跑在了另一种硬件的母机上了。

OK,能解释通之后,来看下解决方案:

  • 换根底镜像 (不必 alpine)
  • 齐全动态编译业务程序(不依赖底层 c 库),比方 go 语言程序编译时能够敞开 cgo (CGO_ENABLED=0),并通知链接器要动态链接 (go build 前面加 -ldflags ‘-d’),但这须要语言和编译工具反对才能够

最终倡议用户根底镜像换成另一个比拟小的镜像: debian:stretch-slim

问题解决。但用户前面感觉 debian:stretch-slim 做进去的镜像太大了,有 6MB 多,而之前基于 alpine 做进去只有 1MB 多,最初应用了一个非官方的批改过 musl libc 的 alpine 镜像作为根底镜像,外面禁止了 AAAA 申请从而防止这个问题。

06、Pod 偶然存活查看失败

景象: Pod 偶然会存活查看失败,导致 Pod 重启,业务偶然连贯异样。

之前从未遇到这种状况,在测试环境尝试复现也没有胜利,只有在用户这个环境才能够复现。这个用户环境流量较大,感觉跟连接数或并发量无关。用户反馈说在友商的环境里没这个问题。

比照市面上其余产品的内核参数,咱们发现有些区别。尝试将节点内核参数改成跟友商的一样,发现问题没有复现。

再比照剖析下内核参数差别,最初发现是 backlog 太小导致的,节点的 net.ipv4.tcp\_max\_syn\_backlog 默认是 1024,如果短时间内并发新建 TCP 连贯太多,SYN 队列就可能溢出,导致局部新连贯无奈建设。解释一下:

TCP 连贯建设会通过三次握手,server 收到 SYN 后会将连贯退出 SYN 队列。当收到最初一个 ACK 后连贯建设,这时会将连贯从 SYN 队列中挪动到 ACCEPT 队列。在 SYN 队列中的连贯都是没有建设齐全的连贯,处于半连贯状态。如果 SYN 队列比拟小,而短时间内并发新建的连贯比拟多,同时处于半连贯状态的连贯就多,SYN 队列就可能溢出,tcp\_max\_syn\_backlog 能够管制 SYN 队列大小,用户节点的 backlog 大小默认是 1024,改成 8096 后就能够解决问题。

07、拜访 externalTrafficPolicy 为 Local 的 Service 对应 LB 有时超时

景象:用户在 TKE 创立了公网 LoadBalancer 类型的 Service,externalTrafficPolicy 设为了 Local,拜访这个 Service 对应的公网 LB 有时会超时。

externalTrafficPolicy 为 Local 的 Service 用于在四层获取客户端实在源 IP,官网参考文档:Source IP for Services with Type=LoadBalancer。

TKE 的 LoadBalancer 类型 Service 实现是应用 CLB 绑定所有节点对应 Service 的 NodePort。CLB 不做 SNAT,报文转发到 NodePort 时源 IP 还是实在的客户端 IP。

如果 NodePort 对应 Service 的 externalTrafficPolicy 不是 Local 的就会做 SNAT,到 pod 时就看不到客户端实在源 IP 了。

但如果是 Local 的话就不做 SNAT。如果本机 node 有这个 Service 的 endpoint 就转到对应 pod,如果没有就间接丢掉,因为如果转到其它 node 上的 pod 就必须要做 SNAT,不然无奈回包,而 SNAT 之后就无奈获取实在源 IP 了。

LB 会对绑定节点的 NodePort 做健康检查探测,查看 LB 的健康检查状态: 发现这个 NodePort 的所有节点都不衰弱。

那么问题来了:

  • 为什么会全不衰弱,这个 Service 有对应的 pod 实例,有些节点上是有 endpoint 的,为什么它们也不衰弱?
  • LB 健康检查全不衰弱,然而为什么有时还是能够拜访后端服务?

跟 LB 的同学确认后咱们发现,如果后端 rs 全不衰弱会激活 LB 的全死全活逻辑,也就是所有后端 rs 都能够转发。那么有 endpoint 的 node 也是不衰弱这个怎么解释?

咱们在有 endpoint 的 node 上抓 NodePort 的包,发现很多来自 LB 的 SYN,然而没有响应 ACK。看起来报文被丢了。咱们持续抓下 cbr0 看下,发现没有来自 LB 的包,阐明报文在 cbr0 之前被丢了。

再察看用户集群环境信息:

k8s 版本 1.12
启用了 ipvs
只有 local 的 service 才有异样

尝试新建一个 1.12 启用 ipvs 和一个没启用 ipvs 的测试集群。也都创立 Local 的 LoadBalancer Service。咱们发现启用 ipvs 的测试集群复现了那个问题,没启用 ipvs 的集群没这个问题。

再尝试创立 1.10 的集群,也启用 ipvs,发现没这个问题。看起来跟集群版本和是否启用 ipvs 无关。

1.12 比照 1.10 启用 ipvs 的集群: 1.12 的会将 LB 的 EXTERNAL-IP 绑到 kube-ipvs0 上,而 1.10 的不会:

$ ip a show kube-ipvs0 | grep -A2 170.106.134.124  
  
    inet 170.106.134.124/32 brd 170.106.134.124 scope global kube-ipvs0  
  
       valid_lft forever preferred_lft forever

170.106.134.124 是 LB 的公网 IP
1.12 启用 ipvs 的集群将 LB 的公网 IP 绑到了 kube-ipvs0 网卡上

kube-ipvs0 是一个 dummy interface,理论不会接管报文。能够看到它的网卡状态是 DOWN,次要用于绑 ipvs 规定的 VIP。因为 ipvs 次要工作在 netfilter 的 INPUT 链,报文通过 PREROUTING 链之后须要决定下一步该进入 INPUT 还是 FORWARD 链。如果是本机 IP 就会进入 INPUT,如果不是就会进入 FORWARD 转发到其它机器。所以 K8s 利用 kube-ipvs0 这个网卡将 service 相干的 VIP 绑在下面,以便让报文进入 INPUT 进而被 ipvs 转发。

当 IP 被绑到 kube-ipvs0 上,内核会主动将下面的 IP 写入 local 路由:

$ ip route show table local | grep 170.106.134.124local 170.106.134.124 dev kube-ipvs0 proto kernel scope host src 170.106.134.124

内核认为在 local 路由里的 IP 是本机 IP,而 linux 默认有个行为:

疏忽任何来自非回环网卡并且源 IP 是本机 IP 的报文。而 LB 的探测报文源 IP 就是 LB IP,也就是 Service 的 EXTERNAL-IP 猜测就是因为这个 IP 被绑到 kube-ipvs0,主动加进 local 路由导致内核间接疏忽了 LB 的探测报文。

带着猜测做实现,试一下将 LB IP 从 local 路由中删除:

ip route del table local local 170.106.134.124 dev kube-ipvs0  proto kernel  scope host  src 170.106.134.124

咱们发现这个 node 的在 LB 的健康检查的状态变成衰弱了。看来就是因为这个 LB IP 被绑到 kube-ipvs0,导致内核疏忽了来自 LB 的探测报文,而后 LB 收不到回包认为不衰弱。

那为什么市面上其余产品没反馈这个问题? 应该是 LB 的实现问题,腾讯云的公网 CLB 的衰弱探测报文源 IP 就是 LB 的公网 IP,而大多数厂商的 LB 探测报文源 IP 是保留 IP 并非 LB 本身的 VIP。

如何解决呢?咱们发现一个内核参数 accept\_local 能够让 linux 接管源 IP 是本机 IP 的报文。咱们试了开启这个参数,的确在 cbr0 收到来自 LB 的探测报文了,阐明报文能被 pod 收到,但抓 eth0 还是没有给 LB 回包。

为什么没有回包?咱们剖析下五元组,要给 LB 回包。那么 目标 IP: 目标 Port 必须是探测报文的 源 IP: 源 Port,所以目标 IP 就是 LB IP。因为容器不在主 netns,发包通过 veth pair 到 cbr0 之后须要再通过 netfilter 解决,报文进入 PREROUTING 链而后发现目标 IP 是本机 IP,进入 INPUT 链,所以报文就出不去了。再剖析下进入 INPUT 后会怎么,因为目标 Port 跟 LB 探测报文源 Port 雷同,是一个随机端口,不在 Service 的端口列表,所以没有对应的 IPVS 规定,IPVS 也就不会转发它,而 kube-ipvs0 上尽管绑了这个 IP,但它是一个 dummy interface,不会收包,所以报文最初又被忽略了。

再看看为什么 1.12 启用 ipvs 会绑 EXTERNAL-IPkube-ipvs0,翻翻 k8s 的 kube-proxy 反对 ipvs 的 proposal,发现有个中央说法有点破绽:

LB 类型 Service 的 status 里有 ingress IP,理论就是 kubectl get service 看到的 EXTERNAL-IP,这里说不会绑定这个 IP 到 kube-ipvs0,但前面又说会给它创立 ipvs 规定。既然没有绑到 kube-ipvs0,那么这个 IP 的报文基本不会进入 INPUT 被 ipvs 模块转发,创立的 ipvs 规定也是没用的。

起初找到作者私聊思考了下,发现设计上的确有这个问题。

咱们看到 1.10 的确也是这么实现的。然而为什么 1.12 又绑了这个 IP 呢?调研后发现是因为 #59976 这个 issue 发现一个问题,起初引入 #63066 这个 PR 修复的,而这个 PR 的行为就是让 LB IP 绑到 kube-ipvs0,这个提交影响 1.11 及其之后的版本。

59976 的问题是因为没绑 LB IP 到 kube-ipvs0 上,在自建集群应用 MetalLB 来实现 LoadBalancer 类型的 Service。而有些网络环境下,pod 是无奈间接拜访 LB 的,导致 pod 拜访 LB IP 时拜访不了。而如果将 LB IP 绑到 kube-ipvs0 上就能够通过 ipvs 转发到 LB 类型 Service 对应的 pod 去,而不须要真正通过 LB,所以引入了 #63066 这个 PR。

长期计划: 将 #63066 这个 PR 的更改回滚下,从新编译 kube-proxy,提供降级脚本降级存量 kube-proxy。

如果是让 LB 健康检查探测反对用保留 IP 而不是本身的公网 IP,也是能够解决,但须要跨团队单干。而且如果多个厂商都遇到这个问题,每家都须要为解决这个问题而做开发调整,代价较高。所以长期计划须要跟社区沟通一起推动,所以我提了 issue,将问题形容的很分明: #79783

小思考: 为什么 CLB 能够不做 SNAT?回包目标 IP 就是实在客户端 IP,但客户端是间接跟 LB IP 建设的连贯,如果回包不通过 LB 是不可能发送胜利的呀。是因为 CLB 的实现是在母机上通过隧道跟 CVM 互联的,多了一层封装,回包始终会通过 LB。

就是因为 CLB 不做 SNAT,失常来自客户端的报文是能够发送到 nodeport,但健康检查探测报文因为源 IP 是 LB IP 被绑到 kube-ipvs0 导致被疏忽,也就解释了为什么健康检查失败,但通过 LB 能拜访后端服务,只是有时会超时。那么如果要做 SNAT 的 LB 岂不是更蹩脚,所有报文都变成 LB IP,所有报文都会被疏忽?

提的 issue 有回复指出:AWS 的 LB 会做 SNAT,但它们不将 LB 的 IP 写到 Service 的 Status 里,只写了 hostname,所以也不会绑 LB IP 到 kube-ipvs0:

然而只写 hostname 也得 LB 反对主动绑域名解析,并且集体感觉只写 hostname 很顺当,通过 kubectl get svc 或者其它 K8s 管理系统无奈间接获取 LB IP,这不是一个好的解决办法。

提了 #79976 这个 PR 能够解决问题: 给 kube-proxy 加 –exclude-external-ip 这个 flag 管制是否为 LB IP,创立 ipvs 规定和绑定 kube-ipvs0

但有人放心减少 kube-proxy flag 会减少 kube-proxy 的调试复杂度,看是否在 iptables 层面解决:

咱们认真一想,的确可行,打算有空实现下,从新提个 PR:

08、结语

至此,咱们一起实现了一段微妙的问题排查之旅,信息量很大并且比较复杂,如果你有些没看懂很失常,倡议珍藏起来重复浏览,一起在技术的路线上打怪降级。以上是本次分享全部内容,欢送大家在评论区分享交换。如果感觉内容有用,欢送转发~想学习更多 K8s 常识与实际办法?在公众号后盾回复 「K8s」,收费浏览本文作者所著 kubernetes 实际指南书⬇️⬇️⬇️。

-End-

原创作者|陈鹏

技术责编|陈鹏

你还遇到过哪些 K8s「怪现象」?欢送在公众号评论区分享你的经验和解决方案。咱们将选取 1 则最有创意的分享,送出腾讯云开发者 - 限定随行杯 1 个(见下图)。5 月 14 日中午 12 点开奖。

在公众号后盾回复「K8s」

收费浏览本文作者所著 kubernetes 实际指南书

退出移动版