大家好,我是腾意。

1.1 了解Kubernetes

Kubernetes是什么?

首先,Kubernetes是谷歌十几年来大规模容器技术应用的重要成果,是谷歌严格保密十几年的秘密武器——Borg的一个开源版本。Borg是谷歌内部使用的久负盛名的大规模集群管理系统,基于容器技术来实现资源管理的自动化,以及跨多个数据中心的资源利用率的最大化。十几年以来,谷歌一直通过Borg管理着数量庞大的应用程序集群。正是由于站在Borg这个前辈的肩膀上,汲取了Borg的经验与教训,所以Kubernetes一经开源就一鸣惊人,并迅速称霸容器领域。Kubernetes也是一个全新的基于容器技术的分布式架构领先方案,是容器云的优秀平台选型方案,已成为新一代的基于容器技术的PaaS平台的重要底层框架,也是云原生技术生态圈的核心,服务网格(Service Mesh)、无服务器架构(Serverless)等新一代分布式架构框架及技术纷纷基于Kubernetes实现,这些都奠定了Kubernetes在基础架构领域的王者地位。

其次,如果我们的系统设计遵循了Kubernetes的设计思想,那么传统系统架构中那些和业务没有多大关系的底层代码或功能模块,就都可以立刻从我们的视线中消失,我们不必再费心于负载均衡器的选型和部署实施问题,不必再考虑引入或自己开发一个复杂的服务治理框架,不必再头疼于服务监控和故障处理模块的开发。总之,使用Kubernetes提供的解决方案,我们不仅节省了不少于30%的开发成本,还可以将精力更加集中于业务本身,而且由于Kubernetes提供了强大的自动化机制,所以系统后期的运维难度和运维成本大幅度降低。

然后,Kubernetes是一个开放的开发平台。与J2EE不同,它不局限于任何一种语言,没有限定任何编程接口,所以不论是用Java、Go、C++还是用Python编写的服务,都可以被映射为Kubernetes的Service(服务),并通过标准的TCP通信协议进行交互。此外,Kubernetes平台对现有的编程语言、编程框架、中间件没有任何侵入性,因此现有的系统也很容易改造升级并迁移到Kubernetes平台上。

最后,Kubernetes是一个完备的分布式系统支撑平台。Kubernetes具有完备的集群管理能力,包括多层次的安全防护和准入机制、多租户应用支撑能力、透明的服务注册和服务发现机制、内建的智能负载均衡器、强大的故障发现和自我修复能力、服务滚动升级和在线扩容能力、可扩展的资源自动调度机制,以及多粒度的资源配额管理能力。同时,Kubernetes提供了完善的管理工具,这些工具涵盖了包括开发、部署测试、运维监控在内的各个环节。因此,Kubernetes是一个全新的基于容器技术的分布式架构解决方案,并且是一个一站式的完备的分布式系统开发和支撑平台。

在正式开始本章的Hello World之旅之前,我们首先要了解Kubernetes的一些基本知识,这样才能理解Kubernetes提供的解决方案。

在Kubernetes中,Service是分布式集群架构的核心。一个Service对象拥有如下关键特征。

◎ 拥有唯一指定的名称(比如mysql-server)。

◎ 拥有一个虚拟IP地址(ClusterIP地址)和端口号。

◎ 能够提供某种远程服务能力。

◎ 能够将客户端对服务的访问请求转发到一组容器应用上。

Service的服务进程通常基于Socket通信方式对外提供服务,比如Redis、Memcached、MySQL、Web Server,或者是实现了某个具体业务的特定TCP Server进程。虽然一个Service通常由多个相关的服务进程提供服务,每个服务进程都有一个独立的Endpoint(IP+Port)访问点,但Kubernetes能够让我们通过Service(ClusterIP+Service Port)连接指定的服务。有了Kubernetes内建的透明负载均衡和故障恢复机制,不管后端有多少个具体的服务进程,也不管某个服务进程是否由于发生故障而被重新部署到其他机器,都不会影响对服务的正常调用。更重要的是,这个Service本身一旦创建就不再变化,这意味着我们再也不用为Kubernetes集群中应用服务进程IP地址变来变去的问题头疼了。

容器提供了强大的隔离功能,所以我们有必要把为Service提供服务的这组进程放入容器中进行隔离。为此,Kubernetes设计了Pod对象,将每个服务进程都包装到相应的Pod中,使其成为在Pod中运行的一个容器(Container)。为了建立Service和Pod间的关联关系,Kubernetes首先给每个Pod都贴上一个标签(Label),比如给运行MySQL的Pod贴上name=mysql标签,给运行PHP的Pod贴上name=php标签,然后给相应的Service定义标签选择器(Label Selector),例如,MySQL Service的标签选择器的选择条件为name=mysql,意为该Service要作用于所有包含name=mysql标签的Pod。这样一来,就巧妙解决了Service与Pod的关联问题。

这里先简单介绍Pod的概念。首先,Pod运行在一个被称为节点(Node)的环境中,这个节点既可以是物理机,也可以是私有云或者公有云中的一个虚拟机,在一个节点上能够运行多个Pod;其次,在每个Pod中都运行着一个特殊的被称为Pause的容器,其他容器则为业务容器,这些业务容器共享Pause容器的网络栈和Volume挂载卷,因此它们之间的通信和数据交换更为高效,在设计时我们可以充分利用这一特性将一组密切相关的服务进程放入同一个Pod中;最后,需要注意的是,并不是每个Pod和它里面运行的容器都能被映射到一个Service上,只有提供服务(无论是对内还是对外)的那组Pod才会被映射为一个服务。

在集群管理方面,Kubernetes将集群中的机器划分为一个Master和一些Node。在Master上运行着集群管理相关的一些进程:kube-apiserver、kube-controller-manager和kube-scheduler,这些进程实现了整个集群的资源管理、Pod调度、弹性伸缩、安全控制、系统监控和纠错等管理功能,并且都是自动完成的。Node作为集群中的工作节点,其上运行着真正的应用程序。在Node上,Kubernetes管理的最小运行单元是Pod。在Node上运行着Kubernetes的kubelet、kube-proxy服务进程,这些服务进程负责Pod的创建、启动、监控、重启、销毁,以及实现软件模式的负载均衡器。

这里讲一讲传统的IT系统中服务扩容和服务升级这两个难题,以及Kubernetes所提供的全新解决思路。服务的扩容涉及资源分配(选择哪个节点进行扩容)、实例部署和启动等环节。在一个复杂的业务系统中,这两个难题基本上要靠人工一步步操作才能得以解决,费时费力又难以保证实施质量。

在Kubernetes集群中,只需为需要扩容的Service关联的Pod创建一个Deployment对象,服务扩容以至服务升级等令人头疼的问题就都迎刃而解了。在一个Deployment定义文件中包括以下3个关键信息。

◎ 目标Pod的定义。

◎ 目标Pod需要运行的副本数量(Replicas)。

◎ 要监控的目标Pod的标签。

在创建好Deployment之后,Kubernetes会根据这一定义创建符合要求的Pod,并且通过在Deployment中定义的Label筛选出对应的Pod实例并实时监控其状态和数量。如果实例数量少于定义的副本数量,则会根据在Deployment对象中定义的Pod模板创建一个新的Pod,然后将此Pod调度到合适的Node上启动运行,直到Pod实例的数量达到预定目标。这个过程完全是自动化的,无须人工干预。有了Deployment,服务扩容就变成一个纯粹的简单数字游戏了,只需修改Deployment中的副本数量即可。后续的服务升级也将通过修改Deployment来自动完成。

1.2 为什么要用Kubernetes

使用Kubernetes的理由很多,最重要的理由是,IT行业从来都是由新技术驱动的。Kubernetes是软件领域近几年来最具创新的容器技术,涵盖了架构、研发、部署、运维等全系列软件开发流程,不仅对互联网公司的产品产生了极大影响,也对传统行业的IT技术产生了越来越强的冲击。基于Kubernetes的新一代容器架构已成为互联网产品及大规模系统的必选方案。2020年3月,虚拟化技术巨头VMware发布了使用Kubernetes重新打造的全新vSphere 7,向全球宣告了其拥抱Kubernetes的决心,堪称虚拟化技术十年来最大的一次演进。vSphere 7通过底层重构,使得用户能够以ESXi管理VM虚拟机的方式来运用Kubernetes的能力。毫无疑问,VMware的这一举动将对IT行业带来重大影响,也宣告了以Kubernetes为核心的容器技术取代、融合虚拟机技术的时代正在加速到来。

