一次网络延迟高的问题排查

问题背景

客户的机房部署了多套不同架构的 k8s 集群:Intel CPU + redhat OS,海光 CPU + 麒麟 OS。其中 Intel CPU + redhat OS 的 k8s 已经稳定运行了很久。海光 CPU + 麒麟 OS 是后来上线的,主要是为了满足信创的要求。但是在实际使用中发现,海光 CPU + 麒麟 OS 的 k8s 集群中,运行的服务在压测时会偶现服务请求 redis 超时的异常。

问题定位

因为同样的服务,在非信创的机器上可以正常运行,因此排除服务本身的问题。主要排查硬件+操作系统+ k8s CNI 这几个环节。这里之所以要排查 CNI 的问题,主要还是考虑这个 CNI 是专门为这个客户开发的,用来实现 Underlay 的网络,以及一系列的特殊网络需求。

整个网络的链路如图所示:

network1

抓包分析

首先是在 Pod 内用 tcpdump 进行长时间的抓包,保存的本地文件。然后进行压测来复现问题。根据日志找到出问题的时间点,用 wireshark 自带的命令对抓包文件按时间切割。

# 每 10s 保存一个分片。
editcap -i 10 tcpdump.cap pod1111

用 wireshark 打开对应时间片段的抓包文件进行分析。这里因为安全要求不方便放上异常包的截图。大概描述一下问题现象:分析的是 tcp 包,表现为异常时间点 redis 回包存在大量重传,且重传的包几乎都在同一时刻到达 Pod 内。

因此下一步要排查 tcp 包的延迟发生链路上的什么位置。因此选择同时在 redis 虚拟机,物理机网卡 eth0,pod 内网卡抓包。然后用同样的方式进行问题复现。

因为有了多个位置点的抓包数据,根据 tcp 的 seq 号就可以分析同一个数据包从 redis 虚拟机发出来的时间,以及到达物理机 eth0 以及 Pod 内的时间。然后根据时间就可以找到延迟点在哪。

分析后发现延迟在 redis→物理机这条链路上。redis 出来的包因为延迟到达了物理机,因此也延迟到达 Pod 内。redis 所在的虚拟机发包后因为一直没有收到回报,因此会触发 tcp 的重传机制。但是重传的包也出现了延迟。最终原始包和重传包在某一刻同时进入了物理机。

因此怀疑是交换机上出现了延迟,但是如果是交换机的问题,那么接入该交换机的其他机器也一定会出问题才对。但是现象仅局限于信创服务器上。所以也和麒麟的供应商沟通了,他们怀疑是一个已知的 cgroup 问题导致的,出现在 4.19 前的内核里。升级内核后果然问题就解决了。

问题分析

上面说的 cgroup 问题,详细来说是 kubelet 中的 cadvisor 在采集 node 的内存信息时,会读取 /sys/fs/cgroup/memory/memory.numa_stat 信息。但是因为内核的实现会导致这个读取信息的系统调用很慢。慢的原因有两点:

  1. cgroup 是通过 cgroup 伪文件系统来管理的,可以通过删除伪文件系统中的文件目录来删除相应的 cgroup。但是内核中代表 cgroup 的结构体会仍然存在,直到所有对它的引用被释放。只有当被删除的 memory cgroup 中的页都被回收掉,相应的引用都被释放,该 memory cgroup 才会被彻底删除。系统中所有的 memory cgroup 数量可以通过 cat /proc/cgroups 来查看。而内存页的回收时间与内核的回收机制有关,如果当中有一些页一直活跃的被使用,就可能永远不会被回收。
  2. cadvisor 读取 /sys/fs/cgroup/memory/memory.numa_stat 信息时,其实是一个系统调用。这个调用的实现也存在性能问题,它会遍历所有的子 cgroup 层级,累加 memory 的使用信息求和,得到总的 memory 使用情况。

因此,在一台一直运行的服务器上,memory cgroup 可能会达到 1w+。cadvisor 在获取 memory cgroup 时可能耗费 1s 以上的时间。在这段时间内,CPU 没法被调度给其他地方使用。

那为什么会导致网络的延迟呢?linux 网络数据包的接收,在之前的文章 linux 网络数据包接收流程(一)中整理过。数据包到达网卡后,依赖硬中断(3)+软中断(6)来触发 CPU 对数据包进行处理。

network2

并且现在的网卡很多都是多队列的,每条队列和某个 CPU 进行绑定,由该 CPU 进行处理。因此如果这个软中断发生在 cadvisor 统计 memory cgroup,进行系统调用时,软中断的处理就可能因此而延迟。如果这个过程持续 1s+,那么引起的现象可能就是对端 tcp 出现重传。如果这个过程持续 2s+,那么因为服务本身读取 redis 的超时时间设置为 2s,就可能出现超时了。

解决方案

长期解决方案就是升级内核。在更高的版本内核中,对 cgroup memory 的计算进行了优化,这里不再会遍历所有的子 memory cgroup 进行统计了。因为本身 cgroup 就已经维护了该信息,直接读取并返回就行了。内核相关修复:https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?h=v6.2-rc7&id=dd8657b6c1cb5e65b13445b4a038736e81cf80ea

短期解决方案是周期执行这条命令:echo 3 > /proc/sys/vm/drop_caches。这会触发内核清理 PageCache, dentries 和 inodes 缓存。这里如果是 echo 1,则只清理 PageCache。echo 2,只清理 dentries 和 inodes 缓存。

runc 的输入输出

概述

runc 是一个基于 OCI 规范实现的程序。在基于 containerd 的容器技术栈上,runc 是一个非常重要但又容易忽略的组件。当执行 docker, nerdctl, ctr 等命令时,其实底层都要调用 runc 去运行容器的进程。不过这篇文章仅会涉及到 runc 的输入输出。

因为在日常的开发中,大多数开发者接触到的都是 docker,因此下面会以 docker 为例,结合底层的 runc 来说明容器进程的输入输出。

标准输入输出

在 linux 上,所有的进程都会打开三个文件描述符:

  • 0: stdin 标准输入
  • 1: stdout 标准输出
  • 2: stderr 标准错误输出

在容器技术上,同样离不开这三个基本概念。在使用 docker run/exec 时,其实就是在 linux cgroup 和 namespace 下运行了一个普通的进程而已。所以这样的进程同样会有输入输出,当我们使用 docker run nginx 的时候,nginx 的日志就会打印到终端上,其实就是将 nginx 进程的标准输入输出通过各种手段输出到终端上了。

-i 参数

docker 提供了 -i 参数,来运行交互式的命令,文档上的说明是:

-i,  --interactive                    Keep STDIN open even if not attached

也就是说即使没有使用 docker attach,也会保持 STDIN 打开。这样我们在终端的 STDIN 就会定向到容器内 sh 进程的 STDIN,这样就可以在终端上输入命令行了。

比如 docker run -i nginx sh。这里就是使用 nginx 镜像启动容器,并运行了 sh 命令,然后终端的 bash 的 STDIN 定向到容器内的 sh。如下图所示:

https://www.myway5.com/wp-content/uploads/2023/03/Snipaste_2023-03-08_13-01-18.png

不过我们也可以发现,这里的输出格式似乎和在终端里 ls 的输出不同。这里就要引入一个新的知识点:tty 与 pty 以及我们的 -t 参数。

-t 参数

官方文档上的说明是:

-t, --tty                            Allocate a pseudo-TTY

tty 得名于早期的电传打字机(Teletype),随着技术的发展,tty 的概念变得更宽泛,它不再指打字机这样的物理设备,很多时候指的是 linux 内核中的 tty 驱动。当我们使用无 GUI 的 linux 时,比如 server 版本的 ubuntu,我们使用 ctrl+alt+f1~f6就可以在 tty1~6 之间进行切换。这里的 tty 就是由 linux 内核模拟出来的。

https://www.myway5.com/wp-content/uploads/2023/03/Snipaste_2023-03-08_13-18-38.png

不过在 GUI 场景中,我们打开的终端并不是使用 tty,而是 pseudo-TTY(下文称之为 pty)。pty 也称为伪终端,也就是说它并不是真实的 tty 设备。引入了 pty 之后,终端的数量就不会再有限制了。我们可以打开任意多的 GUI 终端程序,每打开一个 GUI 终端,就会在 /dev/pts/ 下生成一个数字的文件,这个文件就是 pty slave,另外全局还有共用的一个 pty master,位于 /dev/ptmx。

  1. 通过 GUI 终端,我们可以输入字符,输入的字符会发到 pty master 上,pty master 文件描述符,发送到对应的 pty slave。
  2. pty slave 将输入发送给终端上的 shell 程序,比如 bash。
  3. bash 上执行的任何命令,都会将输出发送给 bash,bash 再发送给 pty slave,之后再经过 tty driver, pty master 到 gui 终端上显示。

