记一次 k8s apiserver watch hang 问题排查

问题背景

注📢: 本文中涉及 apiserver 地址和 ingressgateway 地址, 为脱敏处理, 将会做马赛克处理!!!

传统的 kubernetes apiserver 请求访问链路为客户端直连 apiserver,为了做 apiserver 高可用,通常我们会给 apiserver 前端再套一层4层或7层代理做多个 apiserver 实例的负载均衡。

在我们的场景下,使用了 istio 的 ingressgateway 作为 client -> apiserver 这条链路中的7层代理。链路变成了 client -> ingressgateway -> apiserver ,gateway 暴露 80 端口供客户端访问, 同时通过 istio virtualService + destinationRule 规则配置 gateway 能通过域名访问到 apiserver 6443 端口,从而实现流量路由。

具体链路如下图所示,

现在 apiserver 访问链路

<figcaption style="box-sizing: border-box; display: block; margin-top: 5px; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">现在 apiserver 访问链路</figcaption>

在这样的链路下,我们遇到了如下问题,

在 k8s apiserver 1.18 版本的集群在滚动重启过出现部分组件无法 watch 到事件的情况,客户端 watch 请求偶发 503。需要重启组件,重建 watch 连接才能恢复。

问题现象

本次故障中 apiserver 访问链路

<figcaption style="box-sizing: border-box; display: block; margin-top: 5px; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">本次故障中 apiserver 访问链路</figcaption>

由于 k8s watch 请求是 http2 协议, 故 client 与 gateway 之间保持了长连接,同时 gateway 与后端也保持长连接。

当 apiserver 发生滚动重建后,gateway 与旧 apiserver 连接断开,同时与新 apiserver 建连。此时 client 与 gateway 仍然保持着原来那根连接,故在不重建 7 层 http2 连接的情况下,请求没法打到 new apiserver。

故客户端虽然与 Upstream 网关连接正常,但网关与 apiserver 侧可能仍使用到旧 apiserver 的连接对,导致无法 watch 到事件。

具体现象为

  1. 客户端出现 watch 请求 hang 住, 偶发出现 watch 请求 503, 报错为: server is currently unable to handler the requests

    503客户端截图

  2. 通过 pprof 查看到 client-go ListAndWatch 函数中的 watchHandler 执行了 6000+ 分钟, 实际应该在 5-10 分钟后就退出重连。

503pprof截图

<figcaption style="box-sizing: border-box; display: block; margin-top: 5px; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">503pprof截图</figcaption>

排查过程

首先我的疑点是网关侧(ingressgateway)配置不正确导致了网关和旧 apiserver 连接仍保持,从而导致了客户端(client) watch 请求没有打到新 apiserver 上,但网关侧没有了现场更多信息,且线上 apiserver 集群不能频繁重启,故尝试用风险较低的场景复现问题

环境

  • 两台 apiserver 实例(apiserver1、apiserver2)
  • 一台网关实例 (ingressgateway1)
  • 个人PC启动 k8s client

复现

线下调试集群 apiserver (之后简称该集群为 staging 集群)请求一共有2实例, 脱敏考虑,我们简写其 ip 为 apiserverIp1、apiserverIp2

本地netstat截图

<figcaption style="box-sizing: border-box; display: block; margin-top: 5px; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">本地netstat截图</figcaption>

1. 新建调试网关, 隔离线上流量

先在生产环境单独起了一个 ingressgateway 网关实例,用于隔离线上流量,仅接收我的本地调试流量

单台调试网关实例

<figcaption style="box-sizing: border-box; display: block; margin-top: 5px; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">单台调试网关实例</figcaption>

2. 修改本地 dns, 将流量打向网关

此时在 /etc/hosts 中将 staging 集群 apiserver 请求定向指向该 ingressgateway 实例

hosts注入内容

<figcaption style="box-sizing: border-box; display: block; margin-top: 5px; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">hosts注入内容</figcaption>

3. 本地启动脚本模拟 watch 请求

在本地启动以下脚本 watch staging pod 事件

package main

启动脚本开始 watch pod, 观察到此时 watch pod 正常.

客户端client-go日志

<figcaption style="box-sizing: border-box; display: block; margin-top: 5px; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">客户端client-go日志</figcaption>

此时登录 ingressgateway 容器观察到网关一共有2个与 apiserver 的连接(这儿看到的 8080 端口是因为开了 istio sidecar)

istio-apiserver访问链路

