第03讲:云原生应用的 15 个特征

本课时我将带你学习云原生应用。


微服务架构只是一种软件架构风格,并不限制所采用的实现技术,开发团队可以自由选择最合适的技术来实现。在第 01 课时介绍微服务架构的时候,提到了微服务架构实现最大的挑战是它的复杂度,这些复杂度是微服务架构本身天然所具备的,是每个微服务架构应用绕不开的难题。在实现微服务架构时,开发团队当然希望把全部的精力放在实现业务逻辑上,而不是应对微服务架构自身的复杂度,这就意味着,需要选择能够帮助应对这些复杂性的平台和工具。云原生(Cloud Native)应用就是微服务架构的最佳实现方式。

云原生应用的概念

顾名思义,云原生应用的概念由云和原生两个部分组成,云在这里指的是云平台,也就是平台即服务(Platform as a Service,PaaS);原生应用指的是专门针对云平台而设计和实现的,充分利用了云平台的特性。应用的微服务可以专注于实现业务逻辑,而把微服务架构的复杂度交给云平台来解决。


原生这个词在软件开发中有它独特的含义。原生通常意味着高效和难以移植。原生意味着针对特定的平台而设计,可以充分利用平台的特性,因此运行起来非常高效;同样意味着与特定平台的深度绑定,很难移植到其他平台。云原生应用同样具有这两个特征,对于云原生应用来说,难移植并不是一个问题,毕竟迁移到云平台之后,不会再想迁移回去。

云原生应用的特征

与其他应用相比,总结起来,云原生应用有如下 15 个特征。

单一代码库

云原生应用必须有单一的代码库,并在版本管理系统中进行追踪。单一代码库可以是一个代码仓库,也可以是共享同一根目录的多个代码仓库,其重要性在于每一个代码提交(Commit)都会对应一个不可变的构建版本。在每次代码提交之后,持续集成流程会被触发,最终产生一系列的应用容器镜像,这就在代码提交和构建版本之间建立了一对``一的对应关系,这种一对一的关系保证了每个构建版本都是可追踪的,可以比较不同版本之间的代码变化。


对于微服务架构的应用来说,每个应用由多个服务组成,这些服务应该由单一的代码库进行管理,这保证了构建版本的稳定性。如果一个改动涉及到多个服务,则这个改动应该在一次代码提交中完成对所有相关服务的修改;如果服务的代码分散在多个代码库中,则一个改动会被分成多个代码提交,每个代码提交都会触发一次持续集成流程,产生对应服务的构建版本,这些服务的构建版本只包含了部分改动,是不完整的。在应用部署时,有的服务可能包含了部分改动,而有的服务则没有,这使得部署的应用实际上是不能工作的。因此,微服务架构的应用应该使用单一代码库。

API 优先

云原生应用应该采用 API 优先的设计策略。微服务架构的应用使用公开 API 来作为服务的对外接口,API 屏蔽了服务的内部实现细节。API 优先的设计策略指的是在设计阶段,应该首先设计 API 并确定 API 的细节。API 的设计过程需要多个团队的参与,包括 API 的实现者和可能的使用者,这些团队在充分讨论中最终完成 API 的定义。API 可以使用 OpenAPI 规范描述,从该规范中可以生成 API 文档和进行测试的模拟服务器。


API 优先的策略保证了 API 的稳定性,同时可以减少不必要的后期修改。因为 API 是服务之间的接口,修改 API 就意味着相关的内部实现、测试用例和 API 的使用者都需要进行修改,如果在应用开发中出现了必须修改 API 的情况,那造成的影响是很大的。API 优先确保了尽可能减少在开发中对 API 进行修改。


API 优先的另外一个好处是可以提高开发效率。API 确定之后,可以利用工具生成文档和模拟服务器,API 的使用者可以根据文档来编写使用 API 的代码。利用 Swagger 这样的工具,甚至可以直接生成访问 API 的代码。测试人员可以编写 API 相关的测试用例,并用模拟服务器运行测试。不同的团队可以并行工作,从而提高效率。

依赖管理

云原生应用应该管理自己的依赖,Java 开发人员对依赖管理应该并不陌生,常用的 Java 构建工具 Maven 和 Gradle 都提供了依赖管理的支持。在开发过程中,只需要利用构建工具的支持即可;在管理依赖时,则需要区分应用自带的依赖和运行环境提供的依赖。云原生应用通常会包含全部所需的依赖,尤其是以容器形式运行的应用,典型的例子是微服务的 REST API。云原生应用会自带嵌入式的 Tomcat 这样的服务器来提供 HTTP 服务。

