容器中的网络

容器中的网络

单机网络

docker 容器是一种特殊的进程,docker 容器在创建进程时,指定了这个进程所需要启用的一组 Namespace 参数。这样,容器就只能“看”到当前 Namespace 所限定的资源、文件、设备、状态,或者配置。而对于宿主机以及其他不相关的程序,它就完全看不到了。

容器可以直接使用宿主机的网络栈(–net=host),即:不开启 Network Namespace,比如:

$ docker run -d --net=host --name nginx-host nginx

在这种情况下,这个容器启动后,直接监听的就是宿主机的 80 端口。

通常情况下,容器一般会选择使用自己 Network Namespace 里的网络栈,即:拥有属于自己的 IP 地址和端口。这种情况下,被隔离的容器进程是如何和其他的容器进程进行通信呢?

Docker 项目会默认在宿主机上创建一个名叫 docker0 的网桥,凡是连接在 docker0 网桥上的容器,就可以通过它来进行通信。容器中通过 Veth Pair 来连接到 docker0 网桥上。

什么是 Veth Pair 呢?

veth pair 是成对出现的一种虚拟网络设备接口,一端连着网络协议栈,一端彼此相连。

Veth Pair 的特点总是以两张虚拟网卡(Veth Peer)的形式成对出现的。从其中一个"网卡"发出的数据包,可以直接出现在与它对应的另一张"网卡"上,这两张网卡可以在不同的 Network Namespace 里。所以 Veth Pair 可以用来进行跨 Network Namespace 网络互联。

先来启动两个容器

$ docker run -d  --name nginx-1 nginx:alpine

$ docker run -d  --name nginx-2 nginx:alpine

查看宿主机的网络

