Kubernetes docker 网络

网络命名空间

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

在建立新的网络命名空间,并将某个进程关联到这个网络命名空间后,就出现了类似 于如图 7.1 所示的内核数据结构,所有网站栈变簸都被放入了网络命名空间的数据结构中 。 这个网络命名空间是其进程组私有的,和其他进程组不冲突 。

image-20220703114525847

在新生成的私有命名空间 中只 有回环设备(名为 “lo” 且是停止状态),其他设备默 认都不存在,如果我们需要,则要一一手工建立 。 Docker 容器中的各类网络栈设备都是 Docker Daemon 在启动时自动创建和配置的。

image-20220703114824113

前面提到,由千网络命名空间代表的是一个独立的协议栈,所以它们之间是相互隔离 的,彼此无法通信,在协议栈内部都看不 到对方 。 处于不同命名空间中的网络相互通信,甚至与外部的网络进行通信。应用 Veth 设备对即可

Veth 设备对

引入 Veth 设备对是为了在不同的网络命名空间之间通信,利用它可以直接将两个网 络命名空间连接起来 。 由于要连接两个网络命名空间,所以 Veth 设备都是成对出现的。

image-20220703125103515

在 Docker 内部, Veth 设备对也是连通容器与宿主 机的主要网络设备,离开它是不行的 。

网桥

将这些网络连接起 来 并实现各网络中主机的相互通信呢,可以用网桥。

网桥是一个二层的虚拟网络设备,把若 干个网络接口”连接”起来,以使得网络接口之间的报文能够相互转发 。 网桥能够解析收 发的报文,读取目标 MAC 地址的信息,将其与自己记录的 MAC 表结合,来决策报文的 转发目标网络接口 。

为了实现这些功能,网桥会学习源 MAC 地址(二层网桥转发的依据 就是 MAC 地址 )。在转发报文时,网桥只需向特定的网口进行转发,来避免不必要的网络 交互。如果它遇到一个自己从未学习到 的地址,就无法知道这个报 文应该向哪个网络接口 转发,将报文广播给所有的网络接口(报文来源的网络接口除外)。

在实际的网络中,网络拓扑不可能永久不变。设备如果被移动到另 一 个端口上,却没 有发送任何数据,网桥设备就无法感知这个变化,网桥还是向原来的端口转发数据包,在 这种情况下数据会丢失。如果网桥收到了对应端口 MAC 地址回发的包,则重置超时时间,否则过了超时 时间,就认为设备已经不在那个端口上了,它会重新广播发送 。

Linux 网桥的实现

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

这种 Net Device 网桥 和普通的设备不同,最明显的 一 个特性是它还可以有一个 IP 地址 。

image-20220703133257368

网桥设备 br0 绑定了 eth0 和 ethl。对于网络协议的上层来说,只看得到 br0 就行 。 因为桥接是在数据链路层实现的,上层不需要关心桥接的细节,所以协议 栈上层需要发送的报文被送到 br0, 网桥设备的处理代码判断报文应该被转发到 eth0 还是eth1,还是都可以反过来,从 cthO 或从 ethl 接收到的报文被提交给网桥的处理代码,在这里会判断报文应该被转发、丢弃还是被提交到协议栈上层 。

iptables 和 Netfilter

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

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

  • Netfilter 负责在内核中执行各种挂接的规则,运行在内核模式中;

  • 而 iptables 是在用 户模式下运行的进程,负责协助和维护内核中 Netfilter 的各种规则表 。

二 者相互配合来实 现整个 Linux 网络协议栈中灵活的数据包处理机制 。

image-20220703134153348

image-20220703135227801

docker网络实现

安装 Docker 时,它会自动创建三个网络,bridge(创建容器默认连接到此网络)、 none 、host

  • host:容器将不会虚拟出自己的网卡,配置自己的 IP 等,而是使用宿主机的IP和端口。
  • Container:创建的容器不会创建自己的网卡,配置自己的 IP,而是和一个指定的容器共享 IP、端口范围。
  • None:该模式关闭了容器的网络功能。
  • Bridge:此模式会为每一个容器分配、设置 IP 等,并将容器连接到一个 docker0 虚拟网桥,通过 docker0 网桥以及 Iptables nat 表配置与宿主机通信。