pty 的工作原理如下图:

https://www.myway5.com/wp-content/uploads/2023/03/Snipaste_2023-03-08_13-28-22.png

这时候我们在回到 -t 参数,这里其实就是为容器内的 sh 进程分配了一个 pty slave。当 sh 感知到自己被分配了 pty slave 后,它会采用不同的工作模式。比如会在终端上输出提示符,对输出的格式进行调整,像 bash 之类的还是加上颜色等等。

Untitled

# 下面这段程序会输出很多行,也说明了在 sh 里执行 ls 时,原本的输出是很多行的。
root@5f96c3475e20:/# ls | while read line; do echo $line;done
bin
boot
dev
docker-entrypoint.d
docker-entrypoint.sh
etc
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var

回到 runc

上述提到 stdio,pty 等概念,很好的解决了运行容器时的输入输出的处理。而这些能力也是 runc 本身就提供的,docker 是在其之上进行了封装。

runc 的输入输出处理有四种模式,由 -d(detach),-t 进行组合得到。

terminal && detached

https://www.myway5.com/wp-content/uploads/2023/03/Snipaste_2023-03-08_13-44-19.png

  1. 上层的管控程序,比如 runc-shim 会创建 unix socket
  2. runc 运行在容器内,接管容器的 stdio。然后通过 unix socket 将 pty master 和 fd 发送给上层管控程序。
  3. 将自己的 stdio 通过 pty slave 输入输出。

passthrough && detached

https://www.myway5.com/wp-content/uploads/2023/03/Snipaste_2023-03-08_14-02-40.png

passthrough && foreground

https://www.myway5.com/wp-content/uploads/2023/03/Snipaste_2023-03-08_14-03-28.png

terminal && foreground

https://www.myway5.com/wp-content/uploads/2023/03/Snipaste_2023-03-08_14-03-22.png

参考资料

http://www.wowotech.net/tty_framework/tty_concept.html

http://www.wowotech.net/tty_framework/tty_architecture.html

http://www.wowotech.net/tty_framework/application_view.html

https://blog.51cto.com/u_14592069/5824829

https://dev.to/napicella/linux-terminals-tty-pty-and-shell-192e

https://github.com/opencontainers/runc/blob/main/docs/terminals.md

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 这种抓包工具就可以识别出来了。

OpenFlow 的流表匹配规则

OpenFlow 里有一个重要的概念—流表(FlowTable),通过 Flow Table,我们可以制定交换机处理流量的行为。因此,理解流表匹配规则,是理解 OpenFlow 的重要一环。

OpenvSwitch(下文称为OVS) 是一个开源的软件交换机的实现,同时也是支持 OpenFlow 的,因此下文也会通过 OVS 来说明流表是如何匹配的。

流表匹配规则

流表项

在一个 OpenFlow 的网络中,每一个支持 OpenFlow 的交换机都必须包含至少 1 个流表(table 0),这个流表里会包含 0 或多个流表项。这些流表项描述了流量的匹配规则,计数器以及如果针对这些流量做出动作。

一个流表项通常由以下元素组成

字段 描述
Match Fileds 用来匹配数据包
Priority 匹配流表项的优先级。如果一个数据包被多个流表项匹配到,则会根据优先级进行选择
Counters 当数据包被匹配成功时会更新该字段,主要用来流量统计
Instructions 用来修改 Actions 或 流水线处理
Timeouts 流表项的超时时间
Cookie 一些不透明的数据值。控制器可以使用它来过滤流量统计,流量修改和流量删除。在处理数据包时不使用这些数据值

流水线处理

流表在处理时,就像流水线一下,每个 table 都是一个处理阶段。在每一个 table 里,数据包都可能被:

  • 丢弃。
  • 转发给下一个 table
  • 发送给 controller

https://www.myway5.com/wp-content/uploads/2023/01/openflow-flow.webp

如上图所示,数据包通过 port 进入交换机后,首先会匹配 table 0 中的流表项。如果流表项匹配到了,则会根据该流表项设定的 actions 进行操作。如果未匹配到,则会被丢弃。

数据包在不同的 table 中流转时,只能按照升序进行,也就是说可以从 table 0 → table10,但是不能从 table10 → table0。并且需要注意的是,在流表之间跳转需要使用 goto_table 或 resubmit 语句来将数据包 copy 一份转发到其他的流表中处理。如果没有指定跳转动作,是不会继续在其他 table 中进行匹配的。

https://www.myway5.com/wp-content/uploads/2023/01/openflow-flow-det.webp

OVS 操作演示

为了加深对 OpenFlow 的理解,可以使用 OVS 提供的一个例子进行练习。这个例子中,使用 OVS 的流表实现了交换机二层 Mac 地址的学习,以及 VLAN 的支持。

环境准备

和 ovs 官网不同的是,这里我使用 mininet 快速创建一个实验环境。

sudo mn --topo=single,4 --mac --controller=none

这里创建了一个名为 s1 的交换机和 4 台连接到交换机上的主机 h1, h2, h3, h4,通过 port1, port2, port3, port4 连接。port1 trunk 所有的 vlan,port2 access vlan 20, port3, port3 access vlan 30。

https://www.myway5.com/wp-content/uploads/2023/01/ovs.drawio-1.png

table 0: 准入控制

table 0 是数据包进入交换机的起始位置。我们在这一步去禁止某些数据包的进入。比如:以多播源地址的数据包是非法的,所以我们可以在这里丢弃掉。

sh ovs-ofctl add-flow s1 "table=0, dl_src=01:00:00:00:00:00/01:00:00:00:00:00, actions=drop"

交换机也不应该转发 IEEE 802.1D Spanning Tree Protocol(STP) 数据包,所以我们在可以通过流表项来丢弃掉。

sh ovs-ofctl add-flow s1 "table=0, dl_dst=01:80:c2:00:00:00/ff:ff:ff:ff:ff:f0, actions=drop"

对于其他合法的数据包,我们都提交到流水线的下一阶段(table1) 中去处理。这里我们使用最低的优先级来做兜底

sh ovs-ofctl add-flow s1 "table=0, priority=0, actions=resubmit(,1)"

测试 table 0

这里因为有一些数据包不方便构造去做实际的测试,所以使用 ofproto/trace 去模拟数据库的匹配。

例子1

sh ovs-appctl ofproto/trace s1 in_port=1,dl_dst=01:80:c2:00:00:05

会出现以下输出

Flow: in_port=1,vlan_tci=0x0000,dl_src=00:00:00:00:00:00,dl_dst=01:80:c2:00:00:05,dl_type=0x0000

bridge("s1")
------------
 0. dl_dst=01:80:c2:00:00:00/ff:ff:ff:ff:ff:f0, priority 32768
    drop

Final flow: unchanged
Megaflow: recirc_id=0,eth,in_port=1,dl_src=00:00:00:00:00:00/01:00:00:00:00:00,dl_dst=01:80:c2:00:00:00/ff:ff:ff:ff:ff:f0,dl_type=0x0000
Datapath actions: drop

可以看到以下关键信息:

  1. 数据包匹配到了 dl_dst=01:80:c2:00:00:00/ff:ff:ff:ff:ff:f0, priority 32768 ,根据 action 需要被 drop 掉
  2. Final flow 为 unchanged ,说明数据包本身没有被修改。

例子2

sh ovs-appctl ofproto/trace s1 in_port=1,dl_dst=01:80:c2:00:00:10

出现以下输出

Flow: in_port=1,vlan_tci=0x0000,dl_src=00:00:00:00:00:00,dl_dst=01:80:c2:00:00:10,dl_type=0x0000

bridge("s1")
------------
 0. priority 0
    resubmit(,1)
 1. No match.
    drop

Final flow: unchanged
Megaflow: recirc_id=0,eth,in_port=1,dl_src=00:00:00:00:00:00/01:00:00:00:00:00,dl_dst=01:80:c2:00:00:10/ff:ff:ff:ff:ff:f0,dl_type=0x0000
Datapath actions: drop

可以看出数据包被 resubmit 到 table 1 了,但是因为 table 1 中没有任何流表项,所以被 drop 了。

table 1: VLAN 进入数据包处理