如今,数百家厂商和技术社区共同构建了非常强大的云原生生态,市面上几乎所有提供云基础设施的公司都以原生形式将Kubernetes作为底层平台,可以预见,会有大量的新系统选择Kubernetes,不论这些新系统是运行在企业的本地服务器上,还是被托管到公有云上。阿里云容器服务Kubernetes版ACK(Alibaba Cloud Container Service for Kubernetes)是全球首批通过Kubernetes一致性认证的服务平台。据公开资料,截至2020年,在阿里云的ACK上,已经运行着上万个用户的Kubernetes集群。而腾讯自研的TKEx容器平台的底层也使用了Kubernetes原生技术,服务于腾讯的各种业务系统,包括腾讯会议、腾讯课堂、QQ及腾讯看点等,目前这些业务已运行的Kubernetes集群规模达到几百万CPU核数。百度云容器引擎(Cloud Container Engine)也采用Kubernetes作为容器集群管理系统,于2019年年底也得到了云原生计算基金会的官方认证,而在更早的2018年,百度的深度学习平台PaddlePaddle也宣布支持Kubernetes,并在当年成为Kubernetes官方唯一支持的深度学习框架。华为早在Kubernetes刚开源时就以社区创始成员及白金会员的身份加入其中,华为云的容器引擎(CCE)也基于Kubernetes实现,同时补齐了完整的应用开发、交付与运维流程,为客户提供完整的一站式云上应用生命周期管理方案。

使用Kubernetes会收获哪些好处呢?

首先,可以“轻装上阵”地开发复杂系统。以前需要很多人(其中不乏技术达人)一起分工协作才能设计、实现和运维的分布式系统,在采用Kubernetes解决方案之后,只需一个精悍的小团队就能轻松应对。在这个团队里,只需一名架构师负责系统中服务组件的架构设计,几名开发工程师负责业务代码的开发,一名系统兼运维工程师负责Kubernetes的部署和运维,因为Kubernetes已经帮我们做了很多。

其次,可以全面拥抱以微服务架构为核心思想的新一代容器技术的领先架构,包括基础的微服务架构,以及增强的微服务架构(如服务网格、无服务器架构等)。微服务架构的核心是将一个巨大的单体应用分解为很多小的相互连接的微服务,一个微服务可能由多个实例副本支撑,副本的数量可以随着系统的负荷变化进行调整。微服务架构使得每个服务都可以独立开发、升级和扩展,因此系统具备很高的稳定性和快速迭代能力,开发者也可以自由选择开发技术。谷歌、亚马逊、eBay、Netflix等大型互联网公司都采用了微服务架构,谷歌更是将微服务架构的基础设施直接打包到Kubernetes解决方案中,让我们可以直接应用微服务架构解决复杂业务系统的架构问题。

再次,可以随时随地将系统整体“搬迁”到公有云上。Kubernetes最初的设计目标就是让用户的应用运行在谷歌自家的公有云GCE中,华为云(CCE)、阿里云(ACK)和腾讯云(TKE)全部支持Kubernetes集群,未来会有更多的公有云及私有云支持Kubernetes。除了公有云,私有云也大量采用Kubernetes架构。在私有云与公有云融合的混合云领域,Kubernetes也大显身手。在Kubernetes和容器技术诞生之前,要实现多云和混合云是很困难的,应用开发商需要针对每个云服务商进行定制化开发,导致迁移云服务商时从基础架构到应用程序层面都需要做出相应的改动和适配。有了Kubernetes之后,用户本地的私有云(数据中心)可以与云服务商的Kubernetes集群保持一致的接口,这样应用程序在大部分情况下就不需要与具体的云服务商直接绑定了。

然后,Kubernetes内建的服务弹性扩容机制可以让我们轻松应对突发流量。在服务高峰期,我们可以选择在公有云中快速扩容某些Service的实例副本以提升系统的吞吐量,这样不仅节省了公司的硬件投入,还大大改善了用户体验。中国铁路总公司的12306购票系统,在客流高峰期(如节假日)就租用了阿里云进行分流。

最后,Kubernetes系统架构超强的横向扩容能力可以让我们的竞争力大大提升。对于互联网公司来说,用户规模等价于资产,因此横向扩容能力是衡量互联网业务系统竞争力的关键指标。我们利用Kubernetes提供的工具,不用修改代码,就能将一个Kubernetes集群从只包含几个Node的小集群平滑扩展到拥有上百个Node的大集群,甚至可以在线完成集群扩容。只要微服务架构设计得合理,能够在多个云环境中进行弹性伸缩,系统就能够承受大量用户并发访问带来的巨大压力。

1.3 从一个简单的例子开始

考虑到Kubernetes提供的PHP+Redis留言板的Hello World例子对于绝大多数新手来说比较复杂,难以顺利上手和实践,在此将其替换成一个简单得多的Java Web应用的例子,可以让新手快速上手和实践。

该应用是一个运行在Tomcat里的Web App,结构比较简单,如图1.1所示,JSP页面通过JDBC直接访问MySQL数据库并展示数据。这里出于演示和简化的目的,只要程序正确连接数据库,就会自动完成对应的Table创建与初始化数据的准备工作。所以,当我们通过浏览器访问此应用时,就会显示一个表格页面,其中包含来自数据库的内容。

此应用需要启动两个容器:Web App容器和MySQL容器,并且Web App容器需要访问MySQL容器。如果仅使用Docker启动这两个容器,则需要通过Docker Network或者端口映射的方式实现容器间的网络互访。本例介绍在Kubernetes系统中是如何实现的。

img

图1.1 Java Web应用的结构

1.3.1 环境准备

这里先安装Kubernetes和下载相关镜像,本书建议采用VirtualBox或者VMware Workstation在本机中虚拟一个64位的CentOS 7虚拟机作为学习环境。虚拟机采用NAT的网络模式以便连接外网,然后使用kubeadm快速安装一个Kubernetes集群(安装步骤详见2.2节的说明),之后就可以在这个Kubernetes集群中进行练习了。

1.3.2 启动MySQL服务

首先,为MySQL服务创建一个Deployment定义文件mysql-deploy.yaml,下面给出了该文件的完整内容和说明:

img

以上YAML定义文件中的kind属性用来表明此资源对象的类型,比如这里的属性值表示这是一个Deployment;spec部分是Deployment的相关属性定义,比如spec.selector是Deployment的Pod选择器,符合条件的Pod实例受到该Deployment的管理,确保在当前集群中始终有且仅有replicas个Pod实例在运行(这里设置replicas=1,表示只能运行一个MySQL Pod实例)。当在集群中运行的Pod数量少于replicas时,Deployment控制器会根据在spec.template部分定义的Pod模板生成一个新的Pod实例,spec.template.metadata.labels指定了该Pod的标签,labels必须匹配之前的spec.selector。

创建好mysql-deploy.yaml文件后,为了将它发布到Kubernetes集群中,我们在Master上运行如下命令:

img

接下来,运行kubectl命令查看刚刚创建的Deployment:

img

查看Pod的创建情况时,可以运行下面的命令:

img

可以看到一个名称为mysql-85f4b4cdf4-k97wh的Pod实例,这是Kubernetes根据mysql这个Deployment的定义自动创建的Pod。由于Pod的调度和创建需要花费一定的时间,比如需要确定调度到哪个节点上,而且下载Pod所需的容器镜像也需要一段时间,所以一开始Pod的状态为Pending。在Pod成功创建启动完成后,其状态最终会更新为Running。

我们可以在Kubernetes节点的服务器上通过docker ps指令查看正在运行的容器,发现提供MySQL服务的Pod容器已创建且正常运行,并且MySQL Pod对应的容器多创建了一个Pause容器,该容器就是Pod的根容器。

img

img

最后,创建一个与之关联的Kubernetes Service—MySQL的定义文件(文件名为mysql-svc.yaml),完整的内容和说明如下:

img

其中,metadata.name是Service的服务名(ServiceName);spec.ports属性定义了Service的虚端口;spec.selector确定了哪些Pod副本(实例)对应本服务。类似地,我们通过kubectl create命令创建Service对象:

img

运行kubectl get命令,查看刚刚创建的Service对象:

img

可以发现,MySQL服务被分配了一个值为10.245.161.22的ClusterIP地址(在不同环境中分配的IP地址可能不同)。随后,在Kubernetes集群中新创建的其他Pod就可以通过Service的ClusterIP+端口号3306来连接和访问它了。

通常,ClusterIP地址是在Service创建后由Kubernetes系统自动分配的,其他Pod无法预先知道某个Service的ClusterIP地址,因此需要一个服务发现机制来找到这个服务。为此,Kubernetes最初巧妙地使用了Linux环境变量(Environment Variable)来解决这个问题。根据Service的唯一名称,容器可以从环境变量中获取Service对应的ClusterIP地址和端口号,从而发起TCP/IP连接请求。

1.3.3 启动Tomcat应用

前面定义和启动了MySQL服务,接下来采用同样的步骤完成Tomcat应用的启动。首先,创建对应的RC文件myweb-deploy.yaml,内容如下:

img