<figcaption style="box-sizing: border-box; display: block; margin-top: 5px; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">istio-apiserver访问链路</figcaption>

在istio-apiserver链路下netstat信息

<figcaption style="box-sizing: border-box; display: block; margin-top: 5px; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">在istio-apiserver链路下netstat信息</figcaption>

此时对其中一个 apiserver 实例注入丢包规则,模拟 ingress -> apiserver 请求失败。(另一台 apiserver 正常)

iptables -A OUTPUT -p tcp --dst apiserver_ip_1 --dport 8080 -j DROP

并且在网关侧同时对所有 apiserver 实例 8080 端口进行 tcpdump 抓包,预期情况下,apisever1 将不再出现新请求,同时另一个可用的 apiserver2 应该收到请求。

网关侧对异常apiserver抓包

<figcaption style="box-sizing: border-box; display: block; margin-top: 5px; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">网关侧对异常apiserver抓包</figcaption>

网关侧对正常apiserver抓包

<figcaption style="box-sizing: border-box; display: block; margin-top: 5px; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">网关侧对正常apiserver抓包</figcaption>

此时观察到客户端 watch 请求 Hang 住,同时出现了 503, 复现了故障期间 watch 请求不断开的场景。

排查

此时根据上述现场深挖信息,发现正常的 apiserver 抓包看到并没有收到新请求,同时异常的 apiserver 处于丢包状态无法处理请求,即从网关侧连接后端2个 apiserver 均"异常"了, 故客户端 watch 请求必然出现异常。

客户端client-go日志信息

<figcaption style="box-sizing: border-box; display: block; margin-top: 5px; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">客户端client-go日志信息</figcaption>

此时观察到本地客户端的 4 层 tcp 连接如下 (src port: 49739), 端口未改变过,说明客户端 -> ingress 的 tcp 连接未断开过。

本地netstat截图

<figcaption style="box-sizing: border-box; display: block; margin-top: 5px; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">本地netstat截图</figcaption>

于是我怀疑请求仍然走的老连接打到了异常 apiserver 上,网关侧没有做切换,将访问 apiserver 的请求路由到未丢包的 apiserver 上

找解决方案

根据上述疑点,我发现网关访问新的 apiserver 仍然是能通的,只是客户端 watch 请在于网关未断开的情况下仍使用了老的 apiserver 的连接,没有使用到这根新连接上。

于是解决方案的思路大致为:如何让客户端断开异常的 apiserver 连接并连上新的 apiserver 。

由于在我们的链路下,客户端不直接和 apiserver 通讯,而是由网关进行代理,故问题转移到了如何在网关上找到方式通知客户端进行重连。调研后得到以下两种方案:

  • 方案1:在网关侧配置 stream_idle_timeout 参数, 由于客户端与网关在该异常下长时间没有 http2 的包传输,故必然不会有 h2 stream,那么使用该参数来使得网关侧主动断开没有包传输的老连接,触发客户端重连新连接。
解决方案1

<figcaption style="box-sizing: border-box; display: block; margin-top: 5px; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">解决方案1</figcaption>

  • 方案2:网关侧通过 tcpKeepalive 掐断与异常 apiserver(配置了丢包规则) 连接,使得客户端重连。
解决方案2

<figcaption style="box-sizing: border-box; display: block; margin-top: 5px; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">解决方案2</figcaption>

验证

我首先验证了方案1 ---- stream_idle_timeout 的方式,增加如下 envoyFilter 配置。

apiVersion: networking.istio.io/v1alpha3

网关确实能掐掉异常连接了,但也会掐掉 exec 这类客户端登录容器的"正常连接",即业务方存在需要通过和 apiserver 建立 exec 长连接且长时间不会发送数据包的情况。加上 stream_idle_timeout 会误掐掉这些连接,对业务是有损的,于是放弃了方案一。

接着我开始验证方案2,修改 apiserver destinationRule 配置,增加 tcpKeepalive

tcpKeepalive参数配置

<figcaption style="box-sizing: border-box; display: block; margin-top: 5px; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">tcpKeepalive参数配置</figcaption>

接着按照丢包的方案重新启动本地测试 client 进行验证

感知到报错的客户端日志

<figcaption style="box-sizing: border-box; display: block; margin-top: 5px; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">感知到报错的客户端日志</figcaption>

观察发现客户端在下一次 relist(ListAndWatch) 后恢复正常