进入 table 1 的数据包,都是在 table 0 中被校验有效的数据包。table 1 被设计用来校验数据包的 VLAN,这个校验是基于数据包进入交换机经过的 Port 配置。同时我们也会在这些进入 access port 的数据包 header 里添加 VLAN tag,来保证后续的处理都可以基于这些 VLAN tag 进行。

首先我们添加一个默认 drop 的规则

sh ovs-ofctl add-flow s1 "table=1, priority=0, actions=drop"

对于 trunk port 1,可以接收任意数据包,无论数据包是否有 VLAN header 或者这个 VLAN tag 是多少。所以可以添加一个流表项,将进入 port1 的所有数据包 resubmit 到 table 2 中。

sh ovs-ofctl add-flow s1 "table=1, priority=99, in_port=1, actions=resubmit(,2)"

在 access port 上,只接收没有 VLAN header 的数据包,然后对数据包打上 VLAN tag,然后再提交到下一阶段(table2) 去处理。

sh ovs-ofctl add-flow s1 "table=1, priority=99, in_port=2, vlan_tci=0, actions=mod_vlan_vid:20, resubmit(,2)"
sh ovs-ofctl add-flow s1 "table=1, priority=99, in_port=3, vlan_tci=0, actions=mod_vlan_vid:30, resubmit(,2)"
sh ovs-ofctl add-flow s1 "table=1, priority=99, in_port=4, vlan_tci=0, actions=mod_vlan_vid:30, resubmit(,2)"

测试 table 1

例子1:数据包进入 trunk port

数据包进入 trunk port 1

sh ovs-appctl ofproto/trace s1 in_port=1,vlan_tci=5

得到以下输出,数据包在 table 0 中 resubmit 到 table 1,再到 table 2 后没有规则,被默认丢弃

Flow: in_port=1,vlan_tci=0x0005,vlan_tci1=0x0000,dl_src=00:00:00:00:00:00,dl_dst=00:00:00:00:00:00,dl_type=0x0000

bridge("s1")
------------
 0. priority 0
    resubmit(,1)
 1. in_port=1, priority 99
    resubmit(,2)
 2. No match.
    drop

Final flow: unchanged
Megaflow: recirc_id=0,eth,in_port=1,dl_src=00:00:00:00:00:00/01:00:00:00:00:00,dl_dst=00:00:00:00:00:00/ff:ff:ff:ff:ff:f0,dl_type=0x0000
Datapath actions: drop

例子2:有效的数据包进入 access port

一个没有 802.1Q header 的数据包进入 port 2

sh ovs-appctl ofproto/trace s1 in_port=2

得到以下输出,数据包在 table 0 中 resubmit 到 table 1,再到 table 2 后没有规则,被默认丢弃i

Flow: in_port=2,vlan_tci=0x0000,dl_src=00:00:00:00:00:00,dl_dst=00:00:00:00:00:00,dl_type=0x0000

bridge("s1")
------------
 0. priority 0
    resubmit(,1)
 1. in_port=2,vlan_tci=0x0000, priority 99
    mod_vlan_vid:20
    resubmit(,2)
 2. No match.
    drop

Final flow: in_port=2,dl_vlan=20,dl_vlan_pcp=0,vlan_tci1=0x0000,dl_src=00:00:00:00:00:00,dl_dst=00:00:00:00:00:00,dl_type=0x0000
Megaflow: recirc_id=0,eth,in_port=2,dl_src=00:00:00:00:00:00/01:00:00:00:00:00,dl_dst=00:00:00:00:00:00/ff:ff:ff:ff:ff:f0,dl_type=0x0000
Datapath actions: drop

例子3: 无效的数据包进入 access port

一个有 802.1Q header 的数据包进入 port 2

sh ovs-appctl ofproto/trace s1 in_port=2,vlan_tci=5

得到以下输出,在 table 1 中被 drop 了。

Flow: in_port=2,vlan_tci=0x0005,vlan_tci1=0x0000,dl_src=00:00:00:00:00:00,dl_dst=00:00:00:00:00:00,dl_type=0x0000

bridge("s1")
------------
 0. priority 0
    resubmit(,1)
 1. priority 0
    drop

Final flow: unchanged
Megaflow: recirc_id=0,eth,in_port=2,vlan_tci=0x0005,dl_src=00:00:00:00:00:00/01:00:00:00:00:00,dl_dst=00:00:00:00:00:00/ff:ff:ff:ff:ff:f0,dl_type=0x0000
Datapath actions: drop

table 2: 进入 port 后学习 MAC+VLAN

table 2允许我们实现的交换机学习数据包的 source mac。只需要一个流表项

sh ovs-ofctl add-flow s1 "table=2 actions=learn(table=10, NXM_OF_VLAN_TCI[0..11],NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[], load:NXM_OF_IN_PORT[]->NXM_NX_REG0[0..15]),resubmit(,3)"

learn 这个 action 会基于正在处理的数据包,动态的修改流表。对于 learn 的几个字段说明如下:

table=10

    Modify flow table 10.  This will be the MAC learning table.

NXM_OF_VLAN_TCI[0..11]

    Make the flow that we add to flow table 10 match the same VLAN
    ID that the packet we're currently processing contains.  This
    effectively scopes the MAC learning entry to a single VLAN,
    which is the ordinary behavior for a VLAN-aware switch.

NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[]

    Make the flow that we add to flow table 10 match, as Ethernet
    destination, the Ethernet source address of the packet we're
    currently processing.

load:NXM_OF_IN_PORT[]->NXM_NX_REG0[0..15]

    Whereas the preceding parts specify fields for the new flow to
    match, this specifies an action for the flow to take when it
    matches.  The action is for the flow to load the ingress port
    number of the current packet into register 0 (a special field
    that is an Open vSwitch extension to OpenFlow).

测试 table 2

例子1

sh ovs-appctl ofproto/trace s1 in_port=1,vlan_tci=20,dl_src=50:00:00:00:00:01 -generate

得到以下输出。上面的命令中使用了 -generate,是为了让数据包真实的在 OVS 中生效,不指定的话,OVS 不会真实的生成 table 10 中的流表项。

Flow: in_port=1,vlan_tci=0x0014,vlan_tci1=0x0000,dl_src=50:00:00:00:00:01,dl_dst=00:00:00:00:00:00,dl_type=0x0000

bridge("s1")
------------
 0. priority 0
    resubmit(,1)
 1. in_port=1, priority 99
    resubmit(,2)
 2. priority 32768
    learn(table=10,NXM_OF_VLAN_TCI[0..11],NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],load:NXM_OF_IN_PORT[]->NXM_NX_REG0[0..15])
     -> table=10 vlan_tci=0x0014/0x0fff,dl_dst=50:00:00:00:00:01 priority=32768 actions=load:0x1->NXM_NX_REG0[0..15]
    resubmit(,3)
 3. No match.
    drop

Final flow: unchanged
Megaflow: recirc_id=0,eth,in_port=1,vlan_tci=0x0014/0x1fff,dl_src=50:00:00:00:00:01,dl_dst=00:00:00:00:00:00/ff:ff:ff:ff:ff:f0,dl_type=0x0000
Datapath actions: drop

在 table 10 中确认是否有新的流表项生成

sh ovs-ofctl dump-flows s1 table=10

cookie=0x0, duration=142.170s, table=10, n_packets=0, n_bytes=0, vlan_tci=0x0014/0x0fff,dl_dst=50:00:00:00:00:01 actions=load:0x1->NXM_NX_REG0[0..15]

例子2

sh ovs-appctl ofproto/trace s1 in_port=2,dl_src=50:00:00:00:00:01 -generate

cookie=0x0, duration=193.493s, table=10, n_packets=0, n_bytes=0, vlan_tci=0x0014/0x0fff,dl_dst=50:00:00:00:00:01 actions=load:0x2->NXM_NX_REG0[0..15]

可以看到,在例子 1 中 table 10 中,在 port 1 上学习到了 mac 地址 50:00:00:00:00:01,现在 port 2 中也出现了该 mac 地址,所以这个 mac 地址更新到了 port 2 上。

table 3: 查找目标 Port

这个 table 实现了如何通过 MAC 和 VLAN 查找到目标的 output port。通过以下流表项来实现查找

sh ovs-ofctl add-flow s1 "table=3 priority=50 actions=resubmit(,10), resubmit(,4)"

这个流表项首先将数据包提交到 table 10 中。table 10 中存储了学习到的 mac 地址。如果这个 mac 地址没有被学习过,则 table 10 中不会被匹配。那么就会被 resubmit 到 table 4 中继续处理。

