网络和操作系统内核,对我来说是既生疏又满是吸引,心愿可能拨开层层迷雾找到背地的假相。
在 上一篇文章 中我深入探讨了 Kubernetes 网络模型,这次我想更深刻一点:理解数据包在 Kubernetes 中的传输,为学习 Kubernetes 的 eBPF 网络减速做筹备,加深对网络和操作系统内核的了解。文中可能有疏漏之处,还望大家赐教。
在开始之前,我能够用一句话来总结我的学习成绩:数据包的流转其实就是一个网络套接字描述符(Socket File Descriptor,中文有点简短,以下简称 socket fd)的寻址过程。它不是简略的指 socket fd 的内存地址,还包含它的网络地址。
在 Unix 和类 Unix 零碎中,所有皆文件,也能够通过文件描述符来操作 socket。
基础知识
数据包
既然要探讨数据包的流转,先看看什么是数据包。
网络数据包(network packet),也称为网络数据报(network datagram)或网络帧(Network frame),是通过计算机网络传输的数据单位。拿最常见的 TCP 数据包来看蕴含如下几个局部:
- Ethernet header:链路层信息,次要包含目标 MAC 地址和源 MAC 地址,以及报文的格局,这里是 IP 包。
- IP header:网络层信息,次要包含长度、源 IP 地址和目标 IP 地址以及报文的格局,当然这里必须是 TCP 包。
- TCP header:传输层信息,包含源端口和目标端口。
- 数据:个别是第 7 层的数据,比方 HTTP 等。
这里没有介绍的 checksum 和 FCS 通常是用来查看数据包在传输过程中是否被篡改或者产生了谬误。
应用程序应用 socket 向网络发送数据的过程能够简略了解为应用头信息封装数据的过程:TCP 数据包、IP 数据包、Ethernet 数据包;反过来,从网络接管以太网数据包到应用程序能够解决的数据,就是解包的过程。封包和解包的过程是由内核网络协议栈来实现的。
上面别离说一下 socket 和内核网络协议栈的解决。
socket 套接字
Socket 是一种在计算机网络中应用的编程接口,位于用户空间(用户利用程序运行的空间)和内核网络协议栈(内核中对数据进行封包和解包的组件)之间。
作为编程接口,socket 提供了如下操作(只列出局部):
socket
connect
bind
listen
accept
-
数据传输
send
sendto
sendmsg
recv
recvfrom
recvmsg
getsockname
getpeername
getsockopt
、setsockopt
获取或设置 socket 层或协定层选项close
通过上面的图,能够直观感触各个操作的作用:
开始解说内核网络协议栈之前,先说下数据包在内存中的数据结构:sk_buff。
sk_buff
sk_buff 是 Linux 内核中用于管理网络数据包的数据结构,它蕴含了接管和发送的网络数据包的各种信息和属性,如数据包的协定、数据长度、源和指标地址等。sk_buff 是一种能够在网络层和数据链路层之间传递的数据结构,能够被用于所有类型的网络协议栈,例如 TCP/IP、UDP、ICMP 等。
sk_buff 在 Linux 内核中广泛应用于网络协议栈的各个层级,如数据链路层、网络层、传输层等。sk_buff 数据结构的字段很多,有 4 个重要的字段且都是指针类型。sk_buff 在不同层的应用,就是通过批改这些指针来实现的:加 header(封包)和移除 header(解包)。
这个过程操作做的是指针,数据是零拷贝的,能够极大地晋升效率。
内核网络协议栈
封包
应用程序应用 socket 的 sendmsg 操作发送数据(这里不深刻解说 netfilter、traffic control、queue discipline):
- 先调配 sk_buff
- 接下来开始网络协议栈的解决
- 设置传输层信息(这里是 TCP 头中的源和目标端口)
- 依据指标 IP 查找路由
- 设置网络层信息(源和目标 IP 地址等)
- 调用 netfilter(
LOCAL_OUT
) - 设置接口(interface)和协定(protocol)
- 调用 netfilter(
POST_ROUTING
) - 如果包过长,分段传输
- L2 寻址,即查找能够领有指标 IP 地址的设施的 MAC 地址
- 设置链路层信息,
- 至此内核网络协议栈的操作实现
- 调用 tc(traffic control)egress(能够对包进行重定向)
- 进入队列 queue discipline(qdisc)
- 写入 NIC(network interface controler)
- 发送到网络
解包
NIC 收到网络发来的数据包(这里不深刻解说 direct memory access、netfilter、traffic control):
- 将数据包写如 DMA 中(Direct Memory Access 间接内存拜访,不须要依赖 CPU,由 NIC 间接写入到内存中)
- 调配 sk_buff,并填充元数据,比方 protocol 为 Ethernet 类型,接管数据包的网络接口等
- 将链路层信息保留在 sk_buff 的
mac_header
字段中,并“移除”数据包中的链路层信息(挪动指针) - 接下来开始网络协议栈的解决
- 将网络层信息保留在
network_header
字段中 - 调用 tc ingress
- “移除”网络层信息
- 将传输层信息保留在
transport_header
字段中 - 调用 netfilter(
PRE_ROUTING
) - 查找路由
- 合并多个分包
- 调用 netfilter(
LOCAL_IN
) - “移除”传输层信息
- 查找监听指标端口的 socket,或者发送 reset
- 将数据写入 socket 的接管队列中
- 发信号告诉有数据写入队列
- 至此内核网络协议栈的操作实现
- sk_buff 从 socket 接管队列中出队
- 将数据写入应用程序的缓冲区
- 开释 sk_buff
Kubernetes 的网络模型
另一部分的基础知识就是 Kubernetes 的网络模型了,能够参考之前的那篇 深刻摸索 Kubernetes 网络模型和网络通信。
Kubernetes 中的数据包流转
这里持续探讨之前文章中的三种通信场景,pod 间的通信应用 pod IP 地址。如果要探讨通过 Service 来拜访,则要退出 netfilter 的探讨篇幅会减少不少。
同 pod 内的容器间通信
pod 内两个容器间的形式通常应用回环地址 127.0.0.1
,在封包的 #4 路由过程中确定了应用回环网卡 lo
进行传输。
同节点上的 pod 间通信
curl
收回的申请在封包 #4 过程中确定应用 eth0
接口。而后通过与 eth0
相连的隧道 veth1
达到节点的根网络空间。
veth1
通过网桥 cni0
与其余 pod 相连虚构以太接口 vethX
相连。在封包 #10 L2 寻址中,ARP 申请通过网桥发送给所有相连的接口是否领有原始申请中的目标 IP 地址(这里是 10.42.1.9
)
拿到了 veth0
的 MAC 地址后,在封包 #11 中设置数据包的链路层信息。数据包收回后,通过 veth0
隧道进入 pod httpbin
的 eth0
接口中,而后开始解包的过程。
解包的过程没啥特地,确定了 httpbin 应用的 socket。
不同节点的 pod 间通信
这里略微不同,就是在通过 cni0
发送 ARP 申请没有收到应答,应用根命名空间也就是主机的路由表,确定了指标主机 IP 地址后,而后通过主机的 eth0
放 ARP 申请并收到指标主机的响应。将其 MAC 地址在封包 #11 中写入。
数据包发送到指标主机后,开始解包的过程,最终进入指标 pod。
在集群层面有一张路由表,外面存储着每个节点的 Pod IP 网段(节点退出到集群时会调配一个 Pod 网段(Pod CIDR),比方在 k3s 中默认的 Pod CIDR 是
10.42.0.0/16
,节点获取到的网段是10.42.0.0/24
、10.42.1.0/24
、10.42.2.0/24
,顺次类推)。通过节点的 Pod IP 网段能够判断出申请 IP 的节点,而后申请被发送到该节点。
总结
统计一下在三个场景中,通过内核网络协议栈的解决次数都是两次(包含 netfilter 的解决。),即便是同 pod 或者同节点内。而这两种状况理论都产生在同一个内核空间中。
如果同一个内核空间中的两个 socket 能够间接传输数据,是不是就能够省掉内核网络协议栈解决带来的提早?
下篇持续。