k8s网络通信初探

前言

近年,Docker 已成为最主流的容器技术,已逐渐成为虚拟化主流。相比传统虚拟化方案,具有更轻量、易于移植、环境一致性等优势。为实现Docker 的集群化、规模化管理, 谷歌推出了Kubernetes——一个对容器化应用自动部署、伸缩和管理的开源系统。而Kubernetes框架中的网络问题是系统运行的重中之重。

本文概要分析Kubernetes系统网络通信模式,介绍必要的网络通信基础、Docker容器技术通信原理、Kubernetes几种典型网络通信架构,更加清晰的阐述其网络问题,为日后相关文件的解决提供理论支撑。

  • 一、网络基础

1、Linux基础(namespace+cgroup)

Linux为了实现网络协议栈的多实例,在网络栈中引入了命名空间(Network Namespace),这些独立的协议栈被隔离到不同的命名空间中。处于不同命名空间的网络栈是完全隔离的,彼此之间无法通信,就好像两个 “平行宇宙”。通过这种对网络资源的隔离,就能在一个宿主机上虚拟多个不同的网络环境。  Docker 也正是利用了网络的命名空间特性,实现了不同容器之间网络的隔离。linux支持多种类型的namespace,包括Network,IPC,PID, Mount, UTC, User。创建不同类型的namespace就相当于从不同资源维度在主机上作隔离。在Linux 的网络命名空间内可以有自己独立的路由表及独立的 iptables/Netfilter 设置来提供包转发、NAT 及 IP 包过滤等功能。

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

k8s网络通信初探_第1张图片

Linux CGroup全称Linux Control Group,是Linux内核为了不让某个进程一家独大,而其他进程饿死,所以它的作用就是控制各进程分配的CPU,Memory,IO等。

   

2、Veth 设备对

前面提到,由于命名空间代表的是一个独立的协议栈,所以它们之间无法通信,为了让处于不同命名空间的网络互相通信,甚至和外部的网络进行通信,所以引入 Veth 设备对是为了在不同的网络命名空间之间进行通信,利用它可以直接将两个网络命名空间连接起来。由于要连接两个网络命名空间,所以Veth 设备都是成对出现的,很像一对以太网卡,并且中间有直连网线。既然是一对网卡,那么将其中一端称为另一端的peer。在 Veth 设备的一端发送数据时,它会将数据直接发送到另一端,并触发另一端的接收操作。

k8s网络通信初探_第2张图片

 

3、网桥

Linux 可以支持很多不同的端口,这些端口之间可以进行通讯,这就是网桥的功能,网桥是一个二层网络设备,可以解析收发的报文,读取目标 MAC 地址的信息,和自己记录的 MAC 表结合,来决策报文的转发端口。为了实现这些功能,网桥会学习源 MAC 地址(二层网桥转发的依据就是 MAC 地址)。在转发报文的时候,网桥只需要向特定的网络接口进行转发,从而避免不必要的网络交互。如果它遇到一个自己从未学习到的地址,就无法知道这个报文应该从哪个网口设备转发,于是只好将报文广播给所有的网络设备端口(报文来源的端口除外)。

在实际网络中,网络拓扑不可能永久不变,如果设备移动到一个端口上,而它没用发送任何数据,那么网桥设备就无法范知道这个变化,结果网桥还是向原来的端口转发数据包,在这种情况下数据就会丢失,所以网络还要对学习到的 MAC 地址表加上超时时间(默认为5 分钟)。如果网桥收到了对应端口 MAC 地址返回的包,则重设超时时间,否则过了超时时间后,就认为这个设备已经不在那个端口上了,就会重新发送广播。

在 Linux 的内部网络栈里面实现的网桥设备,作用和上面的描述相同。过去 Linux 主机一般都是只有一个网卡,对于接收到的报文,要不转发,要不丢弃。运行着 Linux 内核的机器本身更就是一台主机,有可能是网络报文的目的地,其收到的报文除了转发和丢弃,还可能被发送到网络协议栈的上层(网络层),从而被自己(这台主机本身的协议栈)消化,所以我们既可以把网桥看做一个二层设备,也可以看作一个三层设备。

Linux 内核时通过一个虚拟的网桥设备(Net Device)来实现桥接的。这种虚拟设备可以绑定若干个以网络桥接设备,从而将它们桥接起来。如下图,这种 Net Device 网桥和普通的设备不同,最明显的一个特性是它还可以有一个 IP 地址。

k8s网络通信初探_第3张图片

