kubelet PLEG 的实现与优化

概述

PLEG 全称是 Pod Lifecycle Event Generator,用来为 kubelet 生成 container runtime 的 pod 生命周期事件,这样 kubelet 就可以根据 pod 的 spec 和 status 对比,来执行对应的控制逻辑。

在 1.1 及之前的 kubelet 中是没有 PLEG 的实现的。kubelet 会为每个 pod 单独启动一个 worker,这个 worker 负责向 container runtime 查询该 pod 对应的 sandbox 和 container 的状态,并进行状态同步逻辑的执行。这种 one worker per pod 的 polling 模型给 kubelet 带来了较大的性能损耗。即使这个 pod 没有任何的状态变化,也要不停的对 container runtime 进行主动查询。

因此在 1.2 中,kubelet 引入了 PLEG,将所有 container runtime 上 sandbox 和 container 的状态变化事件统一到 PLEG 这个单独的组件中,实现了 one worker all pods。这种实现相比于 one worker per pod 已经带来了较大的性能提升,详细实现会在后文进行介绍。但是默认情况下,仍然需要每秒一次的主动向 container runtime 查询,在 node 负载很高的情况下,依然会有一定的性能问题,比较常见的情况是导致 node not ready,错误原因是 PLEG is not healthy

在 1.26 中,kubelet 引入了 Evented PLEG,为了和之前的 PLEG 实现区别,之前的 PLEG 称为 Generic PLEG。当然,Evented PLEG 并不是为了取代 Generic PLEG,而是和 Generic PLEG 配合,降低 Generic PLEG 的 polling 频率,从而提高性能的同时,也能保证实时性。

Generic PLEG

Generic PLEG 定时(默认1s)向 runtime 进行查询,这个过程称为 relist,这里会调用 cri 的 ListPodSandboxListContainers接口。runtime 返回所有的数据之后,PLEG 会根据 sandbox 和 container 上的数据,对应的 Pod 上,并更新到缓存中。同时,组装成事件向 PLEG Channel 发送。

https://www.myway5.com/wp-content/uploads/2023/02/Snipaste_2023-02-27_16-10-20.png

kubelet 会在 pod sync loop 中监听 PLEG Channel,从而针对状态变化执行相应的逻辑,来尽量保证 pod spec 和 status 的一致。

Evented PLEG

引入 Evented PLEG 后,对 Generic PLEG 做了些许调整,主要是 relist 的周期和阈值,以及对缓存的更新策略。

  • relist 的同步周期由 1s 增加到 300s。同步阈值从 3min 增加到 10min。
  • 缓存更新时,updateTime 不再是取本地的时间,而是 runtime 返回的时间。

除此之外,Generic PLEG 会和之前一样运行,这样也保证了及时 Evented PLEG 丢失了一些 状态变更的 event,也可以由 Generic PLEG 兜底。

Evented PLEG 会调用 runtime 的 GetContainerEvents 来监听 runtime 中的事件,然后生成 pod 的 event,并发送到 PLEG Channel 中供 kubelet pod sync loop 消费。

如果 Evented 不能按照预期工作(比如 runtime 不支持 GetContainerEvents),还会降级到 Generic PLEG。降级逻辑是:

  • 停止自己。
  • 停止已有的 Generic PLEG。
  • 更新 Generic PLEG 的 relist 周期和阈值为 1s, 3min。
  • 启动新的 Generic PLEG。

https://www.myway5.com/wp-content/uploads/2023/02/Snipaste_2023-02-27_16-58-56.png

https://www.myway5.com/wp-content/uploads/2023/02/Snipaste_2023-02-27_16-10-20-1.png

因为 Evented PLEG 和 Generic PLEG 会同时更新缓存,所以在更新时还会对比当前值和缓存值的时间戳,保证当前值是更新的状态,才会更新到缓存中。

参考文章

Traceroute 的实现原理

traceroute 是一个很常用的工具,用来检查当前设备到目的 IP 地址的路径以及每个中间设备产生的延迟。如下图所示:

traceroute to baidu.com (110.242.68.66), 64 hops max, 52 byte packets
 1  10.43.244.2 (10.43.244.2)  4.132 ms  2.294 ms  2.683 ms
 2  10.41.0.217 (10.41.0.217)  3.178 ms  1.846 ms  1.686 ms
 3  10.40.0.54 (10.40.0.54)  2.554 ms  2.033 ms  2.174 ms
 4  10.42.0.69 (10.42.0.69)  4.058 ms  2.905 ms  2.957 ms
 5  10.42.0.54 (10.42.0.54)  3.304 ms  3.058 ms *
 6  10.42.0.20 (10.42.0.20)  3.205 ms  3.199 ms  3.086 ms
 7  14.17.22.130 (14.17.22.130)  4.497 ms  3.424 ms  3.195 ms
 8  10.162.89.97 (10.162.89.97)  10.903 ms  4.797 ms  4.136 ms
 9  10.200.52.57 (10.200.52.57)  5.684 ms
    10.200.52.65 (10.200.52.65)  5.288 ms
    10.200.52.73 (10.200.52.73)  7.327 ms
10  * * *
11  * * *

在 mac/linux/windows 下都有类似的工具。因为在 https://github.com/joyme123/gnt 中实现了 traceroute 的能力,这里记录一下。