测试 table 3

下面的命令会让 OVS 学习到 port 1上 VLAN 20 的 mac 地址 f0:00:00:00:00:01

sh ovs-appctl ofproto/trace s1 in_port=1,dl_vlan=20,dl_src=f0:00:00:00:00:01,dl_dst=90:00:00:00:00:01 -generate

得到以下输出,数据包在 table 10 中没有匹配到,所以被 resubmit 到 table 4.

Flow: in_port=1,dl_vlan=20,dl_vlan_pcp=0,vlan_tci1=0x0000,dl_src=f0:00:00:00:00:01,dl_dst=90:00:00:00:00:01,dl_type=0x0000

bridge("s1")
------------
 0. priority 0
    resubmit(,1)
 1. in_port=1, priority 99
    resubmit(,2)
 2. priority 32768
    learn(table=10,NXM_OF_VLAN_TCI[0..11],NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],load:NXM_OF_IN_PORT[]->NXM_NX_REG0[0..15])
     -> table=10 vlan_tci=0x0014/0x0fff,dl_dst=f0:00:00:00:00:01 priority=32768 actions=load:0x1->NXM_NX_REG0[0..15]
    resubmit(,3)
 3. priority 50
    resubmit(,10)
    10. No match.
            drop
    resubmit(,4)
 4. No match.
    drop

Final flow: unchanged
Megaflow: recirc_id=0,eth,in_port=1,dl_vlan=20,dl_src=f0:00:00:00:00:01,dl_dst=90:00:00:00:00:01,dl_type=0x0000
Datapath actions: drop

可以通过以下两种方式验证 port 1 上学习到的 mac 地址:

方法一:

sh ovs-ofctl dump-flows s1 table=10

cookie=0x0, duration=107.451s, table=10, n_packets=0, n_bytes=0, vlan_tci=0x0014/0x0fff,dl_dst=f0:00:00:00:00:01 actions=load:0x1->NXM_NX_REG0[0..15]

方法二:

sh ovs-appctl ofproto/trace s1 in_port=2,dl_src=90:00:00:00:00:01,dl_dst=f0:00:00:00:00:01 -generate

Flow: in_port=2,vlan_tci=0x0000,dl_src=90:00:00:00:00:01,dl_dst=f0:00:00:00:00:01,dl_type=0x0000

bridge("s1")
------------
 0. priority 0
    resubmit(,1)
 1. in_port=2,vlan_tci=0x0000, priority 99
    mod_vlan_vid:20
    resubmit(,2)
 2. priority 32768
    learn(table=10,NXM_OF_VLAN_TCI[0..11],NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],load:NXM_OF_IN_PORT[]->NXM_NX_REG0[0..15])
     -> table=10 vlan_tci=0x0014/0x0fff,dl_dst=90:00:00:00:00:01 priority=32768 actions=load:0x2->NXM_NX_REG0[0..15]
    resubmit(,3)
 3. priority 50
    resubmit(,10)
    10. vlan_tci=0x0014/0x0fff,dl_dst=f0:00:00:00:00:01, priority 32768
            load:0x1->NXM_NX_REG0[0..15]
    resubmit(,4)
 4. No match.
    drop

Final flow: reg0=0x1,in_port=2,dl_vlan=20,dl_vlan_pcp=0,vlan_tci1=0x0000,dl_src=90:00:00:00:00:01,dl_dst=f0:00:00:00:00:01,dl_type=0x0000
Megaflow: recirc_id=0,eth,in_port=2,dl_src=90:00:00:00:00:01,dl_dst=f0:00:00:00:00:01,dl_type=0x0000
Datapath actions: drop

可以看到在 table 10 中匹配到了流表项,学习到了 load:0x1->NXM_NX_REG0[0..15]

table 4: 数据包输出处理

在 table 4 中,我们知道 register 0 包含了需要的 output port,如果该 output port 是 0,则说明需要将数据包 flood。我们也知道数据包的 VLAN 在它的 802.1Q header 上。

sh ovs-ofctl add-flow s1 "table=4 reg0=1 actions=1"

对于要 output 的 port,还需要把 VLAN header 移除掉。

sh ovs-ofctl add-flow s1 "table=4 reg0=2 actions=strip_vlan,2"
sh ovs-ofctl add-flow s1 "table=4 reg0=3 actions=strip_vlan,3"
sh ovs-ofctl add-flow s1 "table=4 reg0=4 actions=strip_vlan,4"

flood 广播或多播包

sh ovs-ofctl add-flow s1 "table=4 reg0=0 priority=99 dl_vlan=20 actions=1,strip_vlan,2"
sh ovs-ofctl add-flow s1 "table=4 reg0=0 priority=99 dl_vlan=30 actions=1,strip_vlan,3,4"
sh ovs-ofctl add-flow s1 "table=4 reg0=0 priority=50            actions=1"

测试 table 4

例子1:广播,多播以及未知目的地址

测试在 port 1 上进入广播包,VLAN 是 30

sh ovs-appctl ofproto/trace s1 in_port=1,dl_dst=ff:ff:ff:ff:ff:ff,dl_vlan=30

Flow: in_port=1,dl_vlan=30,dl_vlan_pcp=0,vlan_tci1=0x0000,dl_src=00:00:00:00:00:00,dl_dst=ff:ff:ff:ff:ff:ff,dl_type=0x0000

bridge("s1")
------------
 0. priority 0
    resubmit(,1)
 1. in_port=1, priority 99
    resubmit(,2)
 2. priority 32768
    learn(table=10,NXM_OF_VLAN_TCI[0..11],NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],load:NXM_OF_IN_PORT[]->NXM_NX_REG0[0..15])
     >> suppressing side effects, so learn action ignored
    resubmit(,3)
 3. priority 50
    resubmit(,10)
    10. No match.
            drop
    resubmit(,4)
 4. reg0=0,dl_vlan=30, priority 99
    output:1
     >> skipping output to input port
    strip_vlan
    output:3
    output:4

Final flow: in_port=1,vlan_tci=0x0000,dl_src=00:00:00:00:00:00,dl_dst=ff:ff:ff:ff:ff:ff,dl_type=0x0000
Megaflow: recirc_id=0,eth,in_port=1,dl_vlan=30,dl_vlan_pcp=0,dl_src=00:00:00:00:00:00,dl_dst=ff:ff:ff:ff:ff:ff,dl_type=0x0000
Datapath actions: pop_vlan,4,3

可以看到 Datapath actions: pop_vlan,4,3,最终数据包被移除 vlan,从 port3,4出去了。

而下面的广播包都会被 drop,因为 VLAN 必须属于 input port

sh ovs-appctl ofproto/trace s1 in_port=1,dl_dst=ff:ff:ff:ff:ff:ff
sh ovs-appctl ofproto/trace s1 in_port=1,dl_dst=ff:ff:ff:ff:ff:ff,dl_vlan=55

例子2: MAC 学习

VLAN 30, port 1 学习 MAC 地址:

sh ovs-appctl ofproto/trace s1 in_port=1,dl_vlan=30,dl_src=10:00:00:00:00:01,dl_dst=20:00:00:00:00:01 -generate

Flow: in_port=1,dl_vlan=30,dl_vlan_pcp=0,vlan_tci1=0x0000,dl_src=10:00:00:00:00:01,dl_dst=20:00:00:00:00:01,dl_type=0x0000

bridge("s1")
------------
 0. priority 0
    resubmit(,1)
 1. in_port=1, priority 99
    resubmit(,2)
 2. priority 32768
    learn(table=10,NXM_OF_VLAN_TCI[0..11],NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],load:NXM_OF_IN_PORT[]->NXM_NX_REG0[0..15])
     -> table=10 vlan_tci=0x001e/0x0fff,dl_dst=10:00:00:00:00:01 priority=32768 actions=load:0x1->NXM_NX_REG0[0..15]
    resubmit(,3)
 3. priority 50
    resubmit(,10)
    10. No match.
            drop
    resubmit(,4)
 4. reg0=0,dl_vlan=30, priority 99
    output:1
     >> skipping output to input port
    strip_vlan
    output:3
    output:4

Final flow: in_port=1,vlan_tci=0x0000,dl_src=10:00:00:00:00:01,dl_dst=20:00:00:00:00:01,dl_type=0x0000
Megaflow: recirc_id=0,eth,in_port=1,dl_vlan=30,dl_vlan_pcp=0,dl_src=10:00:00:00:00:01,dl_dst=20:00:00:00:00:01,dl_type=0x0000
Datapath actions: pop_vlan,4,3

