容器网络-2

    文章大部分来自参考资料,该篇为学习总结 ,小部分为自己的学习笔记, 半原创

前言

物理机的网络还算好理解 , 而容器中是如何通信的,这篇文章将从几个实现容器通信的组件(veth , 路由这些)等介绍起, 然后再到k8s 中的网络实现 ,最后看一下开源框架 Flannel 的实现 这样一个过程了解 k8S 是如何通信的.

网桥

网桥(bridge)硬件来说是交换机的前身, 工作在第二层---链路层 ,那么linux 中的 bridge 又是如何工作的呢?
我们先来看一下网桥的动机

前言

我们上一节学习了 veth知道了veth这个虚拟设备 , 假如在进行数据传输的时候都使用一对 veth对来沟通交流的话, 那么千千万容器之间的通信就太复杂了, 所以就衍生了像交换机/网桥的虚拟设备. 进行中转, 路由. 这样 :
img

网桥的动机

    Linux 可以支持很多不同的端口,这些端口之间当然应该能够通信,如何将这些端口连接起来并实现类似交换机那样的多对多通信呢?这就是网桥的作用了。网桥是一个二层网络设备,可以解析收发的报文,读取目标MAC 地址的信息,和自己记录的MAC 表结合,来决策报文的转发端口。
    
    为了实现这些功能,网桥会学习源MAC 地址(二层网桥转发的依据就是MAC 地址)。在转发报文的时候,网桥只需要向特定的网络接口进行转发,从而避免不必要的网络交互。如果它遇到一个自己从未学习到的地址,就无法知道这个报文应该从哪个网口设备转发,于是只好将报文广播给所有的网络设备端口(报文来源的那个端口除外)。

如何使用 bridge

可以从上图看到, 第一步是创建 veth pair , 然后再将其中一端放置在网桥中就行了
第一步 : 创建 veth pair

-- 指定命名空间 
# ip netns add net1
-- 创建veth-pair
# ip link add veth1 type veth peer name veth1_p
# ip link set veth1 netns net1
-- 赋值ip
# ip netns exec net1 ip addr add 192.168.0.101/24 dev veth1
-- 启动并查看
# ip netns exec net1 ip link set veth1 up
# ip netns exec net1 ip link list
# ip netns exec net1 ifconfig

重复上述步骤,在创建一个新的 netns出来,命名分别为。

  • netns: net2
  • veth pair: veth2, veth2_p
  • ip: 192.168.0.102
    好了,这样我们就在一台 Linux 就创建出来了两个虚拟的网络环境。

第二步 : 创建网桥,联网测试

-- 创建网桥,
# brctl addbr br0
-- 将 veth 设备放置在网桥中去
# ip link set dev veth1_p master br0
# ip link set dev veth2_p master br0
-- 赋值ip,启动设备,启动网桥
# ip addr add 192.168.0.100/24 dev br0
# ip link set veth1_p up
# ip link set veth2_p up
# ip link set br0 up

# brctl show
bridge name     bridge id               STP enabled     interfaces
br0             8000.4e931ecf02b1       no              veth1_p
                                                        veth2_p

第三步 : 联通测试

# ip netns exec net1 ping 192.168.0.102 -I veth1
PING 192.168.0.102 (192.168.0.102) from 192.168.0.101 veth1: 56(84) bytes of data.
64 bytes from 192.168.0.102: icmp_seq=1 ttl=64 time=0.037 ms
64 bytes from 192.168.0.102: icmp_seq=2 ttl=64 time=0.008 ms
64 bytes from 192.168.0.102: icmp_seq=3 ttl=64 time=0.005 ms

linux 中网桥的工作原理

    从源码层面分析工作原理参考 : https://mp.weixin.qq.com/s/JnKz1fUgZmGdvfxOm2ehZg

飞哥的文章写的非常好了, 下面总结一下工作流程 :

img

细化点就是这样 :

img

大致步骤是:

  1. Docker1 往 veth1 上发送数据
  2. 由于 veth1_p 是 veth1 的 pair, 所以这个虚拟设备上可以收到包
  3. veth 收到包以后发现自己是连在网桥上的,于是乎进入网桥处理。在网桥设备上寻找要转发到的端口,这时找到了 veth2_p 开始发送。网桥完成了自己的转发工作
  4. veth2 作为 veth2_p 的对端,收到了数据包
  5. Docker2 里的就可以从 veth2 设备上收到数据了

第一张图的设备层说实话我有点懵 ,这里可以参考 veth , 我们先看这一张图 :

img

然后在看一下 veth 这个设备的数据流程

img

