写点什么

Kubernetes 疑难排查分享: 诡异的 No route to host

  • 2019-12-31
  • 本文字数:4779 字

    阅读完需:约 16 分钟

Kubernetes 疑难排查分享: 诡异的 No route to host

Kubernetes 疑难杂症排查分享,本文通过一个案例,解析 “No route to host” 报错问题,信息量不小,Are you ready ?

问题反馈

有用户反馈 Deployment 滚动更新的时候,业务日志偶尔会报 “No route to host” 的错误。

问题分析

之前没遇到滚动更新会报 “No route to host” 的问题,我们先看下滚动更新导致连接异常有哪些常见的报错:


  • Connection reset by peer: 连接被重置。通常是连接建立过,但 server 端发现 client 发的包不对劲就返回 RST,应用层就报错连接被重置。


比如在 server 滚动更新过程中,client 给 server 发的请求还没完全结束,或者本身是一个类似 grpc 的多路复用长连接,当 server 对应的旧 Pod 删除(没有做优雅结束,停止时没有关闭连接),新 Pod 很快创建启动并且刚好有跟之前旧 Pod 一样的 IP,这时 kube-proxy 也没感知到这个 IP 其实已经被删除然后又被重建了,针对这个 IP 的规则就不会更新,旧的连接依然发往这个 IP,但旧 Pod 已经不在了,后面继续发包时依然转发给这个 Pod IP,最终会被转发到这个有相同 IP 的新 Pod 上,而新 Pod 收到此包时检查报文发现不对劲,就返回 RST 给 client 告知将连接重置。


针对这种情况,建议应用自身处理好优雅结束:Pod 进入 Terminating 状态后会发送 SIGTERM 信号给业务进程,业务进程的代码需处理这个信号,在进程退出前关闭所有连接。


  • Connection refused: 连接被拒绝。通常是连接还没建立,client 正在发 SYN 包请求建立连接,但到了 server 之后发现端口没监听,内核就返回 RST 包,然后应用层就报错连接被拒绝。

  • 比如在 server 滚动更新过程中,旧的 Pod 中的进程很快就停止了(网卡还未完全销毁),但 client 所在节点的 iptables/ipvs 规则还没更新,包就可能会被转发到了这个停止的 Pod (由于 k8s 的 controller 模式,从 Pod 删除到 service 的 endpoint 更新,再到 kube-proxy watch 到更新并更新 节点上的 iptables/ipvs 规则,这个过程是异步的,中间存在一点时间差,所以有可能存在 Pod 中的进程已经没有监听,但 iptables/ipvs 规则还没更新的情况)。针对这种情况,建议给容器加一个 preStop,在真正销毁 Pod 之前等待一段时间,留时间给 kube-proxy 更新转发规则,更新完之后就不会再有新连接往这个旧 Pod 转发了,preStop 示例:


lifecycle:


preStop:


exec:


command:


- /bin/bash


- -c


- sleep 30


另外,还可能是新的 Pod 启动比较慢,虽然状态已经 Ready,但实际上可能端口还没监听,新的请求被转发到这个还没完全启动的 Pod 就会报错连接被拒绝。


针对这种情况,建议给容器加就绪检查 (readinessProbe),让容器真正启动完之后才将其状态置为 Ready,然后 kube-proxy 才会更新转发规则,这样就能保证新的请求只被转发到完全启动的 Pod,readinessProbe 示例:


readinessProbe:


httpGet:


path: /healthz


port: 80


httpHeaders:


- name: X-Custom-Header


value: Awesome


initialDelaySeconds: 15


timeoutSeconds: 1


  • Connection timed out: 连接超时。通常是连接还没建立,client 发 SYN 请求建立连接一直等到超时时间都没有收到 ACK,然后就报错连接超时。

  • 这个可能场景跟前面 Connection refused 可能的场景类似,不同点在于端口有监听,但进程无法正常响应了: 转发规则还没更新,旧 Pod 的进程正在停止过程中,虽然端口有监听,但已经不响应了;或者转发规则更新了,新 Pod 端口也监听了,但还没有真正就绪,还没有能力处理新请求。针对这些情况的建议跟前面一样:加 preStop 和 readinessProbe。


下面我们来继续分析滚动更新时发生 “No route to host” 的可能情况