因为目的地址是未知的,所以数据包被 flood 到 port3,port4 上。然后我们再次测试 MAC 地址是否学习到了。

sh ovs-appctl ofproto/trace s1 in_port=4,dl_src=20:00:00:00:00:01,dl_dst=10:00:00:00:00:01 -generate

Flow: in_port=4,vlan_tci=0x0000,dl_src=20:00:00:00:00:01,dl_dst=10:00:00:00:00:01,dl_type=0x0000

bridge("s1")
------------
 0. priority 0
    resubmit(,1)
 1. in_port=4,vlan_tci=0x0000, priority 99
    mod_vlan_vid:30
    resubmit(,2)
 2. priority 32768
    learn(table=10,NXM_OF_VLAN_TCI[0..11],NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],load:NXM_OF_IN_PORT[]->NXM_NX_REG0[0..15])
     -> table=10 vlan_tci=0x001e/0x0fff,dl_dst=20:00:00:00:00:01 priority=32768 actions=load:0x4->NXM_NX_REG0[0..15]
    resubmit(,3)
 3. priority 50
    resubmit(,10)
    10. vlan_tci=0x001e/0x0fff,dl_dst=10:00:00:00:00:01, priority 32768
            load:0x1->NXM_NX_REG0[0..15]
    resubmit(,4)
 4. reg0=0x1, priority 32768
    output:1

Final flow: reg0=0x1,in_port=4,dl_vlan=30,dl_vlan_pcp=0,vlan_tci1=0x0000,dl_src=20:00:00:00:00:01,dl_dst=10:00:00:00:00:01,dl_type=0x0000
Megaflow: recirc_id=0,eth,in_port=4,dl_src=20:00:00:00:00:01,dl_dst=10:00:00:00:00:01,dl_type=0x0000
Datapath actions: push_vlan(vid=30,pcp=0),2

linux 网络问题排查手册

这里主要记录一些在实际排查网络问题过程中,觉得非常好用的工具或方法。大多数和 k8s 的网络问题相关

1. iptables 处理规则流程图

出处:https://www.zsythink.net/archives/1199

https://www.myway5.com/wp-content/uploads/2022/12/screenshot-20221210-113850.png

2. 使用 xtables-monitor 追踪数据包在 iptables 规则中的流向

这种场景常用在宿主机上有比较复杂的 iptables 规则,导致在排查问题时比较难发现。比如 k8s 中 pod 访问 svc ip 不通时。

首先在数据包必经的地方添加 TRACE,比如 raw 表的 PREROUTING 就是个好地方。

iptables -t raw -A PREROUTING -p tcp --dport 5432 -j TRACE

添加 TRACE 之后,就可以追踪该数据包后续的流向了。通过 trace 的信息,可以看到数据库的流入流出网卡,mark 值,匹配的 iptables 规则等等。非常容易帮我们判断出问题

xtables-monitor --trace

3. 使用 conntrack 查看连接的生命周期

conntrack 是 linux 网络协议栈提供的能力,顾名思义,conntrack 提供的是连接追踪的能力。这里的连接并不是 tcp 的连接,而且传输层的 5 元组(源IP,源端口,目的IP,目的端口,协议)来唯一确定的连接。像 NAT 之类的功能,都是基于 conntrack 之上的。

# 比如这条命令就可以看 10.233.101.170 这个 pod 访问 svc ip 建立的连接请。
conntrack -p tcp -s 10.233.101.170 -L -o ktimestamp

4. no route to host 怎么解决

这个问题相信大多数人都遇到过,之所以要单独写在这里,是因为遇到过一些小众场景非常难排查。

大多数情况下,这个问题可以通过检查下面几项:

  1. 检查机器上的路由表,是否有通往目的 IP 的路由。
  2. 确认网关配置是否正确,网关路由是否正常。
  3. 抓包检查,这里我们可以选择抓 ARP 包。如:tcpdump -i any arp and host xx.xxx.xx.xx -ne。这种方式看似没什么用,但是非常好用,特别是在网络配置非常复杂(多网卡,多网络平面的 k8s 集群)时。有的时候很难准确判断出出去的数据包命中了什么样的路由。这种情况下,我们可以通过 arp 请求的网关 IP,来判断出路由是哪条,且网关是否正常连通。

5. 路由判断理解是否正确

通过第一点中提供的图,我们可以发现,路由判断只在两个地方发生:

  1. PREROUTING 之后
  2. OUTPUT 之前

也就是说,当数据包通过了这两个地方之后,路由就已经确定了。在这之后做任何数据包的修改都不会再影响路由判断。

实际场景:使用 ip rule 可以创建一些策略路由,比如来自于 10.100.0.0/24 的数据包,mark 值为 0x06 时走什么路由策略。当在 POSTROUTING 链上做 SNAT,set mark 的时候,是不会影响路由的。

6. iptables 与 LVS

https://www.myway5.com/wp-content/uploads/2022/12/nf-lvs.png

正常情况下,LVS nat 模式的流量不能做 SNAT,因为不会经过 POSTROUTING 链。需要增加下的配置来让 LVS 数据包经过 POSTROUTING

net.ipv4.vs.conntrack=1

iptables 在 PREROUTING 阶段做的 mark,在 LVS NAT 后仍然有效。

7. 容器网络不通解决思路

这里的容器网络仅指 docker/nerdctl 部署的容器网络,k8s 情况会更复杂,也要结合 CNI 去看,这里不讨论。

实际使用中,常见的使用方式是部署容器时使用端口(以 443 为例)映射,将宿主机的 port 映射到容器的 port。一般的底层实现都是使用 iptables DNAT,将发到 443 端口的流量 DNAT 到容器的 ip:port。这样再根据本机的路由,将流量通过 docker0/nerdctl1 这样的网桥进行转发即可。

根据以上的背景,一般我们可以检查下面几个地方:

  1. 宿主机的 ip_forward 是否打开:sysctl -a | grep ip_forward。需要确认值为 1,这时候宿主机才会将不属于自己的流量进行 FORWARD。
  2. 宿主机的 bridge-nf-call-iptables 是否打开:sysctl -a | grep bridge。需要确认值为 1,这时候 bridge 设备上的流量就会经过 iptables conntrack。
  3. 检查 DNAT 规则。docker/nerdctl 一般都会在 iptables NAT 表上写这些规则。iptables -t nat -L PREROUTING
  4. 检查 FILTER 表。实际使用中遇到过在宿主机上同时使用 docker 和 nerdctl 的用户,docker 默认将 FILTER 表 FORWARD 链改为 DROP Policy,也就是不匹配 FORWARD 下规则的流量都会丢弃。而 nerdctl 则是默认 ACCEPT。这样 nerdctl 启动的容器网络就会不通。

通过 metrics-server 获取的 NodeMetrics 为何会不准确

背景描述

在使用 metrics-server 的 NodeMetrics 获取 node 的 CPU 使用量时,会稍微大于 node 的 CPU 核心数,导致计算剩余可用的 CPU 时出现了负数。此时 node 的 cpu 是 100% 满载,但是理论上无论如何也不会超过 CPU 总量。

问题分析

通过 metrics-server 获取 NodeMetrics 的链路如下:metrics-server → kubelet 10250 端口→ cadvisor。所以问题的本质还是在于 cadvisor 如何统计 CPU 使用量。

通过分析 cadvisor 的代码可以知道,cadvisor 会读取 cgroup 根目录的 cpuacct.usage 中的值,来获取当前累积使用的 CPU 时间。如下图代码所示。

cadvisor1

cpuacct.usage 的描述如下:

  • reports the total CPU time (in nanoseconds) consumed by all tasks in this cgroup (including tasks lower in the hierarchy).

cadvisor 会定期(每秒)对 node 进行采样,保存到 CpuUsage 中。

cadvisor2

当 metrics-server 需要获取当前的 CPU 使用量时,cadvisor 会统计最近 60s 内(即60个采样数据)的累积 CPU 使用时间,并除以总采样时间(ns),得到 CPU 使用量。

cadvisor3

因为 CPU 使用量的时间单位是精确到纳秒(ns)的,因此难免在计算上会有一定的误差,所以当 CPU 满载时出现统计出来的数据会稍大于 CPU 核心数也是正常情况了。

IPv6 学习笔记

1. 概述

