K8s网络模型设计的一个基础原则是:
Docker使用到的与Linux网络有关的主要技术:
为了支持网络协议栈的多个实例,Linux中网络栈中引入了网络命名空间(Network Namespace),这些独立的协议栈被隔离到不同的命名空间中,处于不同命名空间的网络栈是完全隔离的,彼此之间无法通信。通过这种对网络资源的隔离,就能在一个宿主机上虚拟多个不同的网络环境。
Docker正是利用了网络的命名空间特性,实现了不同容器之间网络的隔离。
Linux的网络命名空间内可以有自己独立的路由表及独立的Iptables/Netfilter设置来提供包转发、NAT及IP包过滤等功能。
为了隔离出独立的协议栈,需要纳入命名空间的元素有进程、套接字、网络设备等。进程创建的套接字必须属于某个命名空间,套接字的操作也必须在命名空间内进行。同样,网络设备也必须属于某个命名空间。因为网络设备属于公共资源,所以可以通过修改属性实现这命名空间之间移动。当然是否允许移动和设备的特征有关。
Linux实现网络命名空间的核心:Linux的网络协议十分复杂,为了支持独立的协议栈,相关的全局变量都必须修改为协议栈私有。最好的办法就是让全局变量成为一个Net Namespace变量的成员,然后为协议栈私有。最好的办法就是让这些全局变量成为一个Net Namespace变量的成员,然后为协议栈的函数调用加入一个Namespace参数。
所有的网络设备(物理的或虚拟接口、桥等着内核里都叫作Net Device)都只能属于一个命名空间。通常物理的设备只能关联到root这个命名空间中,虚拟的网络设备(虚拟的以太网接口或者虚拟网口对)则可以被创建并关联到一个给定的命名空间中,而且可以在命名空间之间移动。
网络命名空间代表的是一个独立的协议栈,它们之间是相互隔离的,彼此无法通信,在协议栈内部都看不到对方。打破这种限制,让处于不同命名空间的网络相互通信,甚至和外部的网络进行通信的方法就是:Veth设备对。
Veth设备对重要作用就是:打破相互看不到的协议栈之间的壁垒,就像一个管子,一端连着这个网络命名空间的协议栈,一端连着另一个网络命名空间的协议栈。
所以如果想在两个命名空间之间进行通信,就必须有一个Veth设备对。
命令需要root用户运行
创建一个命名空间:
ip nets add <name>
在命名空间内执行命令:
ip netns exec <name> <command>
如果想执行多个命令,可以先进入内部sh,然后执行:
ip netns exec <name> bash
之后就是在新的命名空间内进行操作了,退出到外面的命名空间,输入exit
可以在不同的网络命名空间之间转移设备,例如Veth设备对的转移。设备里面有一个重要属性:NETIF_F_ETNS_LOCAL,这个属性为"on",则不能转移到其他命名空间内。Veth设备属于可以转移的设备,很多其他设备如lo设备、vxlan设备、ppp设备、bridge设备等都是不可以转移的。
使用ethtool工具可以查看:
ethtool -k br0
netns-local: on [fixed]
netns-local的值是on,就说明不可以转移,否则可以
引入Veth设备对是为了在不同的网络命名空间之间进行通信,利用它可以直接将两个网络命名空间连接起来。由于要连接两个网络命名空间,所以veth设备都是成对出现的,很像一对以太网卡,并且中间有一个直连的网线。既然是一对网卡,将其中一端称为另一端的peer,在Veth设备的一端发送数据时,会将数据直接发送到另一端,并触发另一端的接收操作。
创建Veth设备对,连接到不同的命名空间,并设置它们的地址,让它们通信。
创建Veth设备对:
ip link add veth0 type veth peer name veth1
创建后查看Veth设备对的信息,使用ip link show命令查看所有网络接口:
ip link show
会生成两个设备,一个是veth0,peer是veth1
两个设备都在同一个命名空间,将Veth看作是有两个头的网线,将另一个头甩给另一个命名空间
ip link set veth1 netns netns1
再次查看命名空间,只剩下一个veth0:
ip link show
在netns1命名空间可以看到veth1设备,符合预期。
现在看到的结果是两个不同的命名空间各自有一个Veth的网线头,各显示为一个Device。(Docker的实现里面,除了将Veth放入容器内)
下一步给两个设备veth0、veth1分配IP地址:
ip netns exec netns1 ip addr add 10.1.1.1/24 dev veth1
ip addr add 10.1.1.2/24 dev veth0
现在两个网络命名空间可以互相通信了:
ping 10.1.1.1
ip netns exec netns1 ping 10.1.1.2
至此两个网络命名空间之间就完全相通了。至此就能够理解Veth设备对的原理和用法了。在Docker内部,Veth设备对也是联系容器到外面的重要设备,离开它是不行的。
一旦将Veth设备对的peer端放入另一个命名空间,在本命名空间就看不到了,想查看peer端连接的命名空间,可以使用ethtool工具来查看。
首先在一个命名空间中查询Veth设备对端接口在设备列表中的序列号:
ip nets exec netns1 ethtool -S veth1
NIC statistics:
peer_ifindex: 5
得知另一端的接口设备的序列号是5,再到命名空间中查看序列号5代表的设备:
ip netns exec netns2 ip link | grep 5
veth0
现在就找到下标为5的设备了,是veth0,另一端自然就是另一个命名空间中的veth1了,互为peer。
Linux可以支持多个不同的网络,网络之间能够相互通信,网桥将这些网络连接起来并实现网络中主机的相互通信。
网桥是一个二层的虚拟网络设备,把若干个网络接口"连接起来",以使得网口之间的报文能够互相转发。网桥能够解析收发的报文,读取目标MAC地址的信息,和自己记录的MAC表结合,来决策报文的转发目标网口。为了实现这些功能,网桥会学习源MAC地址(二层网桥转发的依据就是MAC地址)。在转发报文时,网桥只需要向特定的网口进行转发,避免不必要的网络交互。网球如果遇到一个未学习到的地址,就无法知道这个报文应该向哪个网口转发,就将报文广播给所有的网口(报文来源的网口除外)。
在实际网络中,网络拓扑不可能永久不变。网桥需要对学习到的MAC地址表加上超时时间(默认为5分钟),如果网桥收到了对应端口MAC地址回发的包,则重制超时时间,否则过了超时时间后,就认为设备已经不在那个端口上了,就会重新广播发送。
Linux内核支持网口的桥接,与单纯的交换机不同,交换机只是一个二层设备,对于接收到的报文,要么转发,要么丢弃。运行着linux内核的机器本身就是一台主机,有可能是网络报文的目的地,收到的报文除了转发和丢弃,还可能被送到网络层协议栈的上层(网络层),从而被主机本身的协议栈消化,既可以把网桥看作一个二层设备,也可以看作一个三层设备。
Linux内核是通过一个虚拟的网桥设备(Net Device)来实现桥接的。虚拟设备可以绑定若干个以太网接口设备,从而将它们桥接起来。
虚拟的网桥设备和普通设备的不同,最明显的一个特性是可以有一个IP地址:
网桥br0绑定了eth0和eth1。对于网络协议栈的上层来说,只看的到br0。桥接是在数据链路层实现的,上层不需要关心桥接的细节,协议栈上层需要发送的报文被送到br0,网桥设备的处理代码判断报文该被转发到eth0还是eth1,或者两者皆转发。协议栈的上层需要发送的报文被提交给网桥的处理代码,在这里会判断报文应该被转发,丢弃还是提交到协议栈上层。
有时eth0、eth1也可能会作为报文的源地址或目的地址,直接参与报文的发送与接收,从而绕过网桥。
Docker自动完成了对网桥的创建和维护。
新增一个网桥设备:
brctl addbr xxxxx
为网桥增加网口,在Linux中,一个网口其实就是一个物理网卡,将物理网卡和网桥连接起来。
brctl addif xxxxx ethx
网桥的物理网卡作为一个网口,由于在链路层工作,就不再需要IP地址了,这样上面的IP地址自然失效。
ifconfig ethx 0.0.0.0
给网桥配置一个IP地址:
Ifconfig brxxx xxx.xxx.xxx.xxx
这样网桥就有一个IP地址,连接到上面的网卡就是一个纯链路层设备了。
Linux提供了一套机制来为用户实现自定义的数据包处理过程。
在linux网络协议中有一组回调函数挂接点,通过这些挂接点挂接的钩子函数可以在linux网络栈处理数据包的过程中对数据进行一些操作,例如过滤、修改、丢弃等。整个挂接点技术叫作Netfilter和Iptables。
Netfilter负责在内核中执行各种挂接的规则,运行在内核模式中。而Iptables是在用户模式下运行的进程,负责协助维护内核中Netfilter的各种规则表。通过二者的配合来实现整个Linux网络协议栈中灵活的数据包处理机制。
Netfilter可以挂接的规则点有5个(链):
参考下面这篇博客深入理解Iptables、Netfilter、SNAT和DNAT:
Linux系统包含一个完整的路由功能。当IP在处理数据发送或者抓发时,会使用路由表来决定发往哪里。通常情况下,如果主机与目的主机直接相连,那么主机可以直接发送IP报文到目的主机,这个过程比较简单。例如,通过点对点的链接或通过网络共享,如果主机与目的主机没有直接相连,那么主机会将IP报文发送给默认的路由器,然后由路由器来决定往哪发送IP报文。
路由功能由IP层维护的一张路由表来实现。当主机收到数据报文时,用此表来决策接下来应该做什么操作。当从网络侧接收到数据报文时,IP层首先会检查报文的IP地址是否与主机自身的地址相同。如果数据报文中的IP地址是主机自身的地址,那么报文将被发送到传输层相应的协议中去。如果报文中的IP地址不是主机自身的地址,并且配置了路由功能,那么报文将被转发,否则,报文将被丢弃。
路由表中的数据一般是以条目形式存在的。一个典型的路由表条目通常包含以下主要的条目项。
Linux的路由表至少包括两个表:一个是LOCAL,另一个是MAIN。在LOCAL表中会包含所有的本地设备地址。LOCAL路由表是在配置网络设备地址时自动创建的。LOCAL表用于供Linux协议栈识别本地地址,以及进行本地各个不同网络接口之间的数据转发。
可以通过下面的命令查看LOCAL表的内容:
ip route show table local type local
MAIN表用于各类网络IP地址的转发。MAIN表的建立可以使用静态配置生存,也可以使用动态路由发现协议生成。
使用ip route list命令查看当前的路由表。
ip route list
另一个查看路由表的工具:
netstat -rn
Kernel IP routing table
Destination Gateway Genmask Flags MSS Window irtt Iface
0.0.0.0 10.128.232.1 0.0.0.0 UG 0 0 0 ens5
10.128.232.0 0.0.0.0 255.255.252.0 U 0 0 0 ens5
标志是U,说明是可达路由,标志是G,说明这个网络接口连接的是网关,否则说明是直连主机。
Docker支持以下4类网络模式:
在K8s管理模式下,通常只会使用bridge模式,在bridge模式下,Docker Daemon第一次启动时会创建一个虚拟的网桥,默认的名字是docker0,在私有网络空间中给这个网桥分配一个子网。
针对由Docker创建出来的每一个容器,都会创建一个虚拟的以太网设备(Veth设备对),其中一端关联到网桥上,另一端使用Linux的网络命名空间技术,映射到容器内的eth0设备,然后从网桥的地址段内给eth0接口分配一个IP地址。
Docker网络中bridge模式下Docker Daemon启动时创建docker0网桥,并在网桥使用的网段为容器分配IP。
ip addr
iptables-save
Docker创建了docker0网桥,并添加了Iptables规则。
docker run --name register -d registry
ip addr
iptables-save
ip route
可以看到如下情况。
进入容器,查看网络栈,容器内部的IP地址和路由如下:
ip route
ip addr
可以看到,默认停止的回环设备lo已经被启动,外面宿主机连接进来的Veth设备也被命名成了eth0,并且已经配置了地址172.17.0.10。
用带端口映射的命令启动registry
docker run --name register -d -p 1180:5000 registry
启动后查看Iptables的变化
iptables-save
K8s网络的设计主要致力于解决以下场景:
在同一个Pod内的容器共享同一个网络命名空间,共享同一个linux协议栈。对于网络的各类操作,就和它们在同一台机器上一样,设置可以用localhost地址访问彼此的端口。
容器1和容器2共享网络命名空间,打开的端口不会有冲突,可以直接使用linux的本地IPC进行通信(例如消息队列或者管道),互相访问只需要使用localhost就可以了。
每一个Pod都有一个真实的全局IP地址,同一个Node内的不同Pod之间可以直接采用对方Pod的IP地址通信,而且不需要使用其他发现机制,例如DNS或者etcd。
Pod容器既有可能在同一个Node上运行,也有可能在不同的Node上运行,通信分为两类:
1) 同一个Node内的Pod之间的通信
2)不同Node上的Pod之间的通信
Pod的地址是与docker0在同一个网段内的,docker0网段与宿主机网卡是两个完全不同的IP网段,并且不同Node之间通信只能通过宿主机的物理网卡进行,因此要实现位于不同Node上的Pod容器之间通信,就必须想办法通过主机的这个IP地址来进行寻址和通信。
K8s会记录所有正在运行Pod的IP分配信息,并将这些信息保存在etcd中(作为Service的Endpoint)。要想支持不同Node上的Pod之间的通信,就要达到两个条件:
根据条件1,需要在部署k8s时,对docker0的IP地址进行规划,保证每一个Node上的docker0地址没有冲突。可以在规划后手工配置到每个Node上,或者做一个分配规则,由安装的程序自己去分配占用。例如K8s的网络增强开源软件Flannel就能够管理资源池的分配。
根据条件2:Pod中的数据中发出时,需要有一个机制能够知道对方Pod的IP地址,挂在哪个具体的Node上。也就是先要找到Node对应宿主机的IP地址,将数据发送到宿主机的网卡上,然后在宿主机上将相应的数据转到具体的docker0上,一旦数据到达宿主机Node,则那个Node内部的docker0便知道如何将数据发送到Pod。
K8s网络模型要求每个Node上的容器都可以相互访问。一个Pod内的所有容器都需要共用同一个IP地址,意味着一定要使用网络的容器映射模式。Pod会启动一个基础容器,然后将Pod内所有容器都连接到上面,只需要为基础的这个Google_containers/pause容器执行端口映射规则。
1.部署一个RC/Pod
apiVersion: v1
kind: ReplicationController
metadata:
name: frontend
labels:
name: frontend
spec:
replicas: 1
selector:
name: frontend
template:
metadata:
labels:
name: frontend
spec:
containers:
- name: php-redis
image: kubeguide/guestbook-php-frontend
env:
- name: GET_HOSTS_FROM
value: env
ports:
- containerPort: 80
hostPort: 80
假设在一个空的K8s集群上运行,检查下此时Node上网络接口有哪些?
ifconfig
可以看出有一个docker0网桥和一个本地地址的网络端口。
部署RC/Pod配置文件
kubectl create -f frontend-controller.yaml
kubectl get pods
docker ps
运行了两个容器,其中一个是google_containers/pause:latest的镜像。
实际Pod的IP数据流的网络目标都是这个google_containers/pause容器,而google_containers/pause容器实际上只负责接管这个Pod的Endpoint。
通过docker port命令来检验下
docker port
2.发布一个服务
Service允许在多个Pod之间抽象一些服务,服务可以通过提供在同一个Service的多个Pod之间的负载均衡机制来支持水平扩展。K8s集群会为服务分配一个虚拟IP地址,这个IP地址是在K8s的Portal Network中分配的,Portal Network的地址范围则是在Kubmaster上启动API服务进程时,使用–service-cluster-ip-range=xx命令行参数指定的。这个IP段可以是任何段,只要不和docker0或者物理网络的子网冲突就可以。目标为Service IP地址和端口的任何流量都将被重新定向到本地端口。
frontend-service.yaml
apiVersion: v1
kind: Service
metadata:
name: frontend
labels:
name: frontend
spec:
ports:
- port: 80
selector:
name: frontend
然后在K8s集群中定义这个服务
kubectl create -f frontend-service.yaml
kubectl get services
K8s集群已经为这个服务分配了一个虚拟IP地址,这个IP地址是在K8s的Portal Network中分配的。Portal Network的地址范围则是在K8s上启动API服务进程时,使用–service-cluster-ip-range=xx命令行参数指定的。
kube-proxy服务给每一个新创建的服务关联了一个随机的端口号,并且监听特定端口,为服务创建相关的负载均衡对象。K8s的kube-proxy作为一个全功能的代理服务器管理了两个独立的TCP连接:一个是从容器到kube-proxy,另一个是kube-proxy到负载均衡的目标pod。
登陆服务器检查Iptables/Netfilter的规则
iptables-save
所有流量都被导入kube-proxy中,需要kube-proxy完成一些负载均衡工作,创建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: php-redis
image: kubeguide/example-guestbook-php-redis
env:
- name: GET_HOSTS_FROM
value: env
ports:
- containerPort: 80
hostPort: 80
在集群上发布配置文件,等待并观察,确保所有Pod都运行起来了
kubectl create -f frontend-controller.yaml
kubectl get pods -o wide
所有Pod都运行起来后,Service会将匹配到的标签为name=frontend的所有Pod都进行负载分发。因为Service的选择匹配所有这些Pod,负载均衡将会对这3个Pod进行分发。
Fannel可以搭建K8s依赖的底层网络,主要能实现以下两点:
Flannel首先创建了一个名为flannel0网桥,这个网桥一端连接docker0网桥,另一端连接一个叫做flanneld的服务进程。
flannel为不同Node上的Pod分配IP: