0%

Kubernetes 网络权威指南(02):容器网络模型简介

软件运行时需要依赖一系列环境(称为 runtime),而环境的配置是非常麻烦的一件事。所以一个直接的想法就是:软件能否带环境安装?虚拟机的快照就是带环境安装就是一种解决方案,但是虚拟机又有占用资源多,启动慢等缺点。因此 Linux 发展出了另一种虚拟化技术:容器。

容器

容器不是模拟一个完整的操作系统,而是利用操作系统提供的 namespace 机制提供资源隔离。对于容器里的进程来说,可以认为自己是独享这些资源的。容器是进程级别的隔离技术,因此相比虚拟机有启动快、占用资源少、体积小等优点。

目前最流行的 Linux 容器应该是 Docker 了,它是对 Linux 底层容器技术的一种封装,提供简单易用的容器使用接口。Docker 把应用程序和与该程序的依赖打包在同一个文件里,即 Docker Image。运行 Docker Image 即得到一个 Docker 容器。程序在虚拟容器里运行就如同在物理机/虚拟机上运行。

不同容器之间共享操作系统内核,Docker Engine 进行资源分配调度并调用 Linux 内核 namespace API 进行隔离。

Docker 的四大网络模式

利用 network namespace 可以为 Docker 容器创建隔离的网络环境,容器具有完全独立的网络战,与宿主机隔离。当然也可以让容器共享主机或其他容器的 network namespace

容器的网络方案可以分为三大部分:

  • 单机的容器间通信
  • 跨主机的容器间通信
  • 容器与主机间通信

在使用 docker run 命令创建 Docker 容器时,可以使用 --network 选项指定容器的网络模式:

  • bridge 模式:通过 --network=bridge 指定
  • host 模式:通过 --network=host 指定
  • container 模式:--network=container:NAME_or_ID 指定,即 joiner 容器
  • none 模式:通过 --network=none 指定

安装完 docker 之后,Docker 引擎会自动在宿主机上创建三个网络,分别是 bridgehostnone 网络:

1
2
3
4
5
# docker network ls
NETWORK ID NAME DRIVER SCOPE
b874123b3da8 bridge bridge local
70614cd7dc51 host host local
4ee62deefaea none null local

bridge 模式

Docker 在安装后会创建一个名为 docker0 的网桥。bridge 模式是 Docker 默认的网络模式,如果没有指定 --network,Docker 会为每个容器分配 network namespace、设置 IP 等等,并将 Docker 容器连接到 docker0 网桥上(通过 veth pair)。

初始情况下,docker0 网桥没有连接任何网络设备:

1
2
3
# brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.024221cbae88 no

在运行一个 docker 容器后,docker0 网桥上就新挂载了一个新的网卡。这是因为新建容器时,Docker 会创建一个 veth pair,其中一端连接 docker0 网桥,另一端则为 docker 容器自身的网卡。

1
docker run -it ubuntu bash

宿主机上:

1
2
3
4
5
6
7
8
9
10
11
12
13
# brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.024221cbae88 no veth5b415b5

# ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
link/ether fa:20:20:1c:a8:45 brd ff:ff:ff:ff:ff:ff
10: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
link/ether 02:42:21:cb:ae:88 brd ff:ff:ff:ff:ff:ff
46: veth5b415b5@if45: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default
link/ether f2:07:16:71:9a:d7 brd ff:ff:ff:ff:ff:ff link-netnsid 2

在容器内,查看接口的iflink,可以确认和 veth5b415b5(ifindex 46) 是一对 veth pair

1
2
# cat /sys/class/net/eth0/iflink
46

默认情况下,docker0 的 IP 地址是 172.17.0.1,而连接到 docker0 上的容器的 IP 地址范围则是 172.17.0.0/24,这些容器的默认网关都是 docker0。即访问非本机容器网段要经过 docker0 网关转发,而本机同网段之间则直接通过 docker0 网桥进行二层通信。