设计、构建、发布和运行

云原生应用应该有完整的设计、构建、发布和运行流程,如下图所示。


             

设计

设计在云原生应用的开发中必不可少。传统应用通常采用瀑布式的开发流程,瀑布式的开发流程中会分配足够的时间进行设计。云原生应用一般采用敏捷软件开发流程,但是这并不意味着设计变得不再重要,只不过设计过程变成了一个迭代的过程,而且每次设计的范围较小,通常只需要对某些新特性进行设计。

构建

构建阶段从单一代码库中创建出带版本号的二进制工件,构建过程通常由持续集成服务器来完成,每个构建都必须有唯一不变的版本号,构建出来的二进制工件也是不可变的。这就保证了同一个构建版本在经过测试之后,被部署的版本与测试过的版本保持一致。

发布

把构建出来的工件推送到云平台之上,就得到了一个发布版本,发布版本中包含与部署环境相关的配置信息。云原生应用在部署时,通常有开发、测试和生产 3 个环境,在每个环境上的配置信息都不尽相同。发布版本也是不可变的,有唯一的发布号。每一个构建版本都可能对应多个发布版本。

运行

运行阶段在云平台之上运行应用,运行的方式取决于云平台,可以是虚拟机或容器。云平台负责管理应用的运行,包括监控应用运行状态、处理失败的情况和动态水平扩展等。

代码、配置和凭据

代码、配置和凭据是云原生应用开发中创建的三种不同类型的实体。代码包括源代码和相关资源文件;配置是与部署环境相关的配置信息,通常以 XML、YAML、JSON 或属性文件的形式出现,配置中包含的信息包括第三方服务的连接方式、数据库连接信息和应用自身的配置属性等;凭据指的是密码、私钥和 API 密钥等敏感信息。


代码和配置的区别在于,代码不会随着部署环境而变化,而配置则相反。在实践中,应该尽可能把配置从应用中分离出来,进行外部化管理,构建出来的二进制工件中不包含任何配置信息,实际的配置值在部署时根据环境来确定。在运行时,一般使用环境变量来传递配置值,还可以使用类似 Spring Cloud Config 这样的专门配置服务器来管理配置值。凭据都应该从源代码仓库中删除。

日志

日志是应用开发中不可或缺的部分。与传统应用不同的是,云原生应用并不需要对日志的输出方式进行很多配置,只是简单地把日志写到标准输出流(stdout)和标准错误流(stderr)。日志的收集和处理由云平台上的其他服务来提供,这把应用开发人员从日志管理相关的任务中解放出来。云平台上的日志管理服务非常多,开源的典型实现包括 Elastic 技术栈(ElasticSearch + LogStash + Kibana)和 Fluentd。

随时可丢弃

云原生应用的生命周期可能是短暂的,随时可能被终止。云平台可能会随时启动和停止应用的实例,这就要求云原生应用的启动和停止速度都要非常快。当应用的负载突然增大时,可以快速地启动新的实例来处理请求;当应用的实例出现问题时,可以快速启动一个新的实例作为替代。快速停止应用和快速启动应用一样重要,快速停止应用保证了资源可以被及时释放。

支撑服务

云原生应用的运行离不开支撑服务。支撑服务是一个宽泛的概念,包括数据库、消息中间件、缓存、用户认证和授权、存储等。连接这些支撑服务的配置信息应该被抽离出来,在运行时根据部署环境提供实际值。

环境等同

云原生应用的不同部署环境应该是等同的。开发、测试和生产环境之间不应该有差异,环境的等同性保证了云原生应用可以快速的进行部署,这一特征与构建工件的不变性是相辅相成的,两者缺一不可。有了这两个特征之后,每一个唯一版本的构建工件可以被依次部署到不同的环境,在测试环境上经过测试的版本,可以直接部署到生产环境。我们可以确定应用在生产环境上的行为与测试环境中一样。

管理任务

云原生应用运行中可能会需要执行一些管理任务,比如生成报表或者执行一次性的数据查询等,这些任务通常并不属于业务流程的一部分,更多的是为了管理和运维的需要。这些任务在执行中会用到云原生应用所依赖的支撑服务,对于这些任务,应该创建独立的应用,并在同样的云平台上运行。对于定期执行的任务,可以充分利用云平台的支持,比如,Kubernetes 提供了对定时任务(CronJob)的支持。


