Docker给我们带来了不同的网络模式,Kubernetes也以一种不同的方式来解决这些网络模式的挑战,但其方式有些难以理解,特别是对于刚开始接触Kubernetes的网络的开发者来说。我们在前面学习了Kubernetes、Docker的理论,本节将通过一个完整的实验,从部署一个Pod开始,一步一步地部署那些Kubernetes的组件,来剖析Kubernetes在网络层是如何实现及工作的。
这里使用虚拟机来完成实验。如果要部署在物理机器上或者云服务商的环境中,则涉及的网络模型很可能稍微有所不同。不过,从网络角度来看,Kubernetes的机制是类似且一致的。
好了,来看看我们的实验环境:
Kubernetes的网络模型要求每个Node上的容器都可以相互访问。
默认的Docker网络模型提供了一个IP地址段是172.17.0.0/16的docker0网桥。每个容器都会在这个子网内获得IP地址,并且将docker0网桥的IP地址(172.17.42.1)作为其默认网关。需要注意的是,Docker宿主机外面的网络不需要知道任何关于这个172.17.0.0/16的信息或者知道如何连接到其内部,因为Docker的宿主机针对容器发出的数据,在物理网卡地址后面都做了IP伪装MASQUERADE(隐含NAT)。也就是说,在网络上看到的任何容器数据流都来源于那台Docker节点的物理IP地址。这里所说的网络都指连接这些主机的物理网络。
这个模型便于使用,但是并不完美,需要依赖端口映射的机制。
在Kubernetes的网络模型中,每台主机上的docker0网桥都是可以被路由到的。也就是说,在部署了一个Pod时,在同一个集群内,各主机都可以访问其他主机上的Pod IP,并不需要在主机上做端口映射。综上所述,我们可以在网络层将Kubernetes的节点看作一个路由器。如果将实验环境改画成一个网络图,那么它看起来如下图所示:
为了支持Kubernetes网络模型,我们采取了直接路由的方式来实现,在每个Node上都配置相应的静态路由项,例如在node1这个Node上配置了两个路由项:
1
2
3 1route add -net 10.1.20.0 netmask 255.255.255.0 gw 20.0.40.55
2route add -net 10.1.30.0 netmask 255.255.255.0 gw 20.0.40.56
3
node2:
1
2
3 1route add -net 10.1.10.0 netmask 255.255.255.0 gw 20.0.40.54
2route add -net 10.1.30.0 netmask 255.255.255.0 gw 20.0.40.56
3
node3:
1
2
3
4 1route add -net 10.1.10.0 netmask 255.255.255.0 gw 20.0.40.54
2route add -net 10.1.20.0 netmask 255.255.255.0 gw 20.0.40.55
3
4
这意味着,每一个新部署的容器都将使用这个Node(docker0的网桥IP)作为它的默认网关。而这些Node(类似路由器)都有其他docker0的路由信息,这样它们就能够相互连通了。
接下来通过一些实际的案例,来看看Kubernetes在不同的场景下其网络部分到底做了什么。
第1步:部署一个RC/Pod
部署的RC/Pod描述文件如下(frontend-controller.yaml):
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 1apiVersion: v1
2kind: ReplicationController
3metadata:
4 name: frontend
5 labels:
6 name: frontend
7spec:
8 replicas: 1
9 selector:
10 name: frontend
11 template:
12 metadata:
13 labels:
14 name: frontend
15 spec:
16 containers:
17 - name: tomcat
18 image: tomcat
19 env:
20 - name: GET_HOSTS_FROM
21 value: env
22 ports:
23 - containerPort: 18080
24 hostPort: 18080
25
为了便于观察,我们假定在一个空的Kubernetes集群上运行,提前清理了所有Replication Controller、Pod和其他Service.(可以不清除掉蛤)
检查一下此时某个Node上的网络接口有哪些。
Node1的状态是:
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85 1[root@k8s-node1 ~]# ifconfig
2datapath: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1376
3 inet6 fe80::3012:c2ff:fe6c:37e4 prefixlen 64 scopeid 0x20<link>
4 ether 32:12:c2:6c:37:e4 txqueuelen 1000 (Ethernet)
5 RX packets 5148 bytes 145336 (141.9 KiB)
6 RX errors 0 dropped 0 overruns 0 frame 0
7 TX packets 8 bytes 648 (648.0 B)
8 TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
9
10docker0: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500
11 inet 172.17.0.1 netmask 255.255.0.0 broadcast 172.17.255.255
12 ether 02:42:fa:49:9c:b9 txqueuelen 0 (Ethernet)
13 RX packets 0 bytes 0 (0.0 B)
14 RX errors 0 dropped 0 overruns 0 frame 0
15 TX packets 0 bytes 0 (0.0 B)
16 TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
17
18ens192: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
19 inet 20.0.40.54 netmask 255.255.255.0 broadcast 20.0.40.255
20 inet6 fe80::d2a8:dff9:79af:81ad prefixlen 64 scopeid 0x20<link>
21 ether 00:50:56:94:06:d7 txqueuelen 1000 (Ethernet)
22 RX packets 27940383 bytes 9781889476 (9.1 GiB)
23 RX errors 0 dropped 159 overruns 0 frame 0
24 TX packets 17282068 bytes 5207858422 (4.8 GiB)
25 TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
26
27lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
28 inet 127.0.0.1 netmask 255.0.0.0
29 inet6 ::1 prefixlen 128 scopeid 0x10<host>
30 loop txqueuelen 1 (Local Loopback)
31 RX packets 1774557 bytes 228015453 (217.4 MiB)
32 RX errors 0 dropped 0 overruns 0 frame 0
33 TX packets 1774557 bytes 228015453 (217.4 MiB)
34 TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
35
36vethwe-bridge: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1376
37 inet6 fe80::6468:58ff:fe34:ace9 prefixlen 64 scopeid 0x20<link>
38 ether 66:68:58:34:ac:e9 txqueuelen 0 (Ethernet)
39 RX packets 5218 bytes 231248 (225.8 KiB)
40 RX errors 0 dropped 0 overruns 0 frame 0
41 TX packets 164 bytes 45548 (44.4 KiB)
42 TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
43
44vethwe-datapath: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1376
45 inet6 fe80::200f:f1ff:fe14:9a20 prefixlen 64 scopeid 0x20<link>
46 ether 22:0f:f1:14:9a:20 txqueuelen 0 (Ethernet)
47 RX packets 1774557 bytes 228015453 (217.4 MiB)
48 RX errors 0 dropped 0 overruns 0 frame 0
49 TX packets 1774557 bytes 228015453 (217.4 MiB)
50 TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
51
52vethwepl3ff9d10: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1376
53 inet6 fe80::983e:53ff:fe53:f9ea prefixlen 64 scopeid 0x20<link>
54 ether 9a:3e:53:53:f9:ea txqueuelen 0 (Ethernet)
55 RX packets 75549 bytes 10251363 (9.7 MiB)
56 RX errors 0 dropped 0 overruns 0 frame 0
57 TX packets 61748 bytes 14056132 (13.4 MiB)
58 TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
59
60vethweplac9b1dd: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1376
61 inet6 fe80::4402:85ff:fe12:4b06 prefixlen 64 scopeid 0x20<link>
62 ether 46:02:85:12:4b:06 txqueuelen 0 (Ethernet)
63 RX packets 75549 bytes 10251363 (9.7 MiB)
64 RX errors 0 dropped 0 overruns 0 frame 0
65 TX packets 61748 bytes 14056132 (13.4 MiB)
66 TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
67
68vxlan-6784: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 65470
69 inet6 fe80::8c83:3eff:febe:3cc8 prefixlen 64 scopeid 0x20<link>
70 ether 8e:83:3e:be:3c:c8 txqueuelen 1000 (Ethernet)
71 RX packets 885161 bytes 1210978136 (1.1 GiB)
72 RX errors 0 dropped 0 overruns 0 frame 0
73 TX packets 880280 bytes 1210873748 (1.1 GiB)
74 TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
75
76weave: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1376
77 inet 10.43.128.0 netmask 255.240.0.0 broadcast 10.47.255.255
78 inet6 fe80::fccd:a6ff:fed5:d30b prefixlen 64 scopeid 0x20<link>
79 ether fe:cd:a6:d5:d3:0b txqueuelen 1000 (Ethernet)
80 RX packets 75549 bytes 10251363 (9.7 MiB)
81 RX errors 0 dropped 0 overruns 0 frame 0
82 TX packets 61748 bytes 14056132 (13.4 MiB)
83 TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
84
85
可以看出,有一个docker0网桥和一个本地地址的网络端口。现在部署一下我们在前面准备的RC/Pod配置文件,看看发生了什么:
可以看到一些有趣的事情。Kubernetes为这个Pod找了一个主机Node3来运行它。另外,这个Pod获得了一个在Node3的docker0网桥上的IP地址。我们登录Node3查看正在运行的容器:
在Node3上现在运行了两个容器,在我们的RC/Pod定义文件中仅仅包含了一个,那么这第2个是从哪里来的呢?第2个看起来运行的是一个叫作/pause:3.1的镜像,而且这个容器已经有端口映射到它上面了,为什么是这样呢?让我们深入容器内部看一下具体原因。使用Docker的inspect命令来查看容器的详细信息,特别要关注容器的网络模型:
有趣的结果是,在查看完每个容器的网络模型后,我们可以看到这样的配置:我们检查的第1个容器是运行了“pause:3.1”镜像的容器,它使用了Docker默认的网络模型 bridge;而我们检查的第2个容器,也就是在RC/Pod中定义运行的tomcat容器,使用了非默认的网络配置和映射容器的模型,指定了映射目标容器为“pause:3.1”。
一起来仔细思考这个过程,为什么Kubernetes要这么做呢?
首先,一个Pod内的所有容器都需要共用同一个IP地址,这就意味着一定要使用网络的容器映射模式。然而,为什么不能只启动第1个Pod中的容器,而将第2个Pod中的容器关联到第1个容器呢?我们认为Kubernetes是从两方面来考虑这个问题的:首先,如果在Pod内有多个容器的话,则可能很难连接这些容器;其次,后面的容器还要依赖第1个被关联的容器,如果第2个容器关联到第1个容器,且第1个容器死掉的话,第2个容器也将死掉。启动一个基础容器,然后将Pod内的所有容器都连接到它上面会更容易一些。因为我们只需要为基础的这个Google_containers/pause容器执行端口映射规则,这也简化了端口映射的过程。
所以我们启动Pod后的网络模型类似下图:
在这种情况下,实际Pod的IP数据流的网络目标都是这个google_containers/pause容器。上图有点儿取巧地显示了是google_containers/pause容器将端口18080的流量转发给了相关的容器.而pause容器只是看起来转发了网络流量,但它并没有真的这么做。实际上,应用容器直接监听了这些端口,和google_containers/pause容器共享了同一个网络堆栈。这就是为什么在Pod内部实际容器的端口映射都显示到pause容器上了。我们可以使用docker port命令来检验一下。
综上所述,google_containers/pause容器实际上只是负责接管这个Pod的Endpoint,并没有做更多的事情。那么Node呢?它需要将数据流传给pause容器吗?
1
2 1iptables-save
2
如果您是一个空的kubernetes来做这个实验:你会发现上的这些规则,并没有被应用到我们刚刚定义的Pod上。当然,Kubernetes会给每一个Kubernetes节点都提供一些默认的服务,上面的规则就是Kubernetes的默认服务所需要的。关键是,我们没有看到任何IP伪装的规则,并且没有任何指向Pod 10.36.0.1内部的端口映射。
第二步:发布一个服务
我们已经了解了Kubernetes如何处理最基本的元素即Pod的连接问题,接下来看一下它是如何处理Service的。Service允许我们在多个Pod之间抽象一些服务,而且服务可以通过提供在同一个Service的多个Pod之间的负载均衡机制来支持水平扩展。我再次将环境初始化,删除刚创建的rc或pod来确保集群是空的:
然后准备一个名为frontend的Service配置文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 1apiVersion: v1
2kind: Service
3metadata:
4 name: frontend
5 labels:
6 name: frontend
7spec:
8 ports:
9 - port: 80
10# nodePort: 38080
11 selector:
12 name: frontend
13# type:
14 # NodePort
15
16
接着在Kubernetes集群中定义这个服务:
1
2 1kubecatl applf -f frontend-service.yaml
2
1
2 1kubecatl get svc
2
在服务正确创建后,可以看到Kubernetes集群已经为这个服务分配了一个虚拟IP地址10.110.75.165,这个IP地址是在Kubernetes的Portal Network中分配的。而这个Portal Network的地址范围是我们在Kubmaster上启动API服务进程时,使用–service-cluster-ip-range=xx命令行参数指定的:
这个IP段可以是任何段,只要不和docker0或者物理网络的子网冲突就可以。选择任意其他网段的原因是这个网段将不会在物理网络和docker0网络上进行路由。这个PortalNetwork对每一个Node都有局部的特殊性,实际上它存在的意义是让容器的流量都指向默认网关(也就是docker0网桥)。
在继续实验前,先登录到Node1上看一下在我们定义服务后发生了什么变化。首先检查一下iptables或Netfilter的规则:
(ps:书上的图片,由于我没有清空kubernetes,没有做成桥接,就拿书上的图片来解决了)
第1行是挂在PREROUTING链上的端口重定向规则,所有进入的流量如果满足20.1.244.75: 80,则都会被重定向到端口33761。第2行是挂在OUTPUT链上的目标地址NAT,做了和上述第1行规则类似的工作,但针对的是当前主机生成的外出流量。所有主机生成的流量都需要使用这个DNAT规则来处理。简而言之,这两个规则使用了不同的方式做了类似的事情,就是将所有从节点生成的发送给20.1.244.75:80的流量重定向到本地的33761端口。
至此,目标为Service IP地址和端口的任何流量都将被重定向到本地的33761端口。
这个端口连到哪里去了呢?
这就到了kube-proxy发挥作用的地方了。这个kube-proxy服务给每一个新创建的服务都关联了一个随机的端口号,并且监听那个特定的端口,为服务创建相关的负载均衡对象。在我们的实验中,随机生成的端口刚好是33761。通过监控Node1上的Kubernetes-Service的日志,在创建服务时可以看到下面的记录:
现在我们知道,所有流量都被导入kube-proxy中了。我们现在需要它完成一些负载均衡的工作,创建Replication Controller并观察结果,下面是Replication Controller的配置文件
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 1apiVersion: v1
2kind: ReplicationController
3metadata:
4 name: frontend
5 labels:
6 name: frontend
7spec:
8 replicas: 3
9 selector:
10 name: frontend
11 template:
12 metadata:
13 labels:
14 name: frontend
15 spec:
16 containers:
17 - name: nginx
18 image: nginx
19 imagePullPolicy: IfNotPresent
20 env:
21 - name: GET_HOST_FROM
22 value: env
23 ports:
24 - containerPort: 80
25 hostPort: 38080
26
在集群发布上述配置文件后,等待并观察,确保所有Pod都运行起来了:
现在所有的Pod都运行起来了,Service将会把客户端请求负载分发到包含“name=frontend”标签的所有Pod上。
Kubernetes的kube-proxy看起来只是一个夹层,但实际上它只是在Node上运行的一个服务。上述重定向规则的结果就是针对目标地址为服务IP的流量,将Kubernetes的kube-proxy变成了一个中间的夹层。
为了查看具体的重定向动作,我们会使用tcpdump来进行网络抓包操作。
首先,安装tcpdump:
1
2 1yum install -y tcpdump
2
安装完成后,登录Node1,运行tcpdump命令:
1
2 1tcpdump -nn -q -i port 80
2
需要捕获物理服务器以太网接口的数据包,Node1机器上的以太网接口名字叫作ens192。
再打开第1个窗口运行第2个tcpdump程序,不过我们需要一些额外的信息去运行它,即挂接在docker0桥上的虚拟网卡Veth的名称。我们看到只有一个frontend容器在Node1主机上运行,所以可以使用简单的“ip addr”命令来查看最后一个的Veth网络接口:
好了,我们已经在同时捕获两个接口的网络包了。这时再启动第3个窗口,运行一个“docker exec "命令来连接到我们的frontend容器内部(你可以先执行docker ps来获得这个容器的ID):
这些信息说明了什么问题呢?
总而言之,Kubernetes的kube-proxy作为一个全功能的代理服务器管理了两个独立的TCP连接:一个是从容器到kube-proxy:另一个是从kube-proxy到负载均衡的目标Pod。
小结:
本节内容到此结束,谢谢大家的浏览,多多点关注蛤。