注意:在Tomcat容器内,应用将使用环境变量MYSQL_SERVICE_HOST的值连接MySQL服务,但这里为什么没有注册该环境变量呢?这是因为Kubernetes会自动将已存在的Service对象以环境变量的形式展现在新生成的Pod中。其更安全、可靠的方法是使用服务的名称mysql,这就要求集群内的DNS服务(kube-dns)正常运行。运行下面的命令,完成Deployment的创建和验证工作:

img

img

最后,创建对应的Service。以下是完整的YAML定义文件(myweb-svc.yaml):

img

“type:NodePort”和“nodePort:30001”表明此Service开启了NodePort格式的外网访问模式。比如,在Kubernetes集群外,客户端的浏览器可以通过30001端口访问myweb(对应8080的虚端口)。运行kubectl create命令进行创建:

img

运行kubectl get命令,查看已创建的Service:

img

至此,我们的第1个Kubernetes例子便搭建完成了,下一节将验证结果。

1.3.4 通过浏览器访问网页

经过上面的流程,我们终于成功实现了Kubernetes上第1个例子的部署、搭建工作。现在一起来见证成果吧!在你的笔记本上打开浏览器,输入“http://虚拟机IP:30001/demo/”。

比如虚拟机IP为192.168.18.131(可以通过ip a命令进行查询),在浏览器里输入地址http://192.168.18.131:30001/demo/后,可以看到如图1.2所示的网页界面。

img

图1.2 通过浏览器访问Tomcat应用

如果无法打开这个网页界面,那么可能的原因包括:①因为防火墙的设置无法访问30001端口;②因为通过代理服务器上网,所以浏览器错把虚拟机的IP地址当作远程地址;等等。可以在虚拟机上直接运行curl 192.168.18.131:30001来验证能否访问此端口,如果还是不能访问,就肯定不是机器的问题了。

接下来尝试单击“Add…”按钮添加一条记录并提交,如图1.3所示,提交以后,数据就被写入MySQL数据库了。

img

图1.3 在留言板网页添加新的留言

至此,我们就完成了在Kubernetes上部署一个Web App和数据库的例子。可以看到,相对于传统的分布式应用部署方式,在Kubernetes之上仅通过一些很容易理解的配置文件和简单命令就能完成对整个集群的部署。

1.4节将对Kubernetes中的资源对象进行全面讲解,读者可以继续研究本节例子里的一些拓展内容,比如:研究Deployment、Service等配置文件的格式;熟悉kubectl的子命令;手工停止某个Service对应的容器进程,看看会发生什么;修改Deployment文件,改变副本数量并重新发布,观察结果。

1.4 Kubernetes的基本概念和术语

考虑到Kubernetes相关的概念和术语非常多,它们之间的关系也比较复杂,本节将由浅入深地讲解Kubernetes的一些基本概念和术语,对它们更详细的原理和应用说明参见后续章节的内容。

1.4.1 资源对象概述

Kubernetes中的基本概念和术语大多是围绕资源对象(Resource Object)来说的,而资源对象在总体上可分为以下两类。

(1)某种资源的对象,例如节点(Node)、Pod、服务(Service)、存储卷(Volume)。

(2)与资源对象相关的事物与动作,例如标签(Label)、注解(Annotation)、命名空间(Namespace)、部署(Deployment)、HPA、PVC。

资源对象一般包括几个通用属性:版本、类别(Kind)、名称、标签、注解,如下所述。

(1)在版本信息里包括了此对象所属的资源组,一些资源对象的属性会随着版本的升级而变化,在定义资源对象时要特别注意这一点。

(2)类别属性用于定义资源对象的类型。

(3)资源对象的名称(Name)、标签、注解这三个属性属于资源对象的元数据(metadata)。

◎ 资源对象的名称要唯一。

◎ 资源对象的标签是很重要的数据,也是Kubernetes的一大设计特性,比如通过标签来表明资源对象的特征、类别,以及通过标签筛选不同的资源对象并实现对象之间的关联、控制或协作功能。

◎ 注解可被理解为一种特殊的标签,不过更多地是与程序挂钩,通常用于实现资源对象属性的自定义扩展。

我们可以采用YAML或JSON格式声明(定义或创建)一个Kubernetes资源对象,每个资源对象都有自己的特定结构定义(可以理解为数据库中一个特定的表),并且统一保存在etcd这种非关系型数据库中,以实现最快的读写速度。此外,所有资源对象都可以通过Kubernetes提供的kubectl工具(或者API编程调用)执行增、删、改、查等操作。

一些资源对象有自己的生命周期及相应的状态,比如Pod,我们通过kubectl客户端工具创建一个Pod并将其提交到系统中后,它就处于等待调度的状态,调度成功后为Pending状态,等待容器镜像下载和启动、启动成功后为Running状态,正常停止后为Succeeded状态,非正常停止后为Failed状态。同样,PV也是具有明确生命周期的资源对象。对于这类资源对象,我们还需要了解其生命周期的细节及状态变更的原因,这有助于我们快速排查故障。

另外,我们在学习时需要注意与该资源对象相关的其他资源对象或者事务,把握它们之间的关系,同时思考为什么会有这种资源对象产生,哪些是核心的资源对象,哪些是外围的资源对象。由于Kubernetes的快速发展,新的资源对象不断出现,一些旧的资源对象也被遗弃,这也是我们要与时俱进的原因。

为了更好地理解和学习Kubernetes的基本概念和术语,特别是数量众多的资源对象,这里按照功能或用途对其进行分类,将其分为集群类、应用类、存储类及安全类这四大类,在接下来的小节中一一讲解。

1.4.2 集群类

集群(Cluster)表示一个由Master和Node组成的Kubernetes集群。

1.Master

Master指的是集群的控制节点。在每个Kubernetes集群中都需要有一个或一组被称为Master的节点,来负责整个集群的管理和控制。Master通常占据一个独立的服务器(在高可用部署中建议至少使用3台服务器),是整个集群的“大脑”,如果它发生宕机或者不可用,那么对集群内容器应用的管理都将无法实施。

在Master上运行着以下关键进程。

◎ Kubernetes API Server(kube-apiserver):提供HTTP RESTful API接口的主要服务,是Kubernetes里对所有资源进行增、删、改、查等操作的唯一入口,也是集群控制的入口进程。

◎ Kubernetes Controller Manager(kube-controller-manager):Kubernetes里所有资源对象的自动化控制中心,可以将其理解为资源对象的“大总管”。

◎ Kubernetes Scheduler(kube-scheduler):负责资源调度(Pod调度)的进程,相当于公交公司的调度室。

另外,在Master上通常还需要部署etcd服务。

2.Node

Kubernetes集群中除Mater外的其他服务器被称为Node,Node在较早的版本中也被称为Minion。与Master一样,Node可以是一台物理主机,也可以是一台虚拟机。Node是Kubernetes集群中的工作负载节点,每个Node都会被Master分配一些工作负载(Docker容器),当某个Node宕机时,其上的工作负载会被Master自动转移到其他Node上。在每个Node上都运行着以下关键进程。

◎ kubelet:负责Pod对应容器的创建、启停等任务,同时与Master密切协作,实现集群管理的基本功能。

◎ kube-proxy:实现Kubernetes Service的通信与负载均衡机制的服务。

◎ 容器运行时(如Docker):负责本机的容器创建和管理。

Node可以在运行期间动态增加到Kubernetes集群中,前提是在这个Node上已正确安装、配置和启动了上述关键进程。在默认情况下,kubelet会向Master注册自己,这也是Kubernetes推荐的Node管理方式。一旦Node被纳入集群管理范畴,kubelet进程就会定时向Master汇报自身的情报,例如操作系统、主机CPU和内存使用情况,以及当前有哪些Pod在运行等,这样Master就可以获知每个Node的资源使用情况,并实现高效均衡的资源调度策略。而某个Node在超过指定时间不上报信息时,会被Master判定为“失联”,该Node的状态就被标记为不可用(Not Ready),Master随后会触发“工作负载大转移”的自动流程。

我们可以运行以下命令查看在集群中有多少个Node:

img

然后通过kubectl describe node 命令查看某个Node的详细信息:

img

在以上命令的运行结果中会展示目标Node的如下关键信息。

◎ Node的基本信息:名称、标签、创建时间等。

◎ Node当前的运行状态:Node启动后会做一系列自检工作,比如磁盘空间是否不足(DiskPressure)、内存是否不足(MemoryPressure)、网络是否正常(NetworkUnavailable)、PID资源是否充足(PIDPressure)。在一切正常时才设置Node为Ready状态(Ready=True),表示Node处于健康状态,Master就可以在其上调度新的任务了(如启动Pod)。

◎ Node的主机地址与主机名。

◎ Node上的资源数量:描述Node可用的系统资源,包括CPU、内存数量、最大可调度Pod数量等。

◎ Node可分配的资源量:描述Node当前可用于分配的资源量。