宿主机内:

1
2
3
4
5
6
7
8
9
10
# ip -br addr show docker0
docker0 UP 172.17.0.1/16 fe80::42:21ff:fecb:ae88/64

# route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 192.168.64.1 0.0.0.0 UG 100 0 0 eth0
169.254.169.254 192.168.64.2 255.255.255.255 UGH 100 0 0 eth0
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
192.168.64.0 0.0.0.0 255.255.240.0 U 100 0 0 eth0

容器内:

1
2
3
4
5
6
7
8
9
# ip -br addr show
lo UNKNOWN 127.0.0.1/8
eth0@if46 UP 172.17.0.2/16

# route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 172.17.0.1 0.0.0.0 UG 0 0 0 eth0
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0

brige 模式为 Docker 容器创建独立的网络栈,保证容器内的进程使用独立的网络环境,使得容器之间、容器与宿主机之间能够实现网络隔离。

host 模式

连接到 host 网络的容器共享 Docker host 的网络栈,容器的网络配置与 host 王权一样。host 模式下容器不会获得独立的 network namespace,而是和宿主机共用一个 network namesapce。容器也不会虚拟出自己的网卡,而是使用宿主机的 IP。

创建使用 host 网络的容器:

1
docker run -it --network=host ubuntu bash

host 模式下可以看到宿主机所有的网卡信息,直接使用宿主机 IP 地址和主机名和外界通信,无需进行 NAT,也无需通过 Linux bridge 进行转发或数据包封装。

容器内:

1
2
3
4
5
6
7
8
9
10
11
12
ip -br link show
lo UNKNOWN 00:00:00:00:00:00 <LOOPBACK,UP,LOWER_UP>
eth0 UP fa:20:20:1c:a8:45 <BROADCAST,MULTICAST,UP,LOWER_UP>
docker0 DOWN 02:42:21:cb:ae:88 <NO-CARRIER,BROADCAST,MULTICAST,UP>

# route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 192.168.64.1 0.0.0.0 UG 100 0 0 eth0
169.254.169.254 192.168.64.2 255.255.255.255 UGH 100 0 0 eth0
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
192.168.64.0 0.0.0.0 255.255.240.0 U 100 0 0 eth0

host 模式有点是没有性能损耗且配置方便,缺点则有:容器没有隔离、独立的网络栈,网络隔离性不好,可能会造成端口资源冲突等问题。

container 模式

创建容器时,使用 --network=container:NAME_or_ID 模式,在创建新的容器时指定容器的网络和一个已经存在的容器共享一个 network namespace,该容器本身不会进行任何网络配置,这个 Docker 容器没有网卡、IP、路由等信息。需要注意,container 模式只能和已经存在的某一个容器共享 network namespace,但是不能和宿主机共享。

两个容器的进程可以通过 lo 网卡设备通信,Kubernetes 的 Pod 网络采用的就是 Docker 的 container 模式网络。

none 模式

使用 --network=none 来指定 none 模式网络。none 模式下的容器只有 lo 环回网络,没有其他网卡。这种模式下容器拥有自己的 network namespace,但是并没有为容器进行任何网络配置。

Docker 网络技巧

接下来介绍 Docker 中的网络配置、网络端口映射、容器互联、DNS 配置等。

查看容器 IP

在容器外面,可以通过 docker inspect 的方式查看容器 IP:

1
2
3
4
docker inspect ef37f5524b81 | grep "IPAddress"
"SecondaryIPAddresses": null,
"IPAddress": "172.17.0.2",
"IPAddress": "172.17.0.2",

在容器内部的话,则可以通过 docker exec 执行 ip/ifconfig 等命令。

端口映射

使用 docker run 运行容器时,可以使用 -P-p 命令进行容器和主机之间的端口映射。-P 是由 Docker 随机分配映射关系,-p 则需要显式指定主机的端口应该映射到容器的哪个端口。