上图中,网桥设备 br0 绑定了 eth0 和 eth1.对于网络协议栈的上层来说,只看得到 br0。因为桥接是在数据链路层实现的,上层不需要关心网桥的细节,于是协议栈上层需要发送的报文被送到 br0,网桥设备的处理代码判断报文被转发到 eth0 还是 eth1,或者两者皆转发;反过来,从 eth0 或者从 eth1 接收到的报文被提交给网桥的处理代码,在这里会判断报文应该被转发、丢弃还是提交到协议栈上层。而有时 eth0、eth1 也可能会作为报文的源地址或目的地址,直接参与报文的发送与接收,从而绕过网桥。

 

4、容器

容器实际上是结合了namespace 和 cgroup 的一般内核进程,注意,容器就是个进程

所以,当我们使用Docker起一个容器的时候,Docker会为每一个容器创建属于他自己的namespaces,即各个维度资源都专属这个容器了,此时的容器就是一个孤岛,也可以说是一个独立VM就诞生了。当然他不是VM,网上关于二者的区别和优劣有一对资料.更进一步,也可以将多个容器共享一个namespace,比如如果容器共享的是network 类型的namespace,那么这些容器就可以通过 localhost:[端口号]  来通信了。因为此时的两个容器从网络的角度看,和宿主机上的两个内核进程没啥区别。

  • 二、网络模型

1、Docker概述

容器的英文为Container,原始的含义是集装箱。其实容器技术正是来源于集装箱运输的思想。过去海上运输货物类型很多,运输和装卸船效率低。为了提高运输效率,集装箱都诞生了。在一艘船上,无论是衣服、奶粉、甚至汽车,只要装在标准的集装箱中,船东无需关心集装箱内的货物,用标准的运输方式进行托运和装卸作业,极大提高了运输效率。正是借鉴箱运输的思想,容器技术也诞生了。在此之前,部署一个应用至少先需要物理环境,然后是操作系统,之后还有各种中间件环境,最后才能部署应用,部署人员和开发人员都需要费时费力的管理。后来的虚拟化部署一定程度上解决了硬件资源的最大化利用,但并没有解决应用层面的问题,每次应用部署还是需要搭建一套依赖的应用环境。由于虚拟技术使得各自还拥有自己的OS,导致性能也并不理想。Container容器技术的诞生解决了技术领域的“集装箱运输”的问题,利用NameSpace做隔离,对应用进行打包,通过容器引擎,共享一个操作系统,达到了良好的性能指标,实现了Build Once,Run Everywhere,节省了巨大的人力成本,提高运维效率。

标准的 Docker 支持一下 4 类网络模式。(1)host模式:使用 --net=host 指定。()2container模式:使用 --net=container:NAME_or_ID 指定。(3)none模式:使用 --net=none 指定。(4)bridge模式:使用 --net=bridge 指定,为默认设置。

 

2、bridge模式

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

k8s网络通信初探_第4张图片

其中 ip1 是网桥的 IP 地址,Docker Daemon 会在几个备选地址段给它选择一个,通常是 172 开头的一个地址。这个地址和主机的 IP 地址是不重叠的。ip2 是 Docker 给启动容器时,在这个网址段随机选择的一个没有使用的 IP 地址,Docker 占用它并分配给了被启动的容器。相应的 MAC 地址也根据这个 IP 地址,在 02:42:ac:11:00:00 和 02:42:ac:11:ff:ff 的范围内生成,这样做可以确保不会有 ARP 的冲突。启动后,Docker 还将 Veth 对的名字映射到了 eth0 网络接口。ip3 就是主机的网卡地址。

美中不足的是,从 Docker 对 Linux 网络协议栈的操作可以看到,Docker 一开始没有考虑到多主机互联的网络解决方案。Docker 一直以来的理念都是 “简单为美”,几乎所有尝试 Docker 的人,都被它 “用法简单,功能强大” 的特性所吸引,这也是 Docker 迅速走红的一个原因。

 

3、Kubemetes网络模式

    k8s组网要求所有的Pods之间可以在不使用NAT网络地址转换的情况下相互通信,所有的Nodes之间可以在不使用NAT网络地址转换的情况下相互通信,每个Pod自己看到的自己的ip和其他Pod看到的一致。

    k8s网络模型在设计时遵循,每个Pod都拥有一个独立的 IP地址,而且 假定所有 Pod 都在一个可以直接连通的、扁平的网络空间中;不管它们是否运行在同 一 个 Node (宿主机)中,都要求它们可以直接通过对方的 IP 进行访问。

    设计这个原则时因为用户不需要额外考虑如何建立 Pod 之间的连接,也不需要考虑将容器端口映射到主机端口等问题。由于 Kubemetes 的网络模型假设 Pod 之间访问时使用的是对方 Pod 的实际地址,所以一个Pod 内部的应用程序看到的自己的 IP 地址和端口与集群内其他 Pod 看到的一样。它们都是 Pod 实际分配的IP地址 (从dockerO上分配的)。将IP地址和端口在Pod内部和外部都保持一致, 我们可以不使用 NAT 来进行转换,地址空间也自然是平的。

 

  • 三、Kubemetes网络实现