这个报错很明显,IP 无法路由,通常是将报文发到了一个已经彻底销毁的 Pod (网卡已经不在)。不可能发到一个网卡还没创建好的 Pod,因为即便不加存活检查,也是要等到 Pod 网络初始化完后才可能 Ready,然后 kube-proxy 才会更新转发规则。


什么情况下会转发到一个已经彻底销毁的 Pod?借鉴前面几种滚动更新的报错分析,我们推测应该是 Pod 很快销毁了但转发规则还没更新,从而新的请求被转发了这个已经销毁的 Pod,最终报文到达这个 Pod 所在 PodCIDR 的 Node 上时,Node 发现本机已经没有这个 IP 的容器,然后 Node 就返回 ICMP 包告知 client 这个 IP 不可达,client 收到 ICMP 后,应用层就会报错 “No route to host”。


所以关键点在于 Pod 销毁太快,转发规则还没来得及更新,导致后来的请求被转发到已销毁的 Pod。针对这种情况,我们可以给容器加一个 preStop,留时间给 kube-proxy 更新转发规则来解决,可以参考:


https://k8s.imroc.io/best-practice/high-availability-deployment-of-applications#smooth-update-using-prestophook-and-readinessprobe

深入分析

为了弄清楚根本原因,我们请求用户协助搭建了一个可以复现问题的测试环境,最终这个问题在测试环境中可以稳定复现。


仔细观察,实际是部署两个服务:ServiceA 和 ServiceB。使用 ab 压测工具去压测 ServiceA (短连接),然后 ServiceA 会通过 RPC 调用 ServiceB (短连接),滚动更新的是 ServiceB,报错发生在 ServiceA 调用 ServiceB 这条链路。


在 ServiceB 滚动更新期间,新的 Pod Ready 了之后会被添加到 IPVS 规则的 RS 列表,但旧的 Pod 不会立即被踢掉,而是将新的 Pod 权重置为 1,旧的置为 0,通过在 client 所在节点查看 IPVS 规则可以看出来:


root@VM-0-3-ubuntu:~# ipvsadm -ln -t 172.16.255.241:80


Prot LocalAddress:Port Scheduler Flags


-> RemoteAddress:Port Forward Weight ActiveConn InActConn


TCP 172.16.255.241:80 rr


-> 172.16.8.106:80 Masq 0 5 14048


-> 172.16.8.107:80 Masq 1 2 243


为什么不立即踢掉旧的 Pod 呢?因为要支持优雅结束,让存量的连接处理完,等存量连接全部结束了再踢掉它(ActiveConn+InactiveConn=0),这个逻辑可以通过这里的代码确认:


https://github.com/kubernetes/kubernetes/blob/v1.17.0/pkg/proxy/ipvs/graceful_termination.go#L170


然后再通过 ipvsadm -lnc | grep 172.16.8.106 发现旧 Pod 上的连接大多是 TIME_WAIT 状态,这个也容易理解:因为 ServiceA 作为 client 发起短连接请求调用 ServiceB,调用完成就会关闭连接,TCP 三次挥手后进入 TIME_WAIT 状态,等待 2*MSL (2 分钟) 的时长再清理连接。


经过上面的分析,看起来都是符合预期的,那为什么还会出现 “No route to host” 呢?难道权重被置为 0 之后还有新连接往这个旧 Pod 转发?我们来抓包看下:


root@VM-0-3-ubuntu:~# tcpdump -i eth0 host 172.16.8.106 -n -tttt


tcpdump: verbose output suppressed, use -v or -vv for full protocol decode


listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes


2019-12-13 11:49:47.319093 IP 10.0.0.3.36708 > 172.16.8.106.80: Flags [S], seq 3988339656, win 29200, options [mss 1460,sackOK,TS val 3751111666 ecr 0,nop,wscale 9], length 0


2019-12-13 11:49:47.319133 IP 10.0.0.3.36706 > 172.16.8.106.80: Flags [S], seq 109196945, win 29200, options [mss 1460,sackOK,TS val 3751111666 ecr 0,nop,wscale 9], length 0


2019-12-13 11:49:47.319144 IP 10.0.0.3.36704 > 172.16.8.106.80: Flags [S], seq 1838682063, win 29200, options [mss 1460,sackOK,TS val 3751111666 ecr 0,nop,wscale 9], length 0