Docker 容器端口映射原理都是在本地的 iptable 的 nat 表中添加相应的规则,将访问本机 IP 地址 Host:Hostport 的网包进行一次 DNAT,转换成容器 ContainerIP:ContainerPort

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# iptables -t nat -nL
Chain PREROUTING (policy ACCEPT)
target prot opt source destination
DOCKER all -- 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT)
target prot opt source destination

Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
MASQUERADE all -- 172.17.0.0/16 0.0.0.0/0
MASQUERADE tcp -- 172.17.0.2 172.17.0.2 tcp dpt:80

Chain OUTPUT (policy ACCEPT)
target prot opt source destination
DOCKER all -- 0.0.0.0/0 !127.0.0.0/8 ADDRTYPE match dst-type LOCAL

Chain DOCKER (2 references)
target prot opt source destination
RETURN all -- 0.0.0.0/0 0.0.0.0/0
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:80 to:172.17.0.2:80
  • 可以看到 DNAT 发生在 DOCKER 这条 iptables chain
  • DOCKER chain 分别在 PREROUTING 和 OUTPUT 链都引用了该 docker chain

访问外网

要从容器访问外网,其原理就是把 Linux 系统作为路由器,因此需要宿主机的 ip_forward 打开了。至于 SNAT/MASQUERADE,Docker 会自动在 iptables 的 POSTROUTING 链上添加如下规则:

1
2
3
Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
MASQUERADE all -- 172.17.0.0/16 0.0.0.0/0

因此从容器网段出来的、访问外网的包,都需要经过一次 MASQUERADE,即出去的包都要用主机的 IP 地址替换为源地址。

DNS 和主机名

容器中的 DNS 和主机名一般通过三个系统配置文件维护:

  • /etc/resolv.conf:创建容器时,默认与本地主机 /etc/resolv.conf 保持一致
  • /etc/hosts:记录容器自身的一些地址和名称
  • /etc/hostname:记录容器的主机名

直接修改容器内的这三个文件可以立即生效,但是容器重启后修改又会失效。如果想统一、持久化配置所有容器的 DNS,需要修改 Docker Daemon 的配置文件:/etc/docker/daemon.json),可以指定除主机 /etc/resolv.conf 的其他 DNS 信息。同时也可以在 docker run 的时候使用 --dns=address 参数来指定,修改容器的主机名可以通过 --hostname=hostname 的方式来指定。

自定义网络

当启动 Docker Daemon 时,可以对 docker0 网桥进行配置:

  • --bip=CIDR 配置接到这个网桥上的 Docker 容器的 IP 地址网段
  • --mtu=BYTES:配置 docker0 网桥的 MTU

一个主机上 Docker 支持多个网络,可以随时创建一个自定义的网络。Docker 的命令行工具支持直接操作网络,如同操作容器一样:

1
2
# docker network create --subnet 172.100.0.0/16 mynet
8a5d6a38c37034687db311a4ef689dd750b8a1046fed7d471c1b4e7cc756e4ca

该例子创建了一个名为 mynet 的网络,默认使用 bridge模式,网段为 172.100.0.0/16,网关地址 172.100.0.1 分配在了网桥本身上:

1
2
3
4
5
# ip -br addr show
lo UNKNOWN 127.0.0.1/8 ::1/128
eth0 UP 192.168.64.7/20 fe80::f820:20ff:fe1c:a845/64
docker0 DOWN 172.17.0.1/16 fe80::42:21ff:fecb:ae88/64
br-8a5d6a38c370 DOWN 172.100.0.1/16

使用 docker network inspect 可以查看某个网络的详细信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# docker network inspect 8a5d6a38c370
[
{
"Name": "mynet",
"Id": "8a5d6a38c37034687db311a4ef689dd750b8a1046fed7d471c1b4e7cc756e4ca",
"Created": "2022-05-07T10:39:39.869970129+08:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "172.100.0.0/16"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {},
"Options": {},
"Labels": {}
}
]