traceroute 的实现中,利用了一些基本的网络协议的特性:

  • IP (网络层)数据包在网络中传输时,可以通过 TTL 字段来控制这个数据包的生命周期。每经过一个三层设备的转发,这个 TTL 都会减少1,当 TTL 变为 0 时,三层设备就会丢弃这个数据包而不是继续转发它。这样的设计可以避免网络链路形成环时,数据包会被无限的转发。
  • 中间的三层设备在丢弃数据包时,会使用 ICMP 协议,向数据包的源发送方(通过 src ip)发送 ICMP 包,来告知数据包因为 TTL 为 0 而被丢弃。当然有的三层设备不会发送这个 ICMP 消息,所以 traceroute 时部分中间环节会显示 *。并且这种 ICMP 包的 payload 都会包含源数据包的二层和三层 header。
  • traceroute 支持多种协议: TCP、UDP 和 ICMP。当 TCP, UDP 的包到达目标设备后,如果 TCP, UDP 的目的端口不能访问,那么目标设备也会通过 ICMP 消息向源设备告知该端口不可达。如果是使用 ICMP echo request,那么目标设备会返回 ICMP echo reply 向源设备告知收到了 echo request。

有了以上的网络协议特性,traceroute 的实现可行性就有了。以使用 TCP 协议为例,具体的步骤如下:

  1. traceroute 构造 TCP 数据,源端口根据当前进程 ID 生成,目的端口选择一个不太可能使用的端口,比如 33434。
    1. 源端口根据当前进程 ID 生成,这样收到中间网络设备的 ICMP 包时,就可以通过 payload 中携带的源数据包信息判断出这个 ICMP 是响应哪个进程的。
    2. 目的端口选择一个不常用端口,是防止对目的端的 TCP 服务产生影响。并且这个目的端口每次请求都会加1。
  2. traceroute 构建 IP 头,TTL 一开始设置为 1。这样第一个中间设备收到后,就会丢弃这个包,并返回 ICMP 包了。后续 TTL 逐渐加 1,就可以探测到每一个中间设备了。
  3. 当 traceroute 收到 ICMP 包时,先根据源端口判断这个包属不属于当前进程,再根据目的端口判断这个包是第几个发出的。比如目的端口是 33436,那么已知起始目的端口是 33034 的情况下,就知道这个包是第三个发出的。这样根据第三个包发出的时间,就知道延迟情况了。
    1. 如果这个 ICMP 包是 ttl exceeded,说明中间网络设备返回的。
    2. 如果这个 ICMP 包是 destination unreachable,说明是目的设备返回的。

Ping 与 ICMP 协议

概述

ping 命令是一个非常常用的网络工具,通过 ICMP 协议来探测本地到远端地址之间网络的连通性,以及延迟,稳定等性能指标。但是大多数人其实对 ping 命令的实现了解的并不会太多,因为我们日常的开发工作中,很少会和 ICMP 协议打交道。因为最近在开发 https://github.com/joyme123/gnt,目标是通过单个二进制文件,实现大多数的网络工具的能力。所以接触了一些 ping 命令的实现,这里做一些简单的记录和分享。

ping 命令基于 ICMP 协议的实现

ICMP 协议本身这里不多做介绍,网络上有很多很好很详细的资料。比如维基百科上的这篇介绍:Internet_Control_Message_Protocol

ICMP 和 TCP/UDP 这样的协议有这很大的区别,像 TCP/UDP 这种传输层的协议,都是进程级别的,即可以通过 Port 对应到一个或多个进程。因此在运行使用 TCP/UDP 协议的应用时不需要特殊的权限。而 ICMP 则不一样,它是操作系统级别的,因此早期的 linux 上,如果应用要发送 ICMP 包,则必须通过 socket(AF_INET, SOCK_RAW, int protocol) 这种方式来实现,而调用 SOCK_RAW 则需要 root 权限,或者通过 linux network capability。

因为这种特殊性,后来的 linux 又提供了 socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP) 这种方式去发送 ICMP,这里的 SOCK_DGRAM 是 UDP 协议使用的。不过容易误解的是,并不是说用 UDP 协议去包装或实现了 ICMP 的能力,使用这种方式发送出去的仍然是 ICMP 数据包,不过不再需要 root 权限或者特殊的 network capability 设置了。通常称这种方式为 Unprivileged ICMP。linux 同样也提供了 sysctl 的配置去限制这一能力的使用

# 999~59999 指定了允许的用户组 ID 范围,如果所有用户组都不允许可以设置为 1 0
net.ipv4.ping_group_range = 999 59999

不过需要注意的是,通过 SOCK_DGRAM 只能发送这几种 ICMP 请求:ICMP_ECHO, ICMP_TSTAMP or ICMP_MASKREQ.

解决了如何发送 icmp 包的问题,就可以考虑实现几个主要的 ping 命令特性了。

丢包检测

每个 ICMP 包都有一个 sequence 字段,发送的时候可以指定这个 sequence 的值,目的端响应的时候会把这个 sequence 值设置成一样的,表示响应的是哪一个请求包。这样我们就可以知道每个发送出去的 ICMP 的响应包了,那么没有响应的 sequence 就是被丢弃的包。通过这种方式就可以检测出网络中使用存在丢包现象。

延迟检测

ICMP 支持 echo request 和 reply,即通过 ICMP 协议包装的 payload 发送出去,目的端会原样返回。所以我们可以通过在 request 的时候写入发送时的时间,然后收到回包时取出这个时间就能知道延迟了。

关于写入的时间格式,实现方式上可以随意。但是建议使用 unix time,精确到微秒。然后保留 4 字节的秒+4字节的微秒,写入到 payload 的开头。这样的实现方式和 linux ping 的实现一致,像 wireshark 这种抓包工具就可以识别出来了。