草庐IT

理解 Net Device Ingress 和 Egress 双重角色

Lance Zhang 2023-03-28 原文

本文是书稿《图解 VPC & K8s 网络模型》其中一篇。书稿还在继续写,进度不快也不慢,因为二哥不急也不躁。好肉需要慢炖,好书需要多磨。

为什么要单独讲这个话题呢?因为我在和同事讨论 K8s 网络尤其是网络数据流向的时候,会反复提及到网络设备,无论它是物理的还是虚拟的。而网络设备在我们所讨论到的数据流场景里,时而在接收数据,时而在发送数据。也就是说它同时扮演着双重身份:Ingress 和 Egress。

另外我在整理 eBPF 相关的内容,尤其是 tc eBPF 的时候,再一次发现如果不能准确地在数据流中识别出网络设备是 Ingress 还是 Egress ,就无法将代码逻辑和实际运行结果对上号,更勿谈能理解tc eBPF 了。

这样的双重角色扮演就如同一个调皮的孩子,总是带上面具在错综复杂的网络里面东躲西藏,肆意玩耍。而当你好不容易抓到它时,却让你猜猜此时此刻他是谁。

简单来说:对于网卡而言,无论它是物理的还是虚拟的,对于 Ingress 角色,它是首先触碰到数据的人,而对于 Egress 角色,它是最后一个碰到数据的人。

本文我们从一个简单的物理网卡开始,然后对 veth、 bridge 还有 tc eBPF ,分别展开聊聊:

  • 当网卡扮演 Ingress 角色时,它从哪里接收数据,又将数据递交给了谁?
  • 当网卡扮演 Egress 角色时,它从哪里接收数据,又将数据递交给了谁?

1、单个物理网卡


图1

这是一个简单的图,图中有一张物理网卡。我们的台式机通常是这样的配置。橘色的线代表着输入流程,而蓝色的线表示输出流程。

(1)输入流程 Ingress

对于这张网卡而言,输入过程伴随着以下几个重要的事情:物理网卡首先接收到物理信号 -> 物理网卡通过 DMA 机制将数据保存至其专属的 RingBuffer 里面 -> 向 CPU 发起中断 -> OS kernel thread ksoftirqd/x 不断地消费 RingBuffer 里面的数据。

这里的 ksoftirqd 是一个内核线程,每个 CPU 一个,x 为 CPU 编号。如 ksoftirqd/0 为 0 号 CPU 上运行的内核线程。

ksoftirqd/x 将数据以 skb 为处理粒度依次穿过链路层、网络层、TCP/UDP 传输层 。不过 skb 在链路层和网络层还可能直接 forward 给其它网卡,那这样的话传输层就不会收到这个 skb 了。

整个过程如图 2 所示,你可以从整体上感受一下。标号 1 及 1.x 为数据输入和生产过程,这是本文的重点。而标号 3 为数据消费过程,它带着 skb 从入口处的 net_rx_action() 沿着协议栈由底向上穿越协议栈,这个过程对本文所述的所有 Ingress 场景都是通用的,故后文不再赘述这部分。

图2

总结:当物理网卡扮演 Ingress 角色时,它从主机外接收数据,将数据递交给了环形队列,然后由 ksoftirqd/x 进行后续的处理,这个处理过程也称为网络栈下半部分。

(2)输出过程 Egress

从图 1 中,我们大致可以看出来,对于输出过程,数据来源有两种,分别是通过 ip_forward() 过程和通过 ip_local_out() 过程送过来的数据。我们还会发现,在发送数据的路径上,这两个过程只是起点有些不同,剩下的路程大家都一样。

ip_forward() 过程与 skb 在 IP 层路由结果强相关。如图 3 所示,具体来说经过路由的判定,可能需要把 skb forward 至本机网络设备或者网络中的其它主机处理,不过无论是哪种情况,都需要将 skb 送往本机的一个网络设备。

图3

而 ip_local_out() 过程则对应了本机进程通过 socket 发送数据的场景,如图 4 所示。这张图最后标注的“触发 NET_RX 类型软中断”是数据已经被网卡发送完后发生的事情,中断的目的是为了清理 skb ,略过不表。

