1.3 应用的运行与互联互通
如前所述,首先将部署于集群操作系统Kubernetes之上的应用定义为Pod的API对象描述,提交给API Server后经调度器从可用的工作节点中匹配出一个最佳选择,而后由相应工作节点上的kubelet代理程序根据其定义进行Pod创建、运行和状态监控等操作。位于同一网络中的各Pod实例可直接互相通信,但“用后即弃”的Pod自身的IP地址显然无法作为可持续使用的访问入口。Service资源用于为Pod提供固定访问端点,并基于该端点将流量调度至一组Pod实例之上。
1.3.1 Pod与Service
Pod本质上是共享Network、IPC和UTS名称空间以及存储资源的容器集合,如图1-9所示。我们可以把每个Pod对象想象成一个逻辑主机,它类似于现实世界中的物理主机或虚拟机,而运行于同一个Pod对象中多个进程则类似于物理机或虚拟机上独立运行的进程,不同的是,Pod中各进程运行于彼此隔离的容器中,并于各容器间共享网络和存储两种关键资源。
同一Pod内部的各容器具有“超亲密”关系,它们共享网络协议栈、网络设备、路由、IP地址和端口等网络资源,可以基于本地回环接口lo互相通信。每个Pod上还可附加一组“存储卷”(volume)资源,它们同样可由内部所有容器使用而实现数据共享。持久类型的存储卷还能够确保在容器终止后被重启,甚至容器被删除后数据也不会丢失。
同时,这些以Pod形式运行于Kubernetes之上的应用通常以服务类程序居多,其客户端可能来自集群之外,例如现实中的用户,也可能是当前集群中其他Pod中的应用,如图1-10所示。Kubernetes集群的网络模型要求其各Pod对象的IP地址位于同一网络平面内(同一IP网段),各Pod间可使用真实IP地址直接进行通信而无须NAT功能介入,无论它们运行于集群内的哪个工作节点之上,这些Pod对象就像是运行于同一局域网中的多个主机上。
Pod对象拥有生命周期,它会在自身故障或所在的工作节点故障时被替换为一个新的实例,其IP地址通常也会随之改变,这就给集群中的Pod应用间依赖关系的维护带来了麻烦:前端Pod应用(依赖方)无法基于固定地址持续跟踪后端Pod应用(被依赖方)。为此,Kubernetes设计了有着稳定可靠IP地址的Service资源,作为一组提供了相同服务的Pod对象的访问入口,由客户端Pod向目标Pod所属的Service对象的IP地址发起访问请求,并由相关的Service对象调度并代理至后端的Pod对象。
Service是由基于匹配规则在集群中挑选出的一组Pod对象的集合、访问这组Pod集合的固定IP地址,以及对请求进行调度的方法等功能所构成的一种API资源类型,是Pod资源的代理和负载均衡器。Service匹配Pod对象的规则可用“标签选择器”进行体现,并根据标签来过滤符合条件的资源对象,如图1-11所示。标签是附加在Kubernetes API资源对象之上的具有辨识性的分类标识符,使用键值型数据表达,通常仅对用户具有特定意义。一个对象可以拥有多个标签,一个标签也可以附加于多个对象(通常是同一类对象)之上。
每个节点上运行的kube-proxy组件负责管理各Pod与Service之间的网络连接,它并非Kubernetes内置的代理服务器,而是一个基于出站流量的负载均衡器组件。针对每个Service,kube-proxy都会在当前节点上转换并添加相应iptables DNAT规则或ipvs规则,从而将目标地址为某Service对象的ClusterIP的流量调度至该Service根据标签选择器匹配出的Pod对象之上。
即使Service有着固定的IP地址可用作服务访问入口,但现实中用户更容易记忆和使用的还是服务的名称,Kubernetes使用定制的DNS服务为这类需求提供了自动的服务注册和服务发现功能。CoreDNS附件会为集群中的每个Service对象(包括DNS服务自身)生成唯一的DNS名称标识,以及相应的DNS资源记录,服务的DNS名称遵循标准的svc.namespace.svc.cluster-domain格式。例如CoreDNS自身的服务名称为kube-dns.kube-system.svc.cluster.local.,则它的ClusterIP通常是10.96.0.10。
除非出于管理目的有意调整,Service资源的名称和ClusterIP在其整个生命周期内都不会发生变动。kubelet会在创建Pod容器时,自动在/etc/resolv.conf文件中配置Pod容器使用集群上CoreDNS服务的ClusterIP作为DNS服务器,因而各Pod可针对任何Service的名称直接请求相应的服务。换句话说,Pod可通过kube-dns.kube-system.svc.cluster.local.来访问集群DNS服务。
1.3.2 Pod控制器
Pod有着生命周期,且非正常终止的Pod对象本身不具有“自愈”功能,若kubelet在Pod监视周期中发现故障,将重启或创建新的Pod实例来取代故障的实例以完成Pod实例恢复。但因kubelet程序或工作节点故障时导致的Pod实例故障却无从恢复。另外,因资源耗尽或节点故障导致的回收操作也会删除相关的Pod实例。因而,我们需要借助专用于管控Pod对象的控制器资源来构建能够跨越工作节点生命周期的Pod实例,例如Deployment或StatefulSet等控制器。
控制器是支撑Kubernetes声明式API的关键组件,它持续监视对API Server上的API对象的添加、更新和删除等更改操作,并在发生此类事件时执行目标对象相应的业务逻辑,从而将客户端的管理指令转为对象管理所需要的真正的操作过程。简单来说,一个具体的控制器对象是一个控制循环程序,它在单个API对象上创建一个控制循环以确保该对象的状态符合预期。不同类型的Pod控制器在不同维度完成符合应用需求的管理模式,例如Deployment控制器资源可基于“Pod模板”创建Pod实例,并确保实例数目精确反映期望的数量;另外,控制器还支持应用规模中Pod实例的自动扩容、缩容、滚动更新和回滚,以及创建工作节点故障时的重建等运维管理操作。图1-12中的Deployment就是这类控制器的代表实现,是目前最常用的用于管理无状态应用的Pod控制器。
Deployment控制器的定义通常由期望的副本数量、Pod模板和标签选择器组成,它会根据标签选择器对Pod对象的标签进行匹配检查,所有满足选择条件的Pod对象都将受控于当前控制器并计入其副本总数,以确保此数目能精确反映期望的副本数。
然而,在接收到的请求流量负载显著低于或接近已有Pod副本的整体承载能力时,用户需要手动修改Deployment控制器中的期望副本数量以实现应用规模的扩容或缩容。不过,当集群中部署了Metrics Server或Prometheus一类的资源指标监控附件时,用户还可以使用Horizontal Pod Autoscaler(HPA)根据Pod对象的CPU等资源占用率计算出合适的Pod副本数量,并自动修改Pod控制器中期望的副本数以实现应用规模的动态伸缩,提高集群资源利用率,如图1-13所示。
另外,StatefulSet控制器专用于管理有状态应用的Pod实例,其他常用的Pod控制器还有ReplicaSet、DaemonSet、Job和CronJob等,分别用于不同控制模型的工作负载,后面章节会详细介绍。
1.3.3 Kubernetes网络基础
根据1.3.1节的介绍可以总结出,Kubernetes的网络中存在4种主要类型的通信:同一Pod内的容器间通信、各Pod彼此间通信、Pod与Service间的通信以及集群外部的流量与Service间的通信。Kubernetes对Pod和Service资源对象分别使用了专用网络,Service的网络则由Kubernetes集群自行管理,但集群自身并未实现任何形式的Pod网络,仍要借助于兼容CNI(容器网络接口)规范网络插件配置实现,如图1-14所示。这些插件必须满足以下需求:
▪所有Pod间均可不经NAT机制而直接通信;
▪所有节点均可不经NAT机制直接与所有容器通信;
▪所有Pod对象都位于同一平面网络中,而且可以使用Pod自身的地址直接通信。
Pod网络及其IP由Kubernetes的网络插件负责配置和管理,具体使用的网络地址可在管理配置网络插件时指定,例如Flannel网络插件默认使用10.244.0.0/16网络,而Calico默认使用192.168.0.0/16网络。对比来说,Service的地址却是一个虚拟IP地址,它不会被添加在任何网络接口设备上,而是由kube-proxy配置在每个工作节点上的iptables或ipvs规则中以进行流量分发,这些规则的作用域也仅限于当前节点自身,因此每个节点上的kube-proxy都会为每个Service创建相应的规则。
Service IP也称为Cluster IP,专用于集群内通信,一般使用不同于Pod网络的专用地址段,例如10.96.0.0/12网络,各Service对象的IP地址在此范围内由系统创建Service对象时动态分配。集群内的Pod对象可直接用Cluster IP作为目标服务的地址,就像Pod对象直接提供了某种服务。如图1-15中,Client Pod提供相应Service的访问等,但集群网络属于私有网络地址,仅能服务集群内部的流量。
概括来说,Kubernetes集群上会存在3个分别用于节点、Pod和Service的不同网络,3种网络在工作节点之上实现交汇,由节点内核中的路由组件以及iptables/netfilter和ipvs等完成网络间的报文转发,例如Pod与Service,Node与Service之间等的流量,如图1-14所示。
▪节点网络:各主机(Master和Node)自身所属的网络,相关IP地址配置在节点的网络接口,用于各主机之间的通信,例如Master与各Node间的通信。此地址配置在Kubernetes集群构建之前,它并不能由Kubernetes管理,需要管理员在集群构建之前就自行确定其地址配置及管理方式。
▪Pod网络:Pod对象所属的网络,相关地址配置在Pod网络接口,用于各Pod间的通信。Pod网络是一种虚拟网络,需要通过传统的kubenet网络插件或新式的CNI网络插件实现,常见的实现机制有Overlay和Underlay两种。常见的解决方案有十多种,相关插件可以以系统守护进程方式运行于Kubernetes集群之外,也可以托管在Kubernetes之上的DaemonSet控制器,需在构建Kubernetes集群时由管理员选择和部署。
▪Service网络:一个虚拟网络,相关地址不会配置在任何主机或Pod的网络接口之上,而是通过Node上的kube-proxy配置为节点的iptables或ipvs规则,进而将发往此地址的所有流量调度至Service后端的各Pod对象之上;Service网络在Kubernetes集群创建时予以指定,而各Service的地址则在用户创建Service时予以动态配置。
而Kubernetes集群上的服务大致可分为两种:API Server和服务类应用,它们的客户端要么来自集群内的其他Pod,要么来自集群外部的用户或应用程序。前一种通信通常发生在Pod网络和Service网络之上的东西向流量;而后一种通信,尤其是访问运行于Pod中的服务类应用的南北向流量,则需要先经由集群边界进入集群内部的网络中,即由节点网络到达Service网络和Pod网络。Kubernetes上NodePort和LoadBalancer类型的Service,以及Ingress都可为集群引入外部流量。
访问API Server时,人类用户一般借助命令行工具kubectl或图形UI(例如Kubernetes Dashboard)实现,也可通过编程接口实现,包括RESTful API。访问Pod中的应用时,具体访问方式取决于Pod中的应用程序,例如,对于运行Nginx容器的Pod来说,最常用的工具自然是HTTP协议客户端或客户端库,如图1-15所示。
管理员(开发人员或运维人员)使用Kubernetes集群的常见操作包括通过控制器创建Pod,在Pod的基础上创建Service供第二类客户端访问,更新Pod中的应用版本(更新和回滚)以及对应用规模进行扩容或缩容等,另外还有集群附件管理、存储卷管理、网络及网络策略管理、资源管理和安全管理等,这些内容将在后面的章节中展开。显然,这一切的前提是构建出一个可用的Kubernetes集群,下一章将着重讲述。
1.3.4 部署并访问应用
Docker容器技术使得部署应用程序从传统的安装、配置、启动应用程序的方式转为在容器引擎上基于镜像创建和运行容器,而Kubernetes又让创建和运行容器的操作不必再关注其位置,并赋予了它一定程度上动态扩缩容及自愈的能力,从而让用户从主机、系统及应用程序的维护工作中解脱出来。
用到某应用程序时,用户只需要向API Server请求创建一个Pod控制器对象,由控制器根据配置的Pod模板向API Server请求创建出一定数量的Pod实例,每个实例由Master之上的调度器指派给选定的工作节点,并由目标工作节点上的kubelet调用本地容器运行时创建并运行容器化应用。同时,用户还需要额外创建一个具体的Service对象以便为这些Pod对象建立一个固定的访问入口,从而使得其客户端能通过ClusterIP进行访问,或借助CoreDNS提供的服务发现及名称解析功能,通过Service的DNS格式的名称进行访问。应用程序简单的部署示例示意图如图1-16所示。
集群外部客户端需要等边界路由器先将访问流量引入集群内部后,才能进行内部的服务请求和响应,NodePort或LoadBalancer类型的Service以及Ingress资源都是常见的解决方案。不同的是,Service在协议栈的TCP层完成调度,而Ingress则是基于HTTP/HTTPS协议的边缘路由。
kubectl是最常用的集群管理工具之一,它是API Server的客户端程序,通过子命令实现集群及相关资源对象的管理操作,并支持直接命令式、命令式配置清单及声明式配置清单三种操作方式,特性丰富且功能强大。而需作为集群附件额外部署的Dashboard则提供了基于Web界面的图形客户端,它是一个通用目的管理工具,与Kubernetes紧密集成,支持多级别用户授权,能在一定程度上替代kubectl的大多数操作。