以生成报表为例,可以创建一个独立的应用来读取数据库并生成报表,该应用可以有自己独立的容器镜像。如果报表生成是手动触发的,该应用应该独立运行,并提供一个 API 接口来允许外部触发。如果报表生成是定期的,应用部署时可以创建相应的定时任务来运行容器,在容器启动时自动生成报表,生成完毕之后,容器运行结束。下图说明了这两种触发方式的区别,圆角矩形的边框表示应用的边界。


端口绑定

云原生应用在运行时并不负责管理实际的端口绑定,而是由云平台统一管理。比如,一个基于 Spring Boot 的微服务应用通常在 8080 端口运行 HTTP 服务,当应用运行在云平台上时,这个端口只是虚拟机或容器内的端口,并不是外部用户或其他服务访问时的实际端口。云平台对网络进行统一管理,负责分配实际的端口,云平台同时提供了相应的机制来发现访问服务的实际地址和端口。

无状态进程

云原生应用应该是无状态的。所有的状态信息都应该从应用中抽离出来,并保存在支撑服务中,比如数据库中。正因为应用是无状态的,才可以由云平台快速的启动和停止,并进行垂直或水平扩展。

并发性

云原生应用使用水平扩展来并发运行多个实例,使用负载均衡来把请求分配到某个实例进行处理。

遥测数据

云原生应用需要收集一系列遥测数据,包括应用性能指标、运行状态和日志等,这些遥测数据,对于云平台和应用来说同等重要。云平台可以用性能指标来进行自动水平扩展,比如,Kubernetes 支持 Pod 的自动水平扩展,当 CPU 的利用率超过预定的阈值时,会自动启动新的 Pod 来处理请求。性能指标分成两类:一类是业务无关的,比如请求的数量、请求的处理速度、以及平均的请求处理时间等;第二类是业务相关的,需要应用根据业务需求进行收集,比如处理的订单数量和不同商品的销售情况等。云原生应用通常会创建仪表盘来实时展示整体的运行状态,方便运维人员进行监控。

认证和授权

云原生应用应该是安全的,安全应该在应用的设计阶段就充分考虑。在实现中,可以使用基于角色的访问控制(RBAC)来保护 API,已经有大量的开源框架来帮助实现认证和授权。


在理想情况下,云原生应用应该具备上述全部 15 个特征,但是在实际的开发中,不一定能够做到。开发团队可以根据需要,选择对应用最重要的特征来实现。

总结

本课时对云原生应用的15个特征都做了详细的介绍。这些特征有些比较好实现,有些则相对较难一些。在实际的开发中,应该尽可能让自己的云原生应用满足这些特征。


第04讲:什么是 Kubernete 容器化应用

本课时我将带你学习 Kubernetes 的相关内容。


Kubernetes 是一个可移植和可扩展的开源平台,用来管理容器化的工作负载和服务,它可以促进声明式的配置和自动化。Kubernetes 是 Google 基于自身运行大规模工作负载的丰富经验开发出来的,2014 年 Google 开源了 Kubernetes 项目,集合社区的智慧来让 Kubernetes 变得更好。Kubernetes 是目前容器编排领域上的事实标准。绝大多数云平台都提供了 Kubernetes 的支持,同时,它也是本专栏中云原生应用的部署平台。本课时我将对 Kubernetes 及其重要概念进行介绍。


为了进行本课时的介绍,你需要有一个可用的Kubernetes集群。在本地开发环境中,可以使用Docker Desktop或是Minikube,也可以使用云平台上安装好的集群。前提要求是可以使用kubectl来访问集群。

Kubernetes 集群

部署 Kubernetes 之后将运行一个 Kubernetes 集群。Kubernetes 集群中包含很多不同的组件,分别运行在不同的节点(Node)上,其中,组件分成两类:控制平面(Control Plane)组件和节点组件,而节点可以是物理机器或虚拟主机。


控制平面组件分类中的组件如下表所示:



节点组件如下表所示:



除了这些核心组件之外,还可以安装一些附加组件,如 DNS 服务器、Web 界面、容器资源监控和日志管理等。


下图给出了 Kubernetes 中各个组件之间的交互关系。