可以说bridgeveth 是在一个层面上的, 没有经过硬件(网卡) , 没有硬中断, 只有软中断.

所谓网络虚拟化,其实用一句话来概括就是用软件来模拟实现真实的物理网络连接。

Iptables/Netfilter

   下面的内容主要来自: https://mp.weixin.qq.com/s/7PGkF4q0nCp6LhvZMUaMcw  , 大部分非原创 ,自己做了一些解释和补充 !!! 

Iptables/Netfilter 是什么 ,动机

Netfilter负责在内核中执行各种挂接的规则,运行在内核模式中:而Iptable是在用户模式下运行的进程,负责协助维护内核中Netfilter 的各种规则表。
注意 Netfilter 运行在内核提供HOOK API , 更像是一个内核系统调用 ,而 Iptable 则是基于这个系统调用开发出来的应用程序 . 那我们来看一下 Netfilter 提供了哪些系统调用 .

Netfilter 通过向内核协议栈中不同的位置注册 钩子函数(Hooks) 来对数据包进行过滤或者修改操作,这些位置称为 挂载点,主要有 5 个:

  • PRE_ROUTING
  • LOCAL_IN
  • FORWARD
  • LOCAL_OUT
  • POST_ROUTING

img

这 5 个 挂载点 的意义如下:

  • PRE_ROUTING:路由前。数据包进入IP层后,但还没有对数据包进行路由判定前。
  • LOCAL_IN:进入本地。对数据包进行路由判定后,如果数据包是发送给本地的,在上送数据包给上层协议前。
  • FORWARD:转发。对数据包进行路由判定后,如果数据包不是发送给本地的,在转发数据包出去前。
  • LOCAL_OUT:本地输出。对于输出的数据包,在没有对数据包进行路由判定前。
  • POST_ROUTING:路由后。对于输出的数据包,在对数据包进行路由判定后。

从上图可以看出,路由判定是数据流向的关键点。

  1. 第一个路由判定通过查找输入数据包 IP头部 的目的 IP地址 是否为本机的 IP地址,如果是本机的 IP地址,说明数据是发送给本机的。否则说明数据包是发送给其他主机,经过本机只是进行中转。
  2. 第二个路由判定根据输出数据包 IP头部 的目的 IP地址 从路由表中查找对应的路由信息,然后根据路由信息获取下一跳主机(或网关)的 IP地址,然后进行数据传输。

通过向这些 挂载点 注册钩子函数,就能够对处于不同阶段的数据包进行过滤或者修改操作。由于钩子函数能够注册多个,所以内核使用链表来保存这些钩子函数(这些在下面会展示一下 ,也给我们提供编程方法 , 类似于这种用 链表),如下图所示:

img

如上图所示,当数据包进入本地(LOCAL_IN 挂载点)时,就会相继调用 ipt_hook 和 fw_confirm 钩子函数来处理数据包。另外,钩子函数还有优先级,优先级越小越先执行。

正因为挂载点是通过链表来存储钩子函数,所以挂载点又被称为 链,挂载点对应的链名称如下所示:

  • LOCAL_IN 挂载点:又称为 INPUT链。
  • LOCAL_OUT 挂载点:又称为 OUTPUT链。
  • FORWARD 挂载点:又称为 PORWARD链。
  • PRE_ROUTING 挂载点:又称为 PREROUTING链。
  • POST_ROUTING 挂载点:又称为 POSTOUTING链。

什么是 iptables

iptables 是建立在 Netfilter 之上的数据包过滤器,也就是说,iptables 通过向 Netfilter 的挂载点上注册钩子函数来实现对数据包过滤的。那么就有两个问题挂载在哪 ,挂了什么钩子函数 , 认识完这两个问题 , 那么 iptables 就通透了 .
iptables 里有很有规则表 ,把过滤的种类进行分类就分成了各种表 ,每一种表挂载链路的链路不同 , 例如 Filter表只能挂载在以下三个挂载点

  • INPUT链
  • OUTPUT链
  • PORWARD链

Filter表

Filter表 用于过滤数据包。是 iptables 的默认表,因此如果你配置规则时没有指定表,那么就默认使用 Filter表

NAT表

NAT表 用于对数据包的网络地址转换(IP、端口)

Mangle表

Mangle表 用于修改数据包的服务类型或TTL,并且可以配置路由实现QOS

Raw表

Raw表 用于判定数据包是否被状态跟踪处理

img

各个表挂载到不同的链路如图所示 .关于 iptables 的使用就不多介绍了

路由

    该章节来自飞哥的网络文章 : https://mp.weixin.qq.com/s/UHYE6vwMffaAb-o5eNMrDg ,大部分非原创, 小部分补充说明