在 bridge 模式下, Docker Daemon 首次启动时 会创建一个虚拟网桥,默认的名称是 docker0, 然后按照 RPC1918 的模型在私有网络 空间中给这个网桥分配一个子网。针对由 Docker创建的每一个容器,都会创建一个虚拟以太网设备 (Veth设备对),其中一端关联 到网桥上,另一端使用 Linux 的网络命名空间技术映射到容器内的 eth0设备,然后在网桥 的地址段内给 eth0接口分配一个 IP 地址 。

image-20220703163306419

启动后, Docker 还将 Veth 设备对的名称映射到 ethO 网络接口 。 ip3 就是主机的网卡 地址 。

其中 ipl 是网桥的 IP 地址, Docker Daemon 会在几个备选地址 段里给它选一个地址, 通常是以 172 开头的一个地址,这个地址和主机的 IP 地址是不重叠的 。 ip2 是 Docker 在 启动容器时在这个地址段选择的一个没有使用的 IP 地址,它被分配给容器,相应的 MAC 地址也根据这个 IP 地址,在 02:42:ac:11:00:00 和 02:42:ac:11:ff:ff的范围内生成,这样做 可以确保不会有 ARP 冲突 。

在一般情况下, ipl 、 ip2 和 ip3 是不同的 IP 段,所以在默认 不 做任何特殊配置的情况 下,在外部是看不到 ip1 和 ip2 的 。

k8s网络实现

同一个pod

同一个 Pod 内的容器 (Pod 内的容器是不会跨宿主机的)共享同一个网络命名空间,共享同一个 Linux 协议栈。

如图 7.8 中的阴影部分所示,在 Node 上运行若 一 个 Pod 实例 。 在我们的例子中,容 器就是图 7.8 中的容器 1 和容器 2。容器 l 和 容器 2 共享一个网络的命名空司,共享一个 命名空间的结果就是它们好像在一台机器上运行,它们打开的端口不会有冲突,可以直接 使用 Linux 的本地 IPC 进行通信(例如消息队列或者管道)。其实,这和传统的一组普通 程序运行的环境是完全一样的,传统程序不需要针对网络做特别的修改就可以移植,它们 之间的相互访问只需使用 localhost 就可以 。

image-20220703193539713

不同pod

同一个node两个pod

image-20220703195936914

可以看出, Podl 和 Pod2 都是通过 Veth 连接到同一个 dockerO 网桥的,它们的 IP 地址 IPl 、 IP2 都是从 dockerO 的网段上动态获取的,和网桥本身的 IP3 属千同 一 个网段 。

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

由于它们都关联在同 一 个 dockerO 网桥上,地址段相同,所以它们之间是能直接通信的 。

不同Node

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

另一方面 , 这些动态分配且藏在 dockerO 后的“私有 “IP 地址也是可以找到的 。 Kubernetes 会记录所有正在运行的 Pod 的 IP 分配信息,并将这些信息保存在 etcd 中(作 为 Service 的 Endpoint)。这些私有 IP 信息对于 Pod 到 Pod 的通信也是十分重要的,因为 我们的网络模型要求 Pod 到 Pod 使用私有 IP 进行通信。所以首先要知道这些 IP 是什么 。

综上所述,要想支持不同 Node 上 Pod 之间的通信 , 就要满足两个条件

  • 整 个 Kubernetes 集群中对 Pod 的 IP 分配进行规划,不能有冲突(Flannel管理资源池的分配)
  • 找到一种办法,将 Pod 的 IP 和所在 Node 的 IP 关联起来,通过这个关联让 Pod 可以相互访问

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

网络实现的不同

默认的 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, 并不需要在主机上做端口映射 。