图 4 ,图片来源:“开发内功修炼”公众号

总结:当物理网卡扮演 Egress 角色时,它从本机 TCP/IP 协议栈接收数据,将数据通过驱动程序送离本机。

2、veth-pair

是不是觉得单个网卡的场景其实很容易分辨出来 Ingress 和 Egress ?

别得意,我们来加点难度。我们知道 K8s 的默认 CNI flannel 用到了 veth 。veth 是什么以及它的特性二哥就不细说了。我们聊一个话题:图 5 中,当左侧进程向右侧进程通信发送数据时, 左端的 veth_left 是 Ingress 还是 Egress ? 右端的 veth_right 呢?

图 5

结合图 5 上的箭头示意,答案应该不难猜。对 veth_left 来说,它扮演的是 Egress 角色,因为进程需要通过它把数据发送出去。对 veth_right 而言是 Ingress ,因为它需要负责接收数据并把它送给右侧的进程。

下一个问题:既然  veth_left 扮演了 Egress 角色,流量从离开 network namespace 1 之后去哪里了?既然 veth_right 是 Ingress ,那它从哪里接收到流量的?

答案都在图 6 里面。图中标号 2 及 2.x 在进行数据发送的工作,都属于 veth_left 的 Egress 的过程,这个过程是发生在 network namespace 1 里面的,函数调用栈和图 4 一样。而标号 3 为数据消费也即 veth_right 的 Ingress 过程,这个过程和物理网卡一模一样。

图 6

总结:既然 veth 是一对虚拟网卡,那我们把对它俩的总结放在一起。

当 veth 网卡扮演 Egress 角色时,如图 7 中的 veth_left,它从其所在的 network namespace TCP/IP 协议栈接收数据,并将数据递交给了 per CPU input_pkt_queue 队列,并触发软件中断。

当 veth 网卡扮演 Ingress 角色时,如图 7 中的 veth_right,它并没有物理网卡那种环形队列,而是由 ksoftirqd/x 直接从 per CPU input_pkt_queue 队列读取  veth_left 塞进来的数据。

veth_left 和 veth_right 共享了同一个 queue 。典型的生产者 / 消费者设计模式的视感有没有?

图 7

3、bridge

上一节,二哥把 veth pair 单独拿出来和大家一起观赏。可它们终究不是花瓶,它们被创造出来是要有实际使用价值的。veth 典型使用场景就是把一端插入到 bridge 里面,如图 8 所示。

从 veth 的特性来说,流量从下图 veth1-left 流出后,会进入 veth1-right ,这也就意味着流量进入了网桥。

我想这个时候你可以确定 veth1-left 是 Egress ,而 veth1-right 是 Ingress 。那么对于 bridge 的 Port 1 和 Port 2 呢?再进一步,对于 veth2-left 和 veth2-right 呢 ?

图 8

其实对于 bridge 这种虚拟的网桥,它的 port 口也是一个虚拟的概念,说得更直白一点,在内核里它就是一个数据结构:struct net_bridge_port 。这个结构里有 3 个重要的成员:br / port_no / dev 。下面的代码用于插入网络设备到 bridge ,这 3 个成员的作用显而易见。

//file: net/bridge/br_if.c
static struct net_bridge_port *new_nbp(struct net_bridge *br,
struct net_device *dev)
{
//申请插口对象
struct net_bridge_port *p;
p = kzalloc(sizeof(*p), GFP_KERNEL);
//初始化插口
index = find_portno(br);
p->br = br;
p->dev = dev;
p->port_no = index;
...
}
对于图 8 来说, Port 1 (net_bridge_port) 就是一个粘合剂,左手 bridge ,右手 veth1-right 。理解了这点也就明白了对于 bridge 的 Port 而言,它是没有所谓的 Ingress 和 Egress 的概念的。

Port 1 接收数据其实是 veth1-right 在 Ingress,而 bridge 把这个流量 forward 给 veth2-right 时,veth2-right 其实在扮演 Egress 角色。那流量从 veth2-right 传至 veth2-left 的过程和 veth1-left 向 veth1-right 发送数据的过程是完全一样的。