2019-12-13 11:49:47.319153 IP 10.0.0.3.36702 > 172.16.8.106.80: Flags [S], seq 1591982963, win 29200, options [mss 1460,sackOK,TS val 3751111666 ecr 0,nop,wscale 9], length 0


果然是!即使权重为 0,仍然会尝试发 SYN 包跟这个旧 Pod 建立连接,但永远无法收到 ACK,因为旧 Pod 已经销毁了。为什么会这样呢?难道是 IPVS 内核模块的调度算法有问题?尝试去看了下 linux 内核源码,并没有发现哪个调度策略的实现函数会将新连接调度到权重为 0 的 rs 上。


这就奇怪了,可能不是调度算法的问题?继续尝试看更多的代码,主要是 net/netfilter/ipvs/ip_vs_core.c 中的 ip_vs_in 函数,也就是 IPVS 模块处理报文的主要入口,发现它会先在本地连接转发表看这个包是否已经有对应的连接了(匹配五元组),如果有就说明它不是新连接也就不会调度,直接发给这个连接对应的之前已经调度过的 rs (也不会判断权重);如果没匹配到说明这个包是新的连接,就会走到调度这里 (rr, wrr 等调度策略),这个逻辑看起来也没问题。


那为什么会转发到权重为 0 的 rs ?难道是匹配连接这里出问题了?新的连接匹配到了旧的连接?我开始做实验验证这个猜想,修改一下这里的逻辑:检查匹配到的连接对应的 rs 如果权重为 0,则重新调度。然后重新编译和加载 IPVS 内核模块,再重新压测一下,发现问题解决了!没有报 “No route to host” 了。


虽然通过改内核源码解决了,但我知道这不是一个好的解决方案,它会导致 IPVS 不支持连接的优雅结束,因为不再转发包给权重为 0 的 rs,存量的连接就会立即中断。


继续陷入深思……


这个实验只是证明了猜想:新连接匹配到了旧连接。那为什么会这样呢?难道新连接报文的五元组跟旧连接的相同了?


经过一番思考,发现这个是有可能的。因为 ServiceA 作为 client 请求 ServiceB,不同请求的源 IP 始终是相同的,关键点在于源端口是否可能相同。由于 ServiceA 向 ServiceB 发起大量短连接,ServiceA 所在节点就会有大量 TIME_WAIT 状态的连接,需要等 2 分钟 (2*MSL) 才会清理,而由于连接量太大,每次发起的连接都会占用一个源端口,当源端口不够用了,就会重用 TIME_WAIT 状态连接的源端口,这个时候当报文进入 IPVS 模块,检测到它的五元组跟本地连接转发表中的某个连接一致(TIME_WAIT 状态),就以为它是一个存量连接,然后直接将报文转发给这个连接之前对应的 rs 上,然而这个 rs 对应的 Pod 早已销毁,所以抓包看到的现象是将 SYN 发给了旧 Pod,并且无法收到 ACK,伴随着返回 ICMP 告知这个 IP 不可达,也被应用解释为 “No route to host”。


后来无意间又发现一个还在 open 状态的 issue,虽然还没提到 “No route to host” 关键字,但讨论的跟我们这个其实是同一个问题。我也参与了讨论,有兴趣的同学可以看下:


https://github.com/kubernetes/kubernetes/issues/81775

总结

这个问题通常发生的场景就是类似于我们测试环境这种:ServiceA 对外提供服务,当外部发起请求,ServiceA 会通过 RPC 或 HTTP 调用 ServiceB,如果外部请求量变大,ServiceA 调用 ServiceB 的量也会跟着变大,大到一定程度,ServiceA 所在节点源端口不够用,复用 TIME_WAIT 状态连接的源端口,导致五元组跟 IPVS 里连接转发表中的 TIME_WAIT 连接相同,IPVS 就认为这是一个存量连接的报文,就不判断权重直接转发给之前的 rs,导致转发到已销毁的 Pod,从而发生 “No route to host”。


如何规避?集群规模小可以使用 iptables 模式,如果需要使用 ipvs 模式,可以增加 ServiceA 的副本,并且配置反亲和性 (podAntiAffinity),让 ServiceA 的 Pod 部署到不同节点,分摊流量,避免流量集中到某一个节点,导致调用 ServiceB 时源端口复用。