◎ 主机系统信息:包括主机ID、系统UUID、Linux Kernel版本号、操作系统类型与版本、Docker版本号、kubelet与kube-proxy的版本号等。

◎ 当前运行的Pod列表概要信息。

◎ 已分配的资源使用概要信息,例如资源申请的最小、最大允许使用量占系统总量的百分比。

◎ Node相关的Event信息。

如果一个Node存在问题,比如存在安全隐患、硬件资源不足要升级或者计划淘汰,我们就可以给这个Node打一种特殊的标签——污点(Taint),避免新的容器被调度到该Node上。而如果某些Pod可以(短期)容忍(Toleration)某种污点的存在,则可以继续将其调度到该Node上。Taint与Toleration这两个术语属于Kubernetes调度相关的重要术语和概念,在后续章节中会详细讲解。

在集群类里还有一个重要的基础概念——命名空间,它在很多情况下用于实现多租户的资源隔离,典型的一种思路就是给每个租户都分配一个命名空间。命名空间属于Kubernetes集群范畴的资源对象,在一个集群里可以创建多个命名空间,每个命名空间都是相互独立的存在,属于不同命名空间的资源对象从逻辑上相互隔离。在每个Kubernetes集群安装完成且正常运行之后,Master会自动创建两个命名空间,一个是默认的(default)、一个是系统级的(kube-system)。用户创建的资源对象如果没有指定命名空间,则被默认存放在default命名空间中;而系统相关的资源对象如网络组件、DNS组件、监控类组件等,都被安装在kube-system命名空间中。我们可以通过命名空间将集群内部的资源对象“分配”到不同的命名空间中,形成逻辑上分组的不同项目、小组或用户组,便于不同的分组在共享使用整个集群的资源的同时能被分别管理。当给每个租户都创建一个命名空间来实现多租户的资源隔离时,还能结合Kubernetes的资源配额管理,限定不同租户能占用的资源,例如CPU使用量、内存使用量等。

命名空间的定义很简单,如下所示的YAML文件定义了名为development的命名空间:

img

一旦创建了命名空间,我们在创建资源对象时就可以指定这个资源对象属于哪个命名空间。比如在下面的例子中定义了一个名为busybox的Pod,并将其放入development这个命名空间中:

img

此时使用kubectl get命令查看,将无法显示:

img

这是因为如果不加参数,则kubectl get命令将仅显示属于default命名空间的资源对象。

可以在kubectl get命令中加入--namespace参数来操作某个命名空间中的对象:

img

1.4.3 应用类

Kubernetes中属于应用类的概念和相应的资源对象类型最多,所以应用类也是我们要重点学习的一类。

1.Service与Pod

应用类相关的资源对象主要是围绕Service(服务)和Pod这两个核心对象展开的。

一般说来,Service指的是无状态服务,通常由多个程序副本提供服务,在特殊情况下也可以是有状态的单实例服务,比如MySQL这种数据存储类的服务。与我们常规理解的服务不同,Kubernetes里的Service具有一个全局唯一的虚拟ClusterIP地址,Service一旦被创建,Kubernetes就会自动为它分配一个可用的ClusterIP地址,而且在Service的整个生命周期中,它的ClusterIP地址都不会改变,客户端可以通过这个虚拟IP地址+服务的端口直接访问该服务,再通过部署Kubernetes集群的DNS服务,就可以实现Service Name(域名)到ClusterIP地址的DNS映射功能,我们只要使用服务的名称(DNS名称)即可完成到目标服务的访问请求。“服务发现”这个传统架构中的棘手问题在这里首次得以完美解决,同时,凭借ClusterIP地址的独特设计,Kubernetes进一步实现了Service的透明负载均衡和故障自动恢复的高级特性。

通过分析、识别并建模系统中的所有服务为微服务——Kubernetes Service,我们的系统最终由多个提供不同业务能力而又彼此独立的微服务单元组成,服务之间通过TCP/IP进行通信,从而形成强大又灵活的弹性网格,拥有强大的分布式能力、弹性扩展能力、容错能力,程序架构也变得简单和直观许多,如图1.4所示。

img

图1.4 Kubernetes提供的微服务网格架构

接下来说说与Service密切相关的核心资源对象——Pod。

Pod是Kubernetes中最重要的基本概念之一,如图1.5所示是Pod的组成示意图,我们看到每个Pod都有一个特殊的被称为“根容器”的Pause容器。Pause容器对应的镜像属于Kubernetes平台的一部分,除了Pause容器,每个Pod都还包含一个或多个紧密相关的用户业务容器。

img

图1.5 Pod的组成示意图

为什么Kubernetes会设计出一个全新的Pod概念并且Pod有这样特殊的组成结构?原因如下。

◎ 为多进程之间的协作提供一个抽象模型,使用Pod作为基本的调度、复制等管理工作的最小单位,让多个应用进程能一起有效地调度和伸缩。

◎ Pod里的多个业务容器共享Pause容器的IP,共享Pause容器挂接的Volume,这样既简化了密切关联的业务容器之间的通信问题,也很好地解决了它们之间的文件共享问题。

Kubernetes为每个Pod都分配了唯一的IP地址,称之为Pod IP,一个Pod里的多个容器共享Pod IP地址。Kubernetes要求底层网络支持集群内任意两个Pod之间的TCP/IP直接通信,这通常采用虚拟二层网络技术实现,例如Flannel、Open vSwitch等,因此我们需要牢记一点:在Kubernetes里,一个Pod里的容器与另外主机上的Pod容器能够直接通信。

Pod其实有两种类型:普通的Pod及静态Pod(Static Pod)。后者比较特殊,它并没被存放在Kubernetes的etcd中,而是被存放在某个具体的Node上的一个具体文件中,并且只能在此Node上启动、运行。而普通的Pod一旦被创建,就会被放入etcd中存储,随后被Kubernetes Master调度到某个具体的Node上并绑定(Binding),该Pod被对应的Node上的kubelet进程实例化成一组相关的Docker容器并启动。在默认情况下,当Pod里的某个容器停止时,Kubernetes会自动检测到这个问题并且重新启动这个Pod(重启Pod里的所有容器),如果Pod所在的Node宕机,就会将这个Node上的所有Pod都重新调度到其他节点上。Pod、容器与Node的关系如图1.6所示。

img

图1.6 Pod、容器与Node的关系

下面是我们在之前的Hello World例子里用到的myweb这个Pod的资源定义文件:

img

在以上定义中,kind属性的值为Pod,表明这是一个Pod类型的资源对象;metadata里的name属性为Pod的名称,在metadata里还能定义资源对象的标签,这里声明myweb拥有一个name=myweb标签。在Pod里所包含的容器组的定义则在spec部分中声明,这里定义了一个名为myweb且对应的镜像为kubeguide/tomcat-app:v1的容器,并在8080端口(containerPort)启动容器进程。Pod的IP加上这里的容器端口(containerPort)组成了一个新的概念——Endpoint,代表此Pod里的一个服务进程的对外通信地址。一个Pod也存在具有多个Endpoint的情况,比如当我们把Tomcat定义为一个Pod时,可以对外暴露管理端口与服务端口这两个Endpoint。

我们所熟悉的Docker Volume在Kubernetes里也有对应的概念——Pod Volume,Pod Volume是被定义在Pod上,然后被各个容器挂载到自己的文件系统中的。Volume简单来说就是被挂载到Pod里的文件目录。

这里顺便提一下Kubernetes的Event概念。Event是一个事件的记录,记录了事件的最早产生时间、最后重现时间、重复次数、发起者、类型,以及导致此事件的原因等众多信息。Event通常会被关联到某个具体的资源对象上,是排查故障的重要参考信息。之前我们看到在Node的描述信息中包括Event,而Pod同样有Event记录,当我们发现某个Pod迟迟无法创建时,可以用kubectl describe pod xxxx来查看它的描述信息,以定位问题的成因。比如下面这个Event记录信息就表明Pod里的一个容器被探针检测为失败一次:

img

如图1.7所示给出了Pod及Pod周边对象的示意图,后面的部分还会涉及这张图里的对象和概念。

img

图1.7 Pod及周边对象

在继续说明Service与Pod的关系之前,我们需要先学习理解Kubernetes中重要的一个机制——标签匹配机制。

2.Label与标签选择器

Label(标签)是Kubernetes系统中的另一个核心概念,相当于我们熟悉的“标签”。一个Label是一个key=value的键值对,其中的key与value由用户自己指定。Label可以被附加到各种资源对象上,例如Node、Pod、Service、Deployment等,一个资源对象可以定义任意数量的Label,同一个Label也可以被添加到任意数量的资源对象上。Label通常在资源对象定义时确定,也可以在对象创建后动态添加或者删除。我们可以通过给指定的资源对象捆绑一个或多个不同的Label来实现多维度的资源分组管理功能,以便灵活、方便地进行资源分配、调度、配置、部署等管理工作,例如,部署不同版本的应用到不同的环境中,以及监控、分析应用(日志记录、监控、告警)等。一些常用的Label示例如下。

