作者信息:

唐聪、王超凡,腾讯云原出产品中心技能专家,负责腾讯云大规划 TKE 集群和 etcd 控制面稳定性、功能和成本优化作业。

王子勇,腾讯云专家级工程师, 腾讯云核算产品技能服务专家团队负责人。

概况

作为当时我国广泛运用的云视频会议产品,腾讯会议已服务超过 3 亿用户,能高并发支撑千万级用户一起开会。腾讯会议数百万中心服务都布置在腾讯云 TKE 上,经过全球多地域多集群布置实现高可用容灾。在去年用户运用最高峰期间,为了支撑更大规划的并发在线会议的人数,腾讯会议与 TKE 等各团队进行了一轮新的扩容。

可是,在这进程中,一个简略的 etcd 进程重启操作却触发了一个的怪异的 K8s 毛病(不影响用户开会,影响新一轮后台扩容功率),本文介绍了咱们是怎么从问题现象、到问题剖析、斗胆猜想扫除、再次复现、严谨验证、根治隐患的,从 Kubernetes 到 etcd 底层原理,从 TCP RFC 草案再到内核 TCP/IP 协议栈实现,一步步定位并处理问题的详细流程(终究定位到是特别场景触发了内核 Bug)。

期望经过本文,让咱们对 etcd、Kubernetes 和内核的复杂问题定位有一个较为深化的了解,掌握相关办法论,一起也能让咱们更好的了解和运用好 TKE,经过分享咱们的毛病处理进程,提高咱们的透明度。

布景常识

首要给咱们简要介绍下腾讯会议的简要架构图和其运用的中心产品 TKE Serverless 架构图。

腾讯会议极简架构图如下:

腾讯会议重度运用的 TKE Serverless 架构如下:

腾讯会议简直悉数事务都跑在 TKE Serverless 产品上,Master 组件布置在咱们metacluster 中(K8s in K8s),超大集群或许有10多个 APIServer,etcd 由服务化的 etcd 渠道供给,APIServer 访问 etcd 链路为 svc -> cluster-ip -> etcd endpoint。事务各个 Pod 独占一个轻量级的虚拟机,安全性、阻隔性高,事务无需关怀任何 Kubernetes Master、Node 问题,只需要专心事务范畴的开发即可。

问题现象

在一次资源扩容的进程中,腾讯会议的研制同学晚上忽然在群里反应他们上海一个最大集群呈现了事务扩容失败,收到反应后研制同学,第一时刻查看后,还看到了如下反常:

● 部分 Pod 无法创立、毁掉

● 某类资源 Get、list 都是超时

● 个别组件呈现了 Leader Election 过错

● 大部分组件正常,少部分控制器组件有如下 list pvc 资源超时日志

k8s.io/client-go/informers/factory.go:134: Failed to watch
*v1.PersistentVolumeClaim: failed to list 
*v1.PersistentVolumeClaim: the server was unable to return a 
response in the time allotted, but may still be processing the 
request (get persistentvolumeclaims)

可是 APIServer 负载并不高,当时一线研制同学快速给出了一个控制面呈现未知反常的结论。随后 TKE 团队快速进入攻艰模式,开始深化剖析毛病原因。

问题剖析

研制团队首要查看了此集群相关改变记录,发现此集群在几个小时之前,进行了重启 etcd 操作。改变原因是此集群规划很大,在之前的多次扩容后,db size 运用率现已接近 80%,为了防止 etcd db 在事务新一轮扩容进程中被写满,因而体系进行了一个经过批阅流程后的,一个惯例的调大 etcd db quota 操作,而且改变后体系自检 etcd 中心目标正常。

咱们首要剖析了 etcd 的接口延时、带宽、watch 监控等目标,如下图所示,
etcd P99 延时毛刺也就 500ms,节点带宽最大的是平均 100MB/s 左右,初步看并未发现任何反常。

随后又回到 APIServer,一个在恳求在 APIServer 内部阅历的各个中心阶段(如下图所示):

● 鉴权 (校验用户 token、证书是否正确)

● 审计

● 授权 (查看是否用户是否有权限访问对应资源)

