K8S-网络原理

一、K8s网络模型

K8s网络模型设计的一个基础原则是:

  • 每个Pod都拥有一个独立的IP地址,而且假定所有Pod都在一个可以直接连通的、扁平的网络空间中。

  • 同一个Pod内的不同容器将会共享一个网络命名空间,也就是说同一个Linux网络协议栈。意味着同一个Pod内的容器可以通过localhost来连接对方的端口。

二、Docker的网络模型

Docker使用到的与Linux网络有关的主要技术:

  • Network Namespace(网络命名空间)

  • Veth设备对

  • Iptables/Netfilter

  • 网桥

  • 路由

三、网络的命名空间

为了支持网络协议栈的多个实例,Linux中网络栈中引入了网络命名空间(Network Namespace),这些独立的协议栈被隔离到不同的命名空间中,处于不同命名空间的网络栈是完全隔离的,彼此之间无法通信。通过这种对网络资源的隔离,就能在一个宿主机上虚拟多个不同的网络环境。

Docker正是利用了网络的命名空间特性,实现了不同容器之间网络的隔离。

Linux的网络命名空间内可以有自己独立的路由表及独立的Iptables/Netfilter设置来提供包转发、NAT及IP包过滤等功能。

为了隔离出独立的协议栈,需要纳入命名空间的元素有进程、套接字、网络设备等。进程创建的套接字必须属于某个命名空间,套接字的操作也必须在命名空间内进行。同样,网络设备也必须属于某个命名空间。因为网络设备属于公共资源,所以可以通过修改属性实现这命名空间之间移动。当然是否允许移动和设备的特征有关。

1.网络命名空间的实现

Linux实现网络命名空间的核心:Linux的网络协议十分复杂,为了支持独立的协议栈,相关的全局变量都必须修改为协议栈私有。最好的办法就是让全局变量成为一个Net Namespace变量的成员,然后为协议栈私有。最好的办法就是让这些全局变量成为一个Net Namespace变量的成员,然后为协议栈的函数调用加入一个Namespace参数。

所有的网络设备(物理的或虚拟接口、桥等着内核里都叫作Net Device)都只能属于一个命名空间。通常物理的设备只能关联到root这个命名空间中,虚拟的网络设备(虚拟的以太网接口或者虚拟网口对)则可以被创建并关联到一个给定的命名空间中,而且可以在命名空间之间移动。

网络命名空间代表的是一个独立的协议栈,它们之间是相互隔离的,彼此无法通信,在协议栈内部都看不到对方。打破这种限制,让处于不同命名空间的网络相互通信,甚至和外部的网络进行通信的方法就是:Veth设备对。

Veth设备对重要作用就是:打破相互看不到的协议栈之间的壁垒,就像一个管子,一端连着这个网络命名空间的协议栈,一端连着另一个网络命名空间的协议栈。

所以如果想在两个命名空间之间进行通信,就必须有一个Veth设备对

2.网络命名空间的操作

命令需要root用户运行

创建一个命名空间:

cpp 复制代码
ip nets add <name>

在命名空间内执行命令:

cpp 复制代码
ip netns exec <name> <command>

如果想执行多个命令,可以先进入内部sh,然后执行:

cpp 复制代码
ip netns exec <name> bash

之后就是在新的命名空间内进行操作了,退出到外面的命名空间,输入exit

3.网络命名空间的一些技巧

可以在不同的网络命名空间之间转移设备,例如Veth设备对的转移。设备里面有一个重要属性:NETIF_F_ETNS_LOCAL,这个属性为"on",则不能转移到其他命名空间内。Veth设备属于可以转移的设备,很多其他设备如lo设备、vxlan设备、ppp设备、bridge设备等都是不可以转移的。

使用ethtool工具可以查看:

cpp 复制代码
ethtool -k br0
netns-local: on [fixed]

netns-local的值是on,就说明不可以转移,否则可以

四、Veth设备对

引入Veth设备对是为了在不同的网络命名空间之间进行通信,利用它可以直接将两个网络命名空间连接起来。由于要连接两个网络命名空间,所以veth设备都是成对出现的,很像一对以太网卡,并且中间有一个直连的网线。既然是一对网卡,将其中一端称为另一端的peer,在Veth设备的一端发送数据时,会将数据直接发送到另一端,并触发另一端的接收操作。

1.Veth设备对的操作命令

创建Veth设备对,连接到不同的命名空间,并设置它们的地址,让它们通信。

创建Veth设备对:

cpp 复制代码
ip link add veth0 type veth peer name veth1