这意味着,每一个新部署的容器都将使用这个 Node ( docker0 的网桥 IP) 作为它的默认网关。而这些 Node (类似路由器)都有其他 docker0的路由信息,这样它们就能够相互 连通了 。

k8s pod 网络

1
2
3
4
5
docker inspect 6dlb99c£f4ae I grep NetworMode 
"Network.Mode": "bridge",
docker inspect 37b193a4c633 I grep NetworMode
"Network.Mode": "container:6dlb99cff4ae537689ce87d7528f4ba9dbb40ae
711ecc0a5b3f7c39ff5e5e4 95 ",

我们检查的第 1 个容器是运行了 k8s.gcr.io/pause: latest 镜像的容器,它使用了 Docker 默认的网 络模型 bridge; 而我们检查的第 2 个容器,也就是在 RC/Pod 中定义运行的 php-redis 容器, 使用了非默认的网络配笠和映射容器的模型,指定了映射目标容器为 k8s.gcr.io/pause:latest

service网络

在服务正确创建后,可以看到 Kubernetes集群已经为这个服务分配了一个虚拟 IP地址。这个 IP 地址是在 Kubernetes 的 Portal Network 中分配的。 而这个 Portal Network的地址范围是我们在 Kubmaster上启动 API服务进程时,使用–service-cluster-ip-range=xx 命令行参数指定 的

这个 IP 段可以是任何段,只要不和 docker0 或者物理网络的子网冲突就可以 。 选择任 意其他网段的原因是,这个网段将不会在物理网络和 dockerO 网 络上进行路由 。 这个 Portal Network 针对的是每 一 个 Node 都有局部的特殊性,实际上它存在的意义是让容器的流量都指 向默认网关(也就是dockerO网桥)。

目标为 Service IP 地址和端口的任何流呈都将被重定向到本地的 33761 端口(iptable作用)

这个端口连到哪里去了呢?这就到了 kube-proxy 发挥作用的地方了 。这个 kube-proxy 服务 给每一个新创建 的服务都关联了 一个随机的端口号 ,并且监听那个特定的端口,为服务创 建了相关的负载均衡对象。

可以知道,所有流量都被导入 kube-proxy 中了。我们现在需要它完成一些负载均衡工 作,Service 将会把客户端的请求负载分发到包含name=frontend 标签的所有 Pod 上

1
2
3
4
5
6
7
8
9
yum -y install tcpdump

# 登录 Nodel, 运行 tcpdump命令: 需要捕获 物 理 服 务器以太网接口 的 数据包 , eno l677773 6
tcpdump -nn -q -i enol6777736 port 80

# 挂接在 dockerO 桥上的虚拟网卡 Veth 的名称 service ip
tcpdump -nn -q -i veth0558bfa host 20.1.244.75

# 进入pod内部 curl svc ip

image-20220706232002608

  • 让我们在网络图上用实线标出第 1个窗口中网络抓包信息的含义(物理 网卡上 的网络流量),

  • 并用虚线标出第 2 个窗口土质络抓 包信息的含义 ( docker0 网桥上的 网络流量)

image-20220714213543762

虚线绕 过了 Node3 的 kube-proxy, 这么做是因为 Node3 上的 kube-proxy 没有参与这次网络交互。换句话说 , Nadel 的 kube-proxy 服务直娄和负载均衡 到的 Pod进行网络交互。

总而言之, Kubemetes 的 kube-proxy作为一个全功能的代理服务器管理了两个独立的 TCP 连接:一个是从容器到 kube-proxy: 另一个是从 kube-proxy 到负载均 衡的 目标 Pod。

CNI

image-20220717111851928

CNI 定义了容器运行环境与网络插件之间的简单接口规范,通过 一个 JSON Schema 定义 CNI 插件提供的输入和输出参数 。一个容器可以通过绑定多个网络插件加人多个网络 中。