1、概述

在实际的业务场景中,组件之间的管理十分复杂,提别是随着微服务理念逐步深入人心,应用部署的粒度更加细小和灵活。为了支持业务应用组件的通信联系,Kubernetes 网络的设计主要致力解决以下场景。

(1)容器到容器之间的直接通信。

(2)抽象的 Pod 到 Pod 之间的通信。

(3)Pod 到 Service 之间的通信。

(4)集群外部与内部组件之间的通信。

2、容器到容器之间的直接通信

在同一个 Pod 内部的容器(Pod 内的容器是不会垮宿主机的)共享同一个网络命名空间,共享一个 Linux 协议栈。pod中每个docker容器和pod在一个网络命名空间内,所以ip和端口等等网络配置都和pod一样,主要通过docker的一种网络模式:container,新创建的Docker容器不会创建自己的网卡,配置自己的 IP,而是和一个指定的容器共享 IP、端口范围,来实现容器间的直接通信。这么做的结果安全和高效,也能减少将已经存在的程序从物理机或者虚拟机移植到容器下运行的难度。

在下图中的虚线部分就是 Node 上运行着的一个 Pod 实例。在我们的例子中,容器就是图中容器1 和容器 2。容器 1 和容器 2 共享了一个网络的命名空间,它们就好像在一台机器上运行似的,它们打开的端口不会有冲突,可以直接使用 Linux 的本地 IPC 进行通信。它们之间的互相访问只需要使用 localhost 就可以。例如,如果容器 2 运行的是 MySQL,那么容器 1 使用 localhost:3306 就能直接访问这个运行在容器 2 上的 MySQL 了。

k8s网络通信初探_第5张图片

3、抽象的 Pod 到 Pod 之间的通信

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

Pod 容器即由可能在同一个 Node 上运行,也有可能在不同的 Node 上运行,所以通信也分为两类:同一个 Node 内的 Pod 之间的通信和不同 Node 上的 Pod 之间的通信

3.1同Node下Pod的通信

k8s网络通信初探_第6张图片

可以看出,Pod 1 和 Pod 2 都是通过 Veth 连接在同一个 docker0 网桥上的,它们的 IP 地址 IP1、IP2 都是从 docker0 的网段上动态获取的,它们和网桥本身的 IP3 是同一个网段的。另外,在 Pod 1、Pod 2 的 Linux 协议栈上,默认路由都是 docker0 的地址,也就是说所有非本地地址的网络数据,都会被默认发送到 docker0 网桥上,由 docker0 网桥直接中转。

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

 

3.2不同Node下Pod的通信

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

另一方面,这些动态分配且藏在 docker0 之后的所谓 “私有” IP 地址也是可以找到的。Kubernetes 会记录所有正在运行 Pod 的 IP 分配信息,并将这些信息保存在 etcd 中(作为 Service 的 Endpoint)。这些私有 IP 信息对于 Pod 到 Pod 的通信也是十分重要的,因为我们的网络模型要求 Pod 到 Pod 使用私有 IP 进行通信。所以首先要知道这些 IP 是什么。之前提到,Kubernetes 的网络对 Pod 的地址是平面的和直达的,所以这些 Pod 的 IP 规划也很重要,不能由冲突。只要没有冲突,我们就可以想办法在整个 Kubernetes 的集群中找到它。

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

(1)在整个 Kubernetes 集群中对 Pod 的 IP 分配进行规划,不能有冲突;

(2)找到一种办法,将 Pod 的 IP 和所在 Node 的 IP 关联起来,通过这个关联让 Pod 可以互相访问。

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

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

k8s网络通信初探_第7张图片

在上图中,IP 1 对应的是 Pod 1、IP 2 对应的是 Pod 2。Pod 1 在访问 Pod 2 时,首先要将数据从源 Node 的 eth0 发送出去,找到并到达 Node 2 的 eth0。也就是说先要从 IP 3 到 IP 4,之后才是 IP 4 到 IP 2 的递送。

在 Google 的 GEC 环境下,Pod 的 IP 管理(类似 docker0)、分配及它们之间的路由打通都是由 GCE 完成的。Kubernetes 作为主要在 GCE 上面运行的框架,它的设计是假设底层已经具备这些条件,所以它分配完地址并将地址记录下来就完成了它的工作。在实际的 GCE 环境宏,GCE 的网络组建会读取这些信息,实现具体的网络打通。