使用 docker network ls 查看主机上所有 network:

1
2
3
4
5
6
# docker network ls
NETWORK ID NAME DRIVER SCOPE
b874123b3da8 bridge bridge local
70614cd7dc51 host host local
8a5d6a38c370 mynet bridge local
4ee62deefaea none null local
  • DRIVER 字段:表示使用的 Docker 容器网络模式
  • docker daemon 启动后,默认会创建创建三个网络,bridgehostnone,对应 docker 的三大网络模式

使用 docker network rm 删除某个网络。使用 docker network connect 连接一个容器到到网络中,使用 docker network disconnect 将容器和网络断开。

发布服务

docker service 子命令是对 Docker 容器的一层封装,有点类似于 Kubernetes service 概念。如下是一个结合 networkservice 的例子:

1
2
3
docker network create -d bridge foo
docker service publish myservice.foo
docker service attach <container-id> myservice.foo

这三条命令先创建一个 Docker 网络,然后在网络中发布一个服务,最后将这个服务和容器绑定。这样就达到了把容器当做服务发布的目的。另外通过 docker run -publish-service 也可以一次性实现以上三个步骤。

该特性是一个遗留特性,在新版本 Docker 中不推荐使用。docker link 就是把两个容器连接起来,相互通信。例如

1
docker run -d nginx --link=<containerName or ID>:alias

这里 alias 是源容器在 link 下的别名(即对 nginx 容器看来,containerName 和 alias 指代同一个容器,都可以作为容器的主机名)。link 方式最大的便利在于容器可以使用容器名进行通信,link 方式仅仅解决了单机容器间的点对点互联问题。

docker 是通过在容器内,修改容器的 /etc/hosts 文件,将容器名和别名都映射为容器 IP 地址,实现 alias 功能的。

容器网络的第一个标准:CNM

围绕 Docker 生态,目前有两种主流的网络接口方案,即 Docker 主导的 Container Network Model(CNM)和 Kubernetes 社区主推的 Container Network Interface(CNI)。

CNM 标准

CNM 主要由 3 个概念:

  • Network Sandbox:容器网络栈,包括网卡、路由表、DNS 配置等等。对应的技术实现有 network namepsace。一个 Sandbox 可能包含来自多个网络(Network)的多个 Endpoint
  • Endpoint 作为 Sandbox 接入 Network 的截止,是 Network Sandbox 和 Backend Network 的中间桥梁。对应的技术实现有 veth pairtap/tun、OVS 内部端口等等
  • Backend Network:一组可以直接相互通信的 Endpoint 的集合。对应的技术实现有 Linux bridge、VLAN

CNM 依赖两个关键的对象来完成 Docker 的网络管理功能:

  • Network Controller:对外提供分配及管理网络的 APIs。Docker libnetwork 提供多个网络驱动,Network Controller 允许绑定特定的驱动到指定的网络
  • Driver:网络驱动对用户而言是不直接交互的,它通过插件式的接入方式提供最终的网络功能。Driver 负责一个 Network 的管理,包括资源分配和回收

通过以上概念和对象,配合 Docker 的生命周期,通过 API 就能完成管理容器网络的功能。另外 CNM 支持标签,通过 lable,可以以 key-value 对定义元数据,从而自定义 libnetwork 和驱动的行为。

Docker daemon 和 CNM 接口主要在如下流程中存在叫魂

  • Create Network
  • Create Container
  • Delete Container
  • Delete Network

处理命令行,CNM 还支持 remote pluginremote plugin 监听一个指定的端口,Docker Daemon 直接通过这个接口与 remote plugin 交互。

libnetwork

CNM 的原生实现就是 libnetwork,它是 Docker 团队将 Docker 的网络功能从 Docker 核心代码库中分离出去的,用 Go 语言实现的一个独立库。Libnetwork 可以通过插件的形式为 Docker 提供网络功能,用户可以根据自己的需要实现网络驱动,以便提供不同的网络功能。