我们先了解一下路由位于哪里 ,作用是什么 .

img

img

上图是硬件路由器的功能 ,起到路由的作用 , 我们期望的虚拟化中的路由器也希望能够达到这种效果 , 但是请注意硬件中的路由器虚拟化中路由是有区别的 , 硬件中的路由器是数据包在需要主机的时候用到的, 而虚拟化中路由是需要对应的容器或是其他有IP的设备时用到的.

深入路由-发送数据

先看一下使用到路由的地方
img

这张图我们有点熟呀!! 上篇文章我们学习容器网路-1 的时候给出了飞哥介绍网络发送, 接收的过程中就有这么一个图!

我这里再放出来一下 :

img

注意网络层 这里

img

网络层发送的入口函数是 ip_queue_xmit

//file: net/ipv4/ip_output.c
int ip_queue_xmit(struct sk_buff *skb, struct flowi *fl)
{
 // 路由选择过程
 // 选择完后记录路由信息到 skb 上
 rt = (struct rtable *)__sk_dst_check(sk, 0);
 if (rt == NULL) {
  // 没有缓存则查找路由项
  rt = ip_route_output_ports(...);
  sk_setup_caps(sk, &rt->dst);
 }
 skb_dst_set_noref(skb, &rt->dst);
 ...
 //发送
 ip_local_out(skb);
}

在 ip_queue_xmit 里我们开头就看到了路由项查找, ip_route_output_ports 这个函数中完成路由选择。路由选择就是到路由表中进行匹配,然后决定使用哪个网卡发送出去。
Linux 中最多可以有 255 张路由表,其中默认情况下有 local 和 main 两张。使用 ip 命令可以查看路由表的具体配置。拿 local 路由表来举例。

#ip route list table local
local 10.143.x.y dev eth0 proto kernel scope host src 10.143.x.y
local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1

深入路由-接收数据

接收数据包的时候也需要进行路由选择。这是因为 Linux 可能会像路由器一样工作,将收到的数据包通过合适的网卡将其转发出去。

Linux 在 IP 层的接收入口 ip_rcv 执行后调用到 ip_rcv_finish。在这里展开路由选择。如果发现确实就是本设备的网络包,那么就通过 ip_local_deliver 送到更上层的 TCP 层进行处理。
img

如果路由后发现非本设备的网络包,那就进入到 ip_forward 进行转发,最后通过 ip_output 发送出去。
img

linux 路由小结

img
网络包在发送的时候,需要从本机的多个网卡设备中选择一个合适的发送出去。网络包在接收的时候,也需要进行路由选择,如果是属于本设备的包就往上层送到网络层、传输层直到 socket 的接收缓存区中。如果不是本设备上的包,就选择合适的设备将其转发出去。
这里也可以看到了一个数据包是如何从硬件到软件链路传输的.

路由表

刚才讲到了路由有Linux 中最多可以有 255 张路由表,其中默认情况下有 local 和 main 两张 (我们在使用 window 系统时候 ,不也有 host 文件 ,功能有点相似) , 每个网络命名空间都有自己独立的路由表。

img

在默认情况下,Linux 只有 local 和 main 两个路由表。如果内核编译时支持策略路由,那么管理员最多可以配置 255 个独立的路由表。

如果你的服务器上创建了多个网络命名空间的话,那么就会存在多套路由表。以除了默认命名网络空间外,又创了了一个新网络命名空间的情况为例,路由表在整个内核数据结构中的关联关系总结如下图所示。

img

这里上面的命名空间指的是 进程命名空间(Pid Namespace ),下面的命名空间指的是 网络命名空间(Network Namespace ).

路由总结

img

Docker 的网络实现

标准的Docker 支持以下4 类网络模式。

  • host 模式:使用一net=host 指定。
  • container 模式:使用--net=container:NAME or ID 指定。
  • none 模式:使用一net=none 指定。
  • bridge 模式:使用一net=bridge 指定,为默认设置。
    在Kubemetes 管理模式下,通常只会使用bridge 模式,所以本节只介绍bridge 模式下Docker是如何支持网络的。

Docker 在 bridge模式下的网路配置

在bridge 模式下, Docker Daemon 第1 次启动时会创建一个虚拟的网桥,默认的名字是docker0,然后按照RPC1918 的模型,在私有网络空间中给这个网桥分配一个子网。针对由Docker创建出来的每一个容器,都会创建-个虚拟的以太网设备(Veth 设备对),其中一端关联到网桥上,另一端使用Linux 的网络命名空间技术,映射到容器内的eth0 设备(其实是一台 veth),然后从网桥的地址段内给eth0 接口分配一个IP 地址。
img