创建后查看Veth设备对的信息,使用ip link show命令查看所有网络接口:

cpp 复制代码
ip link show

会生成两个设备,一个是veth0,peer是veth1 两个设备都在同一个命名空间,将Veth看作是有两个头的网线,将另一个头甩给另一个命名空间

cpp 复制代码
ip link set veth1 netns netns1

再次查看命名空间,只剩下一个veth0:

cpp 复制代码
ip link show

在netns1命名空间可以看到veth1设备,符合预期。

现在看到的结果是两个不同的命名空间各自有一个Veth的网线头,各显示为一个Device。(Docker的实现里面,除了将Veth放入容器内)

下一步给两个设备veth0、veth1分配IP地址:

cpp 复制代码
ip netns exec netns1 ip addr add 10.1.1.1/24 dev veth1

ip addr add 10.1.1.2/24 dev veth0

现在两个网络命名空间可以互相通信了:

cpp 复制代码
ping 10.1.1.1

ip netns exec netns1 ping 10.1.1.2

至此两个网络命名空间之间就完全相通了。至此就能够理解Veth设备对的原理和用法了。在Docker内部,Veth设备对也是联系容器到外面的重要设备,离开它是不行的。

2.Veth设备对如何查看对端

一旦将Veth设备对的peer端放入另一个命名空间,在本命名空间就看不到了,想查看peer端连接的命名空间,可以使用ethtool工具来查看。

首先在一个命名空间中查询Veth设备对端接口在设备列表中的序列号:

cpp 复制代码
ip nets exec netns1 ethtool -S veth1
NIC statistics:
    peer_ifindex: 5

得知另一端的接口设备的序列号是5,再到命名空间中查看序列号5代表的设备:

cpp 复制代码
ip netns exec netns2 ip link | grep 5
veth0

现在就找到下标为5的设备了,是veth0,另一端自然就是另一个命名空间中的veth1了,互为peer。

五、网桥

Linux可以支持多个不同的网络,网络之间能够相互通信,网桥将这些网络连接起来并实现网络中主机的相互通信。

网桥是一个二层的虚拟网络设备 ,把若干个网络接口"连接起来",以使得网口之间的报文能够互相转发。**网桥能够解析收发的报文,读取目标MAC地址的信息,和自己记录的MAC表结合,来决策报文的转发目标网口。**为了实现这些功能,网桥会学习源MAC地址(二层网桥转发的依据就是MAC地址)。在转发报文时,网桥只需要向特定的网口进行转发,避免不必要的网络交互。网桥如果遇到一个未学习到的地址,就无法知道这个报文应该向哪个网口转发,就将报文广播给所有的网口(报文来源的网口除外)。

在实际网络中,网络拓扑不可能永久不变。网桥需要对学习到的MAC地址表加上超时时间(默认为5分钟),如果网桥收到了对应端口MAC地址回发的包,则重制超时时间,否则过了超时时间后,就认为设备已经不在那个端口上了,就会重新广播发送。

Linux内核支持网口的桥接,与单纯的交换机不同,交换机只是一个二层设备,对于接收到的报文,要么转发,要么丢弃。运行着linux内核的机器本身就是一台主机,有可能是网络报文的目的地,收到的报文除了转发和丢弃,还可能被送到网络层协议栈的上层(网络层),从而被主机本身的协议栈消化,既可以把网桥看作一个二层设备,也可以看作一个三层设备。

1.Linux网桥的实现

Linux内核是通过一个虚拟的网桥设备(Net Device)来实现桥接的。虚拟设备可以绑定若干个以太网接口设备,从而将它们桥接起来。

虚拟的网桥设备和普通设备的不同,最明显的一个特性是可以有一个IP地址:

网桥br0绑定了eth0和eth1。对于网络协议栈的上层来说,只看的到br0。桥接是在数据链路层实现的,上层不需要关心桥接的细节,协议栈上层需要发送的报文被送到br0,网桥设备的处理代码判断报文该被转发到eth0还是eth1,或者两者皆转发。协议栈的上层需要发送的报文被提交给网桥的处理代码,在这里会判断报文应该被转发,丢弃还是提交到协议栈上层。

有时eth0、eth1也可能会作为报文的源地址或目的地址,直接参与报文的发送与接收,从而绕过网桥。

2.网桥的常用操作命令

Docker自动完成了对网桥的创建和维护。

新增一个网桥设备:

cpp 复制代码
brctl addbr xxxxx

为网桥增加网口,在Linux中,一个网口其实就是一个物理网卡,将物理网卡和网桥连接起来。

cpp 复制代码
brctl addif xxxxx ethx

网桥的物理网卡作为一个网口,由于在链路层工作,就不再需要IP地址了,这样上面的IP地址自然失效。

cpp 复制代码
ifconfig ethx 0.0.0.0

给网桥配置一个IP地址:

cpp 复制代码
Ifconfig brxxx xxx.xxx.xxx.xxx

这样网桥就有一个IP地址,连接到上面的网卡就是一个纯链路层设备了。

六、Iptables/Netfilter

Linux提供了一套机制来为用户实现自定义的数据包处理过程。

在linux网络协议中有一组回调函数挂接点,通过这些挂接点挂接的钩子函数可以在linux网络栈处理数据包的过程中对数据进行一些操作,例如过滤、修改、丢弃等。整个挂接点技术叫作Netfilter和Iptables。

Netfilter负责在内核中执行各种挂接的规则,运行在内核模式中。而Iptables是在用户模式下运行的进程,负责协助维护内核中Netfilter的各种规则表。通过二者的配合来实现整个Linux网络协议栈中灵活的数据包处理机制。

四表:raw、mangle、nat、filter

Netfilter可以挂接的规则点有5个(链):

  • INPUT

  • OUTPUT

  • FORWARD

  • PREROUTING

  • POSTROUTING

  • 流入:PREROUTING -> INPUT

  • 流出:OUTPUT -> POSTROUTING

  • 转发:PREROUTING -> FORWARD -> POSTROUTING

七、路由

Linux系统包含一个完整的路由功能。当IP在处理数据发送或者抓发时,会使用路由表来决定发往哪里。通常情况下,如果主机与目的主机直接相连,那么主机可以直接发送IP报文到目的主机,这个过程比较简单。例如,通过点对点的链接或通过网络共享,如果主机与目的主机没有直接相连,那么主机会将IP报文发送给默认的路由器,然后由路由器来决定往哪发送IP报文。

路由功能由IP层维护的一张路由表来实现。当主机收到数据报文时,用此表来决策接下来应该做什么操作。当从网络侧接收到数据报文时,IP层首先会检查报文的IP地址是否与主机自身的地址相同。如果数据报文中的IP地址是主机自身的地址,那么报文将被发送到传输层相应的协议中去。如果报文中的IP地址不是主机自身的地址,并且配置了路由功能,那么报文将被转发,否则,报文将被丢弃。

路由表中的数据一般是以条目形式存在的。一个典型的路由表条目通常包含以下主要的条目项。

  • 目的IP地址:此字段表示目标的IP地址。这个IP地址可以是某台主机的地址,也可以是一个网络地址。如果这个条目包含的是一个主机地址,那么主机ID将被标记为非零。如果这个条目包含的是一个网络地址,主机ID将被标记为零。

  • 下一个路由器的IP地址:下一个路由器并不总是最终的目的路由器,很可能是一个中间路由器。条目给出下一个路由器的地址用来转发从相应接口收到的IP数据报文。

  • 标志:这个字段提供了另一组重要信息,例如目的IP地址是一个主机地址还是一个网络地址。从标志中可以得知下一路由器是一个真实的路由器还是一个直接相连的接口。

  • 网络接口规范:数据报文的网络接口规范,该规范将与该报文一起被转发。

1.路由表的创建

Linux的路由表至少包括两个表:一个是LOCAL,另一个是MAIN。在LOCAL表中会包含所有的本地设备地址。LOCAL路由表是在配置网络设备地址时自动创建的。LOCAL表用于供Linux协议栈识别本地地址,以及进行本地各个不同网络接口之间的数据转发。

可以通过下面的命令查看LOCAL表的内容:

cpp 复制代码
ip route show table local type local

MAIN表用于各类网络IP地址的转发。MAIN表的建立可以使用静态配置生存,也可以使用动态路由发现协议生成。

2.路由表的查看

使用ip route list命令查看当前的路由表。

cpp 复制代码
ip route list

另一个查看路由表的工具:

cpp 复制代码
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的网络实现

Docker支持以下4类网络模式:

  • host模式:使用--net=host指定

  • containe模式:使用--net=container:NAME_or_ID指定

  • none模式:使用--net=none指定

  • bridge模式:使用--net=bridge指定,为默认设置

在K8s管理模式下,通常只会使用bridge模式,在bridge模式下,Docker Daemon第一次启动时会创建一个虚拟的网桥,默认的名字是docker0,在私有网络空间中给这个网桥分配一个子网。

