十一、容器化 vs 虚拟化-Kubernetes(K8s)

发布于:2025-09-09 ⋅ 阅读:(23) ⋅ 点赞:(0)


前言

Kubernetes

  Kubernetes 是一个开源的容器编排引擎,用来对容器化应用进行自动化部署、扩缩和管理。此开源项目由云原生计算基金会(CNCF)托管。


一、介绍

1.概述

Kubernetes 是一个可移植、可扩展的开源平台,用于管理容器化的工作负载和服务,方便进行声明式配置和自动化。Kubernetes 拥有一个庞大且快速增长的生态系统,其服务、支持和工具的使用范围广泛。

帮助了解 Kubernetes 系统的各个部分以及 Kubernetes 用来表示集群的抽象概念, 并帮助你更深入地理解 Kubernetes 是如何工作的。

Kubernetes是一个开源的容器编排部署管理平台,用于管理云平台中多个主机上的容器化应用。Kubernetes的目标是让部署容器化的应用简单并且高效,Kubernetes提供了应用部署、规划、更新、维护的一种机制。

对应用开发者而言,可以把Kubernetes看成一个集群操作系统。Kubernetes提供服务发现、伸缩、负载均衡、自愈甚至选举等功能,让开发者从基础设施相关配置等解脱出来。

可以通过管理控制台、Kubectl命令行、Kubernetes API,使用云容器引擎所提供的Kubernetes托管服务。

2.基本概念

云容器引擎(Cloud Container Engine,简称CCE)提供高度可扩展的、高性能的企业级Kubernetes集群。借助云容器引擎,您可以在云上轻松部署、管理和扩展容器化应用程序。

云容器引擎提供Kubernetes原生API,支持使用kubectl,且提供图形化控制台,让您能够拥有完整的端到端使用体验,使用云容器引擎前,建议您先了解相关的基本概念。

  • 集群(Cluster)
    集群指容器运行所需要的云资源组合,关联了若干云服务器节点、负载均衡等云资源。您可以理解为集群是“同一个子网中一个或多个弹性云服务器(又称:节点)”通过相关技术组合而成的计算机群体,为容器运行提供了计算资源池。

  • 节点(Node)
    在Kubernetes集群中,节点是运行容器化应用程序的工作主机,它们可以是物理服务器或虚拟机,并通过网络连接形成集群。每个节点都安装了必要的组件,如容器运行时(如Docker)和kubelet(用于管理容器)。节点资源被Kubernetes统一调度和管理,用于部署和运行实例(Pod)(容器的最小部署单元),是集群的基础运行环境,保障应用程序的高可用性和弹性扩展。
    关于节点的更多操作请参见创建节点

  • 节点池(NodePool)
    在Kubernetes集群中,节点池是一组具有相同配置和属性的节点集合。这些节点通常具有相同的硬件规格、操作系统版本和Kubernetes节点配置。节点池可以方便地实现集群资源的批量管理与扩展。您可以根据需求创建不同规模和配置的节点池,以满足不同应用程序的负载调度需求,确保资源高效利用。同时,节点池支持弹性伸缩,可根据工作负载自动调整节点数量,从而优化资源利用效率,提升集群的灵活性和可扩展性。
    关于节点池的更多操作请参见创建节点池

  • 虚拟私有云(VPC)
    虚拟私有云是通过逻辑方式进行网络隔离,提供安全、隔离的网络环境。您可以在VPC中定义与传统网络无差别的虚拟网络,同时提供弹性IP、安全组等高级网络服务。
    通过VPC,CCE集群可以实现节点与容器网络的安全隔离,同时支持弹性公网IP和带宽配置,满足集群的灵活扩展需求。
    关于虚拟私有云的更多操作请参见创建虚拟私有云和子网

  • 安全组
    安全组是一个逻辑上的分组,为同一个VPC内具有相同安全保护需求并相互信任的弹性云服务器提供访问策略。安全组创建后,用户可以在安全组中定义各种访问规则,当弹性云服务器加入该安全组后,即受到这些访问规则的保护。
    关于安全组的更多操作请参见添加安全组规则
    集群、虚拟私有云、安全组和节点的关系
    如图1,同一个Region下可以有多个虚拟私有云(VPC)。虚拟私有云由一个个子网组成,子网与子网之间的网络交互通过子网网关完成,而集群就是建立在某个子网中。因此,存在以下三种场景:

    • 不同集群可以创建在不同的虚拟私有云中。
    • 不同集群可以创建在同一个子网中。
    • 不同集群可以创建在不同的子网中。

图1 集群、VPC、安全组和节点的关系
集群、VPC、安全组和节点的关系

  • 实例(Pod)
    在Kubernetes中,Pod是部署应用或服务的最小基本单位。一个Pod可以封装一个或多个应用容器,多个容器通常共享存储和网络资源。每个Pod都有一个独立的网络IP地址,这使得 Pod内的容器可以相互通信,并且可以被集群内的其他Pod访问。同时,Kubernetes提供多种策略选项来管理容器的运行方式,包括重启策略、资源请求和限制、生命周期钩子等。

图2 实例(Pod)
实例(Pod)

  • 容器(Container)
    一个通过Docker镜像创建的运行实例被称为容器。在一个节点(宿主机)上可以运行多个容器。容器的实质是进程,但与直接在宿主机上执行的进程不同,容器进程运行于属于自己的独立的命名空间中。这些命名空间提供了一种隔离机制,使得每个容器都有自己的文件系统、网络接口、进程 ID 等,从而实现了操作系统级别的隔离。

图3 实例Pod、容器Container、节点Node的关系
实例Pod、容器Container、节点Node的关系

  • 工作负载
    工作负载是在Kubernetes上运行的应用程序。无论您的工作负载是单个组件还是协同工作的多个组件,您都可以在Kubernetes上的一组Pod中运行它。在Kubernetes中,工作负载是对一组Pod的抽象模型,用于描述业务的运行载体,包括Deployment、StatefulSet、DaemonSet、Job、CronJob等多种类型。

    • 无状态工作负载:即Kubernetes中的“Deployment”,无状态工作负载支持弹性伸缩与滚动升级,适用于实例完全独立、功能相同的场景,如Web服务器(NGINX)、博客平台(WordPress)等。
    • 有状态工作负载:即Kubernetes中的“StatefulSet”,有状态工作负载支持实例有序部署和删除,每个Pod都有一个持久的标识符,并且可以相互通信,适用于需要持久化存储和实例间相互通信的应用,如分布式键值存储系统(ETCD)、高可用的数据库(MySQL-HA)等。
    • 创建守护进程集:即Kubernetes中的“DaemonSet”,守护进程集确保全部(或者某些)节点都运行一个Pod实例,支持会自动将Pod部署到新加入集群的节点上,它适用于需要在每个节点上运行的服务,如日志收集(fluentd)、监控代理(Prometheus Node Exporter)等。
    • 普通任务:即Kubernetes中的“Job”,普通任务是一次性运行的任务,确保指定数量的Pod成功完成执行。适用于需要在集群中执行一次性任务的场景,如数据备份、批量处理等。
    • 定时任务:即Kubernetes中的“CronJob”,定时任务是按照指定时间周期运行的任务。适用于需要定期执行的任务,如定时数据同步、定时生成报告等。

关于工作负载的更多操作请参见创建工作负载

图4 工作负载与Pod的关系
工作负载与Pod的关系

  • 镜像(Image)
    镜像(Image)是一个模板,是容器应用打包的标准格式,用于创建容器。或者说,镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。在部署容器化应用时可以指定镜像,镜像可以来自于 Docker Hub、容器镜像服务或者用户的私有镜像仓库。例如,开发者可以创建一个包含特定应用程序及其所有依赖的镜像,确保在任何环境中都能以相同的方式运行。
    镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的类和实例一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。
    关于镜像的更多操作请参见上传镜像

图5 镜像、容器、工作负载的关系
镜像、容器、工作负载的关系

  • 命名空间(Namespace)
    命名空间是对一组资源和对象的抽象整合,允许您将相关的资源和对象(如Pods、Services、Deployments等)组织在一起,形成一个逻辑上的分组。不同命名空间中的数据彼此隔离,但它们仍可以共享同一个集群的基础资源(如CPU、内存、存储等)。您可以在不同的命名空间中部署不同的环境,例如开发环境、测试环境和生产环境,这样可以确保环境之间的隔离,同时便于管理和维护。
    在Kubernetes中,大部分资源对象都是命名空间级别的,如Pods、Services、Replication Controllers和Deployments等,这意味着它们属于某一个命名空间(默认是default)。但仍有一部分资源是集群级别的,例如Node、PersistentVolumes等,它们不属于任何命名空间,为所有命名空间中的资源提供服务。
    关于命名空间的更多操作请参见创建命名空间

  • 服务(Service)
    在Kubernetes中,Service用于定义Pods的访问策略。Service类型的取值以及行为如下:

    • ClusterIP:这是默认的Service类型,它会在集群内部为Service分配一个唯一的IP地址。这个IP地址只在集群内部可用,外部无法直接访问。ClusterIP类型的Service通常用于集群内部的通信。

    • NodePort:NodePort类型的Service会在集群的所有节点上打开一个静态端口(NodePort),通过这个端口可以访问Service。这个类型的Service允许外部流量通过节点绑定的弹性IP和指定的端口访问Service,从而实现对外提供服务。

    • LoadBalancer:利用云服务提供商的负载均衡器,将Service暴露给外部网络。外部的负载均衡器可以将流量转发到集群中NodePort服务和ClusterIP服务。

    • DNAT:使用DNAT网关为集群节点提供网络地址转换服务,使多个节点可以共享使用弹性IP。与直接为节点绑定弹性IP的方式相比,DNAT方式增强了可靠性,弹性IP无需与单个节点绑定,任何节点状态的异常不影响其访问。

关于服务的更多操作请参见服务概述

  • 路由(Ingress)
    Kubernetes集群中的Ingress用于管理外部访问集群内服务的规则。它提供基于域名、路径的路由功能,支持负载均衡、TLS终止和SSL证书管理。通过Ingress,可以将多个服务的流量统一管理,对外暴露一个入口点,简化网络配置,提升集群的可扩展性和安全性,是实现微服务架构中服务暴露的重要方式。
    关于路由的更多操作请参见路由概述

  • 网络策略(NetworkPolicy)
    Kubernetes集群中的NetworkPolicy用于定义Pod之间的网络通信策略。它通过指定允许或拒绝的流量规则,控制Pod之间的访问关系,增强集群的网络安全。NetworkPolicy支持基于Pod标签、IP地址和端口的规则配置,能够限制入站和出站流量,防止未经授权的通信,从而保护集群内部服务的安全性。
    关于网络策略的更多操作请参见配置网络策略(NetworkPolicy)限制Pod访问的对象

  • 配置项(ConfigMap)
    Kubernetes集群中的ConfigMap用于存储配置数据,它可以将配置信息(如配置文件、命令行参数等)从Pod中分离出来,以键值对的形式存储。通过ConfigMap,用户可以轻松地在多个Pod之间共享和更新配置,而无需重新构建镜像。它支持多种数据格式(如YAML、JSON),方便灵活地管理应用程序的配置,确保配置的可维护性和可扩展性。
    关于配置项的更多操作请参见创建配置项

  • 密钥(Secret)
    Kubernetes集群中的Secret用于存储敏感信息(如密码、密钥、证书等),它以加密形式存储数据,确保敏感信息的安全性。Secret可以通过挂载或环境变量的形式在Pod中使用,也可以用于存储集群内部的认证信息。通过Secret,用户可以将敏感信息与应用程序代码分离,降低泄露风险,同时实现对敏感数据的集中管理和动态更新,保障集群的安全性和灵活性
    关于密钥的更多操作请参见创建密钥

  • 标签(Label)
    在Kubernetes中,标签(Label)是附加到资源对象(如Pod、Service、Deployment等)上的键值对。标签的主要作用是为这些对象提供额外的、语义化的元数据,以便于用户和系统能够更容易地识别、组织和管理资源。

  • 标签选择器(LabelSelector)
    在Kubernetes中,标签选择器是一种强大的机制,极大地简化了资源管理和操作的复杂性。它允许用户根据资源对象上的标签来选择和分组这些对象,可以对选中的资源组执行批量操作,如流量分配、扩缩容、更新配置、监控状态等。

  • 注解(Annotation)
    Annotation与Label类似,也使用key/value键值对的形式进行定义。但它们在用途和约束上有所不同。
    Label更多地用于资源的选择和管理,具有严格的命名规则,它定义的是Kubernetes对象的元数据(Metadata),并且用于Label Selector为用户提供选择资源的能力。
    Annotation则是用户任意定义的“附加”信息,Kubernetes系统不会直接使用这些注解来控制资源的行为,但它存储的额外信息可以被外部工具获取,用于扩展Kubernetes的功能。

  • 存储卷(PersistentVolume)
    PersistentVolume(PV)是集群的一块存储资源,可以是本地磁盘或网络存储。它具有独立于Pod的生命周期,这意味着即使使用PV的Pod被删除,PV中的数据也不会丢失。

  • 存储声明(PersistentVolumeClaim)
    PersistentVolumeClaim (PVC) 用户对存储资源PV的请求,它指定了存储的大小、访问模式等要求,Kubernetes会自动匹配合适的PV来满足这些要求。
    PV和PVC之间的关系类似于Pod和Node的关系:Pod消耗Node资源,而PVC消耗PV资源。

  • 工作负载弹性伸缩(HPA)
    Horizontal Pod Autoscaling,简称HPA,是Kubernetes中实现POD水平自动伸缩的功能。HPA允许Kubernetes集群根据CPU使用率、内存使用率或其他选择的指标自动增加或减少 Pod 的数量。您可以设置目标指标的阈值,HPA会根据这些阈值自动调整Pod的数量,以保持应用的性能。
    关于工作负载弹性伸缩的更多操作请参见创建HPA策略

  • 节点弹性伸缩(Cluster Autoscale)
    Kubernetes集群中的节点弹性伸缩是根据集群负载动态调整节点数量的功能。当业务负载增加时,自动添加新节点以扩展资源;负载降低时,自动移除多余节点以节省成本。它可以结合集群的资源使用情况(如 CPU、内存利用率)和预设规则,实现节点的自动增减,确保集群资源与业务需求相匹配,提升资源利用效率和集群的灵活性。
    关于节点弹性伸缩的更多操作请参见创建节点弹性策略

  • 亲和性与反亲和性
    在应用没有容器化之前,原先一个虚机上会装多个组件,进程间会有通信。但在做容器化拆分的时候,往往直接按进程拆分容器,比如业务进程一个容器,监控日志处理或者本地数据放在另一个容器,并且有独立的生命周期。这时如果分布在网络中两个较远的点,请求经过多次转发,性能会很差。

    • 亲和性:可以实现就近部署,增强网络能力实现通信上的就近路由,减少网络的损耗。如:应用A与应用B两个应用频繁交互,所以有必要利用亲和性让两个应用尽可能地靠近,甚至在一个节点上,以减少因网络通信而带来的性能损耗。

    • 反亲和性:主要是出于高可靠性考虑,尽量分散实例,某个节点故障的时候,对应用的影响只是N分之一或者只是一个实例。如:当应用采用多副本部署时,有必要采用反亲和性让各个应用实例打散分布在各个节点上,以提高可用性。