◎ 版本标签:release:stable和release:canary。

◎ 环境标签:environment:dev、environment:qa和environment:production。

◎ 架构标签:tier:frontend、tier:backend和tier:middleware。

◎ 分区标签:partition:customerA和partition:customerB。

◎ 质量管控标签:track:daily和track:weekly。

给某个资源对象定义一个Label,就相当于给它打了一个标签,随后可以通过Label Selector(标签选择器)查询和筛选拥有某些Label的资源对象,Kubernetes通过这种方式实现了类似SQL的简单又通用的对象查询机制。Label Selector可以被类比为SQL语句中的where查询条件,例如,“name=redis-slave”这个Label Selector作用于Pod时,可以被类比为“select * from pod where pod's name='redis-slave'”这样的语句。当前有两种Label Selector表达式:基于等式的(Equality-based)Selector表达式和基于集合的(Set-based)Selector表达式。

基于等式的Selector表达式采用等式类表达式匹配标签,下面是一些具体的例子。

◎ name=redis-slave:匹配所有具有name=redis-slave标签的资源对象。

◎ env !=production:匹配所有不具有env=production标签的资源对象,比如“env=test”就是满足此条件的标签之一。

基于集合的Selector表达式则使用集合操作类表达式匹配标签,下面是一些具体的例子。

◎ name in(redis-master,redis-slave):匹配所有具有name=redis-master标签或者name=redis-slave标签的资源对象。

◎ name not in(php-frontend):匹配所有不具有name=php-frontend标签的资源对象。

可以通过多个Label Selector表达式的组合来实现复杂的条件选择,多个表达式之间用“,”进行分隔即可,几个条件之间是“AND”的关系,即同时满足多个条件,比如下面的例子:

img

在前面的留言板例子中只使用了一个“name=XXX”的Label Selector。看一个更复杂的例子:假设为Pod定义了3个Label:release、env和role,不同的Pod定义了不同的Label值,如图1.8所示,如果设置“role=frontend”的Label Selector,则会选取到Node 1和Node 2上的Pod;如果设置“release=beta”的Label Selector,则会选取到Node 2和Node 3上的Pod,如图1.9所示。

img

图1.8 Label Selector的作用范围1

img

图1.9 Label Selector的作用范围2

总之,使用Label可以给对象创建多组标签,Label和Label Selector共同构成了Kubernetes系统中核心的应用模型,可对被管理对象进行精细的分组管理,同时实现了整个集群的高可用性。

Label也是Pod的重要属性之一,其重要性仅次于Pod的端口,我们几乎见不到没有Label的Pod。以myweb Pod为例,下面给它设定了app=myweb标签:

img

对应的Service myweb就是通过下面的标签选择器与myweb Pod发生关联的:

img

所以我们看到,Service很重要的一个属性就是标签选择器,如果我们不小心把标签选择器写错了,就会出现指鹿为马的闹剧。如果恰好匹配到了另一种Pod实例,而且对应的容器端口恰好正确,服务可以正常连接,则很难排查问题,特别是在有众多Service的复杂系统中。

3.Pod与Deployment

前面提到,大部分Service都是无状态的服务,可以由多个Pod副本实例提供服务。通常情况下,每个Service对应的Pod服务实例数量都是固定的,如果一个一个地手工创建Pod实例,就太麻烦了,最好是用模板的思路,即提供一个Pod模板(Template),然后由程序根据我们指定的模板自动创建指定数量的Pod实例。这就是Deployment这个资源对象所要完成的事情了。

先看看之前例子中的Deployment案例(省略部分内容):

img

这里有几个很重要的属性。

◎ replicas:Pod的副本数量。

◎ selector:目标Pod的标签选择器。

◎ template:用于自动创建新Pod副本的模板。

只有一个Pod副本实例时,我们是否也需要Deployment来自动创建Pod呢?在大多数情况下,这个答案是“需要”。这是因为Deployment除自动创建Pod副本外,还有一个很重要的特性:自动控制。举个例子,如果Pod所在的节点发生宕机事件,Kubernetes就会第一时间观察到这个故障,并自动创建一个新的Pod对象,将其调度到其他合适的节点上,Kubernetes会实时监控集群中目标Pod的副本数量,并且尽力与Deployment中声明的replicas数量保持一致。

下面创建一个名为tomcat-deployment.yaml的Deployment描述文件,内容如下:

img

运行以下命令创建Deployment对象:

img

运行以下命令查看Deployment的信息:

img

对以上输出中各字段的含义解释如下。

◎ DESIRED:Pod副本数量的期望值,即在Deployment里定义的replicas。

◎ CURRENT:当前replicas的值,实际上是Deployment创建的ReplicaSet对象里的replicas值,这个值不断增加,直到达到DESIRED为止,表明整个部署过程完成。

◎ UP-TO-DATE:最新版本的Pod的副本数量,用于指示在滚动升级的过程中,有多少个Pod副本已经成功升级。

◎ AVAILABLE:当前集群中可用的Pod副本数量,即集群中当前存活的Pod数量。

Deployment资源对象其实还与ReplicaSet资源对象密切相关,Kubernetes内部会根据Deployment对象自动创建相关联的ReplicaSet对象,通过以下命令,我们可以看到它的命名与Deployment的名称有对应关系:

img

不仅如此,我们发现Pod的命名也是以Deployment对应的ReplicaSet对象的名称为前缀的,这种命名很清晰地表明了一个ReplicaSet对象创建了哪些Pod,对于Pod滚动升级(Pod Rolling update)这种复杂的操作过程来说,很容易排查错误:

img

关于Deployment就先说到这里,最后总结一下它的典型使用场景。

◎ 创建一个Deployment对象来完成相应Pod副本数量的创建。

◎ 检查Deployment的状态来看部署动作是否完成(Pod副本数量是否达到预期的值)。

◎ 更新Deployment以创建新的Pod(比如镜像升级),如果当前Deployment不稳定,则回滚到一个早先的Deployment版本。

◎ 扩展Deployment以应对高负载。

图1.10显示了Pod、Deployment与Service的逻辑关系。

从图1.10中可以看到,Kubernetes的Service定义了一个服务的访问入口地址,前端的应用(Pod)通过这个入口地址访问其背后的一组由Pod副本组成的集群实例。Service与其后端Pod副本集群之间则是通过Label Selector实现无缝对接的,Deployment实际上用于保证Service的服务能力和服务质量始终符合预期标准。

img

图1.10 Pod、Deployment与Service的逻辑关系

4.Service的ClusterIP地址

既然每个Pod都会被分配一个单独的IP地址,而且每个Pod都提供了一个独立的Endpoint(Pod IP+containerPort)以被客户端访问,那么现在多个Pod副本组成了一个集群来提供服务,客户端如何访问它们呢?传统的做法是部署一个负载均衡器(软件或硬件),为这组Pod开启一个对外的服务端口如8000端口,并且将这些Pod的Endpoint列表加入8000端口的转发列表中,客户端就可以通过负载均衡器的对外IP地址+8000端口来访问此服务了。Kubernetes也是类似的做法,Kubernetes内部在每个Node上都运行了一套全局的虚拟负载均衡器,自动注入并自动实时更新集群中所有Service的路由表,通过iptables或者IPVS机制,把对Service的请求转发到其后端对应的某个Pod实例上,并在内部实现服务的负载均衡与会话保持机制。不仅如此,Kubernetes还采用了一种很巧妙又影响深远的设计——ClusterIP地址。我们知道,Pod的Endpoint地址会随着Pod的销毁和重新创建而发生改变,因为新Pod的IP地址与之前旧Pod的不同。Service一旦被创建,Kubernetes就会自动为它分配一个全局唯一的虚拟IP地址——ClusterIP地址,而且在Service的整个生命周期内,其ClusterIP地址不会发生改变,这样一来,每个服务就变成了具备唯一IP地址的通信节点,远程服务之间的通信问题就变成了基础的TCP网络通信问题。

任何分布式系统都会涉及“服务发现”这个基础问题,大部分分布式系统都通过提供特定的API来实现服务发现功能,但这样做会导致平台的侵入性较强,也增加了开发、测试的难度。Kubernetes则采用了直观朴素的思路轻松解决了这个棘手的问题:只要用Service的Name与ClusterIP地址做一个DNS域名映射即可。比如我们定义一个MySQL Service,Service的名称是mydbserver,Service的端口是3306,则在代码中直接通过mydbserver:3306即可访问此服务,不再需要任何API来获取服务的IP地址和端口信息。

之所以说ClusterIP地址是一种虚拟IP地址,原因有以下几点。

