Kubernetes网络自学系列 | 终于等到你:Kubernetes网络

Posted COCOgsta

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Kubernetes网络自学系列 | 终于等到你:Kubernetes网络相关的知识,希望对你有一定的参考价值。

素材来源:《Kubernetes网络权威指南》

一边学习一边整理内容,并与大家分享,侵权即删,谢谢支持!

附上汇总贴:Kubernetes网络自学系列 | 汇总_COCOgsta的博客-CSDN博客


3.2 终于等到你:Kubernetes网络

结束了3.1节的铺垫,我们终于要开始对全书的核心内容——Kubernetes网络的介绍了!

Kubernetes网络包括网络模型、CNI、Service、Ingress、DNS等。

在Kubernetes的网络模型中,每台服务器上的容器有自己独立的IP段,各个服务器之间的容器可以根据目标容器的IP地址进行访问,如图3-4所示。

图3-4 Kubernetes网络模型概览

为了实现这一目标,重点解决以下两点:

· 各台服务器上的容器IP段不能重叠,所以需要有某种IP段分配机制,为各台服务器分配独立的IP段;

· 从某个Pod发出的流量到达其所在服务器时,服务器网络层应当具备根据目标IP地址,将流量转发到该IP所属IP段对应的目标服务器的能力。

总结起来,实现Kubernetes的容器网络重点需要关注两方面:IP地址分配和路由。

3.2.1 Kubernetes网络基础

在开始对Kubernetes网络进行探讨之前,我们先从Pod网络入手,简单介绍Kubernetes网络的基本概念和框架。

1.IP地址分配

Kubernetes使用各种IP范围为节点、Pod和服务分配IP地址。

· 系统会从集群的VPC网络为每个节点分配一个IP地址。该节点IP用于提供从控制组件(如Kube-proxy和Kubelet)到Kubernetes Master的连接;

· 系统会为每个Pod分配一个地址块内的IP地址。用户可以选择在创建集群时通过--pod-cidr指定此范围;

· 系统会从集群的VPC网络为每项服务分配一个IP地址(称为ClusterIP)。大部分情况下,该VPC与节点IP地址不在同一个网段,而且用户可以选择在创建集群时自定义VPC网络。

2.Pod出站流量

Kubernetes处理Pod的出站流量的方式主要分为以下三种:

PodPod

在Kubernetes集群中,每个Pod都有自己的IP地址,运行在Pod内的应用都可以使用标准的端口号,不用重新映射到不同的随机端口号。所有的Pod之间都可以保持三层网络的连通性,比如可以相互ping对方,相互发送TCP/UDP/SCTP数据包。CNI就是用来实现这些网络功能的标准接口。

PodService

Pod的生命周期很短暂,但客户需要的是可靠的服务,因此Kubernetes引入了新的资源对象Service,其实它就是Pod前面的4层负载均衡器。Service总共有4种类型,其中最常用的类型是ClusterIP,这种类型的Service会自动分配一个仅集群内部可以访问的虚拟IP。

Kubernetes通过Kube-proxy组件实现这些功能,每台计算节点上都运行一个Kubeproxy进程,通过复杂的iptables/IPVS规则在Pod和Service之间进行各种过滤和NAT。

Pod到集群外

从Pod内部到集群外部的流量,Kubernetes会通过SNAT来处理。SNAT做的工作就是将数据包的源从Pod内部的IP:Port替换为宿主机的IP:Port。当数据包返回时,再将目的地址从宿主机的IP:Port替换为Pod内部的IP:Port,然后发送给Pod。当然,中间的整个过程对Pod来说是完全透明的,它们对地址转换不会有任何感知。

以上涉及的概念我们在后面的章节会进行详细讲解,本节只是抛砖引玉,不做深入分析。

3.2.2 Kubernetes网络架构综述

谈到Kubernetes的网络模型,就不能不提它著名的“单Pod单IP”模型,即每个Pod都有一个独立的IP,Pod内所有容器共享network namespace(同一个网络协议栈和IP)。

“单Pod单IP”网络模型为我们勾勒了一个Kubernetes扁平网络的蓝图,在这个网络世界里:容器是一等公民,容器之间直接通信,不需要额外的NAT,因此不存在源地址被伪装的情况;Node与容器网络直连,同样不需要额外的NAT。扁平化网络的优点在于:没有NAT带来的性能损耗,而且可追溯源地址,为后面的网络策略做铺垫,降低网络排错的难度等。

总体而言,集群内访问Pod,会经过Service;集群外访问Pod,经过的是Ingress。Service 和Ingress是Kubernetes专门为服务发现而抽象出来的相关概念,后面会做详细讨论。

与CRI之于Kubernetes的runtime类似,Kubernetes使用CNI作为Pod网络配置的标准接口。需要注意的是,CNI并不支持Docker网络,也就是说,docker0网桥会被大部分CNI插件“视而不见”。

当然也有例外,Weave就是一个会处理docker0的CNI插件,具体分析请看后面章节的内容。Kubernetes网络总体架构如图3-5所示。

图3-5 Kubernetes网络总体架构

图3-5描绘了当用户在Kubernetes里创建了一个Pod后,CRI和CNI协同创建Pod所属容器,并为它们初始化网络协议栈的全过程。具体过程如下:

(1)当用户在Kubernetes的Master里创建了一个Pod后,Kubelet观察到新Pod的创建,于是首先调用CRI(后面的runtime实现,比如dockershim、containerd等)创建Pod内的若干个容器。

(2)在这些容器里,第一个被创建的pause容器是比较特殊的,这是Kubernetes系统“赠送”的容器,也称pause容器。里面运行着一个功能十分简单的C程序,具体逻辑是一启动就把自己永远阻塞在那里。一个永远阻塞而且没有实际业务逻辑的pause容器到底有什么用呢?用处很大。我们知道容器的隔离功能利用的是Linux内核的namespace机制,而只要是一个进程,不管这个进程是否处于运行状态(挂起亦可),它都能“占”用着一个namespace。因此,每个Pod内的第一个系统容器pause的作用就是占用一个Linux的network namespace。

(3)Pod内其他用户容器通过加入这个network namespace的方式共享同一个network namespace。用户容器和pause容器之间的关系有点类似于寄居蟹和海螺。因此,Container runtime创建Pod内的用户容器时,调用的都是同一个命令:docker run --net=none。意思是只创建一个network namespace,不初始化网络协议栈。如果这个时候通过ns enter方式进入容器,会看到里面只有一个本地回环设备lo。

(4)容器的eth0是怎么创建出来的呢?答案是CNI。CNI主要负责容器的网络设备初始化工作。Kubelet目前支持两个网络驱动,分别是Kubenet和CNI。Kubenet是一个历史产物,即将废弃,因此本节不过多介绍。CNI有多个实现,官方自带的插件就有p2p、bridge等,这些插件负责初始化pause容器的网络设备,也就是给pause容器内的eth0分配IP等,到时候,Pod内其他容器就使用这个IP与其他容器或节点进行通信。Kubernetes主机内容器的默认组网方案是bridge。flannel、Calico这些第三方插件解决容器之间的跨机通信问题,典型的跨机通信解决方案有bridge和overlay等。

3.2.3 Kubernetes主机内组网模型

Kubernetes经典的主机内组网模型是veth pair+bridge的方式。

前文提到,当Kubernetes调度Pod在某个节点上运行时,它会在该节点的Linux内核中为Pod创建network namespace,供Pod内所有运行的容器使用。从容器的角度看,Pod是具有一个网络接口的物理机器,Pod中的所有容器都会看到此网络接口。因此,每个容器通过localhost就能访问同一个Pod内的其他容器。

Kubernetes使用veth pair将容器与主机的网络协议栈连接起来,从而使数据包可以进出Pod。容器放在主机根network namespace中veth pair的一端连接到Linux网桥,可让同一节点上的各Pod之间相互通信,如图3-6所示。

图3-6 Kubernetes bridge网络模型

如果Kubernetes集群发生节点升级、修改Pod声明式配置、更新容器镜像或节点不可用,那么Kubernetes就会删除并重新创建Pod。在大部分情况下,Pod创建会导致容器IP发生变化。也有一些CNI插件提供Pod固定IP的解决方案,例如Weave、Calico等。

3.2.4 Kubernetes跨节点组网模型

前文提到,Kubernetes典型的跨机通信解决方案有bridge、overlay等,下面我们将简单介绍这两种方案的基本思路。

Kubernetes的bridge跨机通信网络模型如图3-7所示。

图3-7 Kubernetes的bridge跨机通信网络模型

如图3-7所示,Node1上Pod的网段是10.1.1.0/24,接的Linux网桥是10.1.1.1,Node2上Pod的网段是10.1.2.0/24,接的Linux网桥是10.1.2.1,接在同一个网桥上的Pod通过局域网广播通信。我们发现,Node1上的路由表的第二条是:

意思是,所有目的地址是本机上Pod的网络包,都发到cni0这个Linux网桥,进而广播给Pod。

注意看第三条路由规则:

10.1.2.0/24是Node2上Pod的网段,192.168.1.101又恰好是Node2的IP。意思是,目的地址是10.1.2.0/24的网络包,发到Node2上。这时,我们观察Node2上面的第二条路由信息:

就会知道,这个包会被接着发给Node2上的Linux网桥cni0,再广播给目标Pod。回程报文同理(走一条逆向的路径)。因此,我们可以得出一个结论:bridge网络本身不解决容器的跨机通信问题,需要显式地书写主机路由表,映射目标容器网段和主机IP的关系,集群内如果有N个主机,需要N-1条路由表项。

至于overlay网络,它是构建在物理网络之上的一个虚拟网络,其中VXLAN是主流的overlay标准。VXLAN就是用UDP包头封装二层帧,即所谓的MAC in UDP。图3-8所示为典型的overlay网络的拓扑图。

图3-8 典型的overlay网络的拓扑图