总结:当 veth 这样的虚拟网卡插入在 bridge 上时:

图8 中 veth1-left 扮演 Egress 角色,它从其所在的 network namespace TCP/IP 协议栈接收数据,将数据递交给了 per CPU input_pkt_queue 队列,并触发软件中断。

veth1-right 扮演 Ingress 角色,它并没有物理网卡那种环形队列,而是由 ksoftirqd/x 直接从 per CPU input_pkt_queue 队列读取  veth1-left 塞进来的数据。当 ksoftirqd/x 把流量送至链路层时,从 br_forward() 开始进入 forward 流程。这个流程的效果就是流量从 veth1-right 转至 veth2-right 发送出去了。

那自然 veth2-right 这个时候就扮演了 Egress 角色,veth2-left 扮演了 Ingress 角色。

4、veth-pair plus

如果你没有晕的话,那抖索一下精神,我们开始 veth-pair 的进阶版。

上一节我们看到 veth 和 bridge 搭配使用的场景。veth 另一端一定要插在 bridge 上吗?从图 9 你也看到了,答案是:不一定。

图 9

现在我们知道,在图 9 中,从 container-1 发出的流量经过 veth 发出后,veth-p 会以 Ingress 角色开始接收。根据前文的解释,当网卡进行 Ingress 时,流量会被 ksoftirqd/x 送往协议栈进一步处理。这个处理的过程当然也就包括了图 9 中的路由过程。

图 9 这里巧妙的地方是:流量是产生于容器内,但对这份流量的路由却发生在主机 root(default) network namespance 里面,使用的也是主机的路由表。如果路由结果发现需要把这份流量发往其它主机,那自然流量就从主机的 eth0 这个网卡设备离开了。在这个过程中,主机其实扮演了网关的角色。

说到这里,你能理解图 10 的工作过程了吗?它是 K8s host-gateway 网络模型,顾名思义,这种网络模型以 host 为 gateway ,更具体地说,host 的 root network namespace 充当了路由的角色。


图 10


5、tc eBPF

如果你对 tc 和 eBPF 了解得不多或者不感兴趣,可以跳过这部分。

以 Cilium 为代表的 K8s CNI 提供商一直在尝试使用 eBPF 代替 iptables 以便优化服务网格数据面性能。其中 bpf_redicrect() 函数即为其中一项优化产出。

bpf_redicrect() 函数的特性用一句话就能解释清楚:当 veth Ingress 时,将流量直接通过 bpf_redirect() 重定向到另一个 veth Ingress。如图 11 所示。

不过如果你对 veth pair 哪一端在何时会扮演 Ingress 角色了解得不是很清楚的话,上面这句话其实会把你绕晕。

图 11

但在看完二哥这篇文章后,希望你不会再晕了。在图 11 中,从右下 Pod 出来的流量会流到位于 host network ns 这一端的 veth 上,这个 veth 是以 Ingress 角色工作的。

你看到在它身上附上了一个 eBPF 小蜜蜂图标,表示这个时候 eBPF 程序会介入执行,执行的结果就是流量被直接通过 dev_forward_skb()  forward 给了另外一个同样位于 host network ns 端的 veth (如图 11 箭头所指的那个 veth),当然对这个 veth 而言,它会扮演 Egress 角色。

这个过程也可以用下面这样的函数调用层次图来表示。