针对由Docker创建出来的每一个容器,都会创建一个虚拟的以太网设备(Veth设备对),其中一端关联到网桥上,另一端使用Linux的网络命名空间技术,映射到容器内的eth0设备,然后从网桥的地址段内给eth0接口分配一个IP地址。

  • ip1是网桥的IP地址,Docker Daemon会在几个备选地址段里选一个,通常是172开头的一个地址。ip2是Docker在启动容器时,在这个地址段随机选择的一个没有使用的IP地址。相应的MAC地址也根据这个IP地址,在02:42:ac:11:00:00和02:42:ac:11:ff:ff的范围内生存,可以确保不会有ARP的冲突。

  • 启动后,Docker还将Veth对的名字映射到eth0网络接口。ip3就是主机的网卡地址。

  • ip1、ip2和ip3是不同的IP段,外部是看不到ip1和ip2的。同一台机器内的容器之间可以相互通信。不同主机上的容器不能够相互通信。

  • 为了跨节点互相通信,必须在主机的地址上分配端口,然后通过这个端口路由代理到容器上,这种做法显然意味着一定要在容器之间小心谨慎地协调好端口的分配,或者使用动态端口分配技术。

1.查看Docker启动后的系统情况

Docker网络中bridge模式下Docker Daemon启动时创建docker0网桥,并在网桥使用的网段为容器分配IP。

cpp 复制代码
ip addr
iptables-save

Docker创建了docker0网桥,并添加了Iptables规则。

2.查看容器启动后的情况(容器无端口映射)

cpp 复制代码
docker run --name register -d registry
ip addr
iptables-save
ip route

可以看到如下情况。

  • 宿主机器上的Netfilter和路由表都没有变化,说明在不进行端口映射时,Docker的默认网络上没有特殊处理的。相关的NAT和FILTER两个Netfilter链还是空的。

  • 宿主机上的Veth对已经建立,并连接到了容器内。

进入容器,查看网络栈,容器内部的IP地址和路由如下:

cpp 复制代码
ip route
ip addr

可以看到,默认停止的回环设备lo已经被启动,外面宿主机连接进来的Veth设备也被命名成了eth0,并且已经配置了地址172.17.0.10。

3.查看容器启动后的情况(容器端口映射)

用带端口映射的命令启动registry

cpp 复制代码
docker run --name register -d -p 1180:5000 registry

启动后查看Iptables的变化

cpp 复制代码
iptables-save
  • Docker服务在NAT和FILTER两个表内添加的两个Docker子链都是给端口映射用的。例如需要把宿主机的1180端口映射到容器的5000端口。

  • 无论是宿主机接收还是宿主机本地协议栈发出的,目标地址是本地IP地址的包都会经过NAT表中的DOCKER子链。Docker为每一个端口映射都在这个链上增加了到实际容器目标地址和目标端口的转换。

  • 经过这个DNAT的规则修改后的IP包,会重新经过路由模块的判断进行转发。由于目标地址和端口已经是容器的地址和端口,所以数据自然就送到docker0上,从而送到对应的容器内部。

  • 在Forward时,需要在Docker子链中添加一条规则,如果目标端口和地址是指定容器的数据,则允许通过。

  • 在Docker按照端口映射的方式启动容器时,主要的不同就是上述Iptables部分。从而容器内部的路由和网络设备,都和不做端口映射时一样,没有任何变化。

九、K8s的网络实现

K8s网络的设计主要致力于解决以下场景:

  • 容器到容器之间的直接通信

  • 抽象的pod到pod之间的通信

  • Pod到Service之间的通信

  • 集群外部与内部组件之间的通信

1.容器到容器的通信

在同一个Pod内的容器共享同一个网络命名空间,共享同一个linux协议栈。对于网络的各类操作,就和它们在同一台机器上一样,设置可以用localhost地址访问彼此的端口。

容器1和容器2共享网络命名空间,打开的端口不会有冲突,可以直接使用linux的本地IPC进行通信(例如消息队列或者管道),互相访问只需要使用localhost就可以了。

2.Pod之间的通信

每一个Pod都有一个真实的全局IP地址,同一个Node内的不同Pod之间可以直接采用对方Pod的IP地址通信,而且不需要使用其他发现机制,例如DNS或者etcd。

Pod容器既有可能在同一个Node上运行,也有可能在不同的Node上运行,通信分为两类:

  • 同一个Node内的Pod之间的通信和不同Node上的Pod之间的通信