其中ipl 是网桥的IP 地址, Docker Daemon 会在几个备选地址段里给它选一个,通常是172开头的一个地址。这个地址和主机的IP 地址是不重叠的。ip2 是Docker 在启动容器的时候,在这个地址段随机选择的一个没有使用的IP 地址, Docker 占用它并分配给了被启动的容器。相应的MAC 地址也根据这个IP 地址,在02:42:ac: 11 :00:00 02:42:ac:11:ff:ff 的范围内生成,这样做可以确保不会有ARP 的冲突。

启动后, Docker 还将Veth 对的名字映射到了ethO 网络接口。ip3 就是主机的网卡地址。在一般情况下, ipl 、ip2 和ip3 是不同的IP 段,所以在默认不做任何特殊配置的情况下,在外部是看不到ipl 和ip2 的。

这样做的结果就是,同一台机器内的容器之间可以相互通信。不同主机上的容器不能够相互通信。实际上它们甚至有可能会在相同的网络地址范围内(不同的主机上的docker0的地址段可能是一样的)。

为了让它们跨节点互相通信,就必须在主机的地址上分配端口,然后通过这个端口路由或代理到容器上。这种做法显然意味着一定要在容器之间小心谨慎地协调好端口的分配,或者使用动态端口的分配技术。

在不同应用之间协调好端口分配是十分困难的事情,特别是集群水平扩展的时候。而动态的端口分配也会带来高度复杂性,例如:每个应用程序都只能将端口看作一个符号(因为是动态分配的,无法提前设置)。而且API Server 也要在分配完后,将动态端口插入到配置的合适位置。另外,服务也必须能互相之间找到对方等。这些都是Docker 的网络模型在跨主机访问时面临的问题。

下面我们再来看一下各种情况下 docker 在网络中做了哪些操作 .
(1) 启动 docker 进程
img

img

img

可以看到, Docker 创建了docker0 网桥,井添加了Iptables 规则。dockerO网桥和Iptables
规则都处于root 命名空间中。通过解读这些规则,我们发现,在还没有启动任何容器时,如果
启动了Docker Daemon,那么它就己经做好了通信的准备。

(2) 容器启动后的情况
docker run 了一个容器
img

(iptables 没变化就没截图出来了)

  1. 宿主机器上的Netfilter 和路由表都没有变化,说明在不进行端口映射时, Docker 的默认网络是没有特殊处理的。相关的NAT 和FILTER 两个Netfilter 链还是空的。
  2. 宿主机上的Veth对已经建立,并连接到了容器内。

我们再次进入刚刚启动的容器内,看看网络技是什么情况。容器内部的IP 地址和路由如下:
img

我们可以看到,默认停止的回环设备lo 已经被启动,外面宿主机连接进来的Veth设备也被命名成了eth0 ,井且已经配置了地址172.170.1.0 。路由信息表包含一条到docker0的子网路由和一条到docker0的默认路由。

(3) 映射容器端口到宿主机上

img

从新增的规则可以看出, Docker 服务在NAT 和FILTER 两个表内添加的两个DOCKER 子链都是给端口映射用的。例如本例中我们需要把外面宿主机的1180 端口映射到容器的5000 端口。

通过前面的分析我们知道,无论是宿主机接收到的还是宿主机本地协议技发出的,目标地址是本地IP 地址的包都会经过NAT 表中的DOCKER 子链。Docker 为每一个端口映射都在这个链上增加了到实际容器目标地址和目标端口的转换。

K8s的网络实现

k8s 网络的实现 ,在于解决以下的问题.

  • 容器到容器之间的直接通信。
  • 抽象的Pod 到Pod 之间的通信。
    • 同 Node 下的 Pod 的通信
    • 不同 Node 下的 Pod 的通信
  • Pod 到Service 之间的通信。
  • 集群外部与内部组件之间的通信。

最后两个在之前的文章介绍过, 下面所以第一,第二点

容器到容器之间的直接通信

两个容器在一个网桥中,可以直接通信 ip为本机即可, 例如 localhost

Pod 到Pod: 同 Node 下

Node 下 , 也是共享通过一个网桥 ,所以通信决定是没问题的
img

Pod 到Pod: 不同 Node 下

Pod 的地址是与dockel。在同一个网段内的,我们知道dockerO 网段与宿主机网卡是两个完全不同的IP 网段,并且不同Node 之间的通信只能通过宿主机的物理网卡进行,因此要想实现位于不同Node 上的Pod 容器之间的通信,就必须想办法通过主机的这个IP 地址来进行寻址和通信。