◎ ClusterIP地址仅仅作用于Kubernetes Service这个对象,并由Kubernetes管理和分配IP地址(来源于ClusterIP地址池),与Node和Master所在的物理网络完全无关。

◎ 因为没有一个“实体网络对象”来响应,所以ClusterIP地址无法被Ping通。ClusterIP地址只能与Service Port组成一个具体的服务访问端点,单独的ClusterIP不具备TCP/IP通信的基础。

◎ ClusterIP属于Kubernetes集群这个封闭的空间,集群外的节点要访问这个通信端口,则需要做一些额外的工作。

下面是名为tomcat-service.yaml的Service定义文件,内容如下:

img

以上代码定义了一个名为tomcat-service的Service,它的服务端口为8080,拥有tier=frontend标签的所有Pod实例都属于它,运行下面的命令进行创建:

img

我们之前在tomcat-deployment.yaml里定义的Tomcat的Pod刚好拥有这个标签,所以刚才创建的tomcat-service已经对应了一个Pod实例,运行下面的命令可以查看tomcat-service的Endpoint列表,其中172.17.1.3是Pod的IP地址,8080端口是Container暴露的端口:

img

img

你可能有疑问:“说好的Service的ClusterIP地址呢?怎么没有看到?”运行下面的命令即可看到tomcat-service被分配的ClusterIP地址及更多的信息:

img

在spec.ports的定义中,targetPort属性用来确定提供该服务的容器所暴露(Expose)的端口号,即具体的业务进程在容器内的targetPort上提供TCP/IP接入;port属性则定义了Service的端口。前面定义Tomcat服务时并没有指定targetPort,所以targetPort默认与port相同。除了正常的Service,还有一种特殊的Service——Headless Service,只要在Service的定义中设置了clusterIP:None,就定义了一个Headless Service,它与普通Service的关键区别在于它没有ClusterIP地址,如果解析Headless Service的DNS域名,则返回的是该Service对应的全部Pod的Endpoint列表,这意味着客户端是直接与后端的Pod建立TCP/IP连接进行通信的,没有通过虚拟ClusterIP地址进行转发,因此通信性能最高,等同于“原生网络通信”。

接下来看看Service的多端口问题。很多服务都存在多个端口,通常一个端口提供业务服务,另一个端口提供管理服务,比如Mycat、Codis等常见中间件。Kubernetes Service支持多个Endpoint,在存在多个Endpoint的情况下,要求每个Endpoint都定义一个名称进行区分。下面是Tomcat多端口的Service定义样例:

img

img

5.Service的外网访问问题

前面提到,服务的ClusterIP地址在Kubernetes集群内才能被访问,那么如何让集群外的应用访问我们的服务呢?这也是一个相对复杂的问题。要弄明白这个问题的解决思路和解决方法,我们需要先弄明白Kubernetes的三种IP,这三种IP分别如下。

◎ Node IP:Node的IP地址。

◎ Pod IP:Pod的IP地址。

◎ Service IP:Service的IP地址。

首先,Node IP是Kubernetes集群中每个节点的物理网卡的IP地址,是一个真实存在的物理网络,所有属于这个网络的服务器都能通过这个网络直接通信,不管其中是否有部分节点不属于这个Kubernetes集群。这也表明Kubernetes集群之外的节点访问Kubernetes集群内的某个节点或者TCP/IP服务时,都必须通过Node IP通信。

其次,Pod IP是每个Pod的IP地址,在使用Docker作为容器支持引擎的情况下,它是Docker Engine根据docker0网桥的IP地址段进行分配的,通常是一个虚拟二层网络。前面说过,Kubernetes要求位于不同Node上的Pod都能够彼此直接通信,所以Kubernetes中一个Pod里的容器访问另外一个Pod里的容器时,就是通过Pod IP所在的虚拟二层网络进行通信的,而真实的TCP/IP流量是通过Node IP所在的物理网卡流出的。

在Kubernetes集群内,Service的ClusterIP地址属于集群内的地址,无法在集群外直接使用这个地址。为了解决这个问题,Kubernetes首先引入了NodePort这个概念,NodePort也是解决集群外的应用访问集群内服务的直接、有效的常见做法。

以tomcat-service为例,在Service的定义里做如下扩展即可(见代码中的粗体部分):

img

img

其中,nodePort:31002这个属性表明手动指定tomcat-service的NodePort为31002,否则Kubernetes会自动为其分配一个可用的端口。接下来在浏览器里访问http://:31002/,就可以看到Tomcat的欢迎界面了,如图1.11所示。

img

图1.11 通过NodePort访问Service

NodePort的实现方式是,在Kubernetes集群的每个Node上都为需要外部访问的Service开启一个对应的TCP监听端口,外部系统只要用任意一个Node的IP地址+NodePort端口号即可访问此服务,在任意Node上运行netstat命令,就可以看到有NodePort端口被监听:

img

但NodePort还没有完全解决外部访问Service的所有问题,比如负载均衡问题。假如在我们的集群中有10个Node,则此时最好有一个负载均衡器,外部的请求只需访问此负载均衡器的IP地址,由负载均衡器负责转发流量到后面某个Node的NodePort上,如图1.12所示。

img

图1.12 NodePort与负载均衡器

图1.12中的负载均衡器组件独立于Kubernetes集群之外,通常是一个硬件的负载均衡器,也有以软件方式实现的,例如HAProxy或者Nginx。对于每个Service,我们通常需要配置一个对应的负载均衡器实例来转发流量到后端的Node上,这的确增加了工作量及出错的概率。于是Kubernetes提供了自动化的解决方案,如果我们的集群运行在谷歌的公有云GCE上,那么只要把Service的“type=NodePort”改为“type=LoadBalancer”,Kubernetes就会自动创建一个对应的负载均衡器实例并返回它的IP地址供外部客户端使用。其他公有云提供商只要实现了支持此特性的驱动,则也可以达到以上目的。此外,也有MetalLB这样的面向私有集群的Kubernetes负载均衡方案。

NodePort的确功能强大且通用性强,但也存在一个问题,即每个Service都需要在Node上独占一个端口,而端口又是有限的物理资源,那能不能让多个Service共用一个对外端口呢?这就是后来增加的Ingress资源对象所要解决的问题。在一定程度上,我们可以把Ingress的实现机制理解为基于Nginx的支持虚拟主机的HTTP代理。下面是一个Ingress的实例:

img

img

在以上Ingress的定义中,到虚拟域名first.bar.com请求的流量会被路由到service1,到second.foo.com请求的流量会被路由到service2。通过上面的例子,我们也可以看出,Ingress其实只能将多个HTTP(HTTPS)的Service“聚合”,通过虚拟域名或者URL Path的特征进行路由转发功能。考虑到常见的微服务都采用了HTTP REST协议,所以Ingress这种聚合多个Service并将其暴露到外网的做法还是很有效的。

6.有状态的应用集群

我们知道,Deployment对象是用来实现无状态服务的多副本自动控制功能的,那么有状态的服务,比如ZooKeeper集群、MySQL高可用集群(3节点集群)、Kafka集群等是怎么实现自动部署和管理的呢?这个问题就复杂多了,这些一开始是依赖StatefulSet解决的,但后来发现对于一些复杂的有状态的集群应用来说,StatefulSet还是不够通用和强大,所以后面又出现了Kubernetes Operator。

我们先说说StatefulSet。StatefulSet之前曾用过PetSet这个名称,很多人都知道,在IT世界里,有状态的应用被类比为宠物(Pet),无状态的应用则被类比为牛羊,每个宠物在主人那里都是“唯一的存在”,宠物生病了,我们是要花很多钱去治疗的,需要我们用心照料,而无差别的牛羊则没有这个待遇。总结下来,在有状态集群中一般有如下特殊共性。

◎ 每个节点都有固定的身份ID,通过这个ID,集群中的成员可以相互发现并通信。

◎ 集群的规模是比较固定的,集群规模不能随意变动。

◎ 集群中的每个节点都是有状态的,通常会持久化数据到永久存储中,每个节点在重启后都需要使用原有的持久化数据。

◎ 集群中成员节点的启动顺序(以及关闭顺序)通常也是确定的。

◎ 如果磁盘损坏,则集群里的某个节点无法正常运行,集群功能受损。

如果通过Deployment控制Pod副本数量来实现以上有状态的集群,我们就会发现上述很多特性大部分难以满足,比如Deployment创建的Pod因为Pod的名称是随机产生的,我们事先无法为每个Pod都确定唯一不变的ID,不同Pod的启动顺序也无法保证,所以在集群中的某个成员节点宕机后,不能在其他节点上随意启动一个新的Pod实例。另外,为了能够在其他节点上恢复某个失败的节点,这种集群中的Pod需要挂接某种共享存储,为了解决有状态集群这种复杂的特殊应用的建模,Kubernetes引入了专门的资源对象——StatefulSet。StatefulSet从本质上来说,可被看作Deployment/RC的一个特殊变种,它有如下特性。