$ ifconfig
docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.17.0.1  netmask 255.255.0.0  broadcast 172.17.255.255
        inet6 fe80::42:f8ff:fe43:b13a  prefixlen 64  scopeid 0x20<link>
        ether 02:42:f8:43:b1:3a  txqueuelen 0  (Ethernet)
        RX packets 19  bytes 532 (532.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 25  bytes 1278 (1.2 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
...

veth228debd: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet6 fe80::10f4:65ff:fe6f:56d2  prefixlen 64  scopeid 0x20<link>
        ether 12:f4:65:6f:56:d2  txqueuelen 0  (Ethernet)
        RX packets 17  bytes 1554 (1.5 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 58  bytes 5156 (5.0 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

veth28bb8da: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet6 fe80::789f:75ff:fec5:5601  prefixlen 64  scopeid 0x20<link>
        ether 7a:9f:75:c5:56:01  txqueuelen 0  (Ethernet)
        RX packets 50  bytes 4508 (4.4 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 27  bytes 2286 (2.2 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
        
$ brctl show
bridge name	bridge id		STP enabled	interfaces
docker0		8000.000000000000	no		veth228debd
										veth28bb8da

可以看到 docker0 中被插入了 veth228debd 和 veth28bb8da 两个虚拟网卡,这个就是容器对应的 Veth Pair 设备,在宿主机上的虚拟网卡。连个容器,一个对应一个网卡。

看下容器中的网络

$ docker exec -it nginx-1 /bin/sh
$ ifconfig
eth0      Link encap:Ethernet  HWaddr 02:42:AC:11:00:02
          inet addr:172.17.0.2  Bcast:172.17.255.255  Mask:255.255.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:825 errors:0 dropped:0 overruns:0 frame:0
          TX packets:859 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:76962 (75.1 KiB)  TX bytes:79974 (78.0 KiB)

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

容器中有一个 eth0 网卡,这个就是一个 Veth Pair,它的一端在容器的 Network Namespace,另一端位于宿主机上(Host Namespace),并且被“插”在了宿主机的 docker0 网桥上。

这些虚拟网卡被插在网桥中,这些虚拟网卡调用网络协议栈处理数据包的能力,会被网桥接管,由网桥来处理。

1、当容器1访问容器2的时候,首先容器1会通过 eth0 网卡发送一个 ARP 广播,来查找对方的 MAC 地址;

2、当收到这些请求的时候,docker0 网桥就会扮演二层交换机的角色,会把 ARP 广播转发到其他被“插”在 docker0 上的虚拟网卡上;

3、这样,同样连接在 docker0 上的容器2的网络协议栈就会收到这个 ARP 请求,从而将自己的 MAC 地址回复给容器1;

4、有了对方的 MAC 地址,这样就能将请求发送出去了,

5、这样多个容器通过 Veth Pair 打通自己和 docker0 网桥的网络,就能实现相互的网络通信。

k8s

所以被限制在 Network Namespace 里的容器进程,实际上是通过 Veth Pair 设备 + 宿主机网桥的方式,实现了跟同其他容器的数据交换。

宿主机访问宿主机中的容器 IP,宿主机的数据包也是首先被发送到 docker0 网桥,然后通过绑定在网桥中的的 Veth Pair 设备,最终发送到容器中。

两台宿主机中的容器通信见下文

跨主机网络

单机的情况下使用(网桥模式)就是实现不同容器之间的 IP 互相访问,但是容器位于不同的宿主机中,在 Docker 的默认配置下就不能通过 IP 相互访问了。

不同主机中的容器如何通信,这就涉及到跨主机网络访问的原理了,其中 Flannel 就是一种实现。

Flannel 项目是 CoreOS 公司主推的容器网络方案。

Flannel 的工作原理

Flannel 实质上就是一种 Overlay 网络,也就是将 TCP 数据包装在另一种网络包里面进行路由转发和通信。

Flannel 会在每一个宿主机上运行名为 flanneld 代理,主要实现 overlay 网络, 包含给所有 node 分配 subset 段等。Flannel 管理的容器网络中,一台宿主机上所有的容器,都属于该宿主机被分配的一个子网。不同的宿主机,是不同的子网段。

这些子网与宿主机的对应关系,会保存在 Etcd 中。

Flannel 的工作模式

Flannel有下面三种后端实现:

  • UDP 模式:使用设备 flannel.0 进行封包解包,不是内核原生支持,频繁地内核态用户态切换,性能非常差;

  • VXLAN 模式:使用 flannel.1 进行封包解包,内核原生支持,性能较强;

  • host-gw 模式:无需 flannel.1 这样的中间设备,直接宿主机当作子网的下一跳地址,性能最强;

UDP

UDP 模式是 Flannel 项目最早支持的一种方式,也是性能最差的一种方式。这个模式目前已经被启用了。

这个模式是最直接,最容易理解的容器跨主网络实现,所以这里先来看下 UDP 模式的具体实现。

Flannel 进行 UDP 封包与解包都是在用户态通过 tun 设备(flannel0)来实现的,所以在了解 UDP 模式实现之前先来看下 tun 设备的原理。

tun/tap 原理

tun/tap 设备是操作系统内核中的虚拟网络设备,是用软件模拟的网络设备,提供与硬件网络设备完全相同的功能。主要用于用户空间和内核空间传递报文。

k8s

tun/tap 设备与网络设备的区别:

1、对于硬件网络设备而言,一端连接的是物理网络,一端连接的是网络协议栈;

2、对于 tun/tap 设备而言,一端连接的是应用程序(通过字符设备文件 /net/dev/tun),一端连接的是网络协议栈。

操作系统通过 tun/tap 设备向绑定该设备的用户空间的程序发送数据,反之,用户空间的程序可以像操作硬件网络设备那样,通过 tun/tap 设备发送数据。

tun/tap 有两种工作模式,tun 模式 与 tap 模式。tun 设备与 tap 设备工作方式完全相同,区别在于:

1、Tun 设备是一个三层设备,从 /dev/net/tun 字符设备上读取的是 IP 数据包,写入的也只能是 IP 数据包,因此不能进行二层操作,如发送 ARP 请求和以太网广播,常用于一些点对点IP隧道,例如 OpenVPN,IPSec 等;

2、Tap 设备是二层设备,处理的是二层 MAC 层数据帧,从 /dev/net/tun 字符设备上读取的是 MAC 层数据帧,写入的也只能是 MAC 层数据帧。从这点来看, Tap 虚拟设备和真实的物理网卡的能力更接近,可以与物理网卡做 bridge,常用来作为虚拟机模拟网卡使用。

工作原理

k8s

tun/tap 提供与硬件网络设备完全相同的功能,和物理网卡一样,它们的一端连的都是网络协议栈,不同的是,物理网卡一端连接的是物理网络,tun/tap 一端连接的是一个应用层程序。

物理网卡直接连接物理网络接收数据,tun/tap 则通过字符设备文件 /net/dev/tun 作为数据的传输通道,这种方式可以对数据包进行一些自定义的修改(比如封装成 UDP),然后又通过网络协议栈发送出去——这就是目前大多数“代理”的工作原理。

tun/tap 提供的虚拟网卡驱动,从 tcp/ip 协议栈的角度而言,它与真实网卡驱动并没有区别。

了解完 tun/tap 的工作模之后,在来看下 Flannel 中 UDP 模式的实现。

Flannel 会在每一个宿主机上运行名为 flanneld 代理,主要实现 overlay 网络, 包含给所有 node 分配 subset 段等。Flannel 管理的容器网络中,一台宿主机上所有的容器,都属于该宿主机被分配的一个子网。

所以对于 flanneld 来说,只要 Node1 和 Node2 是互通的,那么 flanneld 作为 Node1 上的一个普通进程,就能通过自己的子网号,然后从 Etcd 中找到这个子网对应的宿主机的 IP 地址,进而访问到节点 Node2。

来看下 Flannel 中 UDP 模式的工作过程:

1、node1 中的 container-1 容器访问 node2 的容器 container-2,首先 Node1 中的容器使用 Veth Pair 将请求发送到 Node1 中的 docker0 网桥,然后 docker0 网桥将请求转发到 flanneld;

2、flanneld 在收到 container-1 发给 container-2 的 IP 包之后,就会把这个 IP 包直接封装在一个 UDP 包里,然后发送给 Node2,这个 UDP 包的源地址就是 就是 flanneld 所在的 Node1 的地址,目的地址就是 container-2 所在的宿主机 Node2 的地址;

3、每台宿主机上的 flanneld,都监听着一个 8285 端口,只要把 UDP 数据发送到节点中的 8285 端口即可;

4、宿主机之间通过 UDP 通信,就能将数据发送到对方,节点中监听 8285 端口的进程 flanneld,就能从 UDP 数据包中解析出容器的原 IP 数据包;

5、node2 中的 flanneld 会把接收到的 IP 包,交给 flannel0 中,flannel0 是一个 TUN 设备,Flannel 进程向 flannel0 的数据包,会从用户态转向内核态,交给 Linux 内核网络栈处理;

6、Linux 内核网络栈,会根据本机的路由决定 IP 数据包的下一步流向,这个数据包按照规则会被转发到 docker0 网桥;

7、这样 docker0 网桥会扮演二层交换机的角色,配合 Veth Pair,就能找到对应的容器,进行数据的发送。

k8s

Flannel UDP 模式提供的其实是一个三层的 Overlay 网络,发送端会把 IP 进行 UDP 封装,接收端收到请求,会解析出原 IP 数据包,然后发送给目标容器。

Flannel 在不同宿主机上的两个容器之间打通了一条“隧道”,使得这两个容器可以直接使用 IP 地址进行通信,而无需关心容器和宿主机的分布情况。

Flannel UDP 模式存在严重的性能问题,所以已经被废弃了。

Flannel UDP 模式下容器通信多了一个 flanneld 的处理过程,flanneld 会使用到 flannel0 这个 TUN 设备,仅在 IP 包的发出过程中就会涉及到三次用户态和内核态之间的数据拷贝。

k8s

具体的过程

1、用户态的容器进程发出的 IP 包经过 docker0 网桥进入内核态;

2、IP 包根据路由表进入 TUN(flannel0)设备,从而回到用户态的 flanneld 进程;

3、flanneld 进行 UDP 封包之后重新进入内核态,将 UDP 包通过宿主机的 eth0 发出去。

这些上下文切换和用户态操作的代价是非常高的,所以这就是造成 Flannel UDP 模式性能不好的主要原因。

VXLAN

因为 Flannel UDP 模式性能不好,后来 Flannel 开始支持 VXLAN 模式,并逐渐成为了主流的容器网络方案。

VXLAN,即 Virtual Extensible LAN(虚拟可扩展局域网),是 Linux 内核本身就支持的一种网络虚似化技术。

不同与 TUN 设备的 flannel0,VXLAN 可以在内核中就能进行 vxlan 协议报文封装和解封的过程,不会涉及到内核态和用户态的上下文的切换,所以不存在类似 Flannel UDP 模式下的性能瓶颈。

VXLAN 的覆盖网络的设计思想是:通过现有的三层网络,来构建虚拟的由内核 VXLAN 模块负责维护的二层网络,使得连接在这个 VXLAN 二层网络上的“主机”(虚拟机或者容器都可以)之间,可以像在同一个局域网(LAN)里那样自由通信。当然,实际上,这些“主机”可能分布在不同的宿主机上,甚至是分布在不同的物理机房里。

VXLAN 中使用 VTEP(VXLAN Tunnel End Point(虚拟隧道端点)) 来打通这个二层网络。

VTEP 是 VXLAN 网络中绝对的主角,VTEP 既可以是一台独立的网络设备,也可以是在服务器中的虚拟交换机。源服务器发出的原始数据帧,在 VTEP 上被封装成 VXLAN 格式的报文,并在IP网络中传递到另外一个 VTEP 上,并经过解封转还原出原始的数据帧,最后转发给目的服务器。

VTEP 设备和前面的 flanneld 进程非常相似,不过 VTEP 处理的是二层数据帧(Ethernet frame),并且这个过程是在内核中完成的。

k8s

1、容器发出的请求首先会到 docker0 网桥,然后会被路由到本机 flannel.1(VTEP 设备) 设备进行处理。VTEP 是有 MAC 地址和 IP 地址的;

2、VTEP 设备收到发送端的原始 IP 后,就会加上一个目的 MAC 地址,封装成一个二层数据帧,然后发送给“目的 VTEP 设备”;

不过发送端是如何知道接收端的 MAC 地址和 IP 地址的呢?

首先当有节点加入到 Flannel 网络之后,其他所有节点就会记录一条,当前添加节点的 VTEP 设备(也就是 flannel.1 设备)的 IP 地址。

有了目的 VTEP 设备的 IP 地址之后,需要根据三层 IP 地址查询对应的二层 MAC 地址,这正是 ARP(Address Resolution Protocol )表的功能。

同样,当有节点加入到 Flannel 网络之后,会在每台节点启动时把它的 VTEP 设备对应的 ARP 记录,直接下放到其他每台宿主机上。这样通过 ARP 表,就能找到目的设备了。

Linux 内核会把这个数据帧封装进一个 UDP 包里发出去。

k8s

3、宿主机网络进行封包;

UDP 包是一个四层数据包,所以 Linux 内核会在它前面加上一个 IP 头,目的主机的 IP,组成一个 IP 包,进行发送。

k8s

host-gw

flannel 的 udp 模式和 vxlan 模式都是属于隧道方式,也就是在 udp 的基础之上,构建虚拟网络。

host-gw 模式属于路由的方式,由于没有经过任何封装,无需 flannel.1 这样的中间设备,直接宿主机当作子网的下一跳地址,性能最强。

关于 host-gw 的详细分析,下篇文章在介绍吧。

总结

1、docker 容器的网络都是连接在 docker0 网桥上的,容器中所有的流量都由 docker0 网桥转发出去。

2、同一宿主机中的容器进行通信,借助于 Veth Pair 设备 + 宿主机网桥的方式,就能和其他容器的数据交换;

3、veth pair 是成对出现的一种虚拟网络设备接口,一端连着网络协议栈,一端彼此相连。Veth Pair 的特点总是以两张虚拟网卡(Veth Peer)的形式成对出现的。从其中一个"网卡"发出的数据包,可以直接出现在与它对应的另一张"网卡"上,这两张网卡可以在不同的 Network Namespace 里。所以 Veth Pair 可以用来进行跨 Network Namespace 网络互联;

4、容器跨主机通信就需要借助于一些网络插件来实现了,例如 Flannel;

5、Flannel 有三种工作模式;

  • UDP 模式:使用设备 flannel.0 进行封包解包,不是内核原生支持,频繁地内核态用户态切换,性能非常差;

  • VXLAN 模式:使用 flannel.1 进行封包解包,内核原生支持,性能较强;

  • host-gw 模式:无需 flannel.1 这样的中间设备,直接宿主机当作子网的下一跳地址,性能最强;

参考

【深入剖析 Kubernetes】https://time.geekbang.org/column/intro/100015201?code=UhApqgxa4VLIA591OKMTemuH1%2FWyLNNiHZ2CRYYdZzY%3D
【循序渐进理解CNI机制与Flannel工作原理】https://blog.yingchi.io/posts/2020/8/k8s-flannel.html
【 tun/tap 网络】https://zhuanlan.zhihu.com/p/462501573
【网络虚拟化技术(二): TUN/TAP MACVLAN MACVTAP】https://blog.kghost.info/2013/03/27/linux-network-tun/
【vxlan 协议原理简介 】https://cizixs.com/2017/09/25/vxlan-protocol-introduction/
【容器中的网络】https://boilingfrog.github.io/2022/12/28/容器中的网络/

posted on 2022-12-28 19:44  ZhanLi  阅读(368)  评论(0编辑  收藏  举报