同时再次在本地查看客户端端口为 49739, 4 层 tcp 连接未断开,说明客户端 -> ingress 链路正常.

感知到报错的客户端netstat截图

<figcaption style="box-sizing: border-box; display: block; margin-top: 5px; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">感知到报错的客户端netstat截图</figcaption>

进一步佐证了问题可能是 ingress 连上了不可用的 apiserver,而加上 tcpKeepalive 则让网关检查到 apiserver 不可用(仅网络层),从而切换到另一个可用的 apiserver 实例。

根据 pprof 进行结论的二次验证

由于目前所使用的 client-go 代码中增加了 watchTimeout 的逻辑,会周期性地进行 relist&watch, 从而保障 watch 请求不会长时间 hang 住

client-go watchTimeout 代码段1

<figcaption style="box-sizing: border-box; display: block; margin-top: 5px; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">client-go watchTimeout 代码段1</figcaption>

client-go watchTimeout 代码段2

<figcaption style="box-sizing: border-box; display: block; margin-top: 5px; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">client-go watchTimeout 代码段2</figcaption>

故正常情况下 client-go watch 请求行为如下:

  • 预期: 在每次调用 ListAndWatch 的时间窗口(5-10分钟内)未接收到请求,则会自动断开,进行 relist

在未开启 tcpKeepalive 前,出现了 watchHandler goroutine hang 住问题, 观察下图 pprof 中 goroutine stack 发现,watchHandler 这个 goroutine 运行了超过 10min,未触发 client-go 中 relist 逻辑, 不符合预期。属于 watch hang 住行为。

本地pprof截图

<figcaption style="box-sizing: border-box; display: block; margin-top: 5px; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">本地pprof截图</figcaption>

而开启 tcpKeepalive 后,watchHandler goroutine hang 住问题消失,watchHandler 均不超过 5min,符合预期。watch hang 住问题消失。

问题根因

定位到了 tcpKeepalive 能解这个问题后,咱开始怀疑这个参数的底层逻辑是啥,为啥能解这个问题。

charles k8s抓包链路

<figcaption style="box-sizing: border-box; display: block; margin-top: 5px; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">charles k8s抓包链路</figcaption>

由于这个参数是作用在网络 4 层,于是我第一想法是进行抓包拿更多信息。参考上图的抓包链路,我启动了 charles 进行 k8s apiserver https 抓包。

观察发现未开启 tcpKeepalive 时,异常时(ingress 侧开启了单台 apiserver 丢包) ,客户端的 watch 请求 Tcp/Http 连接均不断开。而开启 tcpKeepalive 后,当网关访问到配置了丢包规则的 apiserver 时出现了 503, 并返回错误给客户端, 同时客户端的7层连接进行重建(streamId 变了),重建后下一次 ListAndWatch 请求可以正常拿到响应。

charles 本地抓包截图1

<figcaption style="box-sizing: border-box; display: block; margin-top: 5px; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">charles 本地抓包截图1</figcaption>

所以是什么导致了这个行为呢?

由于 charles 只能抓取 7 层的数据包,我想看看 4 层的网络行为,依葫芦画瓢,再次通过 wireshark 抓包

wireshark解包截图1

<figcaption style="box-sizing: border-box; display: block; margin-top: 5px; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">wireshark解包截图1</figcaption>

wireshark解包截图2

<figcaption style="box-sizing: border-box; display: block; margin-top: 5px; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">wireshark解包截图2</figcaption>

wireshark抓包截图3

<figcaption style="box-sizing: border-box; display: block; margin-top: 5px; text-align: center; color: rgb(136, 136, 136); font-size: 14px;">wireshark抓包截图3</figcaption>

发现在开启网关侧 tcpKeepalive 后,网关侧访问异常 apiserver 失败,在网关节点上掐断了与异常 apiserver 的,同时客户端在与网关的 60s tcpkeepalive timeout 后,网关侧主动向客户端发送了一条报文,触发了客户端重建7层连接,客户端在连接重建后恢复正常,观察发现请求打到了另外一台正常的 apiserver 上。

思考

client-go 中的 watchTimeout 是依靠 apiserver 主动发起才能进行 relist,在上述丢包场景下,apiserver 无法回包,故 watch 连接 hang 住的问题在 client-go 自身的机制是无法保证能自愈的,故在整条 apiserver 访问连上上,需要加上健康检查/探活逻辑(如 istio ingressgateway 对 apiserver 的探活),才能保证 watch 请求可靠性。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容