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://<nodePort IP>: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,它们未来应该会深入融合,建议读者关注其发展状况。