在实际的集群中,控制平面组件和工作节点组件通常部署在不同的机器上,包含控制平面组件的机器称为集群的主控节点(Master),包含节点组件的机器称为工作节点(Worker)。生产环境的集群中应该至少有 1 个主控节点和 3 个工作节点。

API 与对象

Kubernetes 的 API 采用与 Kubernetes 控制平面组件交互的方式,来管理 Kubernetes 集群。当需要与API 交互时,我们可以使用自带的 kubectl 工具,或使用不同编程语言的客户端库。


Kubernetes 的 API 中定义了很多不同类型的对象,这些对象的使用是声明式的,并且只声明了对象所期望的状态。每个对象中都包含 2 个字段,分别是 spec 和 status,其中 spec 描述了对象的期望状态,而 status 包含的是对象当前的状态。Kubernetes 会确保对象的最终状态与期望的状态保持一致。


在对象的描述中,每个对象都有名称和唯一的标识符,其中有一些字段是通用的。对象的名称由创建者提供,同一类对象的名称不能重复。对象的标识符由 Kubernetes 自动生成,其标签是添加到对象上的名值对,标签主要用来选择满足条件的对象。注解同样以名值对的形式出现,只不过注解的作用是添加附加的信息。从作用上来说,注解的作用类似于 Java 中的注解,很多 Kubernetes 上的框架和第三方库都允许使用自定义的注解来进行配置。


本课时中对相关对象的描述都使用 YAML 格式。

Pod

Kubernetes 中的 Pod 表示由一个或多个容器组成的分组。Pod 里面的容器共享一些资源,包括存储、网络和每个容器的运行配置。我们可以把 Pod 看成是一个应用运行的逻辑主机,相当于一个物理机器或虚拟主机,其中的容器是紧密耦合的。每个 Pod 都有唯一的 IP 地址,其中的容器共享同一个 IP 地址和端口范围,相互之间可以通过 localhost 访问。Pod 还可以使它的容器访问其定义的共享的存储卷(volume),通过这种方式,可以实现 Pod 容器之间的数据共享。


Pod 是 Kubernetes 中调度和运行的基本单元,但在 Kubernetes 上并不能直接运行容器,而是要把容器放在 Pod 中运行,Pod 再把其中的全部容器调度到同一个集群节点上运行。

控制器

作为基本的运行单元,Pod 缺少相应的管理功能。如果一个 Pod 在运行中出现错误,并不会自动创建新的 Pod 来替代它;如果需要管理 Pod,则应该利用 Kubernetes 提供不同类别的控制器,包括副本集、部署、有状态集和守护程序集。

副本集

副本集(ReplicaSet)保证在任何时候都有给定数量的 Pod 副本。当创建副本集时需要指定一个创建 Pod 的模板,同时确定 Pod 的选择器,并给出期望的 Pod 副本数量。在运行时,副本集根据指定的 Pod 选择器来监控当前 Pod 副本的数量,如果副本数量小于期望值,则根据 Pod 模板来创建新的 Pod。一般用标签作为 Pod 的选择器。


下面代码中的 YAML 文件描述了包含 3 个 Pod 副本的副本集,每个 Pod 中只有一个 Nginx 容器,每个 Pod 上都添加了名称为 app、值为 nginx 的标签。副本集使用这个标签作为包含的 Pod 的选择器,在 spec.selector.matchLabels 中指定。spec.containers 指定的是创建 Pod 的模板。


apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.17
          ports:
            - containerPort: 80

部署

部署(Deployment)在副本集上提供了对 Pod 的更新功能,每个部署都有对应的副本集。当部署中创建的 Pod 模板发生变化时,这些 Pod 都需要被更新。在更新时,Kubernetes会创建一个新的副本集来管理新的 Pod,当更新完成后,旧的副本集会被删除。


部署可以采用不同的策略来更新这些 Pod,最常用的策略是滚动更新,它可以保证在更新过程中服务不间断。在进行滚动更新时,Kubernetes首先会创建新的副本集中的 Pod,再删除已有副本集中的 Pod。这个过程是交替进行的:在没有足够数量的新 Pod 运行之前,不会删除已有的 Pod;同样的,在没有足够数量的已有 Pod 被删除之前,不会创建新的 Pod。


下面代码中的 YAML 文件创建了包含 3 个 Pod 副本的部署,它与上一小节中的副本集 YAML 的文件的区别在于,kind 的值是 Deployment。


apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.17
          ports:
            - containerPort: 80

有状态集