● 限速模块 (1.20 后是优先级及公正管理)

● mutating webhook

● validating webhook

● storage/cache

● storage/etcd

恳求在发往 APIServer 前,client 或许导致恳求慢的原因:
● client-go 限速,client-go 默许 qps 5, 假如触发限速,则日志等级 4 以上(高版别 client-go 日志等级 3 以上)能够看到客户端日志中有打印 Throttling request took 相关日志

那究竟是哪个阶段呈现了问题,导致 list pvc 接口超时呢?

● 依据 client QPS 很高、而且经过 kubectl 衔接反常实例也能复现,扫除了 client-go 限速和 client 到 apiserver 网络衔接问题

● 经过审计日志查找到不少 PVC 资源的 Get 和 list 5XX 过错,集合在其中一个实例

● 经过 APIServer Metrics 视图和 trace 日志扫除 webhook 导致的超时等

● 依据 APIServer 访问 etcd 的 Metrics、Trace 日志确认了是 storage/etcd 模块耗时很长,可是只要一个 APIServer 实例、某类资源有问题

那为什么 etcd 侧看到的监控延时很低,而 APIServer 访问 etcd 延时很高,而且仅仅某类资源呈现问题,不是一切资源呢?

首要,咱们查看下 APIServer 的 etcd 延时核算上报代码 (以 Get 接口为例):

它核算的是整个 Get 恳求(实际调用的是 etcd Range 接口)从 client 发出到收到成果的耗时,包含了整个网络链路和 etcd RPC 逻辑处理耗时

而 etcd 的 P99 Range 延时是依据 gRPC 拦截器机制实现的,etcd 在发动 gRPC Server 的时分,会注册一个一元拦截器实现延时核算,在 RPC 恳求进口和履行 RPC 逻辑完结时上报延时,也便是它并不包含 RPC 恳求在数据接纳和发送进程中的耗时,相关逻辑封装在 monitor 函数中,简要逻辑如下所示:

终究一个疑问为什么是某类资源呈现问题?

APIServer 在发动的时分,会依据 Kubernetes 中的每个资源和版别创立一个独立etcd client,并依据配置决议是否敞开 watch cache,每个 client 一般 1 个 TCP 衔接,一个 APIServer 实例会高达上百个 etcd 衔接。etcd client 与 etcd server 通讯运用的是 gRPC 协议,而 gRPC 协议又是依据 HTTP/2 协议的。

PVC 资源超时,Pod、Node 等资源没超时,这说明是 PVC 资源对应的底层 TCP 衔接/应用层 HTTP/2 衔接出了问题。

在 HTTP/2 协议中,音讯被分解独立的帧(Frame),交错发送,帧是最小的数据单位。每个帧会标识属于哪个流(Stream),流由多个数据帧组成,每个流具有一个唯一的 ID,一个数据流对应一个恳求或响应包。如上图所示,client 正在向 server 发送数据流 5 的帧,一起 server 也正在向 client 发送数据流 1 和数据流 3 的一系列帧。一个衔接上有并行的三个数据流,HTTP/2 可依据帧的流 ID 将并行、交错发送的帧从头组装成完好的音讯。

也便是,经过 HTTP/2 的多路复用机制,一个 etcd HTTP/2 衔接,能够满足高并发状况下各种 client 对 PVC 资源的查询、创立、删除、更新、Watch 恳求。

那究竟是这个衔接出了什么问题呢?

清晰是 APIServer 和 etcd 的网络链路呈现了反常之后,咱们又有了如下猜想:

● 反常实例 APIServer 所在节点呈现反常

● etcd 集群 3 个节点底层网络反常

● etcd HTTP/2 衔接最大并发流约束 (单 HTTP/2 衔接最大一起翻开的并发流是有约束的)

● TCP 衔接触发了内核未知 bug,衔接疑似 hang 住相同

● …..

可是咱们对 APIServer 和 etcd 节点进行了详细的体系诊断、网络诊断,除了发现 etcd 节点呈现了少量毛刺丢包,并未发现其他显着问题,当时 etcd 节点也仅运用了 1/3 的节点带宽,可是恳求依然巨慢,因而基本能够扫除带宽超限导致的恳求超时。