如何彻底解决?暂时还没有一个完美的方案。


Issue 85517 讨论让 kube-proxy 支持自定义配置几种连接状态的超时时间,但这对 TIME_WAIT 状态无效。


Issue 81308 讨论 IVPS 的优雅结束是否不考虑不活跃的连接 (包括 TIME_WAIT 状态的连接),也就是只考虑活跃连接,当活跃连接数为 0 之后立即踢掉 rs。这个确实可以更快的踢掉 rs,但无法让优雅结束做到那么优雅了,并且有人测试了,即便是不考虑不活跃连接,当请求量很大,还是不能很快踢掉 rs,因为源端口复用还是会导致不断有新的连接占用旧的连接,在较新的内核版本,SYN_RECV 状态也被视为活跃连接,所以活跃连接数还是不会很快降到 0。


这个问题的终极解决方案将走向何方,我们拭目以待!


原文:http://suo.im/6cn51J


2019-12-31 14:381188

评论

发布
暂无评论
发现更多内容

浅谈人工智能发展六大趋势 | 社区征文

战场小包

人工智能 新春征文 2月月更

毕业设计

Geek_e6f7f6

架构实战营

DG-IoT工业数据云平台

dgiot

物联网 2月月更 2月日更 dgiot dgiot物联网

Windows怎么就突然不好用了?

Jackpop

从冬奥看中国科技(五):漫天塞地物联网

脑极体

Web Components 系列(十一)—— 实现 MyCard 的可复用

编程三昧

前端 组件化 2月月更 webComponents

基于STM32设计的环境检测设备

DS小龙哥

2月月更 STM32环境检测仪

大模型应用新范式:统一特征表示优化(UFO)

百度大脑

PingCode与Jira 敏捷开发管理能力的对比

爱吃小舅的鱼

2022年的元宇宙:抢人、烧钱、突破体验边界

CECBC

一文带你使用 Python 实现Socket编程

宇宙之一粟

Python socket 2月月更

百度AI融通创新工坊招生中!

百度大脑

【网络安全应急响应】实战思路经验分享

H

网络安全 应急响应

零基础想学习Web安全,如何入门?

网络安全学海

黑客 网络安全 信息安全 渗透测试 漏洞

一文搞懂MySQL体系架构

程序猿阿星

架构 MySQL 数据库 MySQL InnoDB

解密体育背后AI黑科技:花样滑冰动作识别、多模视频分类和精彩片段剪辑

百度大脑

阿里稳定性指标1-5-10解读

焦振清

稳定性 1-5-10 MTTR

【C语言】初始字符串 & 转义字符与注释

謓泽

C语言 2月月更

作业八-消息队列数据库建模

曾竞超

架构实战营 「架构实战营」

PaddleDTX v1.0版本发布,安全性灵活性全面提升!

百度大脑

外包学生管理系统的架构文档

王大胖

聊聊 Kafka:Topic 创建流程与源码分析 | 社区征文

老周聊架构

云原生 Apache Pulsar 新春征文 2月月更

关于 AI 边云协同解决方案的研究与讨论 | 社区征文

liuzhen007

AI 新春征文 2月月更

基于飞桨实现的“太空保卫战士”——地球同步静止轨道空间目标检测系统

百度大脑

跨平台应用开发进阶 (四) :uni-app 实现图片上传、预览、删除、压缩

No Silver Bullet

uni-app 跨平台 图片 2月月更

Lyft微服务研发效能提升实践 | 1. 开发和测试环境的历史

俞凡

研发效能 大厂实践 2月月更 lyft

用Weave Net 打开跨主机容器互联大门 | 社区征文

L同学

docker网络 新春征文 网络交换机 weave 集群网络

外包学生管理系统架构设计

唐尤华

架构实战营

人才短缺、成本高昂,制造企业智能化转型路径如何破局?

百度大脑

敏捷宣言及完整解读

爱吃小舅的鱼

敏捷开发框架都有哪些

爱吃小舅的鱼

Kubernetes 疑难排查分享: 诡异的 No route to host_服务革新_华为云原生团队_InfoQ精选文章