副本集中的 Pod 没有标识符,不能进行区分,它唯一保证的是所包含的 Pod 总数。有些应用对运行的实例有启动顺序和唯一性的要求,典型的例子是通过多个实例组成集群的服务,如 Cassandra 和 RabbitMQ 等。这些应用要求实例有固定且唯一的网络标识,才能正确的建立集群,对于这样的应用,应该使用有状态集(StatefulSet)来管理。


有状态集中的 Pod 都有唯一不变的标识符,这个标识符由有状态集的名称和 Pod 的序号组成。如果有状态集的名称是 cassandra,同时期望的 Pod 副本数量是 3,那么 Pod 的名称分别是 cassandra-0、cassandra-1 和 cassandra-2。有状态集需要一个对应的服务来提供 Pod 的 DNS 名称。


有状态集在创建和扩展时有特殊的限制,如果一个有状态集期望的 Pod 副本数量是 N,那么有状态集会从 0 开始依次创建这些 Pod,在第 1 个 Pod 正常运行之前,不会创建第 2 个Pod;在删除 Pod 时,则是从第 N 个 Pod 开始反向依次删除。


下面代码中的 YAML 文件创建了一个有状态集及其关联的服务。


apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
    - port: 80
      name: web
  clusterIP: None
  selector:
    app: nginx
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  serviceName: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.17
          ports:
            - containerPort: 80
              name: web


在下面的 Pod 列表中你可以看到 Pod 的名称。


NAME      READY   STATUS    RESTARTS   AGE
nginx-0   1/1     Running   0          35m
nginx-1   1/1     Running   0          35m
nginx-2   1/1     Running   0          35m

守护程序集

守护程序集(DaemonSet)确保在全部或部分的集群节点上运行 Pod,每个节点上最多运行一个 Pod 副本。如果应用需要执行的任务与节点相关,则应该使用守护程序集,比如收集日志和节点性能指标数据的应用,都应该使用守护程序集来部署。


下面代码中的 YAML 文件创建了一个守护程序集。


apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.17
        ports:
          - containerPort: 80

服务

应用在运行时经常需要为其他应用提供服务。在使用控制器管理应用的 Pod 时,Pod 的 IP 地址会随着 Pod 的创建和删除而发生变化。因此,我们不能使用 Pod 的 IP 地址作为访问服务的方式。Kubernetes 中的服务(Service)作为一组 Pod 的抽象,定义了如何访问这些 Pod,

它实现了服务发现和负载均衡两项重要的功能。


在创建服务时需通过选择器来选择服务所对应的 Pod,应用服务的消费者使用服务的地址来访问 Pod,实际的访问请求会被分发到服务所对应的某个 Pod 上。当 Pod 发生变化时,服务会自动更新所对应的 Pod 列表。


下表给出了服务的几种类型。



下面代码中的 YAML 描述了一个类型为 NodePort 的服务。


apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  type: NodePort
  ports:
    - port: 8080
      targetPort: 80
      protocol: TCP
      name: http
  selector:
    app: nginx


在服务创建之后,服务的使用者可以有两种方式来发现服务。


第一种方式是使用 Pod 的环境变量。在 Pod 运行时,每个服务的主机名和端口都会被自动添加到环境变量中,对应的环境变量名称是在服务名称之后添加 _SERVICE_HOST 和 _SERVICE_PORT 后缀,并且把服务名称转换成大写,把破折号替换成下划线。比如,如果服务名称是 cassandra,那么所对应的环境变量名称分别是 CASSANDRA_SERVICE_HOST 和 CASSANDRA_SERVICE_PORT。


第二种方式是使用 DNS 名称。如果服务的访问者和服务的 Pod 在同一个名称空间中,则直接使用服务的名称进行访问即可;如果在不同的名称空间,则需要使用服务“名称.名称空间”的形式,如 cassandra.demo。

存储卷

容器运行时对内部文件所做的修改是瞬时的,当容器停止之后,相关的修改会丢失,如果应用具备第 03 课时中云原生应用的无状态特征,这并不是一个问题,反而是正确的做法。但是应用所依赖的支撑服务,绝大部分是有状态的,比如数据库和消息中间件等。


Kubernetes 中的(Volume)是存储的抽象表示,解决了数据的持久化问题,卷与使用它的 Pod 具有相同的生命周期。Pod 中的容器可以使用卷共享数据,对于运行在 Pod 中容器里面的应用来说,卷就是文件系统上的一个目录,其中包含了可以访问的文件。Kubernetes 支持不同类型的卷,包括云平台提供的实现和第三方存储服务。