etcd HTTP/2 衔接最大并发流约束的特点是此类资源含有较大的并发恳求数、一起应有部分成功率,不应悉数超时。可是经过咱们一番深化排查,经过审计日志、Metrics 监控发现 PVC 资源的恳求绝大部分都是 5XX 超时过错,简直没有成功的,一起咱们发现了一个 CR 资源也呈现了衔接反常,可是它的并发恳求数很少。依据以上剖析,etcd HTTP/2 衔接最大并发流约束猜想也被扫除。

问题再次堕入未知,此刻,就要寄出终极杀器——抓包大法,来剖析究竟整个 TCP 衔接链路发生了什么。

要经过抓包来剖析详细恳求,首要咱们就要面临一个问题,当时单个 APIServer 到 etcd 一起存在上百个衔接,咱们该怎么缩小规模,定位到详细反常的 TCP 衔接呢?要定位到详细的反常衔接,首要会面临以下几个问题:

  1. 数据量大:APIServer 大部分衔接都会不停的向 etcd 恳求数据,而且部分恳求的数据量比较大,假如抓全量的包剖析起来会比较困难。
  2. 新建衔接无法复现:该问题只影响个其他资源恳求,也便是只影响存量的几个长链接,增量衔接无法复现。
  3. APIServer 和 etcd 之间运用 https 通讯,解密困难,无法有效剖析包的内容:由于长链接现已树立,现已过了 tls 握手阶段,一起节点安全管控约束,短时刻不允许运用 ebpf 等 hook 机制,因而无法拿到解密后的内容。

为了定位到详细的反常衔接,咱们做了以下几个尝试:

  1. 首要针对响应慢的资源,不经过 Loadbalancer,直接恳求 APIServer 对应的 RS,将规模缩小到详细某一个 APIServer 副本上
  2. 针对反常的 APIServer 副本,先将它从 Loadbalancer 的后端摘掉,一方面能够赶快康复事务,另一方面也能够防止有新的流量进来,能够下降抓包数据量(PS:摘掉 RS 的一起,Loadbalancer 支持发双向 RST,能够将客户端和 APIServer 之间的长链接也断掉)。
  3. 对反常的 APIServer 副本进行抓包,抓取 APIServer 恳求 etcd 的流量,一起经过脚本对该反常的 APIServer 建议并发查询,只查询响应慢的资源,然后对抓包数据进行剖析,同一时刻点 APIServer 对 etcd 有许多并发恳求的长衔接即为反常衔接。

定位到反常衔接后,接下来便是剖析该衔接详细为什么反常,经过剖析咱们发现 etcd 回给 APIServer 的包都很小,每个 TCP 包都是 100 字节以下:

经过 ss 指令查看衔接的 TCP 参数,发现 MSS 竟然只要 48 个字节:

这儿简略介绍下 TCP MSS(maximum segment size)参数, 中文名最大分段巨细,单位是字节,它约束每次网络传输的数据包的巨细,一个恳求由多个数据包组成,MSS 不包含 TCP 和 IP 协议包头部分。TCP 中还有一个跟包巨细的参数是 MTU(maximum transmission unit),中文名是最大传输单位,它是互联网设备(路由器等)能够接纳的最大数据包的值,它包含 TCP 和 IP 包头,以及 MSS。

受限于 MTU 值巨细(最大1500),MTU 减去 TCP 和 IP 包头,云底层网络转发所运用的协议包头,MSS 一般在1400左右。可是在咱们这儿,如下图所示,对 ss 核算剖析能够看到,有 10 几个衔接 MSS 只要 48 和 58。恣意一个恳求尤其是查询类的,都会导致恳求被拆分成许多小包发送,应用层必定会呈现各类超时过错,client 进而又会触发各种重试,终究整个衔接呈现完全不可用。

在确认是 MSS 值过小导致上层各种怪异超时现象之后,咱们进一步考虑,是什么当地改掉了 MSS。可是 MSS 协商是在三次握手中树立的,存量的反常衔接比较难找到相关信息。

