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上配置了两个路由项:
route add -net 10.1.20.0 netmask 255.255.255.0 gw 20.0.40.55
route add -net 10.1.30.0 netmask 255.255.255.0 gw 20.0.40.56
node2:
route add -net 10.1.10.0 netmask 255.255.255.0 gw 20.0.40.54
route add -net 10.1.30.0 netmask 255.255.255.0 gw 20.0.40.56
node3:
route add -net 10.1.10.0 netmask 255.255.255.0 gw 20.0.40.54
route add -net 10.1.20.0 netmask 255.255.255.0 gw 20.0.40.55
这意味着,每一个新部署的容器都将使用这个Node(docker0的网桥IP)作为它的默认网关。而这些Node(类似路由器)都有其他docker0的路由信息,这样它们就能够相互连通了。
接下来通过一些实际的案例,来看看Kubernetes在不同的场景下其网络部分到底做了什么。
部署的RC/Pod描述文件如下(frontend-controller.yaml):
apiVersion: v1
kind: ReplicationController
metadata:
name: frontend
labels:
name: frontend
spec:
replicas: 1
selector:
name: frontend
template:
metadata:
labels:
name: frontend
spec:
containers:
- name: tomcat
image: tomcat
env:
- name: GET_HOSTS_FROM
value: env
ports:
- containerPort: 18080
hostPort: 18080
为了便于观察,我们假定在一个空的Kubernetes集群上运行,提前清理了所有Replication Controller、Pod和其他Service.(可以不清除掉蛤)
检查一下此时某个Node上的网络接口有哪些。
Node1的状态是:
[root@k8s-node1 ~]# ifconfig
datapath: flags=4163 mtu 1376
inet6 fe80::3012:c2ff:fe6c:37e4 prefixlen 64 scopeid 0x20
ether 32:12:c2:6c:37:e4 txqueuelen 1000 (Ethernet)
RX packets 5148 bytes 145336 (141.9 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 8 bytes 648 (648.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
docker0: flags=4099 mtu 1500
inet 172.17.0.1 netmask 255.255.0.0 broadcast 172.17.255.255
ether 02:42:fa:49:9c:b9 txqueuelen 0 (Ethernet)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
ens192: flags=4163 mtu 1500
inet 20.0.40.54 netmask 255.255.255.0 broadcast 20.0.40.255
inet6 fe80::d2a8:dff9:79af:81ad prefixlen 64 scopeid 0x20
ether 00:50:56:94:06:d7 txqueuelen 1000 (Ethernet)
RX packets 27940383 bytes 9781889476 (9.1 GiB)
RX errors 0 dropped 159 overruns 0 frame 0
TX packets 17282068 bytes 5207858422 (4.8 GiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73 mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10
loop txqueuelen 1 (Local Loopback)
RX packets 1774557 bytes 228015453 (217.4 MiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 1774557 bytes 228015453 (217.4 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
vethwe-bridge: flags=4163 mtu 1376
inet6 fe80::6468:58ff:fe34:ace9 prefixlen 64 scopeid 0x20
ether 66:68:58:34:ac:e9 txqueuelen 0 (Ethernet)
RX packets 5218 bytes 231248 (225.8 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 164 bytes 45548 (44.4 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
vethwe-datapath: flags=4163 mtu 1376
inet6 fe80::200f:f1ff:fe14:9a20 prefixlen 64 scopeid 0x20
ether 22:0f:f1:14:9a:20 txqueuelen 0 (Ethernet)
RX packets 1774557 bytes 228015453 (217.4 MiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 1774557 bytes 228015453 (217.4 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
vethwepl3ff9d10: flags=4163 mtu 1376
inet6 fe80::983e:53ff:fe53:f9ea prefixlen 64 scopeid 0x20
ether 9a:3e:53:53:f9:ea txqueuelen 0 (Ethernet)
RX packets 75549 bytes 10251363 (9.7 MiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 61748 bytes 14056132 (13.4 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
vethweplac9b1dd: flags=4163 mtu 1376
inet6 fe80::4402:85ff:fe12:4b06 prefixlen 64 scopeid 0x20
ether 46:02:85:12:4b:06 txqueuelen 0 (Ethernet)
RX packets 75549 bytes 10251363 (9.7 MiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 61748 bytes 14056132 (13.4 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
vxlan-6784: flags=4163 mtu 65470
inet6 fe80::8c83:3eff:febe:3cc8 prefixlen 64 scopeid 0x20
ether 8e:83:3e:be:3c:c8 txqueuelen 1000 (Ethernet)
RX packets 885161 bytes 1210978136 (1.1 GiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 880280 bytes 1210873748 (1.1 GiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
weave: flags=4163 mtu 1376
inet 10.43.128.0 netmask 255.240.0.0 broadcast 10.47.255.255
inet6 fe80::fccd:a6ff:fed5:d30b prefixlen 64 scopeid 0x20
ether fe:cd:a6:d5:d3:0b txqueuelen 1000 (Ethernet)
RX packets 75549 bytes 10251363 (9.7 MiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 61748 bytes 14056132 (13.4 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
可以看出,有一个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容器吗?
iptables-save
如果您是一个空的kubernetes来做这个实验:你会发现上的这些规则,并没有被应用到我们刚刚定义的Pod上。当然,Kubernetes会给每一个Kubernetes节点都提供一些默认的服务,上面的规则就是Kubernetes的默认服务所需要的。关键是,我们没有看到任何IP伪装的规则,并且没有任何指向Pod 10.36.0.1内部的端口映射。
我们已经了解了Kubernetes如何处理最基本的元素即Pod的连接问题,接下来看一下它是如何处理Service的。Service允许我们在多个Pod之间抽象一些服务,而且服务可以通过提供在同一个Service的多个Pod之间的负载均衡机制来支持水平扩展。我再次将环境初始化,删除刚创建的rc或pod来确保集群是空的:
然后准备一个名为frontend的Service配置文件:
apiVersion: v1
kind: Service
metadata:
name: frontend
labels:
name: frontend
spec:
ports:
- port: 80
# nodePort: 38080
selector:
name: frontend
# type:
# NodePort
接着在Kubernetes集群中定义这个服务:
kubecatl applf -f frontend-service.yaml
kubecatl get svc
在服务正确创建后,可以看到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的配置文件
apiVersion: v1
kind: ReplicationController
metadata:
name: frontend
labels:
name: frontend
spec:
replicas: 3
selector:
name: frontend
template:
metadata:
labels:
name: frontend
spec:
containers:
- name: nginx
image: nginx
imagePullPolicy: IfNotPresent
env:
- name: GET_HOST_FROM
value: env
ports:
- containerPort: 80
hostPort: 38080
在集群发布上述配置文件后,等待并观察,确保所有Pod都运行起来了:
现在所有的Pod都运行起来了,Service将会把客户端请求负载分发到包含“name=frontend”标签的所有Pod上。
Kubernetes的kube-proxy看起来只是一个夹层,但实际上它只是在Node上运行的一个服务。上述重定向规则的结果就是针对目标地址为服务IP的流量,将Kubernetes的kube-proxy变成了一个中间的夹层。
为了查看具体的重定向动作,我们会使用tcpdump来进行网络抓包操作。
首先,安装tcpdump:
yum install -y tcpdump
安装完成后,登录Node1,运行tcpdump命令:
tcpdump -nn -q -i port 80
需要捕获物理服务器以太网接口的数据包,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。
小结:
本节内容到此结束,谢谢大家的浏览,多多点关注蛤。