◎ StatefulSet里的每个Pod都有稳定、唯一的网络标识,可以用来发现集群内的其他成员。假设StatefulSet的名称为kafka,那么第1个Pod叫kafka-0,第2个叫kafka-1,以此类推。

◎ StatefulSet控制的Pod副本的启停顺序是受控的,操作第n个Pod时,前n-1个Pod已经是运行且准备好的状态。

◎ StatefulSet里的Pod采用稳定的持久化存储卷,通过PV或PVC来实现,删除Pod时默认不会删除与StatefulSet相关的存储卷(为了保证数据安全)。

StatefulSet除了要与PV卷捆绑使用,以存储Pod的状态数据,还要与Headless Service配合使用,即在每个StatefulSet定义中都要声明它属于哪个Headless Service。StatefulSet在Headless Service的基础上又为StatefulSet控制的每个Pod实例都创建了一个DNS域名,这个域名的格式如下:

img

比如一个3节点的Kafka的StatefulSet集群对应的Headless Service的名称为kafka,StatefulSet的名称为kafka,则StatefulSet里3个Pod的DNS名称分别为kafka-0.kafka、kafka-1.kafka、kafka-2.kafka,这些DNS名称可以直接在集群的配置文件中固定下来。

StatefulSet的建模能力有限,面对复杂的有状态集群时显得力不从心,所以就有了后来的Kubernetes Operator框架和众多的Operator实现了。需要注意的是,Kubernetes Operator框架并不是面向普通用户的,而是面向Kubernetes平台开发者的。平台开发者借助Operator框架提供的API,可以更方便地开发一个类似StatefulSet的控制器。在这个控制器里,开发者通过编码方式实现对目标集群的自定义操控,包括集群部署、故障发现及集群调整等方面都可以实现有针对性的操控,从而实现更好的自动部署和智能运维功能。从发展趋势来看,未来主流的有状态集群基本都会以Operator方式部署到Kubernetes集群中。

7.批处理应用

除了无状态服务、有状态集群、常见的第三种应用,还有批处理应用。批处理应用的特点是一个或多个进程处理一组数据(图像、文件、视频等),在这组数据都处理完成后,批处理任务自动结束。为了支持这类应用,Kubernetes引入了新的资源对象——Job,下面是一个计算圆周率的经典例子:

img

Jobs控制器提供了两个控制并发数的参数:completions和parallelism,completions表示需要运行任务数的总数,parallelism表示并发运行的个数,例如设置parallelism为1,则会依次运行任务,在前面的任务运行后再运行后面的任务。Job所控制的Pod副本是短暂运行的,可以将其视为一组容器,其中的每个容器都仅运行一次。当Job控制的所有Pod副本都运行结束时,对应的Job也就结束了。Job在实现方式上与Deployment等副本控制器不同,Job生成的Pod副本是不能自动重启的,对应Pod副本的restartPolicy都被设置为Never,因此,当对应的Pod副本都执行完成时,相应的Job也就完成了控制使命。后来,Kubernetes增加了CronJob,可以周期性地执行某个任务。

8.应用的配置问题

通过前面的学习,我们初步理解了三种应用建模的资源对象,总结如下。

◎ 无状态服务的建模:Deployment。

◎ 有状态集群的建模:StatefulSet。

◎ 批处理应用的建模:Job。

在进行应用建模时,应该如何解决应用需要在不同的环境中修改配置的问题呢?这就涉及ConfigMap和Secret两个对象。

ConfigMap顾名思义,就是保存配置项(key=value)的一个Map,如果你只是把它理解为编程语言中的一个Map,那就大错特错了。ConfigMap是分布式系统中“配置中心”的独特实现之一。我们知道,几乎所有应用都需要一个静态的配置文件来提供启动参数,当这个应用是一个分布式应用,有多个副本部署在不同的机器上时,配置文件的分发就成为一个让人头疼的问题,所以很多分布式系统都有一个配置中心组件,来解决这个问题。但配置中心通常会引入新的API,从而导致应用的耦合和侵入。Kubernetes则采用了一种简单的方案来规避这个问题,如图1.13所示,具体做法如下。

◎ 用户将配置文件的内容保存到ConfigMap中,文件名可作为key,value就是整个文件的内容,多个配置文件都可被放入同一个ConfigMap。

◎ 在建模用户应用时,在Pod里将ConfigMap定义为特殊的Volume进行挂载。在Pod被调度到某个具体Node上时,ConfigMap里的配置文件会被自动还原到本地目录下,然后映射到Pod里指定的配置目录下,这样用户的程序就可以无感知地读取配置了。

◎ 在ConfigMap的内容发生修改后,Kubernetes会自动重新获取ConfigMap的内容,并在目标节点上更新对应的文件。

img

图1.13 ConfigMap配置集中化的一种简单方案

接下来说说Secret。Secret也用于解决应用配置的问题,不过它解决的是对敏感信息的配置问题,比如数据库的用户名和密码、应用的数字证书、Token、SSH密钥及其他需要保密的敏感配置。对于这类敏感信息,我们可以创建一个Secret对象,然后被Pod引用。Secret中的数据要求以BASE64编码格式存放。注意,BASE64编码并不是加密的,在Kubernetes 1.7版本以后,Secret中的数据才可以以加密的形式进行保存,更加安全。

9.应用的运维问题

本节最后说说与应用的自动运维相关的几个重要对象。

首先就是HPA(Horizontal Pod Autoscaler),如果我们用Deployment来控制Pod的副本数量,则可以通过手工运行kubectl scale命令来实现Pod扩容或缩容。如果仅仅到此为止,则显然不符合谷歌对Kubernetes的定位目标——自动化、智能化。在谷歌看来,分布式系统要能够根据当前负载的变化自动触发水平扩容或缩容,因为这一过程可能是频繁发生、不可预料的,所以采用手动控制的方式是不现实的,因此就有了后来的HPA这个高级功能。我们可以将HPA理解为Pod横向自动扩容,即自动控制Pod数量的增加或减少。通过追踪分析指定Deployment控制的所有目标Pod的负载变化情况,来确定是否需要有针对性地调整目标Pod的副本数量,这是HPA的实现原理。Kubernetes内置了基于Pod的CPU利用率进行自动扩缩容的机制,应用开发者也可以自定义度量指标如每秒请求数,来实现自定义的HPA功能。下面是一个HPA定义的例子:

img

根据上面的定义,我们可以知道这个HPA控制的目标对象是一个名为php-apache的Deployment里的Pod副本,当这些Pod副本的CPU利用率的值超过90%时,会触发自动动态扩容,限定Pod的副本数量为1~10。HPA很强大也比较复杂,我们在后续章节中会继续深入学习。

接下来就是VPA(Vertical Pod Autoscaler),即垂直Pod自动扩缩容,它根据容器资源使用率自动推测并设置Pod合理的CPU和内存的需求指标,从而更加精确地调度Pod,实现整体上节省集群资源的目标,因为无须人为操作,因此也进一步提升了运维自动化的水平。VPA目前属于比较新的特性,也不能与HPA共同操控同一组目标Pod,它们未来应该会深入融合,建议读者关注其发展状况。

1.4.4 存储类

存储类的资源对象主要包括Volume、Persistent Volume、PVC和StorageClass。

首先看看基础的存储类资源对象——Volume(存储卷)。

Volume是Pod中能够被多个容器访问的共享目录。Kubernetes中的Volume概念、用途和目的与Docker中的Volume比较类似,但二者不能等价。首先,Kubernetes中的Volume被定义在Pod上,被一个Pod里的多个容器挂载到具体的文件目录下;其次,Kubernetes中的Volume与Pod的生命周期相同,但与容器的生命周期不相关,当容器终止或者重启时,Volume中的数据也不会丢失;最后,Kubernetes支持多种类型的Volume,例如GlusterFS、Ceph等分布式文件系统。

Volume的使用也比较简单,在大多数情况下,我们先在Pod上声明一个Volume,然后在容器里引用该Volume并将其挂载(Mount)到容器里的某个目录下。举例来说,若我们要给之前的Tomcat Pod增加一个名为datavol的Volume,并将其挂载到容器的/mydata-data目录下,则只对Pod的定义文件做如下修正即可(代码中的粗体部分):

img

img

Kubernetes提供了非常丰富的Volume类型供容器使用,例如临时目录、宿主机目录、共享存储等,下面对其中一些常见的类型进行说明。

1.emptyDir

一个emptyDir是在Pod分配到Node时创建的。从它的名称就可以看出,它的初始内容为空,并且无须指定宿主机上对应的目录文件,因为这是Kubernetes自动分配的一个目录,当Pod从Node上移除时,emptyDir中的数据也被永久移除。emptyDir的一些用途如下。

◎ 临时空间,例如用于某些应用程序运行时所需的临时目录,且无须永久保留。