IPv6 全称是 Internet Protocol Version 6,不过虽然是叫 Version 6,事实上是网络层协议的第二代标准协议。其出现主要是为了解决 IPv4 在实际应用场景中存在的一些缺陷。

与 IPv4 的优缺点对比

摘抄自:华为 IPv6 技术白皮书

问题 IPv4缺陷 IPv6优势
地址空间 IPv4 地址只有32位,因此总共可表示的地址在 43 亿左右。另外由于历史原因,IP 地址的分配也非常不均衡:美国占全球地址 空间的一半左右,而欧洲则相对匮乏;亚太地区则更加匮乏。IPv4 中用来解决地址短缺的方法有:CIDR 和 NAT。不过这两种方案都有其本身的缺点。 IPv6 有 128 位。理论上总共可以支持 43亿x43亿x43亿x43亿的地址。
报文格式 IPv4报头包含可选字段Options,内 容涉及Security、Timestamp、 Record route等,这些Options可以将 IPv4报头长度从20字节扩充到60字 节。携带这些Options的IPv4报文在 转发过程中往往需要中间路由转发 设备进行软件处理,对于性能是个 很大的消耗,因此实际中也很少使 用。 IPv6和IPv4相比,去除了IHL、 Identifier、Flag、Fragment Offset、Header Checksum、 Option、Paddiing域,只增加了流 标签域,因此IPv6报文头的处理 较IPv4更为简化,提高了处理效 率。另外,IPv6为了更好支持各 种选项处理,提出了扩展头的概 念,新增选项时不必修改现有结 构,理论上可以无限扩展,体现 了优异的灵活性。
自动配置和重新编制 由于IPv4地址只有32比特,并且地 址分配不均衡,导致在网络扩容或 重新部署时,经常需要重新分配IP 地址,因此需要能够进行自动配置 和重新编址,以减少维护工作量。 目前IPv4的自动配置和重新编址机 制主要依靠DHCP协议。 IPv6协议内置支持通过地址自动 配置方式使主机自动发现网络并 获取IPv6地址,大大提高了内部 网络的可管理性。
路由聚合 由于IPv4发展初期的分配规划问 题,造成许多IPv4地址分配不连 续,不能有效聚合路由。日益庞大 的路由表耗用大量内存,对设备成 本和转发效率产生影响,这一问题 促使设备制造商不断升级其产品, 以提高路由寻址和转发性能。 巨大的地址空间使得IPv6可以方 便的进行层次化网络部署。层次 化的网络结构可以方便的进行路 由聚合,提高了路由转发效率。
端对端安全 IPv4协议制定时并没有仔细针对安全性进行设计,因此固有的框架结构并不能支持端到端的安全。 IPv6中,网络层支持IPSec的认证 和加密,支持端到端的安全。
QoS 随着网络会议、网络电话、网络电 视迅速普及与使用,客户要求有更 好的QoS来保障这些音视频实时转 发。IPv4并没有专门的手段对QoS 进行支持。 IPv6新增了流标记域,提供QoS 保证。
支持移动特性 随着Internet的发展,移动IPv4出现 了一些问题,比如:三角路由,源 地址过滤等。 IPv6协议规定必须支持移动特 性。和移动IPv4相比,移动IPv6 使用邻居发现功能可直接实现外 地网络的发现并得到转交地址, 而不必使用外地代理。同时,利 用路由扩展头和目的地址扩展头 移动节点和对等节点之间可以直 接通信,解决了移动IPv4的三角 路由、源地址过滤问题,移动通 信处理效率更高且对应用层透 明。

IPv6 地址

表示方法

IPv6 总共有 128 位,通过分为8组,每组 16 位,由 4 个十六进制数表示。每组之间由冒号分隔。如:FC00:0000:130F:0000:0000:09C0:876A:130B。为了方便书写,提供了一些压缩后的写法:

  • 可以省略前缀0。所以这个地址还可以写成:FC00:0:130F:0:0:9C0:876A:130B
  • 地址中包含的连续两个或多个均为0的组,可以用双冒号”::”代替。所以进一步缩写成:FC00:0:130F::9C0:876A:130B。不过需要注意的是,一个 IPv6 地址中只能有一个 “::”,因为如果有多个的话,就无法辨别出每个 “::” 代表几组 0。

地址结构

类似 IPv4 的设计,一个 IPv6 地址也是由两部分组成:

  • 网络前缀:n 位,相当于 IPv4 的网络号。
  • 接口标识:128-n位,相当于 IPv4 地址中的主机号。

地址分类

IPv6 地址分为单播地址,任播地址,组播地址。相比于 IPv4,取消了广播地址,以更丰富的组播地址代替,同时增加了任播地址。

单播地址

单播地址用来表示一个节点的一个网络接口的地址。有以下几种单播地址:

类型 说明
未指定地址 ::/128。该地址表示讴歌接口或者节点还没有 IP 地址。
环回地址 ::1/128。与 IPv4 中的 127.0.0.1 作用相同
全球单播地址 类似于 IPv4 中的单播地址。由 全球路由前缀(Global routing prefix,至少48位)+子网ID(Subnet ID)+接口标识(Interface ID)组成。全球路由前缀由提供商指定给一个组织机构,因此也可以起到聚合路由的作用。
链路本地地址 链路本地地址是 IPv6 中的应用范围受限制的地址类型,只能在连接到同一本地链 路的节点之间使用。它使用了特定的本地链路前缀FE80::/10(最高10位值为 1111111010),同时将接口标识添加在后面作为地址的低64比特。当一个节点启动IPv6协议栈时,启动时节点的每个接口会自动配置一个链路本地 地址(其固定的前缀+EUI-64规则形成的接口标识)。在 IPv4 中,链路本地地址为 169.254.0.0/16
唯一本地地址 唯一本地地址是另一种应用范围受限的地址,它仅能在一个站点内使用。由于本地站点地址的废除(RFC3879),唯一本地地址被用来代替本地站点地址。唯一本地地址的作用类似于IPv4中的私网地址,任何没有申请到提供商分配的全 球单播地址的组织机构都可以使用唯一本地地址。唯一本地地址只能在本地网络内部被路由转发而不会在全球网络中被路由转发。唯一本地地址的固定前缀为FC00::/7,二进制表示为 1111 110

任播地址

任播地址一般用来表示一组节点上的接口,当数据包发往任播地址时,中间路由设备会将数据包发往最近的一个节点上的接口。所以可以看出,任播地址是被设计用来给多个主机或者节点提供相同服务时提供冗余功能和负载均衡功能的。不过目前实际应用中,任播地址只能分配给路由设备,并不能应用于主机等设备。并且任播地址不能作为 IPv6 报文的源地址。

任播地址并没有单独的地址空间,和单播地址使用相同的地址空间。

组播地址

IPv6的组播与IPv4相同,用来标识一组接口,一般这些接口属于不同的节点。一个节点 可能属于0到多个组播组。发往组播地址的报文被组播地址标识的所有接口接收。例如 组播地址FF02::1表示链路本地范围的所有节点,组播地址FF02::2表示链路本地范围的 所有路由器。

一个IPv6组播地址由前缀,标志(Flag)字段、范围(Scope)字段以及组播组ID (Global ID)4个部分组成:

  • 前缀:IPv6组播地址的前缀是FF00::/8。

  • 标志字段(Flag):长度4bit,目前只使用了最后一个比特(前三位必须置0), 当该位值为0时,表示当前的组播地址是由IANA所分配的一个永久分配地址;当 该值为1时,表示当前的组播地址是一个临时组播地址(非永久分配地址)。

  • 范围字段(Scope):长度4bit,用来限制组播数据流在网络中发送的范围,该字 段取值和含义的对应关系如图1-5所示。

  • 组播组ID(Group ID):长度112bit,用以标识组播组。目前,RFC2373并没有将 所有的112位都定义成组标识,而是建议仅使用该112位的最低32位作为组播组 ID,将剩余的80位都置0。这样每个组播组ID都映射到一个唯一的以太网组播 MAC地址(RFC2464)。

image-20220220175332438

被请求节点组播地址通过节点的单播或任播地址生成。当一个节点具有了单播或任播地址,就会对应生成一个被请求节点组播地址,并且加入这个组播组。一个单播地址或任播地址对应一个被请求节点组播地址。该地址主要用于邻居发现机制和地址重复检测功能。