而在实际的生产中,因为安全、费用、合规等种种原因,Kubernetes 的客户不可能全部使用 Google 的 GCE 环境,所以在实际的私有云环境中,除了部署 Kubernetes 和 Docker,还需要额外的网络配置,甚至通过一些软件来实现 Kubernetes 对网络的要求。做到这些后,Pod 和 Pod 之间才能无差别地透明通信。

 

4、Pod 到 Service 之间的通信

pod的ip地址是不持久的,当集群中pod的规模缩减或者pod故障或者node故障重启后,新的pod的ip就可能与之前的不一样的。所以k8s中衍生出来Service来解决这个问题。

Service管理了多个Pods,每个Service有一个虚拟的ip,要访问service管理的Pod上的服务只需要访问你这个虚拟ip就可以了,这个虚拟ip是固定的,当service下的pod规模改变、故障重启、node重启时候,对使用service的用户来说是无感知的,因为他们使用的service的ip没有变。当数据包到达Service虚拟ip后,数据包会被通过k8s给该servcie自动创建的负载均衡器路由到背后的pod容器。

    在k8s里,iptables规则是由kube-proxy配置,kube-proxy监视APIserver的更改,因为集群中所有service(iptables)更改都会发送到APIserver上,所以每台kubelet-proxy监视APIserver,当对service或pod虚拟IP进行修改时,kube-proxy就会在本地更新,以便正确发送给后端pod。

4.1、pod到service包的流转:

k8s网络通信初探_第8张图片

 

1)数据包从pod1所在eth0离开,通过veth对的另一端veth0传给网桥cbr0,网桥找不到service的ip对应的mac,交给了默认路由,到达了root命名空间的eth0

2)root命名空间的eth0接受数据包之前会经过iptables进行过滤,iptables接受数据包后使用kube-proxy在node上配置的规则响应service,然后数据包的目的ip重写为service后端指定的pod的ip了

 

4.2service到pod包的流转

收到包的pod会回应数据包到源pod,源ip是发送方ip,目标IP是接收方,数据包进行回复时经过iptables,iptables使用内核机制conntrack记住它之前做的选择,又将数据包源ip重新为service的ip,目标ip不变,然后原路返回至pod1的eth0

 

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

将k8s集群服务暴露给互联网上用户使用,有两个问题;从k8s的service访问Internet,以及从Internet访问k8s的service两个方面。node通过Internet网关时可以将流量路由到Internet,但是pod具有自己的IP地址,Internet王冠上的NAT转换并不适用。下边以node主机通过iptables的nat为例说明。node到internet包的流转

k8s网络通信初探_第9张图片

数据包源自pod1网络命名空间,通过veth对连接到root网络命名空间,紧接着,转发表里没有IP对应的mac,会发送到默认路由,到达root网络命名空间的eth0

那么在到达root网络明明空间之前,iptables会修改数据包,现在数据包源ip是pod1的,继续传输会被Internet网关拒绝掉,因为网关NAT仅转发node的ip,解决方案:使iptables执行源NAT更改数据包源ip,让数据包看起来是来自于node而不是pod

iptables修改完源ip之后,数据包离开node,根据转发规则发给Internet网关,Internet网关执行另一个NAT,内网ip转为公网ip,在Internet上传输。

数据包回应时,也是按照:Internet网关需要将公网IP转换为私有ip,到达目标node节点,再通过iptables修改目标ip并且最终传送到pod的eth0虚拟网桥。

 

  • 四、总结

    在当前形势下,越来越多的企业选择Drocker+Kubernetes模式来实现虚拟化平台的管理,k8s采用的是扁平化的网络模型,每个 Pod 都有自己的 IP,并且可以直接通信。与一些主流技术结合,使得k8s功能更加强大。如CNI规范使得 Kubernetes可以灵活选择多种 Plugin 实现集群网络;Network Policy则赋予了 Kubernetes 强大的网络访问控制机制。

通过对该课题的探究,深深体会到了k8s的强大,在日后的场景中将会有更加广泛的应用。Docker+Kubernetes的网络通信方式通过将每个软件应用在单独隔离的容器中,提高了软硬件的使用效率,也拓展了软件应用的可移植性,并且能够对大规模数量的容器进行有效的管理部署。K8s网络通信随着各种方案的出现还在不断的发展强大中,大浪淘沙,最后流传的必将是最优秀的网络通信方案,本文只是浅谈k8s的网络通信,更深的技术还需要更加深入的探究。

 

 

你可能感兴趣的:(k8s,docker,pod,k8s网络)