和bridge网络类似,Pod同样接在Linux网桥上,目的地址落在本机Pod网段的网络包同样发给Linux网桥cni0。不同的是,目的Pod在其他节点上的路由表规则,例如:

这次是直接发给本机的tun/tap设备tun0,而tun0就是overlay隧道网络的入口。我们注意到,集群内所有机器都只需要这么一条路由表,不需要像bridge网络那样,写N-1条路由表项。如何将网络包正确地传递到目标主机的隧道口另一端呢?以flannel的实现为例,它会借助一个分布式的数据库,记录目的容器IP与所在主机的IP的映射关系,而且每个节点上都会运行一个agent。例如,flanneld会监听在tun0上进行的封包和解包操作。例如,Node1上的容器发包给Node2上的容器,flanneld会在tun0处将一个目的地址是192.168.1.101:8472的UDP包头(校验和置成0)封装到这个包的外层,然后借着主机网络的东风顺利到达Node2。监听在Node2的tun0上的flanneld捕获这个特殊的UDP包(检验和为0),知道这是一个overlay的封包,于是解开UDP包头,将它发给本机的Linux网桥cni0,进而广播给目的容器。

bridge和overlay是Kubernetes最早采用的跨机通信方案,但随着集成Weave和Calico等越来越多的CNI插件,Kubernetes也支持虚拟路由等方式,在后面的章节中会详细介绍。

3.2.5 Podhosts文件

与宿主机一样,容器也有/etc/hosts文件,用来记录容器的hostname和IP地址的映射关系。通过向Pod的/etc/hosts文件中添加条目,可以在Pod级别覆盖对hostname的解析。

当一个Pod被创建后,默认情况下,hosts文件只包含IPv4和IPv6的样板内容,例如localhost和主机名称。除了默认的样板内容,我们可能有向Pod的hosts文件添加额外的条目的需求,例如将foo.local、bar.local解析为127.0.0.1,将foo.remote、bar.remote解析为10.1.2.3。怎么办呢?总不能让用户手动修改hosts文件吧?在Kubernetes 1.7版本以后,Kubernetes提供downward API,支持用户通过PodSpec的HostAliases字段添加这些自定义的条目,如下所示:

于是,Pod的hosts文件的内容类似如下:

为什么我们不建议用户在Docker容器启动后手动修改Pod的/etc/hosts文件,而是建议通过使用HostAliases的方式进行修改呢?最主要的原因是该文件由Kubelet托管,用户修改该hosts文件的任何内容都会在容器重启或Pod重新调度后被Kubelet覆盖。

注:如果Pod启用了hostNetwork(即使用主机网络),那么将不能使用HostAliases特性,因为Kubelet只管理非hostNetwork类型Pod的hosts文件。

3.2.6 Podhostname

Docker使用UTS namespace进行主机名(hostname)隔离,而Kubernetes的Pod也继承了Docker的UTS namespace隔离技术,即Pod之间主机名相互隔离,但Pod内容器分享同一个主机名。

Docker主要有两种使用UTS namespace的用法:

· 第一种是docker run --uts="" busybox。这种用法在创建容器的同时会新创建一个UTS namespace;

· 第二种是docker run --uts="host" busybox。这种用法在创建容器的同时会使用物理机的UTS namespace。

除此之外,Kubernetes在处理UTS namespace时也会考虑Pod的网络模式。

如上所示,从代码里我们可以分析出:如果Kubelet判断Pod使用宿主机网络(即host-Network),则会将UTS的mode设置为“host”,也就是使用物理机的UTS namespace。因此,如果这时容器修改主机名,则会影响宿主机的主机名。

如果容器想要修改主机名(通过hostname命令),则需要privileged权限。修改容器主机名后,容器重启或被Kubelet重建都会恢复成原来的主机名,主要原因是容器重启会导致创建新的UTS namespace。

一个Pod内如果有多个容器,修改任意一个容器的hostname都会影响其他容器,因为Pod共享UTS namespace。

以下是完整的实验过程:首先创建一个容器,指定容器名为busyboxtest:

然后进入该容器查看hostname:

修改该容器的hostname为test123,如下所示:

为了证明容器重启带来的UTS namespace的改变会导致hostname被覆盖,我们先找到该容器对应的PID,然后查看该容器的namespace文件链接:

重启容器(容器ID保持不变,但PID发生了变化):

如上所示,可以看到容器重启后UTS的namespace发生了变化,而且对hostname的修改也被覆盖了。

以上是关于Kubernetes网络自学系列 | 终于等到你:Kubernetes网络的主要内容,如果未能解决你的问题,请参考以下文章

Kubernetes网络自学系列 | 找到你并不容易:从集群内访问服务

Kubernetes网络自学系列 | 找到你并不容易:从集群外访问服务

Kubernetes网络自学系列 | 打通CNI与Kubernetes:Kubernetes网络驱动

Kubernetes网络自学系列 | 前方高能:Kubernetes网络故障定位指南

Kubernetes网络自学系列 | iptables

Kubernetes网络自学系列 | Kubernetes网络策略:为你的应用保驾护航