◎ 长时间任务执行过程中使用的临时目录。

◎ 一个容器需要从另一个容器中获取数据的目录(多容器共享目录)。

在默认情况下,emptyDir使用的是节点的存储介质,例如磁盘或者网络存储。还可以使用emptyDir.medium属性,把这个属性设置为“Memory”,就可以使用更快的基于内存的后端存储了。需要注意的是,这种情况下的emptyDir使用的内存会被计入容器的内存消耗,将受到资源限制和配额机制的管理。

2.hostPath

hostPath为在Pod上挂载宿主机上的文件或目录,通常可以用于以下几方面。

◎ 在容器应用程序生成的日志文件需要永久保存时,可以使用宿主机的高速文件系统对其进行存储。

◎ 需要访问宿主机上Docker引擎内部数据结构的容器应用时,可以通过定义hostPath为宿主机/var/lib/docker目录,使容器内部的应用可以直接访问Docker的文件系统。

在使用这种类型的Volume时,需要注意以下几点。

◎ 在不同的Node上具有相同配置的Pod,可能会因为宿主机上的目录和文件不同,而导致对Volume上目录和文件的访问结果不一致。

◎ 如果使用了资源配额管理,则Kubernetes无法将hostPath在宿主机上使用的资源纳入管理。

在下面的例子中使用了宿主机的/data目录定义了一个hostPath类型的Volume:

img

3.公有云Volume

公有云提供的Volume类型包括谷歌公有云提供的GCEPersistentDisk、亚马逊公有云提供的AWS Elastic Block Store(EBS Volume)等。当我们的Kubernetes集群运行在公有云上或者使用公有云厂家提供的Kubernetes集群时,就可以使用这类Volume。

4.其他类型的Volume

◎ iscsi:将iSCSI存储设备上的目录挂载到Pod中。

◎ nfs:将NFS Server上的目录挂载到Pod中。

◎ glusterfs:将开源GlusterFS网络文件系统的目录挂载到Pod中。

◎ rbd:将Ceph块设备共享存储(Rados Block Device)挂载到Pod中。

◎ gitRepo:通过挂载一个空目录,并从Git库克隆(clone)一个git repository以供Pod使用。

◎ configmap:将配置数据挂载为容器内的文件。

◎ secret:将Secret数据挂载为容器内的文件。

动态存储管理

Volume属于静态管理的存储,即我们需要事先定义每个Volume,然后将其挂载到Pod中去用,这种方式存在很多弊端,典型的弊端如下。

◎ 配置参数烦琐,存在大量手工操作,违背了Kubernetes自动化的追求目标。

◎ 预定义的静态Volume可能不符合目标应用的需求,比如容量问题、性能问题。

所以Kubernetes后面就发展了存储动态化的新机制,来实现存储的自动化管理。相关的核心对象(概念)有三个:Persistent Volume(简称PV)、StorageClass、PVC。

PV表示由系统动态创建(dynamically provisioned)的一个存储卷,可以被理解成Kubernetes集群中某个网络存储对应的一块存储,它与Volume类似,但PV并不是被定义在Pod上的,而是独立于Pod之外定义的。PV目前支持的类型主要有gcePersistentDisk、AWSElasticBlockStore、AzureFile、AzureDisk、FC(Fibre Channel)、NFS、iSCSI、RBD(Rados Block Device)、CephFS、Cinder、GlusterFS、VsphereVolume、Quobyte Volumes、VMware Photon、Portworx Volumes、ScaleIO Volumes、HostPath、Local等。

我们知道,Kubernetes支持的存储系统有多种,那么系统怎么知道从哪个存储系统中创建什么规格的PV存储卷呢?这就涉及StorageClass与PVC。StorageClass用来描述和定义某种存储系统的特征,下面给出一个具体的例子:

img

从上面的例子可以看出,StorageClass有几个关键属性:provisioner、parameters和reclaimPolicy,系统在动态创建PV时会用到这几个参数。简单地说,provisioner代表了创建PV的第三方存储插件,parameters是创建PV时的必要参数,reclaimPolicy则表明了PV回收策略,回收策略包括删除或则保留。需要注意的是,StorageClass的名称会在PVC(PV Claim)中出现,下面就是一个典型的PVC定义:

img

img

PVC正如其名,表示应用希望申请的PV规格,其中重要的属性包括accessModes(存储访问模式)、storageClassName(用哪种StorageClass来实现动态创建)及resources(存储的具体规格)。

有了以StorageClass与PVC为基础的动态PV管理机制,我们就很容易管理和使用Volume了,只要在Pod里引用PVC即可达到目的,如下面的例子所示:

img

除了动态创建PV,PV动态扩容、快照及克隆的能力也是Kubernetes社区正在积极研发的高级特性。

1.4.5 安全类

安全始终是Kubernetes发展过程中的一个关键领域。

从本质上来说,Kubernetes可被看作一个多用户共享资源的资源管理系统,这里的资源主要是各种Kubernetes里的各类资源对象,比如Pod、Service、Deployment等。只有通过认证的用户才能通过Kubernetes的API Server查询、创建及维护相应的资源对象,理解这一点很关键。

Kubernetes里的用户有两类:我们开发的运行在Pod里的应用;普通用户,如典型的kubectl命令行工具,基本上由指定的运维人员(集群管理员)使用。在更多的情况下,我们开发的Pod应用需要通过API Server查询、创建及管理其他相关资源对象,所以这类用户才是Kubernetes的关键用户。为此,Kubernetes设计了Service Account这个特殊的资源对象,代表Pod应用的账号,为Pod提供必要的身份认证。在此基础上,Kubernetes进一步实现和完善了基于角色的访问控制权限系统——RBAC(Role-Based Access Control)。

在默认情况下,Kubernetes在每个命名空间中都会创建一个默认的名称为default的Service Account,因此Service Account是不能全局使用的,只能被它所在命名空间中的Pod使用。通过以下命令可以查看集群中的所有Service Account:

img

Service Account是通过Secret来保存对应的用户(应用)身份凭证的,这些凭证信息有CA根证书数据(ca.crt)和签名后的Token信息(Token)。在Token信息中就包括了对应的Service Account的名称,因此API Server通过接收到的Token信息就能确定Service Account的身份。在默认情况下,用户创建一个Pod时,Pod会绑定对应命名空间中的default这个Service Account作为其“公民身份证”。当Pod里的容器被创建时,Kubernetes会把对应的Secret对象中的身份信息(ca.crt、Token等)持久化保存到容器里固定位置的本地文件中,因此当容器里的用户进程通过Kubernetes提供的客户端API去访问API Server时,这些API会自动读取这些身份信息文件,并将其附加到HTTPS请求中传递给API Server以完成身份认证逻辑。在身份认证通过以后,就涉及“访问授权”的问题,这就是RBAC要解决的问题了。

首先我们要学习的是Role这个资源对象,包括Role与ClusterRole两种类型的角色。角色定义了一组特定权限的规则,比如可以操作某类资源对象。局限于某个命名空间的角色由Role对象定义,作用于整个Kubernetes集群范围内的角色则通过ClusterRole对象定义。下面是Role的一个例子,表示在命名空间default中定义一个Role对象,用于授予对Pod资源的读访问权限,绑定到该Role的用户则具有对Pod资源的get、watch和list权限:

img

接下来就是如何将Role与具体用户绑定(用户授权)的问题了。我们可以通过RoleBinding与ClusterRoleBinding来解决这个问题。下面是一个具体的例子,在命名空间default中将“pod-reader”角色授予用户“Caden”,结合对应的Role的定义,表明这一授权将允许用户“Caden”从命名空间default中读取pod。

img

在RoleBinding中使用subjects(目标主体)来表示要授权的对象,这是因为我们可以授权三类目标账号:Group(用户组)、User(某个具体用户)和Service Account(Pod应用所使用的账号)。

在安全领域,除了以上针对API Server访问安全相关的资源对象,还有一种特殊的资源对象——NetworkPolicy(网络策略),它是网络安全相关的资源对象,用于解决用户应用之间的网络隔离和授权问题。NetworkPolicy是一种关于Pod间相互通信,以及Pod与其他网络端点间相互通信的安全规则设定。

NetworkPolicy资源使用标签选择Pod,并定义选定Pod所允许的通信规则。在默认情况下,Pod间及Pod与其他网络端点间的访问是没有限制的,这假设了Kubernetes集群被一个厂商(公司/租户)独占,其中部署的应用都是相互可信的,无须相互防范。但是,如果存在多个厂商共同使用一个Kubernetes集群的情况,则特别是在公有云环境中,不同厂商的应用要相互隔离以增加安全性,这就可以通过NetworkPolicy来实现了。

版权声明:如无特殊说明,文章均为本站原创,版权所有,转载需注明本文链接

本文链接:http://www.bianchengvip.com/article/Getting-started-with-Kubernetes/