在 CNI 模型中只涉及两个概念:容器和网络。

  • 容器 : 是拥有独立 Linux 网络命名空间的环境,例如使用 Docker 或 rkt 创建的容 器 。关 键之处是容器需要拥有自己的 Linux 网络命名空间,这是扛入网络的必要 条件 。
  • 网络:表示可以互连的一组实体,这些实体拥有各自独立、唯一航 IP 地址,可以 是容摇、物理机或者其他网络设备(比如路由器)等 。 可以将容器添加到一个或 多个网络中,也可以从一个或多个网络中删除 。

Flannel 插件的原理

可以搭建 Kubernetes 依赖的底层网络,是因为它能实现以下两点

  • 协助 Kubernetes, 给每一个 Node上的 Docker容器都分配互不冲突的 IP地址。
  • 在这些 IP地址之间建立一个覆盖网络 (OverlayNetwork),通过这个覆盖网络,将数据包原封不动地传递到目标容器 内 。

Flannel 首先创建了一个名为 flanne!O 的网 桥,而且这个网桥的一端连接dockerO 网桥,另一端连接一个叫作 flanneld 的服务进程 。

flanneld 进程并不简单,它上连 etcd, 利用 etcd 来管理可分配的 IP 地址段资源 ,同时 监控 etcd 中每个 Pod 的实际地址,并在内存中建立了一个 Pod 节点路由表;它下连 dockerO 和物理网络,使用内存中 的 Pod 节点路由表,将 dockerO 发给它的数据包包装起来,利用物理 网络的连接将数据包投递到目标 flanneld 上,从而完成 Pod 到 Pod 之间的直接地址通信 。

我们看一下 Flannel 是如何做到为不同 Node 上的 Pod 分配的 IP 不产生冲突的 。其实 想到 Flannel 使用了集中的 etcd 存储就很容易理解了 。 它每次分配的地址段都在同 一个公 共区域获取,这样大家自然能够相互协调,不产生冲突了。而且在 Flannel 分配好地址段 后,后面的事情是由 Docker 完成的, Flannel 通过修改 Docker 的启动参数将分配给它的地 址段传递进去:

–bip=1 72.17.18 .1/24 通过这些操作, Flannel 就控制了每个 Node 上的 dockerO 地址段的地址,也就保障了

所有 Pod 的 IP 地址都在同一个水平网络中且不产生冲突了 。

Calico

Calico 保证 所有容器之间的数据流最都是通过 IP 路由的方式完成互联互通的 。Calico 节点组网时可以 直接利用数据中心的网络结构 (L2 或者 L3),不需要额外的 NAT 、隧道或者 Overlay Jetwork, 没有额外的封包解包,能够节约 CPU 运算,提高网络效 率。Calico 在小规模集群中可以直接互联,在大规模集群中可以通过 额外的 BGP route reflector来完成。

image-20220813123241504

  • Felix: Calico Agent, 运行在每个 Node 上,负责为容器设登网络资源 (IP 地址、 路由规则 、 iptables 规则等),保证跨主机容器网络互通 。

  • etcd: Calico 使用的后端存储 。

  • BGPClient:负责把Felix在各Node上设置的路由信息通过BGP广播到Calico网络。

  • Route Reflector: 通过一个或者多个 BGP Route Reflector 完成大规模集群的分级路

    由分发

  • CalicoCtl: Calico 命令行管理工具 。

部署Calico应用

  • 修改 Kubemetes 服务的启动参数,并重启服务
  • 设置 Master 上 kube-apiserver 服务的启动参数: –allow-privileged=true (因为 calico-node 需要以特权模式运行在各 Node 上)。
  • 设置各 Node 上 kubelet 服务的启动参数 : –network-plugin=cni (使用 CNI 网络插 件)。

IPIP 是一种将各 Node 的路由之间做 一个 tunnel, 再把两个网 络连接起来的模式。BGP 模式则直接使用物理机作为虚拟路由器 (vRouter),不再创建额外的 tunnel

Network Policy

策略控制器需要实现一个 API Listener,监听用户设置的 NetworkPolicy 定义,并将网络访问规则通过各 Node 的 Agent 进行实际设 置 (Agent则需要通过 CNI 网络插件实现)。

image-20220903213550256