内核剖析进程

抓包剖析

为了进一步搞清楚问题发生的根本原因,咱们在危险可控的状况下,在事务低峰期,清晨1点,自动又做了一次相似的改变,来尝试复现问题。从抓到的包看 TCP 的选项,发现 MSS 协商的都是比较大,没有特别小的状况:

仅 SYN, SYN+ack 包带有 MSS 选项,而且值都大于 1000, 扫除底层网络设备篡改了 MSS 形成的问题。

内核剖析

那内核当中,什么当地会修改 MSS 的值?

假设一开始不了解内核代码,可是咱们能知道这个 MSS 字段是经过 ss 指令输出的,那么能够从 ss 指令代码入手。该指令来自于 iproute2 这个包,查找下 MSS 关键词, 可知在 ss 程序中,经过内核供给的 sock_diag netlink 接口, 查询到的信息。在tcp_show_info函数中做解析展示:

可知 MSS 字段来自内核的 tcpi_snd_mss。

之后,从内核里边查找该值赋值的当地:

  2   2749  net/ipv4/tcp.c <<tcp_get_info>>
             info->tcpi_snd_mss = tp->mss_cache;

继续找mss_cache的赋值方位:

只要2处,第一处是tcp_init_sock中调用,当中的赋值是初始值tcp_mss_DEFAULT 536U, 与抓到的现场不匹配,直接可疏忽。

第二处:

这儿面或许 2 个当地会影响,一个是pmtu, 其他一个是 tcp_bound_to_half_wnd.

抓包里边没显着看到 MTU 反常形成的流反常反应信息。聚焦在窗口部分:

这儿有个很可疑的当地。若是窗口很小,那么终究会取窗口与68字节 -tcp_header_len 的最大值,tcp_header_len 默许 20 字节的话,刚好是 48 字节。和咱们抓包看到的最小的 MSS 为 48 共同, 嫌疑很大。

那什么当地会修改最大窗口巨细?

TCP 修改的当地并不多,tcp_rcv_synsent_state_process中收到 SYN 包修改(状况不契合咱们当时的 case),其他首要的是在tcp_ack_update_window函数中,收 ack 之后去更新:

剖析收到的 ack 包,咱们能发现对方布告的窗口, 除了 SYN 之外,都是29字节:

SYN 包里边能看到扩大因子是:

按理说,核算出来的窗口依照

   if (likely(!tcp_hdr(skb)->syn))
        nwin <<= tp->rx_opt.snd_wscale;

核算,应该是14848字节,可是从 MSS 的体现看,好像这个 scale 丢掉了。实际上,比照正常和反常的衔接,发现的确 TCP 的 scale 选项在内核里边,真的丢了:

从 ss 里边比照正常和反常的衔接看,不仅仅是 window scale 没了,连 timestamp, sack 选项也一起消失了!很奇特!

咱们来看看 ss 里边获取的这些字段对应到内核的什么值:

ss 代码:

对应到内核 tcp_get_info 函数的信息:

那内核什么当地会清空 window scale 选项?

查找把 wscale_ok 改为 0 的当地,实际上并不多,咱们能够比较容易确认是 tcp_clear_options 函数干的:

static inline void tcp_clear_options(struct tcp_options_received *rx_opt)
{
    rx_opt->tstamp_ok = rx_opt->sack_ok = 0;
    rx_opt->wscale_ok = rx_opt->snd_wscale = 0;
}

他一起会清空 ts, sack, scale 选项, 和咱们的场景再匹配不过。

查找 tcp_clear_options 的调用方,首要在tcp_v4_conn_request和cookie_check_timestamp 两个当地,详细调用方位的逻辑都和 SYN cookie 特性有较强关联性。

    if (want_cookie && !tmp_opt.saw_tstamp)
        tcp_clear_options(&tmp_opt);
