暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

晴耕 · 白话之Kubernetes网络篇: 从docker0开始

晴耕小筑 2019-11-24
382

这里,没有难懂的知识;

只有最浅显的文字,

最直观的图示,

最鲜活的例子;

晴耕 · 白话,让知识更简单!


点击文末阅读原文,查看本文在晴耕小筑网站上的完整内容


Linux里面的network bridge(网桥)和network namespace是两个非常重要的概念,它们是理解Kubernetes网络工作原理的基础。本文从docker0出发,讨论network bridge的工作原理。

网络名字空间(network namespace)是Linux内核提供的一项非常重要的功能,它是网络虚拟化技术的基础,也是Kubernetes和以Docker为代表的容器技术在实现它们各自的网络时所依赖的基础。所以,要理解Kubernetes的网络工作原理,首先要从network namespace入手。


在Linux内核里,每个network namespace都有它自己的网络设置,比如像:网络接口(network interfaces),路由表(routing tables)等。我们利用network namespace,可以把不同的网络设置彼此隔离开来。当运行多个Docker容器的时候,Docker会在每个容器内部创建相应的network namespace,从而实现不同容器之间的网络隔离。


但是光有隔离还不行,因为容器还要和外界进行网络联通,所以除了network namespace以外,另一个重要的概念是网桥(network bridge)。它是由Linux内核提供的一种链路层设备,用于在不同网段之间转发数据包。Docker就是利用网桥来实现容器和外界之间的通信的。默认情况下,Docker服务会在它所在的机器上创建一个名为docker0的网桥。下面我们就先从docker0入手,通过一系列动手实验,了解网桥在容器之间进行通信时所起的作用。


实验环境


在开始实验之前,首先确保Docker服务所在的宿主机上已经安装了iproute2, bridge-utils, iptables这几个依赖。因为在本文以及后续Kubernetes网络的系列文章中,我们会经常用到这些依赖包所提供的命令行工具。如果没有安装这些依赖的话,可以手动进行安装。比如,以Alpine Linux为例:


$ apk add iproute2 bridge-utils iptables


或者,也可以使用本系列教程配套的GitHub项目lab-k8s(地址见下面)为我们提供的,名为morningspace/lab-dind的Docker镜像,里面预装了所有的依赖。在撰写这一系列文章的时候,笔者就是利用该镜像在一台Mac笔记本上完成的所有实验。GitHub地址:


github.com/morningspace/lab-k8s


由于目前Mac上运行的Docker Desktop依然是把Docker放在一个虚拟机上跑的,而这个虚拟机通常情况下又不太容易直接访问到,所以如果我们想查看虚拟机上的网络设置会比较困难。这就是为什么笔者建议我们在做实验的时候使用lab-dind镜像的原因。该镜像是一个标准的Alpine Linux,进入容器以后,还可以执行各种docker命令,进行我们的实验。比如在容器内部启动另一个容器,这和在物理机上的操作体验是完全一样的。这就是所谓的dind(Docker-in-Docker)技术。虽然严格地说,这种操作方式和跑在物理机上的Linux,以及相应的Docker环境还是有一定的差异的,但这并不妨碍我们做实验。


如果我们已经把lab-k8s项目克隆到了本地,那么就可以直接在项目的根目录下运行docker-compose命令来启动lab-dind容器:


$ docker-compose up -d dind
Creating lab-k8s_dind_1_9791018c4641 ... done


等容器成功启动起来以后,我们再通过如下命令进入到容器内部:


$ docker-compose exec dind bash
bash-4.4#


接下来,就可以开始进行各种实验了。本文从这里开始,包括这一系列的后续文章,里面提到的所有实验环节都假设大家是在lab-dind容器里进行的。


什么是Docker0


现在我们来看一下Docker的默认网桥docker0。首先,我们利用brctl命令,列出当前系统的所有网桥:


$ brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.024277d8e553 no


在我们的实验环境里,目前就只有docker0这一个网桥。执行ifconfig,我们还可以列出docker0的详细信息:


$ ifconfig docker0
docker0 Link encap:Ethernet HWaddr 02:42:77:D8:E5:53
inet addr:172.17.0.1 Bcast:172.17.255.255 Mask:255.255.0.0
UP BROADCAST MULTICAST MTU:1500 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)


从输出结果里,我们可以看到,docker0的IP地址为172.17.0.1,子网掩码为255.255.0.0。


前面我们提到过,docker0是由Docker服务在启动时自动创建出来的以太网桥。默认情况下,所有Docker容器都会连接到docker0,然后再通过这个网桥来实现容器和外界之间的通信。那么,Docker具体是怎么做到这一点的呢?我们先用docker network ls命令来看一下Docker默认提供的几种网络:


$ docker network ls
NETWORK ID NAME DRIVER SCOPE
d307261937e9 bridge bridge local
9a4e6a25b62e host host local
53cf3a3b2e4f none null local


我们知道,Docker容器在启动时,如果没有显式指定加入任何网络,就会默认加入到名为bridge的网络。而这个bridge网络就是基于docker0实现的。


除此以外,这里的host和none是有特殊含义的。其中,加入host网络的容器,可以实现和Docker daemon守护进程(也就是Docker服务)所在的宿主机网络环境进行直接通信;而none网络,则表示容器在启动时不带任何网络设备。


启动第一个容器


现在,我们就来看一下Docker容器在加入bridge网络的过程中,容器以及宿主机网络设置的变化情况。首先,我们通过docker network inspect命令,看一下bridge网络目前的状态:


$ docker network inspect bridge
[
{
"Name": "bridge",
"Id": "d307261937e987e9a0d46279c2033824920167f31f1b0371a9f7dfc52b9e55ca",
"Scope": "local",
"Driver": "bridge",
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.17.0.0/16"
}
]
},
"Containers": {},
... ...
}
]


我们注意到,Subnet字段的值为172.17.0.0/16。这表明了,bridge网络位于172.17.0.0网段,子网掩码为255.255.0.0。这和前面ifconfig命令看到的结果保持一致,docker0的IP地址刚好位于这一网段内。


对172.17.0.0/16这种写法感到陌生的同学,可以在网上搜一下CIDR。这是一种被称为“无类域间路由”的标记方法,其英文全称为Classless Inter-Domain Routing(简称CIDR)。这个名字听起来有点唬人,不过没关系,我们只要记住,“/”前面的部分代表当前网段的网络ID,“/”后面的部分代表子网掩码中连续1的个数。比如,在我们的例子里,16就表示连续16个1,对应的子网掩码就是:11111111.11111111.00000000.00000000,十进制表示就是:255.255.0.0。


另外,我们还注意到,这里的Containers字段目前是空的。接下来,我们启动一个新的Docker容器:


$ docker run -dit --name busybox1 busybox sh
Unable to find image 'busybox:latest' locally
latest: Pulling from library/busybox
ff5eadacfa0b: Pull complete
Digest: sha256:c888d69b73b5b444c2b0bd70da28c3da102b0aeb327f3a297626e2558def327f
Status: Downloaded newer image for busybox:latest
4710242fd42dc97b8f36470ceb8a29c32979a60f00cccc8d55edcab04216d6d3


可以看到,Docker为我们在当前宿主机上启动了一个名为busybox1的容器。这个时候,再查看bridge网络:


$ docker network inspect bridge
[
{
"Name": "bridge",
"Id": "d307261937e987e9a0d46279c2033824920167f31f1b0371a9f7dfc52b9e55ca",
"Scope": "local",
"Driver": "bridge",
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.17.0.0/16"
}
]
},
"Containers": {
"4710242fd42dc97b8f36470ceb8a29c32979a60f00cccc8d55edcab04216d6d3": {
"Name": "busybox1",
"EndpointID": "2c82c71f3dc5b34e283c7f72c300912ce0f0e11890e7570c0b72bc748a5c1184",
"MacAddress": "02:42:ac:11:00:02",
"IPv4Address": "172.17.0.2/16",
"IPv6Address": ""
}
},
... ...
}
]