Libnetwork 的 Network Controller 负责将一个 docker 网络和 Network driver 进行对接。每个 Network driver 负责为对接的网络提供服务。Network driver 可以分为原生驱动和远程驱动(第三方插件),原生驱动包括 none、bridge、overlay 和 Macvlan,驱动也可以按照试用范围分为本地的和跨主机的。

无论使用哪种网络类型,Docker Daemon 和 Libnetwork 的交互过程都是一样的。它很好地对各种 network drvier 进行了抽象。这个交互过程大致如下:

  • Docker Daemon 创建 Network Controller 实例
  • Network Controller 创建容器网络,此时就会开始调用真是的 Network drvier 接口
  • 创建 Endpoint,进行 IP 分配,准备网络设备接口
  • 创建 Sandbox,使用容器对应的 network namespace 命令
  • 已有的 Endpoint 加入 Sandbox,完成容器与网络设备的对接

Libnetwork 使得容器与网络分开的,在创建容器之间,就可以先创建好网络,然后决定让容器加入哪个网络。

libnetwork 扩展

libnetwork 支持 remote 类型的 driver,它提供了自定义网络的功能。remote driver 作为服务端,libnetwork 作为客户端,两者通过 JSON 格式的 REST 请求进行交互。这种形式对网络实现的可维护性、可扩展性有很大的好处,从而使得 libnetwork 可以支持千差万别的网络实施方案。

Contiv 就是 Cisco 主导开发的一个 remote driver,它基于 OVS 提供 Docker 容器网络的 SDN 能力,功能上支持 VLAN、VxLAN 和 QoS。

libnetwork 小结

不管是 CNM 还是 CNI,其最终目标都是以一致的编程接口,抽象网络实现。世界上有许多广泛的网络解决方案,它们适配不同的应用场景。当然容器生态圈存在两个网络标准,并不是一件好事。由于 Kubernetes 已经在容器编排领域占据话语权,因此未来的主流可能还是得看 CNI。

容器组网的挑战

Docker Swarm、Mesos 和 Kubernetes 为大规模的容器集群提供了资源调度、服务发现、扩容缩容等功能,但是并未提供网络基础设施。要支持大规模的容器集群,网络是最基础的一环。容器网络的挑战:以隔离的方式部署容器,在提供隔离自己容器内数据所需功能的同时,保持有效的连通性。

Docker 原生的容器网络组网模型,主要死单主机模式,基于以下几个假定:

  • 它充分利用与容器相连接的本地 Linux 网桥
  • 每个宿主机都有集群看的见的公共 IP 地址
  • 每个容器都有集群看不见的专有的 IP 地址
  • 通过 NAT 将容器的专有 IP 地址绑定到主机的公共 IP 地址上
  • iptable 用于容器与用户基础网络之间的网络隔离
  • 负载均衡系统将服务映射到一组容器 IP 地址和端口

容器网络最大的挑战在跨容器通信层面:

  • 原生 Docker 容器不解决跨主机通信问题,而大规模的容器部署必然涉及不同主机上的网络通信
  • 容器重启更加频繁,重启 IP 地址后将发生变化,需要更加高效的网络地址管理
  • 容器 IP 地址的分配是动态的,只有运行后才知道
  • 容器部署数量远大于虚机
  • 大部分容器网络方案都会使用 iptables 和 NAT,造成通信的两端看不到对方真实的 IP 地址,并且 iptable 和 NAT 的结合使用限制了可扩展性和性能
  • 大规模使用 veth 设备会影响网络性能
  • 过多的 MAC 地址