IPv6中没有广播地址,也不使用ARP。但是仍然需要从IP地址解析到MAC地址的 功能。在IPv6中,这个功能通过邻居请求NS(Neighbor Solicitation)报文完成。 当一个节点需要解析某个IPv6地址对应的MAC地址时,会发送NS报文,该报文目的IP就是需要解析的IPv6地址对应的被请求节点组播地址;只有具有该组播地 址的节点会检查处理。

被请求节点组播地址由前缀FF02::1:FF00:0/104和单播地址的最后24位组成。

IPv6 报文格式

IPv6 除了在大小上做了变动,也针对 IPv4 报文格式在实际应用场景中的设计不合理之处做了优化。IPv6 报文主要由三部分组成:

  • IPv6 基本报头:8个字段,固定为 40 字节。
  • IPv6 扩展报头:扩展报头是链式结构的,理论上可无限扩展
  • 上层协议数据单元:一般由上层协议报头和它的有效载荷构成,有效载荷可以是一个 ICMPv6 报文、一个 TCP 报文或一个 UDP 报文。

一个 IPv6 的基本报头格式如下:

image-20220220193727771

这些字段的解释如下:

  • Version:版本号,长度为4bit。对于IPv6,该值为6。

  • Traffic Class:流类别,长度为8bit。等同于IPv4中的TOS字段,表示IPv6数据报的 类或优先级,主要应用于QoS。

  • Flow Label:流标签,长度为20bit。IPv6中的新增字段,用于区分实时流量,不同 的流标签+源地址可以唯一确定一条数据流,中间网络设备可以根据这些信息更加 高效率的区分数据流。

  • Payload Length:有效载荷长度,长度为16bit。有效载荷是指紧跟IPv6报头的数据 报的其它部分(即扩展报头和上层协议数据单元)。该字段只能表示最大长度为 65535字节的有效载荷。如果有效载荷的长度超过这个值,该字段会置0,而有效 载荷的长度用逐跳选项扩展报头中的超大有效载荷选项来表示。

  • Next Header:下一个报头,长度为8bit。该字段定义紧跟在IPv6报头后面的第一个 扩展报头(如果存在)的类型,或者上层协议数据单元中的协议类型。

  • Hop Limit:跳数限制,长度为8bit。该字段类似于IPv4中的Time to Live字段,它 定义了IP数据报所能经过的最大跳数。每经过一个设备,该数值减去1,当该字段 的值为0时,数据报将被丢弃。

  • Source Address:源地址,长度为128bit。表示发送方的地址。

  • Destination Address:目的地址,长度为128bit。表示接收方的地址。

通过上述描述可以知道,IPv6 的基本报头相比于 IPv4 的报头做了简化,去除了IHL、identifiers、Flags、Fragment Offset、Header Checksum、 Options、Paddiing域,只增了流标签域。这样的设计可以提升路由设备对数据的处理性能。

在IPv4中,IPv4报头包含可选字段Options,内容涉及security、Timestamp、Record route 等,这些Options可以将IPv4报头长度从20字节扩充到60字节。在转发过程中,处理携带这些Options的IPv4报文会占用设备很大的资源,因此实际中也很少使用。

IPv6将这些Options从IPv6基本报头中剥离,放到了扩展报头中,扩展报头被置于IPv6 报头和上层协议数据单元之间。一个IPv6报文可以包含0个、1个或多个扩展报头,仅 当需要设备或目的节点做某些特殊处理时,才由发送方添加一个或多个扩展头。与 IPv4不同,IPv6扩展头长度任意,不受40字节限制,这样便于日后扩充新增选项,这一特征加上选项的处理方式使得IPv6选项能得以真正的利用。但是为了提高处理选项头 和传输层协议的性能,扩展报头总是8字节长度的整数倍。

当使用多个扩展报头时,前面报头的Next Header字段指明下一个扩展报头的类型,这 样就形成了链状的报头列表。目前,RFC 2460中定义了6个IPv6扩展头:逐跳选项报头、目的选项报头、路由报头、分段报头、认证报头、封装安全净载报头。

image-20220220194519027

ICMPv6

ICMPv6(Internet Control Message Protocol for the IPv6)是IPv6的基础协议之一。

在IPv4中,Internet控制报文协议ICMP(Internet Control Message Protocol)向源节点报 告关于向目的地传输IP数据包过程中的错误和信息。它为诊断、信息和管理目的定义 了一些消息,如:目的不可达、数据包超长、超时、回应请求和回应应答等。在IPv6 中,ICMPv6除了提供ICMPv4常用的功能之外,还是其它一些功能的基础,如邻接点 发现、无状态地址配置(包括重复地址检测)、PMTU发现等。

ICMPv6的协议类型号(即IPv6报文中的Next Header字段的值)为58。

image-20220220201051653

报文中字段解释如下:

  • Type:表明消息的类型,0至127表示差错报文类型,128至255表示消息报文类型。

  • Code:表示此消息类型细分的类型。

  • Checksum:表示ICMPv6报文的校验和。

邻居发现

邻居发现协议NDP(Neighbor Discovery Protocol)是IPv6协议体系中一个重要的基础协 议。邻居发现协议替代了IPv4的ARP(Address Resolution Protocol)和ICMP路由器发现 (Router Discovery),它定义了使用ICMPv6报文实现地址解析,跟踪邻居状态,重复 地址检测,路由器发现以及重定向等功能。

地址解析

邻居发现协议 NDP(Neighbor Discovery Protocol) 是基于 ICMPv6 的一个三层协议,用来取代 IPv4 的 ARP 协议。其以太网协议类型为 0x86DD。地址解析过程中使用了两种 ICMPv6 报文:邻居请求报文 NS(Neighbor Solicitation) 和邻居通告报文 NA(Neighbor Advertisement)。

  • NS 报文:Type 字段值为 135,Code 字段值为 0,在地址解析中的作用类似于 IPv4 中的 ARP 请求报文。
  • NA 报文:Type 字段值为 136,Code 字段值为0,在地址解析中的作用类似于 IPv4 中的 ARP 响应报文。

image-20220220202534018

Host A在向Host B发送报文之前它必须要解析出Host B的链路层地址,所以首先Host A 会发送一个NS报文,其中源地址为Host A的IPv6地址,目的地址为Host B的被请求节 点组播地址,需要解析的目标IP为Host B的IPv6地址,这就表示Host A想要知道Host B 的链路层地址。同时需要指出的是,在NS报文的Options字段中还携带了Host A的链路 层地址。

当Host B接收到了NS报文之后,就会回应NA报文,其中源地址为Host B的IPv6地址, 目的地址为Host A的IPv6地址(使用NS报文中的Host A的链路层地址进行单播),Host B的链路层地址被放在Options字段中。这样就完成了一个地址解析的过程。

跟踪邻居状态

通过邻居或到达邻居的通信,会因各种原因而中断,包括硬件故障、接口卡的热插入 等。如果目的地失效,则恢复是不可能的,通信失败;如果路径失效,则恢复是可能 的。 因此节点需要维护一张邻居表,每个邻居都有相应的状态,状态之间可以迁移。

RFC2461中定义了5种邻居状态,分别是:未完成(Incomplete)、可达 (Reachable)、陈旧(Stale)、延迟(Delay)、探查(Probe)

image-20220220202903445

下面以A、B两个邻居节点之间相互通信过程中A节点的邻居状态变化为例(假设A、B 之前从未通信),说明邻居状态迁移的过程。

  1. A先发送NS报文,并生成缓存条目,此时,邻居状态为Incomplete。
  2. 若B回复NA报文,则邻居状态由Incomplete变为Reachable,否则固定时间后邻居状态由Incomplete变为Empty,即删除表项。
  3. 经过邻居可达时间,邻居状态由Reachable变为Stale,即不确定邻居节点的可达性。
  4. 如果在Reachable状态,A收到B的非请求NA报文,且报文中携带的B的链路层地址和表项中不同,则邻居状态马上变为Stale。
  5. 在STALE状态到达老化时间后进入Delay状态。
  6. 在经过一段固定时间(5秒)后,邻居状态由Delay变为Probe,其间若有NA应答, 则邻居状态由Delay变为Reachable。
  7. 在Probe状态,A每隔一定时间间隔(1秒)发送单播NS,发送固定次数(3次) 后,有应答则邻居状态变为Reachable,否则邻居状态变为Empty,即删除表项。

重复地址检测

重复地址检测DAD(Duplicate Address Detect)是在接口使用某个IPv6单播地址之前进 行的,主要是为了探测是否有其它的节点使用了该地址。尤其是在地址自动配置的时 候,进行DAD检测是很必要的。 一个IPv6单播地址在分配给一个接口之后且通过重复 地址检测之前称为试验地址(Tentative Address)。此时该接口不能使用这个试验地址 进行单播通信,但是仍然会加入两个组播组:ALL-NODES组播组和试验地址所对应的 Solicited-Node组播组。