就会发现,Containers字段里出现了busybox1,也就是我们刚刚启动的那个容器。这说明busybox1容器已经成功地加入到了我们的bridge网络里。如果这个时候查看docker0网桥:


$ brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.024277d8e553 no vethe657f66


会发现,和一开始执行brctl show的输出结果相比,interfaces字段的位置多了一个名叫vethe657f66的网络接口。实际上,这是一种虚拟以太网设备(Virtual Ethernet Device,简称veth)。确切地说,这不是一个设备,而是一对设备,所以也被称为“veth pair”。它包含两个总是成对出现的网络接口,分别连接不同的network namespace。一端的网络接口接收到数据以后,就会立刻传送给另一端,从而在两个network namespace之间建立起了一个“通道”,实现了彼此之间的网络连通。通常,这一对接口本身并不会被分配IP地址。在我们的例子里,这个veth pair的一端位于容器busybox1里,另一端则位于宿主机上,也就是这里的vethe657f66。执行ip addr show命令查看该接口的详细信息:


$ ip addr show vethe657f66
6: vethe657f66@if5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
link/ether 1a:e1:8a:b5:d3:93 brd ff:ff:ff:ff:ff:ff link-netnsid 1


以看到,vethe657f66后面跟着一个后缀@if5。其中的数字5,代表了作为这个veth pair的另一端(即位于busybox1容器里的那个网络接口)在对应的network namespace里所有网络接口中的位置序号。也就是每当我们执行ip addr show命令的时候,输出结果里每个网络接口前面的那个数字。如果我们在busybox1里执行ip addr show:


$ docker exec busybox1 ip addr show
... ...
5: eth0@if6: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever


就可以看到,eth0的序号正是5,和宿主机里vethe657f66后面的@if5是一致的。与此同时,busybox1里eth0的后缀是@if6。对照前面vethe657f66的输出结果,它的序号正是6。这说明,宿主机里的vethe657f66和busybox1里的eth0构成了一对veth pair。利用这个“通道”,我们的busybox1就可以实现和外界的通信了。


启动另一个容器


接下来,我们再来启动另一个容器:


$ docker run -dit --name busybox2 busybox sh
fa6c607330b5cf06753e86e89b0fa7c9620e7187a4905a67c07499fdc477d4c2


然后,查看bridge网络:


$ docker network inspect bridge
[
{
"Name": "bridge",
"Id": "d307261937e987e9a0d46279c2033824920167f31f1b0371a9f7dfc52b9e55ca",
"Scope": "local",
"Driver": "bridge",
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.17.0.0/16"
}
]
},
"Containers": {
"4710242fd42dc97b8f36470ceb8a29c32979a60f00cccc8d55edcab04216d6d3": {
"Name": "busybox1",
"EndpointID": "2c82c71f3dc5b34e283c7f72c300912ce0f0e11890e7570c0b72bc748a5c1184",
"MacAddress": "02:42:ac:11:00:02",
"IPv4Address": "172.17.0.2/16",
"IPv6Address": ""
},
"fa6c607330b5cf06753e86e89b0fa7c9620e7187a4905a67c07499fdc477d4c2": {
"Name": "busybox2",
"EndpointID": "2980a88338acb0b07ea4deb1e5a25d0264ece9fb16c5a8386469e853a101684a",
"MacAddress": "02:42:ac:11:00:03",
"IPv4Address": "172.17.0.3/16",
"IPv6Address": ""
}
},
... ...
}
]


可以看到,每次启动新的容器,默认总是会加入到这个bridge网络里的。现在的Containers字段,已经包含两个容器了,分别是busybox1和busybox2。我们还可以继续查看docker0网桥,以及veth pair分别在宿主机和容器里的网络接口信息,这里就不再赘述了。


验证网络连通性


查看本段内容击文末阅读原文



IPTables和-p参数


查看本段内容击文末阅读原文


文章转载自晴耕小筑,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论