pkt -> NIC -> TC ingress -> handle_ing()
|-verdict = tc_classify() // exec BPF code
| |-bpf_redirect() // return verdict
|
|-switch (verdict) {
case TC_ACK_REDIRECT:
skb_do_redirect() // to the target net device
|-if ingress:
| dev_forward_skb()
|-else:
dev_queue_xmit()

下面是网易轻舟的一篇文章里面所附的图。它画出了网易轻舟基于 Cilium 网络方案的探索和实践细节,包括跨节点 Pod 间通信、同节点 Pod 间通信、Pod 访问外网等各类常见的场景。

图中  cilium_net/cilium_host​ 是一对 veth pair ,它们在 Kernel 4.19​ + Cilium ​1.8​ 部署中已经没什么作用了(事实上社区在考虑去掉它们)。

我想有了上面所有的铺垫和知识,至少对标号 ① ② 所示的流程,你应该能看得懂了。

图 12


6、总结

文末,二哥来做个小总结:

  1. 对于网卡而言,无论它是物理的还是虚拟的,对于 Ingress 角色,它是首先触碰到数据的人,而对于 Egress 角色,它是最后一个碰到数据的人。
  2. 对于 Ingress 过程,无论是物理网卡还是虚拟网卡,在它接收到数据后,总是通过 ksoftirqd/x 进行网络栈下半部分处理。
  3. 对于 Egress 过程,无论是物理网卡还是虚拟网卡,在它从链路层那里拿到数据后,总是通过网卡驱动程序将数据送离本设备。
  4. Ingress 过程和 Egress 过程在内核中的处理路径完全不同,也更无对称可言。
  5. 对于 veth ,无论是搭配 bridge 还是用于 host-gateway 场景,在数据流经过的关键位置区分是 Ingress 还是 Egress ,有助于理解系统对数据流的处理行为。
  6. tc eBPF 强依赖 Ingress 和 Egress,理解了它们也才能更好地理解 bpf_redirect() 。

有关理解 Net Device Ingress 和 Egress 双重角色的更多相关文章

  1. CAN协议的学习与理解 - 2

    最近在学习CAN,记录一下,也供大家参考交流。推荐几个我觉得很好的CAN学习,本文也是在看了他们的好文之后做的笔记首先是瑞萨的CAN入门,真的通透;秀!靠这篇我竟然2天理解了CAN协议!实战STM32F4CAN!原文链接:https://blog.csdn.net/XiaoXiaoPengBo/article/details/116206252CAN详解(小白教程)原文链接:https://blog.csdn.net/xwwwj/article/details/105372234一篇易懂的CAN通讯协议指南1一篇易懂的CAN通讯协议指南1-知乎(zhihu.com)视频推荐CAN总线个人知识总

  2. TimeSformer:抛弃CNN的Transformer视频理解框架 - 2

    Transformers开始在视频识别领域的“猪突猛进”,各种改进和魔改层出不穷。由此作者将开启VideoTransformer系列的讲解,本篇主要介绍了FBAI团队的TimeSformer,这也是第一篇使用纯Transformer结构在视频识别上的文章。如果觉得有用,就请点赞、收藏、关注!paper:https://arxiv.org/abs/2102.05095code(offical):https://github.com/facebookresearch/TimeSformeraccept:ICML2021author:FacebookAI一、前言Transformers(VIT)在图

  3. ruby - 易于初学者理解的 Ruby 库 - 2

    关闭。这个问题不符合StackOverflowguidelines.它目前不接受答案。我们不允许提问寻求书籍、工具、软件库等的推荐。您可以编辑问题,以便用事实和引用来回答。关闭3年前。Improvethisquestion我正处于学习Ruby的阶段,我想查看一些小型库的源代码以了解它们是如何构建的。我不知道什么是小型图书馆,但希望SO能推荐一些易于理解的图书馆来学习。因此,如果有人知道一两个非常小的库,这是新手Rubyists学习的好例子,请推荐!我想使用Manveru'sInnatelib,因为它试图保持在2000LOC以下,但我还不熟悉其中经常使用的Ruby速记。也许大约100-5

  4. ruby - 无法理解 `puts{}.class` 和 `puts({}.class)` 之间的区别 - 2

    由于匿名block和散列block看起来大致相同。我正在玩它。我做了一些严肃的观察,如下所示:{}.class#=>Hash好的,这很酷。空block被视为Hash。print{}.class#=>NilClassputs{}.class#=>NilClass为什么上面的代码和NilClass一样,下面的代码又显示了Hash?puts({}.class)#Hash#=>nilprint({}.class)#Hash=>nil谁能帮我理解上面发生了什么?我完全不同意@Lindydancer的观点你如何解释下面几行:print{}.class#NilClassprint[].class#A

  5. ruby-on-rails - (Ruby,Rails) 基于角色的身份验证和用户管理...? - 2

    我正在寻找用于Rails的优质管理插件。似乎大多数现有的插件/gem(例如“restful_authentication”、“acts_as_authenticated”)都围绕着self注册等展开。但是,我正在寻找一种功能齐全的基于管理/管理角色的解决方案——但不是简单地附加到另一个非基于角色的解决方案。如果我找不到,我想我会自己动手......只是不想重新发明轮子。 最佳答案 RyanBates最近做了两个关于授权的railscast(注意身份验证和授权之间的区别;身份验证检查用户是否如她所说的那样,授权检查用户是否有权访问资源

  6. ruby - 如何理解 Ruby 中的发送者和接收者? - 2

    我很难理解Ruby中sender和receiver的实际含义。它们一般是什么意思?到目前为止,我只是将它们理解为方法调用和获取其返回值的调用。但是,我知道我的理解还远远不够。谁能给我一个Ruby中发送者和接收者的具体解释? 最佳答案 面向对象中的一个核心概念是消息传递和早期概念化,这在很大程度上借鉴了计算的Actor模型。艾伦·凯(AlanKay)创造了面向对象一词并发明了最早的OO语言之一SmallTalk,他拥有voicedregretatusingatermwhichputthefocusonobjectsinsteadofo

  7. ruby-on-rails - Rails - 理解 application.js 和 application.css - 2

    rails新手。只是想了解\assests目录中的这两个文件。例如,application.js文件有如下行://=requirejquery//=requirejquery_ujs//=require_tree.我理解require_tree。只是将所有JS文件添加到当前目录中。根据上下文,我可以看出requirejquery添加了jQuery库。但是它从哪里得到这些jQuery库呢?我没有在我的Assets文件夹中看到任何jquery.js文件——或者直接在我的整个应用程序中没有看到任何jquery.js文件?同样,我正在按照一些说明安装TwitterBootstrap(http:

  8. ruby - 你如何理解 Ruby 中的这个三元条件? - 2

    我在某些代码中遇到了三元组,但我无法理解条件:str.split(/',\s*'/).mapdo|match|match[0]==?,?match:"somestring"end.join我确实理解我是在某些点上拆分字符串并将总结果转换为数组,然后依次处理数组的每个元素。除此之外,我不知道发生了什么。 最佳答案 一种(稍微)不那么令人困惑的写法是:str.split(/',\s*'/).mapdo|match|ifmatch[0]==?,matchelse"somestring"endend.join我认为多行三元语句很糟糕,尤其是

  9. ruby - 您如何将 S3 理解为 Ruby 中的分层目录结构? - 2

    有没有人成功地将S3存储桶读取为子文件夹?文件夹1--子文件夹2----文件3----文件4--文件1--文件2文件夹2--子文件夹3--文件5--文件6我的任务是读取文件夹1。我希望看到子文件夹2、文件1和文件2,但看不到文件3或文件4。现在,因为我将存储桶键限制为prefix=>'folder1/',你仍然会得到file3和4,因为它们在技术上具有folder1前缀。似乎真正做到这一点的唯一方法是吸收folder1下的所有键,然后使用字符串搜索从结果数组中实际排除file3和file4。有没有人有过这方面的经验?我知道像Transmit和Cyber​​duck这样的FTP风格的S3

  10. 关于yolov5训练时参数workers和batch-size的理解 - 2

    关于yolov5训练时参数workers和batch-size的理解yolov5训练命令workers和batch-size参数的理解两个参数的调优总结yolov5训练命令python.\train.py--datamy.yaml--workers8--batch-size32--epochs100yolov5的训练很简单,下载好仓库,装好依赖后,只需自定义一下data目录中的yaml文件就可以了。这里我使用自定义的my.yaml文件,里面就是定义数据集位置和训练种类数和名字。workers和batch-size参数的理解一般训练主要需要调整的参数是这两个:workers指数据装载时cpu所使

随机推荐