关于亲和性与反亲和性的更多操作请参见工作负载调度策略概述

  • 资源配额(Resource Quota)
    允许管理员为命名空间设置资源使用总和的限制,例如CPU、内存、磁盘空间和网络带宽等。

  • 资源限制(Limit Range)
    默认情况下,K8s中所有容器都没有任何CPU和内存限制。LimitRange用来给命名空间中的对象(如Pod等)增加资源限制。
    LimitRange对象提供的限制能够实现以下能力:

    • 在一个命名空间中对每个Pod或容器的最小/最大资源使用量进行限制。

    • 在一个命名空间中对每个PersistentVolumeClaim能申请的最小/最大存储空间进行限制。

    • 在一个命名空间中对一种资源的申请值和限制值的比值进行控制。

    • 设置一个命名空间中对计算资源的默认申请/限制值,并且自动在运行时注入到多个容器中。

  • 环境变量
    环境变量是指容器运行环境中设定的一个变量,您可以在创建容器模板时设定不超过30个的环境变量。环境变量可以在工作负载部署后修改,为工作负载提供了极大的灵活性。
    在CCE中设置环境变量与Dockerfile中的“ENV”效果相同。

  • 模板(Chart)
    Kubernetes集群可以通过Helm实现软件包管理,这里的Kubernetes软件包被称为模板(Chart)。Helm对于Kubernetes的关系类似于在Ubuntu系统中使用的apt命令,或是在CentOS系统中使用的yum命令,它能够快速查找、下载和安装模板(Chart)。
    模板(Chart)是一种Helm的打包格式,它只是描述了一组相关的集群资源定义,而不是真正的容器镜像包。模板中仅仅包含了用于部署Kubernetes应用的一系列YAML文件,您可以在Helm模板中自定义应用程序的一些参数设置。在模板的实际安装过程中,Helm会根据模板中的YAML文件定义在集群中部署资源,相关的容器镜像并不会包含在模板包中,而是依旧从YAML中定义好的镜像仓库中进行拉取。
    对于应用开发者而言,需要将容器镜像包发布到镜像仓库,并通过Helm的模板将安装应用时的依赖关系统一打包,预置一些关键参数,来降低应用的部署难度。
    对于应用使用者而言,可以使用Helm查找模板(Chart)包并支持调整自定义参数。Helm会根据模板包中的YAML文件直接在集群中安装应用程序及其依赖,应用使用者不用编写复杂的应用部署文件,即可以实现简单的应用查找、安装、升级、回滚、卸载。
    关于模板的更多操作请参见模板概述

  • API Server
    API Server(即kube-apiserver组件)是Kubernetes集群的核心组件之一,是整个Kubernetes系统的统一入口。API Server负责处理所有来自客户端的API请求,供用户、集群中的不同部分和集群外部组件相互通信,所有对集群资源(如Pod、Service、Deployment等)的操作都必须通过API Server完成。
    API Server的关键特性和功能如下:

    • Kubernetes API接口暴露:API Server提供了一个RESTful API,用于管理和操作Kubernetes资源,如Pod、Service、Deployment等。
    • 认证与授权:
      • 认证(Authentication):验证请求者的身份(如通过 Token、客户端证书、用户名密码等)。
      • 授权(Authorization):检查已认证用户是否有权执行请求的操作(如通过 RBAC、ABAC 等策略)。
    • 准入控制(Admission Control):在资源创建/更新/删除前,通过准入控制器(Admission Controllers)对请求进行校验或修改。
    • API版本管理:支持多版本API(如v1、apps/v1等),方便功能迭代和兼容性维护。
    • 与其他组件的交互:
      • etcd:读写etcd以持久化集群状态。
      • kube-controller-manager:通过API监听资源变化,执行控制逻辑(如节点控制器、 replication 控制器)。
      • kube-scheduler:通过API获取待调度Pod,更新调度结果。
      • kubelet:向API上报节点和Pod状态,接收Pod配置并执行。
      • kubectl:用户通过kubectl调用API操作集群。

更多关于API Server的配置说明,请参见kube-apiserver

  • 调度、抢占和驱逐
    在 Kubernetes 中,调度(scheduling)指的是确保 Pod 匹配到合适的节点, 以便 kubelet 能够运行它们。 抢占(Preemption)指的是终止低优先级的 Pod 以便高优先级的 Pod 可以调度到 Node 上的过程。 驱逐(Eviction)是在资源匮乏的节点上,主动让一个或多个 Pod 失效的过程。
    更多关于调度、抢占和驱逐的配置说明,请参见 Kubernetes 文档/概念/调度、抢占和驱逐

二、容器与Kubernetes

1.容器

  1. 容器与Docker

容器技术起源于Linux,是一种内核虚拟化技术,提供轻量级的虚拟化,以便隔离进程和资源。尽管容器技术已经出现很久,却是随着Docker的出现而变得广为人知。Docker是第一个使容器能在不同机器之间移植的系统。它不仅简化了打包应用的流程,也简化了打包应用的库和依赖,甚至整个操作系统的文件系统能被打包成一个简单的可移植的包,这个包可以被用来在任何其他运行Docker的机器上使用。

容器和虚拟机具有相似的资源隔离和分配方式,容器虚拟化了操作系统而不是硬件,更加便携和高效。

图1 容器 vs 虚拟机
容器 vs 虚拟机

相比于使用虚拟机,容器有如下优点:

  • 更高效地利用系统资源
    由于容器不需要进行硬件虚拟以及运行完整操作系统等额外开销,容器对系统资源的利用率更高。无论是应用执行速度、内存损耗或者文件存储速度,都要比传统虚拟机技术更高效。因此,相比虚拟机技术,一个相同配置的主机,往往可以运行更多数量的应用。

  • 更快速的启动时间
    传统的虚拟机技术启动应用服务往往需要数分钟,而Docker容器应用,由于直接运行于宿主内核,无需启动完整的操作系统,因此可以做到秒级、甚至毫秒级的启动时间,大大节约了开发、测试、部署的时间。

  • 一致的运行环境
    开发过程中一个常见的问题是环境一致性问题。由于开发环境、测试环境、生产环境不一致,导致有些问题并未在开发过程中被发现。而Docker的镜像提供了除内核外完整的运行时环境,确保了应用运行环境一致性。

  • 更轻松的迁移
    由于Docker确保了执行环境的一致性,使得应用的迁移更加容易。Docker可以在很多平台上运行,无论是物理机、虚拟机,其运行结果是一致的。因此可以很轻易地将在一个平台上运行的应用,迁移到另一个平台上,而不用担心运行环境的变化导致应用无法正常运行的情况。

  • 更轻松的维护和扩展
    Docker使用的分层存储以及镜像的技术,使得应用重复部分的复用更为容易,也使得应用的维护更新更加简单,基于基础镜像进一步扩展镜像也变得非常简单。此外,Docker团队同各个开源项目团队一起维护了大批高质量的官方镜像,既可以直接在生产环境使用,又可以作为基础进一步定制,大大地降低了应用服务的镜像制作成本。

  1. Docker容器典型使用流程

Docker容器有如下三个主要概念:

  • 镜像:Docker镜像里包含了已打包的应用程序及其所依赖的环境。它包含应用程序可用的文件系统和其他元数据,如镜像运行时的可执行文件路径。
  • 镜像仓库:Docker镜像仓库用于存放Docker镜像,以及促进不同人和不同电脑之间共享这些镜像。当编译镜像时,要么可以在编译它的电脑上运行,要么可以先上传镜像到一个镜像仓库,然后下载到另外一台电脑上并运行它。某些仓库是公开的,允许所有人从中拉取镜像,同时也有一些是私有的,仅部分人和机器可接入。
  • 容器:Docker容器通常是一个Linux容器,它基于Docker镜像被创建。一个运行中的容器是一个运行在Docker主机上的进程,但它和主机,以及所有运行在主机上的其他进程都是隔离的。这个进程也是资源受限的,意味着它只能访问和使用分配给它的资源(CPU、内存等)。

典型的使用流程如图2所示:

图2 Docker容器典型使用流程

Docker容器典型使用流程

  • 首先开发者在开发环境机器上开发应用并制作镜像。
    Docker执行命令,构建镜像并存储在机器上。

  • 开发者发送上传镜像命令。
    Docker收到命令后,将本地镜像上传到镜像仓库。

  • 开发者向生产环境机器发送运行镜像命令。
    生产环境机器收到命令后,Docker会从镜像仓库拉取镜像到机器上,然后基于镜像运行容器。

  1. 使用示例

下面使用Docker将基于Nginx镜像打包一个容器镜像,并基于容器镜像运行应用,然后推送到容器镜像仓库。

(1) 安装Docker

Docker几乎支持在所有操作系统上安装,用户可以根据需要选择要安装的Docker版本。
以“CentOS 7.5 64bit(40GiB)”操作系统为例,使用华为云镜像快速安装Docker。

①执行以下命令,添加yum源。

yum install epel-release -y
yum clean all

②执行以下命令,安装需要的软件包。

yum install -y yum-utils device-mapper-persistent-data lvm2

③执行以下命令,设置Docker yum源。

yum-config-manager --add-repo https://mirrors.huaweicloud.com/docker-ce/linux/centos/docker-ce.repo
sed -i 's+download.docker.com+mirrors.huaweicloud.com/docker-ce+' /etc/yum.repos.d/docker-ce.repo

④执行以下命令,查看可用的Docker版本。

yum list docker-ce --showduplicates | sort -r

回显结果如下:

Loading mirror speeds from cached hostfile
Loaded plugins: fastestmirror
docker-ce.x86_64            3:26.1.4-1.el7              docker-ce-stable
docker-ce.x86_64            3:26.1.3-1.el7              docker-ce-stable
docker-ce.x86_64            3:26.1.2-1.el7              docker-ce-stable
...

⑤执行以下命令,安装指定版本的Docker。建议安装的Docker版本在18.06.0(包含)至24.0.9(包含)之间,以便后续设置镜像加速器,关于镜像加速器的详细说明以及适用区域请参见设置镜像加速器

sudo yum install docker-ce-24.0.9 docker-ce-cli-24.0.9 containerd.io

⑥执行以下命令,启动Docker服务。

systemctl enable docker  # 设置Docker服务在系统启动时自动启动
systemctl start docker   # 启动Docker服务

⑦检查安装结果。

docker --version

回显结果如下:

Docker version 24.0.9, build 2936816

(2) Docker打包镜像

Docker提供了一种便捷的描述应用打包的方式,叫做Dockerfile。通过Dockerfile定制一个简单的Nginx镜像。

①通过设置镜像加速器,可以对部分常用的开源镜像下载进行加速,帮助解决由于运营商网络原因导致从第三方镜像仓库(如 Docker Hub)拉取镜像时出现下载慢甚至失败的问题。

②在mynginx路径下,创建一个名为Dockerfile的文件。

mkdir mynginx
cd mynginx
touch Dockerfile

③执行以下命令,编辑Dockerfile文件。

vim Dockerfile

增加文件内容如下:

# 使用Nginx镜像作为基础镜像
FROM nginx:latest 