另一方面,这些动态分配且藏在docker0之后的所谓"私有" IP 地址也是可以找到的。Kubemetes 会记录所有正在运行Pod 的IP 分配信息,并将这些信息保存在etcd 中(作为Service的Endpoint) 。这些私有IP 信息对于Pod 到Pod 的通信也是十分重要的,因为我们的网络模型
要求Pod 到Pod 使用私有IP 进行通信。

之前提到, Kubemetes 的网络对Pod 的地址是平面的和直达的,所以这些Pod 的IP 规划也很重要,不能有冲突。只要没有冲突,我们就可以想办法在整个Kubemetes 的集群中找到它。

综上所述,要想支持不同Node 上的Pod 之间的通信,就要达到两个条件:

  • 在整个Kubemetes 集群中对Pod 的IP 分自己进行规划,不能有冲突:

  • 找到一种办法,将Pod 的IP 和所在Node 的IP 关联起来,通过这个关联让Pod 可以互
    相访问。

      根据条件2 的要求. Pod 中的数据在发出时,需要有一个机制能够知道对方Pod 的IP 地址挂在哪个具体的Node 上。也就是说先要找到Node 对应宿主机的IP 地址,将数据发送到这个宿主机的网卡上,然后在宿主机上将相应的数据转到具体的docker0上。一旦数据到达宿主机Node. 则那个Node 内部的dockerO便知道如何将数据发送到Pod 。
    

img

我们直接看一下开源网络框架的实现吧

开源网络框架 - Flannel

    本节大部分来自 : https://www.open-open.com/news/view/1aa473a

Flannel实质上是一种“覆盖网络(overlay network)”,也就是将TCP数据包装在另一种网络包里面进行路由转发和通信,目前已经支持UDP、VxLAN、AWS VPC和GCE路由等数据转发方式。

数据从源容器中发出后,经由所在主机的docker0虚拟网卡转发到flannel0虚拟网卡,这是个P2P的虚拟网卡,flanneld服务监听在网卡的另外一端。

img

Flannel通过Etcd服务维护了一张节点间的路由表,

源主机的flanneld服务将原本的数据内容UDP封装后根据自己的路由表投递给目的节点的flanneld服务,数据到达以后被解包,然后直 接进入目的节点的flannel0虚拟网卡,然后被转发到目的主机的docker0虚拟网卡,最后就像本机容器通信一下的有docker0路由到达目标容器。

问题1 : 为什么每个节点上的Docker会使用不同的IP地址段?

为了不让ip冲突
这个事情看起来很诡异,但真相十分简单。其实只是单纯的因为Flannel通过Etcd分配了每个节点可用的IP地址段后,偷偷的修改了Docker的启动参数,见下图。
img

这个是在运行了Flannel服务的节点上查看到的Docker服务进程运行参数。

注意其中的“--bip=172.17.18.1/24”这个参数,它限制了所在节点容器获得的IP范围。

这个IP范围是由Flannel自动分配的,由Flannel通过保存在Etcd服务中的记录确保它们不会重复。

问题2 : 为什么在发送节点上的数据会从docker0路由到flannel0虚拟网卡,在目的节点会从flannel0路由到docker0虚拟网卡?

我们来看一眼安装了Flannel的节点上的路由表。下面是数据发送节点的路由表:
img
这个是数据接收节点的路由表:

img

例如现在有一个数据包要从IP为172.17.18.2的容器发到IP为172.17.46.2的容器。根据数据发送节点的路由表,它只与 172.17.0.0/16匹配这条记录匹配,因此数据从docker0出来以后就被投递到了flannel0。同理在目标节点,由于投递的地址是一个容 器,因此目的地址一定会落在docker0对于的172.17.46.0/24这个记录上,自然的被投递到了docker0网卡。

总结一下 Flannel 是如何解决我们上面讲的两个问题的

  • 利用etcd 来管理可分配的IP 地址段资源,同时监控etcd 中每个Pod 的实际地址,并在内存中建立了一个Pod 节点路由表 , 然后再启动容器时增加参数使得ip 不会冲突
  • 增加一个网桥和路由,使得数据能够路由和转发

其他

NAT (Network Address Translation,网络地址转换)

最典型的一个应用就是外网地址到内网地址的转换 , 例如外界需要发送一个包到内网的 192.168.0.234 那么外界TCP 发送的时候 IP 层的目的IP肯定不是192.168.0.234 而是网络的入口地址 , 然后经过地址转换才得到这个数据包原来是发送给 192.168.0.234 的 .

参考资料

posted @   float123  阅读(178)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示