IPv6重复地址检测技术和IPv4中的免费ARP类似:节点向试验地址所对应的Solicited- Node组播组发送NS报文。NS报文中目标地址即为该试验地址。如果收到某个其他站点 回应的NA报文,就证明该地址已被网络上使用,节点将不能使用该试验地址通讯。

image-20220220203031376

Host A的IPv6地址FC00::1为新配置地址,即FC00::1为Host A的试验地址。Host A向 FC00::1的Solicited-Node组播组发送一个以FC00::1为请求的目标地址的NS报文进行重 复地址检测,由于FC00::1并未正式指定,所以NS报文的源地址为未指定地址。当Host B收到该NS报文后,有两种处理方法:

  • 如果Host B发现FC00::1是自身的一个试验地址,则Host B放弃使用这个地址作为 接口地址,并且不会发送NA报文。

  • 如果Host B发现FC00::1是一个已经正常使用的地址,Host B会向FF02::1发送一个 NA报文,该消息中会包含FC00::1。这样,Host A收到这个消息后就会发现自身的 试验地址是重复的。Host A上该试验地址不生效,被标识为duplicated状态。

路由器发现

路由器发现功能用来发现与本地链路相连的设备,并获取与地址自动配置相关的前缀和其他配置参数。

在IPv6中,IPv6地址可以支持无状态的自动配置,即主机通过某种机制获取网络前缀信 息,然后主机自己生成地址的接口标识部分。路由器发现功能是IPv6地址自动配置功 能的基础,主要通过以下两种报文实现:

  • 路由器通告RA(Router Advertisement)报文:每台设备为了让二层网络上的主机 和设备知道自己的存在,定时都会组播发送RA报文,RA报文中会带有网络前缀 信息,及其他一些标志位信息。RA报文的Type字段值为134。

  • 路由器请求RS(Router Solicitation)报文:很多情况下主机接入网络后希望尽快 获取网络前缀进行通信,此时主机可以立刻发送RS报文,网络上的设备将回应RA 报文。RS报文的Tpye字段值为133。

重定向

当网关设备发现报文从其它网关设备转发更好,它就会发送重定向报文告知报文的发 送者,让报文发送者选择另一个网关设备。重定向报文也承载在ICMPv6报文中,其 Type字段值为137,报文中会携带更好的路径下一跳地址和需要重定向转发的报文的目 的地址等信息。

image-20220220203223504

Host A需要和Host B通信,Host A的默认网关设备是Switch A,当Host A发送报文给 Host B时报文会被送到Switch A。Switch A接收到Host A发送的报文以后会发现实际上 Host A直接发送给Switch B更好,它将发送一个重定向报文给主机A,其中报文中更好 的路径下一跳地址为Switch B,Destination Address为Host B。Host A接收到了重定向报 文之后,会在默认路由表中添加一个主机路由,以后发往Host B的报文就直接发送给 Switch B。

当设备收到一个报文后,只有在如下情况下,设备会向报文发送者发送重定向报文:

  • 报文的目的地址不是一个组播地址。
  • 报文并非通过路由转发给设备。
  • 经过路由计算后,路由的下一跳出接口是接收报文的接口。
  • 设备发现报文的最佳下一跳IP地址和报文的源IP地址处于同一网段。
  • 设备检查报文的源地址,发现自身的邻居表项中有用该地址作为全球单播地址或链路本地地址的邻居存在。

Path MTU

在IPv4中,报文如果过大,必须要分片进行发送,所以在每个节点发送报文之前,设备都会根据发送接口的最大传输单元MTU(Maximum Transmission Unit)来对报文进 行分片。但是在IPv6中,为了减少中间转发设备的处理压力,中间转发设备不对IPv6报文进行分片,报文的分片将在源节点进行。当中间转发设备的接口收到一个报文后, 如果发现报文长度比转发接口的MTU值大,则会将其丢弃;同时将转发接口的MTU值 通过ICMPv6报文的“Packet Too Big”消息发给源端主机,源端主机以该值重新发送 IPv6报文,这样带来了额外流量开销。PMTU发现协议可以动态发现整条传输路径上各 链路的MTU值,减少由于重传带来的额外流量开销。

PMTU协议是通过ICMPv6的Packet Too Big报文来完成的。首先源节点假设PMTU就是 其出接口的MTU,发出一个试探性的报文,当转发路径上存在一个小于当前假设的 PMTU时,转发设备就会向源节点发送Packet Too Big报文,并且携带自己的MTU值, 此后源节点将PMTU的假设值更改为新收到的MTU值继续发送报文。如此反复,直到 报文到达目的地之后,源节点就能知道到达目的地的PMTU了。

image-20220220203415347

整条传输路径需要通过4条链路,每条链路的MTU分别是1500、1500、1400、1300,当 源节点发送一个分片报文的时候,首先按照PMTU为1500进行分片并发送分片报文,当 到达MTU为1400的出接口时,设备返回Packet Too Big错误,同时携带MTU值为1400的 信息。源节点接收到之后会将报文重新按照PMTU为1400进行分片并再次发送一个分片 报文,当分片报文到达MTU值为1300的出接口时,同样返回Packet Too Big错误,携带 MTU值为1300的信息。之后源节点重新按照PMTU为1300进行分片并发送分片报文, 最终到达目的地,这样就找到了该路径的PMTU。

Linux IPv6

条目 ipv4 ipv6
sysctl 配置项 net.ipv4.conf net.ipv6.conf
ip 地址 通过 ip a查看时可以看到 inet 后面的就是 ipv4 地址。 通过 ip a查看时可以看到 inet6 后面的就是 ipv6 地址。一般会有多个,scope global 的是全局唯一单播地址或唯一本地地址(fc或fd开头),scope link 是链路本地地址(fe80 开头)。
抓包 tcpdump icmp/ tcpdump ip tcpdump icmp6 / tcpdump ip6
ping ping ping6 或 ping -6
traceroute6 traceroute traceroute6
邻居地址解析 arping ndisc
路由表 ip r ip -6 r
邻居地址表 ip neigh 或 arp -n ip -6 neigh
DNS 解析 dig dig -6

Kubernetes 的 IPv4/IPv6 双栈

IPv4/IPv6 双栈是由 IPv4 向 IPv6 过渡阶段的一种解决方案,双栈即一个网络接口同时拥有 IPv4 和 IPv6 的地址,这样在和远端通信时,如果远端支持 IPv6,就使用 IPv6 进行通信,否则也可以使用 IPv4 进行通信。Kubernetes 在 1.20 后开始支持双栈。当然,除了对 Kubernetes 版本有要求外,CNI 插件也必须支持双栈才行。

要在 Kubernetes 中开启双栈,需要做以下配置:

  • kube-apiserver:
    • --service-cluster-ip-range=<IPv4 CIDR>,<IPv6 CIDR>
  • kube-controller-manager:
    • --cluster-cidr=<IPv4 CIDR>,<IPv6 CIDR>
    • --service-cluster-ip-range=<IPv4 CIDR>,<IPv6 CIDR>
    • --node-cidr-mask-size-ipv4|--node-cidr-mask-size-ipv6 对于 IPv4 默认为 /24,对于 IPv6 默认为 /64
  • kube-proxy:
    • --cluster-cidr=<IPv4 CIDR>,<IPv6 CIDR>

IPv6 地址速查

平常接触 IPv4 地址较多,因此一眼就大概知道某个地址代表什么含义,但是 IPv6 中往往比较难分辨,这里提供一个表格供对照参考。

地址类型 IPv4 IPv6
环回地址 127.0.0.1 ::1/128
私网地址 10.0.0.0 – 10.255.255.255, 172.16.0.0 – 172.31.255.255,192.168.0.0 – 192.168.255.255 前缀FC00::/7(1111 110),范围:FC~FD。
链路本地地址 169.254.0.0/16 fe80::/10
组播地址 被请求节点组播地址由前缀FF02::1:FF00:0/104和单播地址的最后24位组成。
广播地址 广播地址使用该网络范围内的最大地址。 即主机部分的各比特位全部为 1 的地址。在网络 10.1.1.0/24 中,其广播地址是 10.1.1.255。

参考

  • 华为 《IPv6 技术白皮书》。本文大多数内容都是参考或摘抄自该白皮书。