# 用"hello world"覆盖index.html原有内容
RUN echo "hello world" > /usr/share/nginx/html/index.html

# 允许外界访问容器的80端口
EXPOSE 80

执行以下命令,打包镜像。

docker build -t hello .

其中-t表示给镜像加一个标签,也就是给镜像取名,这里镜像名为hello。结尾的符号. 表示在当前目录下执行该打包命令。

⑤执行以下命令,查看镜像是否创建成功。

docker images

回显结果如下,则说明hello镜像已经创建成功。

REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
hello               latest              1ff61881be30        10 seconds ago      236MB

(3) 把镜像推送到镜像仓库

①登录SWR控制台,在左侧选择“我的镜像”,然后单击右侧“客户端上传镜像”,在弹出的窗口中单击“生成临时登录指令”,然后复制该指令在本地机器上执行,登录到SWR镜像仓库。

②上传镜像前需要给镜像取一个完整的名称,如下所示:

docker tag hello swr.cn-east-3.myhuaweicloud.com/container/hello:v1

这里swr.cn-east-3.myhuaweicloud.com是仓库地址,每个区域的地址不同,v1则是hello镜像分配的版本号。

  • swr.cn-east-3.myhuaweicloud.com是仓库地址,每个区域的地址不同。
  • container是组织名,组织一般在SWR中创建,如果没有创建则首次上传的时候会自动创建,组织名在单个区域内全局唯一,需要选择合适的组织名称。
  • v1则是hello镜像分配的版本号。

③执行以下命令,将镜像上传至SWR。

docker push swr.cn-east-3.myhuaweicloud.com/container/hello:v1

④执行以下命令,即可拉取(下载)该镜像。

docker pull swr.cn-east-3.myhuaweicloud.com/container/hello:v1

2.Kubernetes

  1. Kubernetes是什么

Kubernetes是一个很容易地部署和管理容器化的应用软件系统,使用Kubernetes能够方便对容器进行调度和编排。

对应用开发者而言,可以把Kubernetes看成一个集群操作系统。Kubernetes提供服务发现、伸缩、负载均衡、自愈甚至选举等功能,让开发者从基础设施相关配置等解脱出来。

Kubernetes可以把大量的服务器看做一台巨大的服务器,在一台大服务器上面运行应用程序。无论Kubernetes的集群有多少台服务器,在Kubernetes上部署应用程序的方法永远一样。

图1 在Kubernetes集群上运行应用程序
在Kubernetes集群上运行应用程序

  1. Kubernetes集群架构

Kubernetes集群包含控制节点和工作节点,应用部署在工作节点上,且可以通过配置选择应用部署在某些特定的节点上。
Kubernetes集群的架构如下所示:

图2 Kubernetes集群架构
Kubernetes集群架构

(1) 控制节点

控制节点是集群的控制节点,由API Server、Scheduler、Controller Manager和ETCD四个组件构成。

  • API Server:各组件互相通讯的中转站,接受外部请求,并将信息写到ETCD中。
  • Controller Manager:执行集群级功能,例如复制组件,跟踪工作节点,处理节点故障等等。
  • Scheduler:负责应用调度的组件,根据各种条件(如可用的资源、节点的亲和性等)将容器调度到Node上运行。
  • ETCD:一个分布式数据存储组件,负责存储集群的配置信息。

在生产环境中,为了保障集群的高可用,通常会部署多个控制节点,如CCE的集群高可用模式就是3个控制节点。

(2) 工作节点

工作节点是集群的计算节点,即运行容器化应用的节点。

  • kubelet:kubelet主要负责同Container Runtime打交道,并与API Server交互,管理节点上的容器。
  • kube-proxy:应用组件间的访问代理,解决节点上应用的访问问题。
  • Container Runtime:容器运行时,如Docker,最主要的功能是下载镜像和运行容器。
  1. Kubernetes的扩展性

Kubernetes开放了容器运行时接口(CRI)、容器网络接口(CNI)和容器存储接口(CSI),这些接口让Kubernetes的扩展性变得最大化,而Kubernetes本身则专注于容器调度。

  • CRI(Container Runtime Interface):容器运行时接口,提供计算资源,CRI隔离了各个容器引擎之间的差异,而通过统一的接口与各个容器引擎之间进行互动。
  • CNI(Container Network Interface):容器网络接口,提供网络资源,通过CNI接口,Kubernetes可以支持不同网络环境。例如CCE就是开发的CNI插件支持Kubernetes集群运行在VPC网络中。
  • CSI(Container Storage Interface):容器存储接口,提供存储资源,通过CSI接口,Kubernetes可以支持各种类型的存储。例如CCE就可以方便地对接块存储(EVS)、文件存储(SFS)和对象存储(OBS)。
  1. Kubernetes中的基本对象

图3 Kubernetes基本对象
Kubernetes基本对象

  • Pod
    Pod是Kubernetes创建或部署的最小单位。一个Pod封装一个或多个容器(container)、存储资源(volume)、一个独立的网络IP以及管理控制容器运行方式的策略选项。
  • Deployment
    Deployment是对Pod的服务化封装。一个Deployment可以包含一个或多个Pod,每个Pod的角色相同,所以系统会自动为Deployment的多个Pod分发请求。
  • StatefulSet
    StatefulSet是用来管理有状态应用的对象。和Deployment相同的是,StatefulSet管理了基于相同容器定义的一组Pod。但和Deployment不同的是,StatefulSet为它们的每个Pod维护了一个固定的ID。这些Pod是基于相同的声明来创建的,但是不能相互替换,无论怎么调度,每个Pod都有一个永久不变的ID。
  • Job
    Job是用来控制批处理型任务的对象。批处理业务与长期伺服业务(Deployment)的主要区别是批处理业务的运行有头有尾,而长期伺服业务在用户不停止的情况下永远运行。Job管理的Pod根据用户的设置把任务成功完成就自动退出(Pod自动删除)。
  • CronJob
    CronJob是基于时间控制的Job,类似于Linux系统的crontab,在指定的时间周期运行指定的任务。
  • DaemonSet
    DaemonSet是这样一种对象(守护进程),它在集群的每个节点上运行一个Pod,且保证只有一个Pod,这非常适合一些系统层面的应用,例如日志收集、资源监控等,这类应用需要每个节点都运行,且不需要太多实例,一个比较好的例子就是Kubernetes的kube-proxy。
  • Service
    Service是用来解决Pod访问问题的。Service有一个固定IP地址,Service将访问流量转发给Pod,而且Service可以给这些Pod做负载均衡。
  • Ingress
    Service是基于四层TCP和UDP协议转发的,Ingress可以基于七层的HTTP和HTTPS协议转发,可以通过域名和路径做到更细粒度的划分。
  • ConfigMap
    ConfigMap是一种用于存储应用所需配置信息的资源类型,用于保存配置数据的键值对。通过ConfigMap可以方便地做到配置解耦,使得不同环境有不同的配置。
  • Secret
    Secret是一种加密存储的资源对象,您可以将认证信息、证书、私钥等保存在Secret中,而不需要把这些敏感数据暴露到镜像或者Pod定义中,从而更加安全和灵活。
  • PersistentVolume(PV)
    PV指持久化数据存储卷,主要定义的是一个持久化存储在宿主机上的目录,比如一个NFS的挂载目录。
  • PersistentVolumeClaim(PVC)
    Kubernetes提供PVC专门用于持久化存储的申请,PVC可以让您无需关心底层存储资源如何创建、释放等动作,而只需要申明您需要何种类型的存储资源、多大的存储空间。
  1. 搭建Kubernetes集群

Kubernetes网站上有多种搭建Kubernetes集群的方法,例如minikube、kubeadm等。

  1. Kubernetes对象的描述

kubernetes中资源可以使用YAML描述(如果您对YAML格式不了解,可以参考YAML语法),也可以使用JSON。其内容可以分为如下四个部分:

  • typeMeta:对象类型的元信息,声明对象使用哪个API版本,哪个类型的对象。
  • objectMeta:对象的元信息,包括对象名称、使用的标签等。
  • spec:对象的期望状态,例如对象使用什么镜像、有多少副本等。
  • status:对象的实际状态,只能在对象创建后看到,创建对象时无需指定。

图4 YAML描述文件
YAML描述文件

  1. 在Kubernetes上运行应用

(1) 将图4中的内容去除status存为一个名为nginx-deployment.yaml的文件,如下所示:

apiVersion: apps/v1
kind: Deployment
metadata:
  name:  nginx
  labels:
    app:  nginx
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 3
  template:
    metadata:
      labels:
        app:  nginx
    spec:
      containers:
      - name:  nginx
        image:  nginx:alpine
        resources:
          requests:
            cpu: 100m
            memory: 200Mi
          limits:
            cpu: 100m
            memory: 200Mi
      imagePullSecrets:
      - name: default-secret

(2) 使用kubectl连接集群后,执行如下命令:

# kubectl create -f nginx-deployment.yaml 
deployment.apps/nginx created

(3) 命令执行后,Kubernetes集群中会创建3个Pod,使用如下命令可以查询到Deployment和Pod:

# kubectl get deploy
NAME    READY   UP-TO-DATE   AVAILABLE   AGE
nginx   3/3     3            3           9s

# kubectl get pods
NAME                     READY   STATUS    RESTARTS   AGE
nginx-685898579b-qrt4d   1/1     Running   0          15s
nginx-685898579b-t9zd2   1/1     Running   0          15s
nginx-685898579b-w59jn   1/1     Running   0          15s

到此为止,了解了容器和Docker、Kubernetes集群、Kubernetes基本概念,并通过一个示例了解kubectl的最基本使用,后续将深入介绍Kubernetes对象的概念以及使用方法,并介绍对象之间的关系。

3.使用Kubectl命令操作集群

1. kubectl

kubectl是Kubernetes集群的命令行工具,您可以将kubectl安装在任意一台机器上,通过kubectl命令操作Kubernetes集群。

CCE集群的kubectl安装请参见通过kubectl连接集群。连接后您可以执行kubectl cluster-info查看集群的信息,如下所示。

# kubectl cluster-info
Kubernetes master is running at https://*.*.*.*:5443
CoreDNS is running at https://*.*.*.*:5443/api/v1/namespaces/kube-system/services/coredns:dns/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

执行kubectl get nodes可以查看集群中的节点信息。

# kubectl get nodes
NAME            STATUS    ROLES     AGE       VERSION
192.168.0.153   Ready     <none>    7m        v1.15.6-r1-20.3.0.2.B001-15.30.2
192.168.0.207   Ready     <none>    7m        v1.15.6-r1-20.3.0.2.B001-15.30.2
192.168.0.221   Ready     <none>    7m        v1.15.6-r1-20.3.0.2.B001-15.30.2

更多kubectl命令请参考kubectl 快速参考

2. 基础命令

  1. get

get命令用于获取集群的一个或一些资源的详细信息。

该命令可以列出集群所有资源的详细信息,包括集群节点、运行的Pod、Deployment、Service等。

集群中可以创建多个命名空间,未指定命名空间的情况下,默认指定为–namespace=default,即查询default命名空间下的资源。

例如:

获取所有Pod的详细信息:

kubectl get pod -o wide

获取所有命名空间下的运行的所有Pod:

kubectl get pod --all-namespaces

获取所有命名空间下的运行的所有Pod的标签:

kubectl get pod --show-labels

获取该节点的所有命名空间:

kubectl get namespace

类似可以使用“kubectl get svc”,“kubectl get nodes”,“kubectl get deploy”等获取其他资源的信息。

以YAML格式输出Pod的详细信息:

kubectl get pod <podname> -o yaml

以JSON格式输出Pod的详细信息:

kubectl get pod <podname> -o json
kubectl get pod rc-nginx-2-btv4j -o=custom-columns=LABELS:.metadata.labels.app

其中LABELS为显示的列标题,可以自己设置,“.metadata.labels.app”为查询的数据需要按照之前的YAML或JSON获取。

  1. create

create命令用于根据文件或输入创建集群资源。

如果已经定义了相应资源的YAML或JSON文件,直接使用以下命令即可创建文件内定义的资源。

kubectl create -f <filename>
  1. expose

expose将一个资源包括Pod、Deployment等公开为一个新的Service。

kubectl expose deployment <deployname> --port=81 --type=NodePort --target-port=80 --node-port=31000 --name=<service-name>

以上命令会给Deployment创建一个NodePort类型服务,–port为服务端口(用于集群访问),–type为服务类型,–target-port为服务对应后端Pod的端口,–node-port表示NodePort端口(用于集群外访问)。其中–node-port为可选参数,未指定时,集群将在30000~32767范围内随机分配。

  1. run

创建单Pod或Deployment的快捷命令,适合测试环境。

例如:

kubectl run <deployname> --image=nginx:latest

同时,可以在创建Pod或Deployment时指定运行的命令:

kubectl run <deployname> --image=busybox --command -- ping example.com
  1. set

在对象上设置特定功能。

例如:

滚动更新一个Deployment的容器镜像改为1.0版本:

kubectl set image deployment/<deployname> <containername>=<containername>:1.0
  1. edit

edit提供了另一种更新资源的操作。

例如:

使用edit直接更新Pod的命令为:

kubectl edit pod po-nginx-btv4j

上面命令的效果等效于:

kubectl get pod po-nginx-btv4j -o yaml >> /tmp/nginx-tmp.yaml
vim /tmp/nginx-tmp.yaml
# do some changes here 
kubectl replace -f /tmp/nginx-tmp.yaml
  1. explain

查看文档或参考资料。

例如:

查看Pod的相关文档:

kubectl explain pod
  1. delete

根据资源名或标签删除资源。

例如:

立刻删除该Pod:

kubectl delete pod <podname> --now 
kubectl delete -f nginx.yaml
kubectl delete deployment <deployname>

3. 部署命令

  1. rollout

管理资源的发布。

例如:

查看指定资源的部署状态:

kubectl rollout status deployment/<deployname>

查看指定资源的发布历史:

kubectl rollout history deployment/<deployname>

回滚指定资源,默认回滚至上一个版本:

kubectl rollout undo deployment/test-nginx
  1. scale

scale用于程序在负载加重或缩小时将副本进行扩容或缩小。

kubectl scale deployment <deployname> --replicas=<newnumber>
  1. autoscale

autoscale命令提供了自动根据工作负载的CPU利用率对其副本进行扩缩的功能。autoscale命令会给工作负载(Deployment、ReplicaSet、StatefulSet)指定一个副本数的范围,在实际运行中根据所有Pod的平均CPU利用率自动在指定的范围内对Pod进行扩容或缩容。如果未指定目标利用率或设置为负数,将使用默认的自动扩缩策略。

kubectl autoscale deployment <deployname> --min=<minnumber> --max=<maxnumber> --cpu-percent=<cpu>

4. 集群管理命令

  1. cordon、drain、uncordon

有时候会遇到这样一个场景,一个节点需要升级,但是在该节点上又有许多运行的Pod,或者该节点已经瘫痪,需要保证业务功能的完善,则需要使用这组命令将该节点上运行的Pod调度到其他节点上。使用步骤如下:

(1) 使用cordon命令将一个节点标记为不可调度。这意味着新的Pod将不会被调度到该节点上。

    kubectl cordon <nodename>

CCE中默认为节点私网IP。

(2) 使用drain命令,驱逐该节点上的Pod,将运行在该节点上运行的Pod平滑的搬迁到其他节点上。

kubectl drain <nodename> --ignore-daemonsets --delete-emptydir-data

–ignore-daemonsets表示忽略DaemonSet所控制的Pod,–delete-emptydir-data表示即使存在使用emptyDir(腾空节点时将删除本地数据)的Pod也会继续腾空节点。

(3) 对该节点进行一些节点维护的操作,如重置节点等。

(4) 节点维护完后,使用uncordon命令解锁该节点,使其重新变得可调度。

kubectl uncordon <nodename>
  1. cluster-info

查看在集群中运行的插件:

kubectl cluster-info

查看详细信息:

kubectl cluster-info dump
  1. ‌top*