bool cookie_check_timestamp(struct tcp_options_received *tcp_opt,
            struct net *net, bool *ecn_ok)
{
    /* echoed timestamp, lowest bits contain options */
    u32 options = tcp_opt->rcv_tsecr & TSMASK;
    if (!tcp_opt->saw_tstamp)  {
        tcp_clear_options(tcp_opt);
        return true;
    }

两个条件都比较共同,看起来是 SYN cookie 收效的状况下,对方没有传递 timestamp 选项过来(实际上,依照 SYN cookie 的原理,发送给对端的回包中,会保存有编码进 tsval 字段低 6 位的选项信息),就会调用 tcp_clear_options, 清空窗口扩大因子等选项。

从体系日志里边,咱们也能观察到的确触发了 SYN cookie 逻辑:

所以,根因终于开始清晰,etcd 重启,触发了许多 APIServer 瞬间到 etcd 的新建衔接,短时刻的许多新建衔接触发了 SYN cookie 维护查看逻辑。可是由于客户端没有在后续包中将 timestamp 选项传过来,形成了窗口扩大因子丢掉,影响传输功能

客户端为什么不在每一个包都发送 timestamp,而是只在第一个 SYN 包发送?

首要,咱们来看看 RFC 规范,协商了 TCP timestamp 选项后,是应该选择性的发?仍是每一个都发?

答案是,后续每一个包 TCP 包都需要带上时刻戳选项。

那么,咱们的内核中为什么 SYN 包带了 TCP timestamp 选项,可是后续的包没有了呢?

查找 tsval 关键词:

能够看到 tcp_syn_options 函数中,自动建连时分,会依据 sysctl_tcp_timestmps 配置选项决议是否敞开时刻戳选项。

查看客户端体系,该选项的确是翻开的,契合预期:

net.ipv4.tcp_timestamps = 1

那为什么其他包都不带了呢?客户端视点,发了 SYN,带上时刻戳选项,收到服务端 SYN+ack 以及时刻戳,走到 tcp_rcv_synsent_state_process 函数中,调用 tcp_parse_options 解析 TCP 选项:

这儿,就算服务端回包带了 TCP 时刻戳选项,本机也要看两个 sysctl:

● sysctl_tcp_timestamps 这个比较好理解。内核标准的。

● sysctl_tcp_wan_timestamps 这个看起来是针对外网翻开时刻戳的选项?很怪异。

此处就会有坑了,假如 wan_timestamps 选项没翻开,saw_tstamp 不会设置为1, 后续也就不会再发送 TCP 时刻戳选项。

查看 wan_timestamps 设置,的确默许是封闭的:

net.ipv4.tcp_wan_timestamps = 0

所以这儿本相也就清晰了:由于 tcp_timestamps 选项翻开,所以内核建连是会发送时刻戳选项协商,一起由于 tcp_wan_timestamps 封闭,在运用非私有网段的状况下,会形成后续的包不带时刻戳(云环境容器管控特别网段的原因)。

和 SYN cookie 的场景组合在一起,终究形成了 MSS 很小,功能较差的问题。

tcp_wan_timestamps 是内部的特性,是为了处理外网时刻戳不正确而加的特性,TKE 团队发现该问题后已反应给相关团队优化,当时现已优化完结。

总结

本文问题表象,APIServer 资源恳求慢,看似比较简略,实则在经过 APIServer Metrics 目标、etcd Metrics 目标、APIServer 和 etcd Trace 日志、审计日志、APIServer 和 etcd 源码深化剖析后,扫除了各种可疑原因,终究发现是一个非常底层的网络问题。

面临底层网络问题,在找到稳定复现的办法后,咱们经过抓包神器 tcpdump,丰厚强壮的网络东西 iproute2 包(iproute2 包中的 ss 指令,能够获取 TCP 的许多底层信息,比如 rtt,窗口因子,重传次数等,对诊断问题很有帮助),再结合TCP RFC、linux 源码(代码面前无秘密,不管是用户态东西仍是内核态),多团队的协作,成功破案。

经过此事例,更让咱们深化体会到,永远要对现网出产环境保持敬畏之心,任何操作都或许会引发不可预知的危险,监控体系不仅要检测改变服务中心目标,更要对主调方的中心目标进行深化检测。

参与本文问题定位的还有腾讯云网络专家赵奇园、内核专家杨玉玺。