在实际的 Kubernetes 集群中,数据存储通常需要进行统一管理,有专门的服务负责提供存储,而 Pod 是存储的消费者。


Kubernetes 提供了两个对象,即持久卷和持久卷要求,持久卷可以由管理员手动创建,或者由存储服务动态创建,用户则以持久卷要求的形式来声明所需要使用的持久卷的大小和访问模式;持久卷要求可以与手动创建的持久卷进行绑定,如果需要动态创建,则由存储服务完成创建之后,再进行绑定。一旦完成绑定,持久卷可以用类似卷的方式来访问。


下面代码中的 YAML 文件描述了一个容量为 1G 的持久卷,使用的存储类是 standard。


apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv
spec:
  storageClassName: standard
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/mnt/data"


下面代码中的 YAML 文件描述了一个持久卷要求,所请求的空间大小是 1G,刚好可以由上面 YAML 文件中的持久卷来满足。


apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc
spec:
  storageClassName: standard
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi


如果持久卷是动态创建的,则需要使用存储类来描述不同的存储方式,存储类通常由存储服务提供。在持久卷要求中,声明所需要使用的存储类后,它对应的存储服务会负责创建持久卷。


下面代码的 YAML 文件中,部署中 Pod 里的 volumes 声明中的 persistentVolumeClaim 引用的持久卷要求的名称是pvc,与上面的YAML文件相对应。


apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: pvc
      containers:
      - name: nginx
        image: nginx:1.17
        ports:
          - containerPort: 80
        volumeMounts:
          - mountPath: "/usr/share/nginx/html"
            name: data

任务和定时任务

当需要执行一次性任务时,我们可以使用任务对象在创建任务时指定需要运行的 Pod 模板。当需要定期执行任务时,则可以使用定时任务对象,在创建时,需要指定定时任务的 cron 表达式。


下面代码中的 YAML 文件描述了执行一次的任务。


apiVersion: batch/v1
kind: Job
metadata:
  name: job-hostname
spec:
  template:
    spec:
      containers:
      - name: busybox
        image: busybox
        command:
          - hostname
      restartPolicy: Never
  backoffLimit: 1


下面代码中的 YAML 文件描述了每分钟执行一次的定期任务,定期任务在每次执行时都会创建新的 Job 对象。


apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: cronjob-date
spec:
  schedule: "* * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: busybox
            image: busybox
            command:
            - date
          restartPolicy: OnFailure

配置表

在第 03 课时介绍云原生应用的时候,我们提到了配置外部化,当它运行在 Kubernetes 上时,应该使用配置表(ConfigMap)来管理配置。配置表可以看成是包含名值对的哈希表。


Spring Boot 应用使用 application.properties 文件来管理配置。下面代码中的 ConfigMap 对象,仅包含一个名为 application.properties 的配置项,其对应值是 application.properties 文件的实际内容。


apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  application.properties: |-
    server.port = 9090
    spring.application.name = myapp


下面代码中的 YAML 文件创建了 Spring Boot 应用的部署。在 Pod 模板中,我们从配置表中创建了一个存储卷,配置表中的每个名值对都会被转换成一个文件:文件名是名称,而文件的内容是相应的值。应用容器把该存储卷映射到 /etc/config 目录上,通过环境变量 SPRING_CONFIG_LOCATION 可以让 Spring Boot从/etc/config 目录中加载配置文件,从而应用 application.properties 文件的配置。


apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-app
  labels:
    app: demo-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: demo-app
  template:
    metadata:
      labels:
        app: demo-app
    spec:
      containers:
        - name: app
          image: localhost:5000/demo-app
          env:
            - name: SPRING_CONFIG_LOCATION
              value: /etc/config/
          volumeMounts:
            - name: config-volume
              mountPath: /etc/config
      volumes:
        - name: config-volume
          configMap:
            name: app-config

总结

Kubernetes是本专栏所介绍的云原生微服务应用的运行平台。本课时对Kubernetes中的基本概念进行了介绍,包括Kubernetes集群、Pod、副本集、部署、有状态集、守护程序集等,还介绍了服务、存储卷、任务和定时任务、以及配置表的相关内容。通过本课时,可以让你对Kubernetes有基本的了解。


更多推荐