显示资源(CPU/Memory/Storage)使用,该命令需要集群中的Metrics Server正常运行。

  1. taint*`

修改一个或多个节点上的污点。

  1. certificate*

修改证书资源。

5. 故障诊断和调试命令

  1. describe

describe命令类似于get,同样用于获取资源的相关信息。不同的是,get获得的是定义该资源的详细信息,而describe获得的是资源在集群内相关的状态信息。describe命令同get类似,但是describe命令不支持-o选项,对于同一类型的资源,describe命令输出的信息格式,内容域相同。

kubectl describe pod <podname>

如果发现是查询某个资源的信息,使用get命令能够获取更加详尽的信息。但是如果想要查询某个资源的状态,如某个Pod并不是在running状态,这时需要获取更详尽的状态信息时,就应该使用describe命令。

  1. logs

logs命令用于显示Pod运行中,容器内程序输出到标准输出的内容。如果要获得tail -f的方式,需使用-f选项。

kubectl logs -f <podname>
  1. exec

与Docker的exec用法相似,如果一个Pod中,有多个容器,需要使用-c选项指定容器。

kubectl exec -it <podname> -- bash
kubectl exec -it <podname> -c <containername> -- bash
  1. port-forward*

转发一个或多个本地端口至一个Pod。

例如:

侦听本地端口5000并转发到创建的Pod里的端口6000:

kubectl port-forward deploy/my-deployment 5000:6000
  1. cp

复制文件或目录到容器:

kubectl cp /tmp/foo <podname>:/tmp/bar -c <containername>

将/tmp/foo本地文件复制到远程Pod中特定容器的/tmp/bar下。

  1. auth*

检查授权。

  1. attach*

attach命令效果类似于logs -f,退出查看使用ctrl-c。如果一个Pod中有多个容器,要查看具体的某个容器的输出,需要在Pod名后使用-c containername指定运行的容器。

kubectl attach <podname> -c <containername>

6. 高级命令

  1. replace

replace命令用于对已有资源进行更新、替换。当需要更新资源的一些属性的时候,如果修改副本数量,增加、修改标签,更改镜像版本,修改端口等,都可以直接修改原YAML文件,然后执行replace命令。

kubectl replace -f <filename>

资源名称不能被更新。

  1. apply*

apply命令提供了比patch,edit等更严格的更新资源的方式。通过apply,用户可以将资源的configuration使用source control的方式维护在版本库中。每次有更新时,将配置文件推送server,然后使用kubectl apply将更新应用到资源。Kubernetes会在应用更新前将当前配置文件中的配置同已经应用的配置做比较,并只更新更改的部分,而不会主动更改任何用户未指定的部分。apply命令的使用方式同replace相同,不同的是,apply不会删除原有资源,然后创建新的。apply直接在原有资源的基础上进行更新。同时kubectl apply还会在资源中添加一条注释,标记当前的apply,类似于git操作。

kubectl apply -f <filename>
  1. patch

如果一个容器已经在运行,这时需要对一些容器属性进行修改,又不想删除容器,或不方便通过replace的方式进行更新。Kubernetes还提供了一种在容器运行时,直接对容器进行修改的方式,就是patch命令。 例如已存在一个Pod的标签为app=nginx1,如果需要在运行过程中,将其修改为app=nginx2。

kubectl patch pod <podname> -p '{"metadata":{"labels":{"app":"nginx2"}}}'
  1. convert*

不同的API版本之间转换配置文件。

7. 设置命令

  1. label

更新资源上的标签:

kubectl label pods my-pod new-label=newlabel
  1. annotate

更新资源上的注释:

kubectl annotate pods my-pod icon-url=http://*****
  1. completion

用于实现kubectl工具自动补全。

8. 其他命令

  1. api-versions

打印受支持的API版本:

kubectl api-versions
  1. api-resources

打印支持的API资源:

kubectl api-resources
  1. config*

修改kubeconfig文件:用于访问API,比如配置认证信息。

  1. help

所有命令帮助。

  1. version

打印客户端和服务版本信息。

kubectl version

三、Pod、Label和Namespace

1.Pod:Kubernetes中的最小调度对象

  1. 容器组(Pod)

容器组(Pod)是Kubernetes创建或部署的最小单位。一个Pod封装一个或多个容器(Container)、存储资源(Volume)、一个独立的网络IP以及管理控制容器运行方式的策略选项。

Pod使用主要分为两种方式:

  • Pod中运行一个容器。这是Kubernetes最常见的用法,您可以将Pod视为单个封装的容器,但是Kubernetes是直接管理Pod而不是容器。

  • Pod中运行多个需要耦合在一起工作、需要共享资源的容器。通常这种场景下应用包含一个主容器和几个辅助容器(SideCar Container),如图1所示,例如主容器为一个web服务器,从一个固定目录下对外提供文件服务,而辅助容器周期性的从外部下载文件存到这个固定目录下。

图1 Pod

Pod

实际使用中很少直接创建Pod,而是使用Kubernetes中称为Controller的抽象层来管理Pod实例,例如Deployment和Job。Controller可以创建和管理多个Pod,提供副本管理、滚动升级和自愈能力。通常,Controller会使用Pod Template来创建相应的Pod。

  1. 创建Pod

kubernetes中资源可以使用YAML描述(如果您对YAML格式不了解,可以参考YAML语法),也可以使用JSON,如下示例描述了一个名为nginx的Pod,这个Pod中包含一个名为container-0的容器,使用nginx:alpine镜像,使用的资源为100m CPU、200Mi内存。

apiVersion: v1                      # Kubernetes的API Version
kind: Pod                           # Kubernetes的资源类型
metadata:
  name: nginx                       # Pod的名称
spec:                               # Pod的具体规格(specification)
  containers:
  - image: nginx:alpine             # 使用的镜像为 nginx:alpine
    name: container-0               # 容器的名称
    resources:                      # 申请容器所需的资源
      limits:
        cpu: 100m
        memory: 200Mi
      requests:
        cpu: 100m
        memory: 200Mi
  imagePullSecrets:                 # 拉取镜像使用的证书,在CCE上必须为default-secret
  - name: default-secret

如上面YAML的注释,YAML描述文件主要为如下部分:

  • metadata:一些名称/标签/namespace等信息。
  • spec:Pod实际的配置信息,包括使用什么镜像,volume等。

如果去查询Kubernetes的资源,您会看到还有一个status字段,status描述kubernetes资源的实际状态,创建时不需要配置。这个示例是一个最小集,其他参数定义后面会逐步介绍。

Pod定义好后就可以使用kubectl创建,如果上面YAML文件名称为nginx.yaml,则创建命令如下所示,-f表示使用文件方式创建。

$ kubectl create -f nginx.yaml
pod/nginx created

Pod创建完成后,可以使用kubectl get pods命令查询Pod的状态,如下所示。

$ kubectl get pods
NAME           READY   STATUS    RESTARTS   AGE
nginx          1/1     Running   0          40s

可以看到此处nginx这个Pod的状态为Running,表示正在运行;READY为1/1,表示这个Pod中有1个容器,其中1个容器的状态为Ready。

可以使用kubectl get命令查询具体Pod的配置信息,如下所示,-o yaml表示以YAML格式返回,还可以使用-o json,以JSON格式返回。

$ kubectl get pod nginx -o yaml

您还可以使用kubectl describe命令查看Pod的详情。

$ kubectl describe pod nginx

删除pod时,Kubernetes终止Pod中所有容器。 Kubernetes向进程发送SIGTERM信号并等待一定的秒数(默认为30)让容器正常关闭。如果它没有在这个时间内关闭,Kubernetes会发送一个SIGKILL信号终止该进程。

Pod的停止与删除有多种方法,比如按名称删除,如下所示。

$ kubectl delete po nginx
pod "nginx" deleted

同时删除多个Pod。

$ kubectl delete po pod1 pod2

删除所有Pod。

$ kubectl delete po --all
pod "nginx" deleted

根据Label删除Pod,Label详细内容将会在下一个章节介绍。

$ kubectl delete po -l app=nginx
pod "nginx" deleted
  1. 使用环境变量

环境变量是容器运行环境中设定的一个变量。

环境变量为应用提供极大的灵活性,您可以在应用程序中使用环境变量,在创建容器时为环境变量赋值,容器运行时读取环境变量的值,从而做到灵活的配置,而不是每次都重新编写应用程序制作镜像。

环境变量的使用方法如下所示,配置spec.containers.env字段即可。

apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
    containers:
    - image: nginx:alpine
      name: container-0
      resources:
        limits:
          cpu: 100m
          memory: 200Mi
        requests:
          cpu: 100m
          memory: 200Mi
      env:                            # 环境变量
      - name: env_key
        value: env_value
    imagePullSecrets:
    - name: default-secret

执行如下命令查看容器中的环境变量,可以看到env_key这个环境变量,其值为env_value。

$ kubectl exec -it nginx -- env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=nginx
TERM=xterm
env_key=env_value

环境变量还可以引用ConfigMap和Secret,具体使用方法请参见在环境变量中引用ConfigMap在环境变量中引用Secret

  1. 容器启动命令

启动容器就是启动主进程,但有些时候,启动主进程前,需要一些准备工作。比如MySQL类的数据库,可能需要一些数据库配置、初始化的工作,这些工作要在最终的MySQL服务器运行之前做完。这些操作,可以在制作镜像时通过在Dockerfile文件中设置ENTRYPOINT或CMD来完成,如下所示的Dockerfile中设置了ENTRYPOINT [“top”, “-b”]命令,其将会在容器启动时执行。

FROM ubuntu
ENTRYPOINT ["top", "-b"]

实际使用时,只需配置Pod的containers.command参数,该参数是list类型,第一个参数为执行命令,后面均为命令的参数。

apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - image: nginx:alpine
    name: container-0
    resources:
      limits:
        cpu: 100m
        memory: 200Mi
      requests:
        cpu: 100m
        memory: 200Mi
    command:                     # 启动命令
    - top
    - "-b"
  imagePullSecrets:
   - name: default-secret
  1. 容器的生命周期

Kubernetes提供了容器生命周期钩子,在容器的生命周期的特定阶段执行调用,比如容器在停止前希望执行某项操作,就可以注册相应的钩子函数。目前提供的生命周期钩子函数如下所示。

  • 启动后处理(PostStart):容器启动后触发。
  • 停止前处理(PreStop):容器停止前触发。

实际使用时,只需配置Pod的‌lifecycle.postStart‌或‌lifecycle.preStop‌参数,如下所示。

apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - image: nginx:alpine
    name: container-0
    resources:
      limits:
        cpu: 100m
        memory: 200Mi
      requests:
        cpu: 100m
        memory: 200Mi
    lifecycle:
      postStart:                 # 启动后处理
        exec:
          command:
          - "/postStart.sh"
      preStop:                   # 停止前处理
        exec:
          command:
          - "/preStop.sh"
  imagePullSecrets:
   - name: default-secret

2.Liveness Probe:健康检查机制

  1. 存活探针

Kubernetes提供了自愈的能力,具体就是能感知到容器崩溃,然后能够重启这个容器。但是有时候例如Java程序内存泄漏了,程序无法正常工作,但是JVM进程却是一直运行的,对于这种应用本身业务出了问题的情况,Kubernetes提供了Liveness Probe机制,通过检测容器响应是否正常来决定是否重启,这是一种很好的健康检查机制。

毫无疑问,每个Pod最好都定义Liveness Probe,否则Kubernetes无法感知Pod是否正常运行。

Kubernetes支持如下三种探测机制。

  • HTTP GET:向容器发送HTTP GET请求,如果Probe收到2xx或3xx,说明容器是健康的。
  • TCP Socket:尝试与容器指定端口建立TCP连接,如果连接成功建立,说明容器是健康的。
  • Exec:Probe执行容器中的命令并检查命令退出的状态码,如果状态码为0则说明容器是健康的。

与存活探针对应的还有一个就绪探针(Readiness Probe),将在就绪探针(Readiness Probe)中会详细介绍。

  1. HTTP GET

HTTP GET方式是最常见的探测方法,其具体机制是向容器发送HTTP GET请求,如果Probe收到2xx或3xx,说明容器是健康的,定义方法如下所示。

apiVersion: v1
kind: Pod
metadata:
  name: liveness-http
spec:
  containers:
  - name: liveness
    image: nginx:alpine
    livenessProbe:           # liveness probe
      httpGet:               # HTTP GET定义
        path: /
        port: 80
  imagePullSecrets: 
  - name: default-secret

创建这个Pod。

$ kubectl create -f liveness-http.yaml
pod/liveness-http created

如上,这个Probe往容器的80端口发送HTTP GET请求,如果请求不成功,Kubernetes会重启容器。

查看Pod详情。

$ kubectl describe po liveness-http
Name:               liveness-http
......
Containers:
  liveness:
    ......
    State:          Running
      Started:      Mon, 03 Aug 2020 03:08:55 +0000
    Ready:          True
    Restart Count:  0
    Liveness:       http-get http://:80/ delay=0s timeout=1s period=10s #success=1 #failure=3
    Environment:    <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-vssmw (ro)
......

可以看到Pod当前状态是Running,Restart Count为0,说明没有重启。如果Restart Count不为0,则说明已经重启。

  1. TCP Socket

TCP Socket尝试与容器指定端口建立TCP连接,如果连接成功建立,说明容器是健康的,定义方法如下所示。

apiVersion: v1
kind: Pod
metadata:
  labels:
    test: liveness
  name: liveness-tcp
spec:
  containers:
  - name: liveness
    image: nginx:alpine
    livenessProbe:           # liveness probe
      tcpSocket:
        port: 80
  imagePullSecrets: 
  - name: default-secret
  1. Exec

Exec即执行具体命令,具体机制是Probe执行容器中的命令并检查命令退出的状态码,如果状态码为0则说明健康,定义方法如下所示。

apiVersion: v1
kind: Pod
metadata:
  labels:
    test: liveness
  name: liveness-exec
spec:
  containers:
  - name: liveness
    image: nginx:alpine
    args:
    - /bin/sh
    - -c
    - touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600
    livenessProbe:           # liveness probe
      exec:                  # Exec定义
        command:
        - cat
        - /tmp/healthy
  imagePullSecrets: 
  - name: default-secret

上面定义在容器中执行‌cat /tmp/healthy‌命令,如果成功执行并返回0,则说明容器是健康的。上面定义中,30秒后命令会删除‌/tmp/healthy‌,这会导致Liveness Probe判定Pod处于不健康状态,然后会重启容器。

  1. Liveness Probe高级配置

上面liveness-http的describe命令回显中有如下行。

Liveness: http-get http://:80/ delay=0s timeout=1s period=10s #success=1 #failure=3

这一行表示Liveness Probe的具体参数配置,其含义如下:

  • delay:延迟,delay=0s,表示在容器启动后立即开始探测,没有延迟时间
  • timeout:超时,timeout=1s,表示容器必须在1s内进行响应,否则这次探测记作失败
  • period:周期,period=10s,表示每10s探测一次容器
  • success:成功,#success=1,表示连续1次成功后记作成功
  • failure:失败,#failure=3,表示连续3次失败后会重启容器

以上存活探针表示:容器启动后立即进行探测,如果1s内容器没有给出回应则记作探测失败。每次间隔10s进行一次探测,在探测连续失败3次后重启容器。

这些是创建时默认设置的,您也可以手动配置,如下所示。

apiVersion: v1
kind: Pod
metadata:
  name: liveness-http
spec:
  containers:
  - name: liveness
    image: nginx:alpine
    livenessProbe:
      httpGet:
        path: /
        port: 80
      initialDelaySeconds: 10    # 容器启动后多久开始探测
      timeoutSeconds: 2          # 表示容器必须在2s内做出相应反馈给probe,否则视为探测失败
      periodSeconds: 30          # 探测周期,每30s探测一次
      successThreshold: 1        # 连续探测1次成功表示成功
      failureThreshold: 3        # 连续探测3次失败表示失败

initialDelaySeconds一般要设置大于0,这是由于很多情况下容器虽然启动成功,但应用就绪也需要一定的时间,需要等就绪时间之后才能返回成功,否则就会导致probe经常失败。

另外failureThreshold可以设置多次循环探测,这样在实际应用中健康检查的程序就不需要多次循环,这一点在开发应用时需要注意。

  1. 配置有效的Liveness Probe
  • Liveness Probe应该检查什么

一个好的Liveness Probe应该检查应用内部所有关键部分是否健康,并使用一个专有的URL访问,例如/health,当访问/health 时执行这个功能,然后返回对应结果。这里要注意不能做鉴权,不然probe就会一直失败导致陷入重启的死循环。

另外检查只能限制在应用内部,不能检查依赖外部的部分,例如当前端web server不能连接数据库时,这个就不能看成web server不健康。

  • Liveness Probe必须轻量

Liveness Probe不能占用过多的资源,且不能占用过长的时间,否则所有资源都在做健康检查,这就没有意义了。例如Java应用,就最好用HTTP GET方式,如果用Exec方式,JVM启动就占用了非常多的资源。

3.Label:组织Pod的利器

  1. 为什么需要Label

当资源变得非常多的时候,如何分类管理就非常重要了,Kubernetes提供了一种机制来为资源分类,那就是Label(标签)。Label非常简单,但是却很强大,Kubernetes中几乎所有资源都可以用Label来组织。

Label的具体形式是key-value的标记对,可以在创建资源的时候设置,也可以在后期添加和修改。

以Pod为例,当Pod变得多起来后,就显得杂乱且难以管理,如下图所示。

图1 没有分类组织的Pod

没有分类组织的Pod

如果我们为Pod打上不同标签,那情况就完全不同了,如下图所示。

图2 使用Label组织的Pod

使用Label组织的Pod

  1. 添加Label

Label的形式为key-value形式,使用非常简单,如下,为Pod设置了‌app=nginx‌和‌env=prod‌两个Label。

apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:                     # 为Pod设置两个Label    
    app: nginx    
    env: prod
spec:
  containers:
  - image: nginx:alpine
    name: container-0
    resources:
      limits:
        cpu: 100m
        memory: 200Mi
      requests:
        cpu: 100m
        memory: 200Mi
  imagePullSecrets:
  - name: default-secret

Pod有了Label后,在查询Pod的时候带上‌--show-labels‌就可以看到Pod的Label。

$ kubectl get pod --show-labels
NAME              READY   STATUS    RESTARTS   AGE   LABELS
nginx             1/1     Running   0          50s   app=nginx,env=prod

还可以使用-L只查询某些固定的Label。

$ kubectl get pod -L app,env 
NAME              READY   STATUS    RESTARTS   AGE   APP     ENV
nginx             1/1     Running   0          1m    nginx   prod

对已存在的Pod,可以直接使用kubectl label命令直接添加Label。

$ kubectl label pod nginx creation_method=manual
pod/nginx labeled

$ kubectl get pod --show-labels
NAME              READY   STATUS    RESTARTS   AGE   LABELS
nginx             1/1     Running   0          50s   app=nginx, creation_method=manual,env=prod
  1. 修改Label

对于已存在的Label,如果要修改的话,需要在命令中带上‌--overwrite‌,如下所示。

$ kubectl label pod nginx env=debug --overwrite
pod/nginx labeled

$ kubectl get pod --show-labels
NAME              READY   STATUS    RESTARTS   AGE   LABELS
nginx             1/1     Running   0          50s   app=nginx,creation_method=manual,env=debug

4.Namespace:资源分组

  1. 为什么需要Namespace

Label虽然好,但只用Label的话,那Label会非常多,有时候会有重叠,而且每次查询之类的动作都带一堆Label非常不方便。

Kubernetes提供了Namespace来做资源组织和划分,使用多Namespace可以将包含很多组件的系统分成不同的组。Namespace也可以用来做多租户划分,这样多个团队可以共用一个集群,使用的资源用Namespace划分开。

不同的Namespace下的资源名称可以相同,Kubernetes中大部分资源可以用Namespace划分,不过有些资源不行,例如Node、PV等,它们属于全局资源,不属于某一个Namespace,后面会逐步接触到。

通过如下命令可以查询到当前集群下的Namespace。

$ kubectl get ns
NAME               STATUS   AGE
default            Active   36m
kube-node-lease Active   36m
kube-public        Active   36m
kube-system        Active   36m

到目前为止,我们都是在default Namespace下操作,当使用kubectl get而不指定Namespace时,默认为default Namespace。

看下kube-system下面有些什么东西。

$ kubectl get po --namespace=kube-system
NAME                                      READY   STATUS    RESTARTS   AGE
coredns-7689f8bdf-295rk                   1/1     Running   0          9m11s
coredns-7689f8bdf-h7n68                   1/1     Running   0          11m
everest-csi-controller-6d796fb9c5-v22df   2/2     Running   0          9m11s
everest-csi-driver-snzrr                  1/1     Running   0          12m
everest-csi-driver-ttj28                  1/1     Running   0          12m
everest-csi-driver-wtrk6                  1/1     Running   0          12m
icagent-2kz8g                             1/1     Running   0          12m
icagent-hjz4h                             1/1     Running   0          12m
icagent-m4bbl                             1/1     Running   0          12m

可以看到kube-system有很多Pod,其中coredns是用于做服务发现、everest-csi是用于对接存储服务、icagent是用于对接监控系统。

这些通用的、必须的应用放在kube-system这个命名空间中,能够做到与其他Pod之间隔离,在其他命名空间中不会看到kube-system这个命名空间中的东西,不会造成影响。

  1. 创建Namespace

使用如下方式定义Namespace。

apiVersion: v1 
kind: Namespace 
metadata: 
  name: custom-namespace 

使用kubectl命令创建。

$ kubectl create -f custom-namespace.yaml
namespace/custom-namespace created 

您还可以使用kubectl create namespace命令创建。

$ kubectl create namespace custom-namespace 
namespace/custom-namespace created 

在指定Namespace下创建资源。

$ kubectl create -f nginx.yaml -n custom-namespace 
pod/nginx created 

这样在custom-namespace下,就创建了一个名为nginx的Pod。

  1. Namespace的隔离说明

Namespace只能做到组织上划分,对运行的对象来说,它不能做到真正的隔离。举例来说,如果两个Namespace下的Pod知道对方的IP,而Kubernetes依赖的底层网络没有提供Namespace之间的网络隔离的话,那这两个Pod就可以互相访问。

四、Pod的编排与调度

1.无状态负载(Deployment)

  1. 无状态负载(Deployment)

Pod是Kubernetes创建或部署的最小单位,但是Pod是被设计为相对短暂的一次性实体,Pod可以被驱逐(当节点资源不足时)、随着集群的节点崩溃而消失。Kubernetes提供了Controller(控制器)来管理Pod,Controller可以创建和管理多个Pod,提供副本管理、滚动升级和自愈能力,其中最为常用的就是Deployment。

图1 Deployment
Deployment

一个Deployment可以包含一个或多个Pod副本,每个Pod副本的角色相同,所以系统会自动为Deployment的多个Pod副本分发请求。

Deployment集成了上线部署、滚动升级、创建副本、恢复上线的功能,在某种程度上,Deployment实现无人值守的上线,大大降低了上线过程的复杂性和操作风险。

  1. 创建Deployment

以下示例为创建一个名为nginx的Deployment负载,使用nginx:latest镜像创建两个Pod,每个Pod占用100m CPU、200Mi内存。

apiVersion: apps/v1      # 注意这里与Pod的区别,Deployment是apps/v1而不是v1
kind: Deployment         # 资源类型为Deployment
metadata:
  name: nginx            # Deployment的名称
spec:
  replicas: 2            # Pod的数量,Deployment会确保一直有2个Pod运行         
  selector:              # Label Selector
    matchLabels:
      app: nginx
  template:              # Pod的定义,用于创建Pod,也称为Pod template
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx:latest
        name: container-0
        resources:
          limits:
            cpu: 100m
            memory: 200Mi
          requests:
            cpu: 100m
            memory: 200Mi
      imagePullSecrets:
      - name: default-secret

从这个定义中可以看到Deployment的名称为nginx,spec.replicas定义了Pod的数量,即这个Deployment控制2个Pod;spec.selector是Label Selector(标签选择器),表示这个Deployment会选择Label为app=nginx的Pod;spec.template是Pod的定义,内容与Pod中的定义完全一致。

将上面Deployment的定义保存到deployment.yaml文件中,使用kubectl创建这个Deployment。

使用kubectl get查看Deployment和Pod,可以看到READY值为2/2,前一个2表示当前有2个Pod运行,后一个2表示期望有2个Pod,AVAILABLE为2表示有2个Pod是可用的。

$ kubectl create -f deployment.yaml
deployment.apps/nginx created

$ kubectl get deploy
NAME           READY     UP-TO-DATE   AVAILABLE   AGE
nginx          2/2       2            2           4m5s
  1. Deployment如何控制Pod

继续查询Pod,如下所示。

$ kubectl get pods
NAME                     READY     STATUS    RESTARTS   AGE
nginx-7f98958cdf-tdmqk   1/1       Running   0          13s
nginx-7f98958cdf-txckx   1/1       Running   0          13s

如果删掉一个Pod,您会发现立马会有一个新的Pod被创建出来,如下所示,这就是前面所说的Deployment会确保有2个Pod在运行,如果删掉一个,Deployment会重新创建一个,如果某个Pod故障或有其他问题,Deployment会自动拉起这个Pod。

$ kubectl delete pod nginx-7f98958cdf-txckx

$ kubectl get pods
NAME                     READY     STATUS    RESTARTS   AGE
nginx-7f98958cdf-tdmqk   1/1       Running   0          21s
nginx-7f98958cdf-tesqr   1/1       Running   0          1s

看到有如下两个名为nginx-7f98958cdf-tdmqknginx-7f98958cdf-tesqr的Pod, 其中nginx是直接使用Deployment的名称,-7f98958cdf-tdmqk和-7f98958cdf-tesqr是kubernetes随机生成的后缀。

您也许会发现这两个后缀中前面一部分是相同的,都是7f98958cdf,这是因为Deployment不是直接控制Pod的,Deployment是通过一种名为ReplicaSet的控制器控制Pod,通过如下命令可以查询ReplicaSet,其中rs是ReplicaSet的缩写。

$ kubectl get rs
NAME               DESIRED   CURRENT   READY     AGE
nginx-7f98958cdf   2         2         2         1m

这个ReplicaSet的名称为nginx-7f98958cdf,后缀-7f98958cdf也是随机生成的。

Deployment控制Pod的方式如图2所示,Deployment控制ReplicaSet,ReplicaSet控制Pod。

图2 Deployment通过ReplicaSet控制Pod

Deployment通过ReplicaSet控制Pod

如果使用kubectl describe命令查看Deployment的详情,您就可以看到ReplicaSet,如下所示,可以看到有一行NewReplicaSet: nginx-7f98958cdf (2/2 replicas created),而且Events里面事件确是把ReplicaSet的实例扩容到2个。在实际使用中您也许不会直接操作ReplicaSet,但了解Deployment通过控制ReplicaSet来控制Pod会有助于您定位问题。

$ kubectl describe deploy nginx
Name:                   nginx
Namespace:              default
CreationTimestamp:      Sun, 16 Dec 2018 19:21:58 +0800
Labels:                 app=nginx

...

NewReplicaSet:   nginx-7f98958cdf (2/2 replicas created)
Events:
  Type    Reason             Age   From                   Message
  ----    ------             ----  ----                   -------
  Normal  ScalingReplicaSet  5m    deployment-controller  Scaled up replica set nginx-7f98958cdf to 2
  1. 升级

在实际应用中,升级是一个常见的场景,Deployment能够很方便地支撑应用升级。

Deployment可以设置不同的升级策略,有如下两种。

  • RollingUpdate:滚动升级,即逐步创建新Pod再删除旧Pod,为默认策略。
  • Recreate:替换升级,即先把当前Pod删掉再重新创建Pod。

Deployment的升级可以是声明式的,也就是说只需要修改Deployment的YAML定义即可,比如使用kubectl edit命令将上面Deployment中的镜像修改为nginx:alpine。修改完成后再查询ReplicaSet和Pod,发现创建了一个新的ReplicaSet,Pod也重新创建了。

$ kubectl edit deploy nginx

$ kubectl get rs
NAME               DESIRED   CURRENT   READY     AGE
nginx-6f9f58dffd   2         2         2         1m
nginx-7f98958cdf   0         0         0         48m

$ kubectl get pods
NAME                     READY     STATUS    RESTARTS   AGE
nginx-6f9f58dffd-tdmqk   1/1       Running   0          1m
nginx-6f9f58dffd-tesqr   1/1       Running   0          1m

Deployment可以通过maxSurge和maxUnavailable两个参数控制升级过程中同时重新创建Pod的比例,这在很多时候是非常有用,配置如下所示。

spec:
  strategy:
    rollingUpdate:
      maxSurge: 0.25
      maxUnavailable: 0.25
    type: RollingUpdate
  • maxSurge:表示在滚动更新过程中,允许超出期望副本数的最大实例数或比例,即决定可以同时创建多少个新Pod替换旧Pod,默认值为25%。实际升级过程中,比例会换算为绝对数,并向上取整。

  • 例如spec.replicas为2,默认状态下最多同时创建2*0.25=1个(向上取整)Pod,即系统中最多同时存在3个Pod。

  • maxUnavailable:表示在滚动更新过程中,允许处于不可用状态的最大实例数量或比例,即实际运行Pod数可低于期望副本数的最大限制,默认为25%。实际升级过程中,比例会换算为绝对数,并向下取整。

  • 例如spec.replicas为2,默认状态下最多有2*0.25=0个(向下取整)Pod失效,即实际运行Pod数不可低于期望副本数,系统中最少有2个Pod处于运行状态。换言之,在升级过程中,一直会有2个Pod处于运行状态,每次新建一个Pod,等这个Pod创建成功后再删掉一个旧Pod,直至Pod全部为新Pod。

  1. 回滚

回滚也称为回退,即当发现升级出现问题时,让应用回到老的版本。Deployment可以非常方便地回滚到老版本。

例如上面升级的新版镜像有问题,可以执行kubectl rollout undo命令进行回滚。

$ kubectl rollout undo deployment nginx
deployment.apps/nginx rolled back

Deployment之所以能如此容易地做到回滚,是因为Deployment是通过ReplicaSet控制Pod的,升级后之前ReplicaSet都一直存在,Deployment回滚做的就是使用之前的ReplicaSet再次把Pod创建出来。Deployment中保存ReplicaSet的数量可以使用revisionHistoryLimit参数限制,默认值为10。

2.有状态负载(StatefulSet)

  1. 有状态负载(StatefulSet)

Deployment控制器下的Pod都有个共同特点,那就是每个Pod除了名称和IP地址不同,其余完全相同。需要的时候,Deployment可以通过Pod模板创建新的Pod;不需要的时候,Deployment就可以删除任意一个Pod。

但是在某些场景下,这并不满足需求,比如有些分布式的场景,要求每个Pod都有自己单独的状态时,比如分布式数据库,每个Pod要求有单独的存储,这时Deployment无法满足业务需求。

分布式有状态应用的特点主要是应用中每个部分的角色不同(即分工不同),比如数据库有主备、Pod之间有依赖,在Kubernetes中部署有状态应用对Pod有如下要求:

  • Pod能够被别的Pod找到,要求Pod有固定的标识。

  • 每个Pod有单独存储,Pod被删除恢复后,必须读取原来的数据,否则状态就会不一致。

Kubernetes提供了StatefulSet来解决这个问题,其具体如下:

  • StatefulSet给每个Pod提供固定名称,Pod名称增加从0-N的固定后缀,Pod重新调度后Pod名称和HostName不变。

  • StatefulSet通过Headless Service给每个Pod提供固定的访问域名。

  • StatefulSet通过创建固定标识的PVC保证Pod重新调度后还是能访问到相同的持久化数据。

图1 StatefulSet
StatefulSet

  1. 创建Headless Service

如前所述,创建Statefulset需要一个Headless Service用于Pod访问。

使用如下文件描述Headless Service,其中:

  • spec.clusterIP:必须设置为None,表示Headless Service。
  • spec.ports.port:Pod间通信端口号。
  • spec.ports.name:Pod间通信端口名称。
apiVersion: v1
kind: Service       # 对象类型为Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
    - name: nginx     # Pod间通信的端口名称
      port: 80        # Pod间通信的端口号
  selector:
    app: nginx        # 选择标签为app:nginx的Pod
  clusterIP: None     # 必须设置为None,表示Headless Service

执行如下命令创建Headless Service。

# kubectl create -f headless.yaml 
service/nginx created

创建完成后可以查询Service。

# kubectl get svc
NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
nginx        ClusterIP   None         <none>        80/TCP    5s
  1. 创建Statefulset

Statefulset的YAML定义与其他对象基本相同,主要有两个差异点:

  • serviceName指定了Statefulset使用哪个Headless Service,需要填写Headless Service的名称。

  • volumeClaimTemplates是用来申请持久化声明PVC ,这里定义了一个名为data的模板,它会为每个Pod创建一个PVC,storageClassName指定了持久化存储的类型,在PV、PVC和StorageClass会详细介绍;volumeMounts是为Pod挂载存储。当然如果不需要存储的话可以删除volumeClaimTemplates和volumeMounts字段。

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: nginx
spec:
  serviceName: nginx                             # headless service的名称
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: container-0
          image: nginx:alpine
          resources:
            limits:
              cpu: 100m
              memory: 200Mi
            requests:
              cpu: 100m
              memory: 200Mi
          volumeMounts:                           # Pod挂载的存储
          - name:  data
            mountPath:  /usr/share/nginx/html     # 存储挂载到/usr/share/nginx/html
      imagePullSecrets:
        - name: default-secret
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes:
      - ReadWriteMany
      resources:
        requests:
          storage: 1Gi
      storageClassName: csi-nas                   # 持久化存储的类型

执行如下命令创建。

# kubectl create -f statefulset.yaml 
statefulset.apps/nginx created

命令执行后,查询一下StatefulSet和Pod,可以看到Pod的名称后缀从0开始到2,逐个递增。

# kubectl get statefulset
NAME    READY   AGE
nginx   3/3     107s

# kubectl get pods
NAME      READY   STATUS    RESTARTS   AGE
nginx-0   1/1     Running   0          112s
nginx-1   1/1     Running   0          69s
nginx-2   1/1     Running   0          39s

此时如果手动删除nginx-1这个Pod,然后再次查询Pod,可以看到StatefulSet重新创建了一个名称相同的Pod,通过创建时间5s可以看出nginx-1是刚刚创建的。

# kubectl delete pod nginx-1
pod "nginx-1" deleted

# kubectl get pods
NAME      READY   STATUS    RESTARTS   AGE
nginx-0   1/1     Running   0          3m4s
nginx-1   1/1     Running   0          5s
nginx-2   1/1     Running   0          1m10s

进入容器查看容器的hostname,可以看到同样是nginx-0、nginx-1和nginx-2。

# kubectl exec nginx-0 -- sh -c 'hostname'
nginx-0
# kubectl exec nginx-1 -- sh -c 'hostname'
nginx-1
# kubectl exec nginx-2 -- sh -c 'hostname'
nginx-2

同时可以看一下StatefulSet创建的PVC,可以看到这些PVC,都以“PVC名称-StatefulSet名称-编号”的方式命名,并且处于Bound状态。

# kubectl get pvc
NAME           STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
data-nginx-0   Bound    pvc-f58bc1a9-6a52-4664-a587-a9a1c904ba29   1Gi        RWX            csi-nas        2m24s
data-nginx-1   Bound    pvc-066e3a3a-fd65-4e65-87cd-6c3fd0ae6485   1Gi        RWX            csi-nas        101s
data-nginx-2   Bound    pvc-a18cf1ce-708b-4e94-af83-766007250b0c   1Gi        RWX            csi-nas        71s

StatefulSet的网络标识

StatefulSet创建后,可以看下Pod是有固定名称的,那Headless Service是如何起作用的呢,那就是使用DNS,为Pod提供固定的域名,这样Pod间就可以使用域名访问,即便Pod被重新创建而导致Pod的IP地址发生变化,这个域名也不会发生变化。

Headless Service创建后,每个Pod的IP都会有下面格式的域名。

<pod-name>.<svc-name>.<namespace>.svc.cluster.local

例如上面的三个Pod的域名就是:

  • nginx-0.nginx.default.svc.cluster.local
  • nginx-1.nginx.default.svc.cluster.local
  • nginx-2.nginx.default.svc.cluster.local

实际访问时可以省略后面的..svc.cluster.local。

下面命令会使用tutum/dnsutils镜像创建一个Pod,进入这个Pod的容器,使用nslookup命令查看Pod对应的域名,可以发现能解析出Pod的IP地址。这里可以看到DNS服务器的地址是10.247.3.10,这是在创建CCE集群时默认安装CoreDNS插件,用于提供DNS服务,后续在Kubernetes网络会详细介绍CoreDNS的作用。

$ kubectl run -i --tty --image tutum/dnsutils dnsutils --restart=Never --rm /bin/sh 
If you don't see a command prompt, try pressing enter.
/ # nslookup nginx-0.nginx
Server:         10.247.3.10
Address:        10.247.3.10#53
Name:   nginx-0.nginx.default.svc.cluster.local
Address: 172.16.0.31

/ # nslookup nginx-1.nginx
Server:         10.247.3.10
Address:        10.247.3.10#53
Name:   nginx-1.nginx.default.svc.cluster.local
Address: 172.16.0.18

/ # nslookup nginx-2.nginx
Server:         10.247.3.10
Address:        10.247.3.10#53
Name:   nginx-2.nginx.default.svc.cluster.local
Address: 172.16.0.19

此时如果手动删除这两个Pod,查询被StatefulSet重新创建的Pod的IP,然后使用nslookup命令解析Pod的域名,可以发现nginx-0.nginx和nginx-1.nginx仍然能解析到对应的Pod。这就保证了StatefulSet网络标识不变。

StatefulSet存储状态

上面说了StatefulSet可以通过PVC做持久化存储,保证Pod重新调度后还是能访问到相同的持久化数据,在删除Pod时,PVC不会被删除。

图2 StatefulSet的Pod重建过程
[点击放大]

下面将通过实际操作验证这一点是如何做到的,执行下面的命令,在nginx-1的目录/usr/share/nginx/html中写入一些内容,例如将index.html的内容修改为“hello world”。

# kubectl exec nginx-1 -- sh -c 'echo hello world > /usr/share/nginx/html/index.html'

修改完后,如果在Pod中访问“http://localhost”,那就会返回“hello world”。

# kubectl exec -it nginx-1 -- curl localhost
hello world

此时如果手动删除nginx-1这个Pod,然后再次查询Pod,可以看到StatefulSet重新创建了一个名称相同的Pod,通过创建时间4s可以看出nginx-1是刚刚创建的。

# kubectl delete pod nginx-1
pod "nginx-1" deleted

# kubectl get pods
NAME       READY   STATUS    RESTARTS   AGE
nginx-0    1/1     Running   0          14m
nginx-1    1/1     Running   0          4s
nginx-2    1/1     Running   0          13m

再次访问该Pod的index.html页面,会发现仍然返回“hello world”,这说明这个Pod仍然是访问相同的存储。

# kubectl exec -it nginx-1 -- curl localhost
hello world

3.普通任务(Job)和定时任务(CronJob)

  1. 普通任务(Job)和定时任务(CronJob)

Job和CronJob是负责批量处理短暂的一次性任务(short lived one-off tasks),即仅执行一次的任务,它保证批处理任务的一个或多个Pod成功结束。

  • Job:是Kubernetes用来控制批处理型任务的资源对象。批处理业务与长期伺服业务(Deployment、StatefulSet)的主要区别是批处理业务的运行有头有尾,而长期伺服业务在用户不停止的情况下永远运行。Job管理的Pod根据用户的设置把任务成功完成就自动退出(Pod自动删除)。

  • CronJob:是基于时间的Job,就类似于Linux系统的crontab文件中的一行,在指定的时间周期运行指定的Job。

任务负载的这种用完即停止的特性特别适合一次性任务,比如持续集成。

  1. 创建Job

以下是一个Job配置,其计算π到2000位并打印输出。Job结束需要运行50个Pod,这个示例中就是打印π 50次,并行运行5个Pod,Pod如果失败最多重试5次。

apiVersion: batch/v1
kind: Job
metadata:
  name: pi-with-timeout
spec:
  completions: 50            # 运行的次数,即Job结束需要成功运行的Pod个数
  parallelism: 5             # 并行运行Pod的数量,默认为1
  backoffLimit: 5            # 表示失败Pod的重试最大次数,超过这个次数不会继续重试。
  activeDeadlineSeconds: 100  # 表示Pod超期时间,一旦达到这个时间,Job及其所有的Pod都会停止。
  template:                  # Pod定义
    spec: 
      containers:
      - name: pi
        image: perl
        command:
        - perl
        - "-Mbignum=bpi"
        - "-wle"
        - print bpi(2000)
      restartPolicy: Never

根据completions和parallelism的设置,可以将Job划分为以下几种类型。

表1 任务类型|Job类型
|说明

使用示例

一次性Job

创建一个Pod直至其成功结束

数据库迁移

固定结束次数的Job

依次创建一个Pod运行直至completions个成功结束

处理工作队列的Pod

固定结束次数的并行Job

依次创建多个Pod运行直至completions个成功结束

多个Pod同时处理工作队列

并行Job

创建一个或多个Pod直至有一个成功结束

多个Pod同时处理工作队列

  1. 创建CronJob

相比Job,CronJob就是一个加了定时的Job,CronJob执行时是在指定的时间创建出Job,然后由Job创建出Pod。

apiVersion: batch/v1
kind: CronJob
metadata:
name: cronjob-example
spec:
schedule: “0,15,30,45 * * * *” # 定时相关配置
jobTemplate: # Job的定义
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: pi
image: perl
command:
- perl
- “-Mbignum=bpi”
- “-wle”
- print bpi(2000)

CronJob的格式从前到后就是:

Minute
Hour
Day of month
Month
Day of week

如 "0,15,30,45 * * * * " ,前面逗号隔开的是分钟,后面第一个* 表示每小时,第二个 * 表示每个月的哪天,第三个表示每月,第四个表示每周的哪天。

如果您想要每个月的第一天里面每半个小时执行一次,那就可以设置为" 0,30 * 1 * * " 如果您想每个星期天的3am执行一次任务,那就可以设置为 “0 3 * * 0”。

4.守护进程集(DaemonSet)

守护进程集(DaemonSet)

DaemonSet(守护进程集)在集群的每个节点上运行一个Pod,且保证只有一个Pod,非常适合一些系统层面的应用,例如日志收集、资源监控等,这类应用需要每个节点都运行,且不需要太多实例,一个比较好的例子就是Kubernetes的kube-proxy。

DaemonSet跟节点相关,如果节点异常,也不会在其他节点重新创建。
图1 DaemonSet
[点击放大]
创建DaemonSet

下面是一个DaemonSet的示例。

apiVersion: apps/v1
kind: DaemonSet
metadata:
name: nginx-daemonset
labels:
app: nginx-daemonset
spec:
selector:
matchLabels:
app: nginx-daemonset
template:
metadata:
labels:
app: nginx-daemonset
spec:
nodeSelector: # 节点选择,当节点拥有daemon=need时才在节点上创建Pod
daemon: need
containers:
- name: nginx-daemonset
image: nginx:alpine
resources:
limits:
cpu: 250m
memory: 512Mi
requests:
cpu: 250m
memory: 512Mi
imagePullSecrets:
- name: default-secret

这里可以看出DaemonSet没有Deployment或StatefulSet中的replicas参数,因为DaemonSet会在每个目标节点上固定部署一个Pod。

Pod模板中有个nodeSelector,指定了只在有“daemon=need”的节点上才创建Pod,如下图所示,DaemonSet只在指定标签的节点上创建Pod。如果需要在每一个节点上创建Pod可以删除该标签。
图2 DaemonSet在指定标签的节点上创建Pod
[点击放大]

创建DaemonSet:

$ kubectl create -f daemonset.yaml
daemonset.apps/nginx-daemonset created

查询发现nginx-daemonset没有Pod创建。

$ kubectl get ds
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
nginx-daemonset 0 0 0 0 0 daemon=need 16s

$ kubectl get pods
No resources found in default namespace.

这是因为节点上没有daemon=need这个标签,使用如下命令可以查询节点的标签。

$ kubectl get node --show-labels
NAME STATUS ROLES AGE VERSION LABELS
192.168.0.212 Ready 83m v1.15.6-r1-20.3.0.2.B001-15.30.2 beta.kubernetes.io/arch=amd64 …
192.168.0.94 Ready 83m v1.15.6-r1-20.3.0.2.B001-15.30.2 beta.kubernetes.io/arch=amd64 …
192.168.0.97 Ready 83m v1.15.6-r1-20.3.0.2.B001-15.30.2 beta.kubernetes.io/arch=amd64 …

给192.168.0.212这个节点打上标签,然后再查询,发现已经创建了一个Pod,并且这个Pod是在192.168.0.212这个节点上。

$ kubectl label node 192.168.0.212 daemon=need
node/192.168.0.212 labeled

$ kubectl get ds
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
nginx-daemonset 1 1 0 1 0 daemon=need 116s

$ kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE
nginx-daemonset-g9b7j 1/1 Running 0 18s 172.16.3.0 192.168.0.212

再给192.168.0.94这个节点打上标签,发现又创建了一个Pod:

$ kubectl label node 192.168.0.94 daemon=need
node/192.168.0.94 labeled

$ kubectl get ds
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
nginx-daemonset 2 2 1 2 1 daemon=need 2m29s

$ kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE
nginx-daemonset-6jjxz 0/1 ContainerCreating 0 8s 192.168.0.94
nginx-daemonset-g9b7j 1/1 Running 0 42s 172.16.3.0 192.168.0.212

如果修改掉192.168.0.94节点的标签,可以发现DaemonSet会删除这个节点上的Pod。

$ kubectl label node 192.168.0.94 daemon=no --overwrite
node/192.168.0.94 labeled

$ kubectl get ds
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
nginx-daemonset 1 1 1 1 1 daemon=need 4m5s

$ kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE
nginx-daemonset-g9b7j 1/1 Running 0 2m23s 172.16.3.0 192.168.0.212

5.亲和与反亲和调度

在守护进程集(DaemonSet)中讲到使用nodeSelector选择Pod要部署的节点,其实Kubernetes还支持更精细、更灵活的调度机制,那就是亲和(affinity)与反亲和(anti-affinity)调度。

Kubernetes支持节点和Pod两个层级的亲和与反亲和。通过配置亲和与反亲和规则,可以允许您指定硬性限制或者偏好,例如将前台Pod和后台Pod部署在一起、某类应用部署到某些特定的节点、不同应用部署到不同的节点等等。
Node Affinity(节点亲和)

您肯定也猜到了亲和性规则的基础肯定也是标签,先来看一下CCE集群中节点上有些什么标签。

$ kubectl describe node 192.168.0.212
Name: 192.168.0.212
Roles:
Labels: beta.kubernetes.io/arch=amd64
beta.kubernetes.io/os=linux
failure-domain.beta.kubernetes.io/is-baremetal=false
failure-domain.beta.kubernetes.io/region=cn-east-3
failure-domain.beta.kubernetes.io/zone=cn-east-3a
kubernetes.io/arch=amd64
kubernetes.io/availablezone=cn-east-3a
kubernetes.io/eniquota=12
kubernetes.io/hostname=192.168.0.212
kubernetes.io/os=linux
node.kubernetes.io/subnetid=fd43acad-33e7-48b2-a85a-24833f362e0e
os.architecture=amd64
os.name=EulerOS_2.0_SP5
os.version=3.10.0-862.14.1.5.h328.eulerosv2r7.x86_64

这些标签都是在创建节点的时候CCE会自动添加上的,下面介绍几个在调度中会用到比较多的标签。

failure-domain.beta.kubernetes.io/region:表示节点所在的区域,如果上面这个节点标签值为cn-east-3,表示节点在上海一区域。
failure-domain.beta.kubernetes.io/zone:表示节点所在的可用区(availability zone)。
kubernetes.io/hostname:节点的hostname。

另外在Label:组织Pod的利器章节还介绍自定义标签,通常情况下,对于一个大型Kubernetes集群,肯定会根据业务需要定义很多标签。

在守护进程集(DaemonSet)中介绍了nodeSelector,通过nodeSelector可以让Pod只部署在具有特定标签的节点上。如下所示,Pod只会部署在拥有gpu=true这个标签的节点上。

apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
nodeSelector: # 节点选择,当节点拥有gpu=true时才在节点上创建Pod
gpu: true

通过节点亲和性规则配置,也可以做到同样的事情,如下所示。

apiVersion: apps/v1
kind: Deployment
metadata:
name: gpu
labels:
app: gpu
spec:
selector:
matchLabels:
app: gpu
replicas: 3
template:
metadata:
labels:
app: gpu
spec:
containers:
- image: nginx:alpine
name: gpu
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
cpu: 100m
memory: 200Mi
imagePullSecrets:
- name: default-secret
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: gpu
operator: In
values:
- “true”

看起来这要复杂很多,但这种方式可以得到更强的表达能力,后面会进一步介绍。

这里affinity表示亲和,nodeAffinity表示节点亲和,requiredDuringSchedulingIgnoredDuringExecution非常长,不过可以将这个分作两段来看:

前半段requiredDuringScheduling表示下面定义的规则必须强制满足(require)才会调度Pod到节点上。
后半段IgnoredDuringExecution表示已经在节点上运行的Pod不需要满足下面定义的规则,即去除节点上的某个标签,那些需要节点包含该标签的Pod不会被重新调度。

另外操作符operator的值为In,表示标签值需要在values的列表中,其他operator取值如下。

NotIn:标签的值不在某个列表中
Exists:某个标签存在
DoesNotExist:某个标签不存在
Gt:标签的值大于某个值(字符串比较)
Lt:标签的值小于某个值(字符串比较)

需要说明的是并没有nodeAntiAffinity(节点反亲和),因为NotIn和DoesNotExist可以提供相同的功能。

下面来验证这段规则是否生效,首先给192.168.0.212这个节点打上gpu=true的标签。

$ kubectl label node 192.168.0.212 gpu=true
node/192.168.0.212 labeled

$ kubectl get node -L gpu
NAME STATUS ROLES AGE VERSION GPU
192.168.0.212 Ready 13m v1.15.6-r1-20.3.0.2.B001-15.30.2 true
192.168.0.94 Ready 13m v1.15.6-r1-20.3.0.2.B001-15.30.2
192.168.0.97 Ready 13m v1.15.6-r1-20.3.0.2.B001-15.30.2

创建这个Deployment,可以发现所有的Pod都部署在了192.168.0.212这个节点上。

$ kubectl create -f affinity.yaml
deployment.apps/gpu created

$ kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE
gpu-6df65c44cf-42xw4 1/1 Running 0 15s 172.16.0.37 192.168.0.212
gpu-6df65c44cf-jzjvs 1/1 Running 0 15s 172.16.0.36 192.168.0.212
gpu-6df65c44cf-zv5cl 1/1 Running 0 15s 172.16.0.38 192.168.0.212

节点优先选择规则

上面讲的requiredDuringSchedulingIgnoredDuringExecution是一种强制选择的规则,节点亲和还有一种优先选择规则,即preferredDuringSchedulingIgnoredDuringExecution,表示会根据规则优先选择哪些节点。

为演示这个效果,先为上面的集群添加一个节点,且这个节点跟另外三个节点不在同一个可用区,创建完之后查询节点的可用区标签,如下所示,新添加的节点在cn-east-3c这个可用区。

$ kubectl get node -L failure-domain.beta.kubernetes.io/zone,gpu
NAME STATUS ROLES AGE VERSION ZONE GPU
192.168.0.100 Ready 7h23m v1.15.6-r1-20.3.0.2.B001-15.30.2 cn-east-3c
192.168.0.212 Ready 8h v1.15.6-r1-20.3.0.2.B001-15.30.2 cn-east-3a true
192.168.0.94 Ready 8h v1.15.6-r1-20.3.0.2.B001-15.30.2 cn-east-3a
192.168.0.97 Ready 8h v1.15.6-r1-20.3.0.2.B001-15.30.2 cn-east-3a

下面定义一个Deployment,要求Pod优先部署在可用区cn-east-3a的节点上,可以像下面这样定义,使用preferredDuringSchedulingIgnoredDuringExecution规则,给cn-east-3a设置权重(weight)为80,而gpu=true权重为20,这样Pod就优先部署在cn-east-3a的节点上。

apiVersion: apps/v1
kind: Deployment
metadata:
name: gpu
labels:
app: gpu
spec:
selector:
matchLabels:
app: gpu
replicas: 10
template:
metadata:
labels:
app: gpu
spec:
containers:
- image: nginx:alpine
name: gpu
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
cpu: 100m
memory: 200Mi
imagePullSecrets:
- name: default-secret
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80
preference:
matchExpressions:
- key: failure-domain.beta.kubernetes.io/zone
operator: In
values:
- cn-east-3a
- weight: 20
preference:
matchExpressions:
- key: gpu
operator: In
values:
- “true”

来看实际部署后的情况,可以看到部署到192.168.0.212这个节点上的Pod有5个,而192.168.0.100上只有2个。

$ kubectl create -f affinity2.yaml
deployment.apps/gpu created

$ kubectl get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE
gpu-585455d466-5bmcz 1/1 Running 0 2m29s 172.16.0.44 192.168.0.212
gpu-585455d466-cg2l6 1/1 Running 0 2m29s 172.16.0.63 192.168.0.97
gpu-585455d466-f2bt2 1/1 Running 0 2m29s 172.16.0.79 192.168.0.100
gpu-585455d466-hdb5n 1/1 Running 0 2m29s 172.16.0.42 192.168.0.212
gpu-585455d466-hkgvz 1/1 Running 0 2m29s 172.16.0.43 192.168.0.212
gpu-585455d466-mngvn 1/1 Running 0 2m29s 172.16.0.48 192.168.0.97
gpu-585455d466-s26qs 1/1 Running 0 2m29s 172.16.0.62 192.168.0.97
gpu-585455d466-sxtzm 1/1 Running 0 2m29s 172.16.0.45 192.168.0.212
gpu-585455d466-t56cm 1/1 Running 0 2m29s 172.16.0.64 192.168.0.100
gpu-585455d466-t5w5x 1/1 Running 0 2m29s 172.16.0.41 192.168.0.212

上面这个例子中,对于节点排序优先级如下所示,有个两个标签的节点排序最高,只有cn-east-3a标签的节点排序第二(权重为80),只有gpu=true的节点排序第三,没有的节点排序最低。
图1 优先级排序顺序
[点击放大]

这里您看到Pod并没有调度到192.168.0.94这个节点上,这是因为这个节点上部署了很多其他Pod,资源使用较多,所以并没有往这个节点上调度,这也侧面说明preferredDuringSchedulingIgnoredDuringExecution是优先规则,而不是强制规则。
工作负载亲和(podAffinity)

节点亲和的规则只能影响Pod和节点之间的亲和,Kubernetes还支持Pod和Pod之间的亲和,例如将应用的前端和后端部署在一起,从而减少访问延迟。Pod亲和同样有requiredDuringSchedulingIgnoredDuringExecution和preferredDuringSchedulingIgnoredDuringExecution两种规则。
说明:

对于工作负载亲和来说,使用requiredDuringSchedulingIgnoredDuringExecution和preferredDuringSchedulingIgnoredDuringExecution规则时, topologyKey字段不允许为空。

来看下面这个例子,假设有个应用的后端已经创建,且带有app=backend的标签。

$ kubectl get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE
backend-658f6cb858-dlrz8 1/1 Running 0 2m36s 172.16.0.67 192.168.0.100

将前端frontend的pod部署在backend一起时,可以做如下Pod亲和规则配置。

apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
labels:
app: frontend
spec:
selector:
matchLabels:
app: frontend
replicas: 3
template:
metadata:
labels:
app: frontend
spec:
containers:
- image: nginx:alpine
name: frontend
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
cpu: 100m
memory: 200Mi
imagePullSecrets:
- name: default-secret
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- topologyKey: kubernetes.io/hostname
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- backend

创建frontend然后查看,可以看到frontend都创建到跟backend一样的节点上了。

$ kubectl create -f affinity3.yaml
deployment.apps/frontend created

$ kubectl get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE
backend-658f6cb858-dlrz8 1/1 Running 0 5m38s 172.16.0.67 192.168.0.100
frontend-67ff9b7b97-dsqzn 1/1 Running 0 6s 172.16.0.70 192.168.0.100
frontend-67ff9b7b97-hxm5t 1/1 Running 0 6s 172.16.0.71 192.168.0.100
frontend-67ff9b7b97-z8pdb 1/1 Running 0 6s 172.16.0.72 192.168.0.100

这里有个topologyKey字段(用于划分拓扑域),意思是先圈定topologyKey指定的范围,当节点上的标签键、值均相同时会被认为同一拓扑域,然后再选择下面规则定义的内容。这里每个节点上都有kubernetes.io/hostname,所以看不出topologyKey起到的作用。

如果backend有两个Pod,分别在不同的节点上。

$ kubectl get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE
backend-658f6cb858-5bpd6 1/1 Running 0 23m 172.16.0.40 192.168.0.97
backend-658f6cb858-dlrz8 1/1 Running 0 2m36s 172.16.0.67 192.168.0.100

给192.168.0.97和192.168.0.94打上prefer=true的标签。

$ kubectl label node 192.168.0.97 prefer=true
node/192.168.0.97 labeled
$ kubectl label node 192.168.0.94 prefer=true
node/192.168.0.94 labeled

$ kubectl get node -L prefer
NAME STATUS ROLES AGE VERSION PREFER
192.168.0.100 Ready 44m v1.15.6-r1-20.3.0.2.B001-15.30.2
192.168.0.212 Ready 91m v1.15.6-r1-20.3.0.2.B001-15.30.2
192.168.0.94 Ready 91m v1.15.6-r1-20.3.0.2.B001-15.30.2 true
192.168.0.97 Ready 91m v1.15.6-r1-20.3.0.2.B001-15.30.2 true

将podAffinity的topologyKey定义为prefer,则节点拓扑域的划分如图2所示。

  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - topologyKey: prefer
        labelSelector:
          matchExpressions: 
          - key: app
            operator: In 
            values: 
            - backend

图2 拓扑域示意图
[点击放大]

调度时,会根据prefer标签划分节点拓扑域,本示例中192.168.0.97和192.168.0.94被划作同一拓扑域。如果当拓扑域中运行着app=backend的Pod,即使该拓扑域中并非所有节点均运行了app=backend的Pod(本例该拓扑域中仅192.168.0.97节点上存在app=backend的Pod),frontend同样会部署在此拓扑域中(这里的192.168.0.97或192.168.0.94)。

$ kubectl create -f affinity3.yaml
deployment.apps/frontend created

$ kubectl get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE
backend-658f6cb858-5bpd6 1/1 Running 0 26m 172.16.0.40 192.168.0.97
backend-658f6cb858-dlrz8 1/1 Running 0 5m38s 172.16.0.67 192.168.0.100
frontend-67ff9b7b97-dsqzn 1/1 Running 0 6s 172.16.0.70 192.168.0.97
frontend-67ff9b7b97-hxm5t 1/1 Running 0 6s 172.16.0.71 192.168.0.97
frontend-67ff9b7b97-z8pdb 1/1 Running 0 6s 172.16.0.72 192.168.0.94

工作负载反亲和(podAntiAffinity)

前面讲了Pod的亲和,通过亲和将Pod部署在一起,有时候需求却恰恰相反,需要将Pod分开部署,例如Pod之间部署在一起会影响性能的情况。
说明:

对于工作负载反亲和来说,使用requiredDuringSchedulingIgnoredDuringExecution规则时, Kubernetes默认的准入控制器 LimitPodHardAntiAffinityTopology要求topologyKey字段只能是kubernetes.io/hostname。如果您希望使用其他定制拓扑逻辑,可以更改或者禁用该准入控制器。

下面例子中定义了反亲和规则,这个规则表示根据kubernetes.io/hostname标签划分节点拓扑域,且如果该拓扑域中的某个节点上已经存在带有app=frontend标签的Pod,那么拥有相同标签的Pod将不能被调度到该拓扑域内的其他节点上。

apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
labels:
app: frontend
spec:
selector:
matchLabels:
app: frontend
replicas: 5
template:
metadata:
labels:
app: frontend
spec:
containers:
- image: nginx:alpine
name: frontend
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
cpu: 100m
memory: 200Mi
imagePullSecrets:
- name: default-secret
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- topologyKey: kubernetes.io/hostname #节点拓扑域
labelSelector: #Pod标签匹配规则
matchExpressions:
- key: app
operator: In
values:
- frontend

创建并查看部署效果,示例中根据kubernetes.io/hostname标签划分节点拓扑域,在拥有kubernetes.io/hostname标签的节点中,每个节点的标签值均不同,因此一个拓扑域中只有一个节点。当一个拓扑域中(此处为一个节点)已经存在frontend标签的Pod时,该拓扑域不会被继续调度具有相同标签的Pod。本例中只有4个节点,因此还有一个Pod处于Pending状态无法调度。

$ kubectl create -f affinity4.yaml
deployment.apps/frontend created

$ kubectl get po -o wide
NAME READY STATUS RESTARTS AGE IP NODE
frontend-6f686d8d87-8dlsc 1/1 Running 0 18s 172.16.0.76 192.168.0.100
frontend-6f686d8d87-d6l8p 0/1 Pending 0 18s
frontend-6f686d8d87-hgcq2 1/1 Running 0 18s 172.16.0.54 192.168.0.97
frontend-6f686d8d87-q7cfq 1/1 Running 0 18s 172.16.0.47 192.168.0.212
frontend-6f686d8d87-xl8hx 1/1 Running 0 18s 172.16.0.23 192.168.0.94

五、配置管理

1.ConfigMap

2.Secret

六、Kubernetes网络

1.容器网络

2.Service

3.Ingress

4.就绪探针(Readiness Probe)

5.网络策略(NetworkPolicy)

七、持久化存储

1.Volume

2.PV、PVC和StorageClass

八、认证与授权

1.ServiceAccount

2.RBAC

九、弹性伸缩

1.弹性伸缩


本文的引用仅限自我学习如有侵权,请联系作者删除。
参考知识
Kubernetes基础知识



网站公告

今日签到

点亮在社区的每一天
去签到