1) 同一个Node内的Pod之间的通信

  • Pod1和Pod2都是通过Veth连接在同一个docker0网桥上的,IP地址IP1、IP2都是从docker0到网段上动态获取的,和网桥本身的IP3是同一个网段的。

  • 在Pod1、Pod2的Linux协议上,默认路由都是docker0的地址,也就是说所有非本地地址的网络数据,都会被默认发送到dockero网桥上,由docker0网桥直接中转。

  • 综上所述,Pod1和Pod2关联在同一个docker0网桥上,地址段相同,所以Pod1和Pod2之间是能直接通信的。

2)不同Node上的Pod之间的通信

Pod的地址是与docker0在同一个网段内的,docker0网段与宿主机网卡是两个完全不同的IP网段,并且不同Node之间通信只能通过宿主机的物理网卡进行,因此要实现位于不同Node上的Pod容器之间通信,就必须想办法通过主机的这个IP地址来进行寻址和通信。

K8s会记录所有正在运行Pod的IP分配信息,并将这些信息保存在etcd中(作为Service的Endpoint)。要想支持不同Node上的Pod之间的通信,就要达到两个条件:

  • 在整个K8s集群中对Pod的IP分配进行规划,不能有冲突

  • 将Pod的IP和所在Node的IP关联起来,通过这个关联让Pod可以互相访问

根据条件1,需要在部署k8s时,对docker0的IP地址进行规划,保证每一个Node上的docker0地址没有冲突。可以在规划后手工配置到每个Node上,或者做一个分配规则,由安装的程序自己去分配占用。例如K8s的网络增强开源软件Flannel就能够管理资源池的分配。

根据条件2:Pod中的数据中发出时,需要有一个机制能够知道对方Pod的IP地址,挂在哪个具体的Node上。也就是先要找到Node对应宿主机的IP地址,将数据发送到宿主机的网卡上,然后在宿主机上将相应的数据转到具体的docker0上,一旦数据到达宿主机Node,则那个Node内部的docker0便知道如何将数据发送到Pod。

十、Pod和Service网络实战

K8s网络模型要求每个Node上的容器都可以相互访问。一个Pod内的所有容器都需要共用同一个IP地址,意味着一定要使用网络的容器映射模式。Pod会启动一个基础容器,然后将Pod内所有容器都连接到上面,只需要为基础的这个Google_containers/pause容器执行端口映射规则。

1.部署一个RC/Pod

cpp 复制代码
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上网络接口有哪些?

cpp 复制代码
ifconfig

可以看出有一个docker0网桥和一个本地地址的网络端口。

部署RC/Pod配置文件

cpp 复制代码
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命令来检验下

cpp 复制代码
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

cpp 复制代码
apiVersion: v1
kind: Service
metadata:
  name: frontend
  labels:
    name: frontend
spec:
  ports:
  - port: 80
  selector:
   name: frontend

然后在K8s集群中定义这个服务

cpp 复制代码
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的规则

cpp 复制代码
iptables-save

所有流量都被导入kube-proxy中,需要kube-proxy完成一些负载均衡工作,创建Replication Controller并观察结果。

cpp 复制代码
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都运行起来了

cpp 复制代码
kubectl create -f frontend-controller.yaml

kubectl get pods -o wide

所有Pod都运行起来后,Service会将匹配到的标签为name=frontend的所有Pod都进行负载分发。因为Service的选择匹配所有这些Pod,负载均衡将会对这3个Pod进行分发。

相关推荐
lichenyang4531 天前
Docker 学习笔记(四):Dockerfile,把项目打成自己的镜像
docker·容器
lichenyang4531 天前
Docker 学习笔记(三):Docker 网络、bridge、子网和容器互通
docker·容器
lichenyang4531 天前
Docker 学习笔记(二):docker run 的参数到底在控制什么?
docker·容器
运维开发故事4 天前
基于 Arthas 的多集群在线诊断系统设计与实现
kubernetes
Patrick_Wilson6 天前
从「改个端口」到 502:Next.js on k8s 的容器端口、Service 映射与 env 覆盖
docker·kubernetes·next.js
探索云原生6 天前
K8s 1.36 这个 GA 特性,把 initContainer 拉模型的 hack 干掉了
ai·云原生·kubernetes
云恒要逆袭6 天前
运行你的第一个Docker容器
后端·docker·容器
Java之美7 天前
一次k8s升级引发的DevicePlugin注册失败
云原生·kubernetes
程序员老赵8 天前
10 分钟部署 OpenCode:Docker 一键安装,浏览器打开就能用 AI 写代码(附完整命令与排错)
docker·容器·ai编程