这里来看一些针对性地解决方案:

  • 使用 Macvlan 和 IPvlan 替代 veth,可以降低处理 veth 网络的性能开销
  • 避免 NAT 的基本思想是将容器和虚机、主机同等对象。直接连接到物理接口,即把容器的 veth/Macvlan 网卡与主机的物理网卡桥接。这种方法需要需要主机的物理接口,将其 IP 地址移到桥接接口上。
  • 避免 NAT 第二种思想是把主机变成全面的路由器,甚至是使用 BGP 的路由器。

Docker 目前拥有两类跨主机通信网络解决方案:

  • 原生方式:Docker 官方支持且无需配置,开箱即用
  • 得益于 CNM 接口标准,libnetwork 支持第三方网络管理工具以插件的形式,为 Docker 提供网络功能。这些包括 flannel、weave、calico

在选择容器网络解决方案时,需要思考以下三个问题:

  • 会在基于容器的基础设施上运行哪种类型的应用程序?
  • 性能和可扩展性方面的要求是什么?
  • 需要将容器与虚拟机、裸机互联起来吗?

容器组网方案一览

任何一个主流的容器组网方案无非是网络虚拟 + IP 地址分配。业界主流的容器解决方案,大致可以分为两类:隧道方案和路由方案。

  • 隧道网络也称为 Overlay 网络,它的优点是适用于几乎所有网络基础架构,它唯一的要求是主机之间三层互通。典型的 Overlay 网络插件有:
    • Flannel:源于 CoreOS,支持自研的 UDP 封包和 VxLAN 协议。它的思想是每个主机负责一个网段,该主机上的容器从这个特定的网段分配一个 IP 地址。当一台主机 A 上的容器 A1 访问另一台主机 B 上的容器 B1 时,报文首先通过 Linux bridge 到达主机 A,主机 A 上会有一个 flannel.0 设备,它会根据报文的目的地址,知道报文需要发送到哪个主机上,然后进行 UDP/VxLAN 封包。该方案有两个缺点:1. 封包存在开销 2. 容器跨主机迁移带来 IP 的变化
    • Weave:源于 Weaveworks,Weave 的思路是共享 IP 和非绑定,节点之间通过协议共享信息
  • 路由方案:之所以需要使用隧道,是因为宿主机之间并不敢感知容器网络的地址,因此没有办法将 IP 包投递到正确的地方。只能能把容器网络宣告到物理网络之中,就能正常地对容器之间的数据流进行路由了,这就是路由方案的思路。路由方案的典型插件包括:
    • Calico:源自 Tigera,基于 BGP 的路由方案,支持很细致的 ACL 控制。容器内配置一条路由,指向主机的 IP 地址。每台主机本身就是路由器,知道如何对目的容器网段进行路由。用传统的 BGP 路由就可以实现路由的传递。
    • Macvlan:从逻辑和 kernel 层来看,是隔离性和性能最优的方案
    • Metaswitch:容器内部配置一个路由指向自己宿主机的地址,这是一个存三层的网络,因此性能接近与原生网络

所有的容器组网方案也可以按照如下划分:

  • L2 overlay:基于 VxLAN 技术构建一个大二层网络,它使得容器可以在任意宿主机键迁移而不需要改变其 IP 地址,这也是大二层 overlay 网络的优势,在动态迁移时具有很高的灵活性。
  • L3 overlay 方案组网类似于 L2 overlay,但会在每个节点上增加一个网关,每个节点上的容器在同一个子网内,可以直接 overlay 二层通信,但是跨节点的容器间通信只能走 L3,会经过网关转发。这使得不同容器网段之间也可以相互通信。flannel 的 UDP 模式可以认为采用的就是 L3 overlay 方案
  • L2 underlay:L2 underlay 网络就是链路层互通的底层网络,IPvlan L2 模式和 Macvlan 可以认为 L2 underlay 类型的网络
  • L3 underlay:虚拟网络和物理网络之间通过路由直接打通。IPvlan 的 L3 模式,flannel 的 host-gw 模式、Calico 都可以认为是 L3 underlay 的网络。但是基于三层转发的设计对网络架构可能会有一定的侵入性