介绍以下几个方面:容器编排、Kubernetes适用条件、Kubernetes设计原理和体系结构以及Kubernetes支持的不同运行时环境。读者将熟悉开源仓库的整体结构,并为解决其余问题打好基础。
Kubernetes的主要功能是容器编排,是指确保所有容器都按照计划运行在物理机或虚拟机上。这些容器在部署环境和集群配置的约束下被打包执行大量工作负载。此外,Kubernetes必须密切关注所有运行中的容器,替换运行中止、无响应或其他非正常状态的容器。后续章节将会介绍Kubernetes的更多功能,本节将重点介绍容器及其编排。
硬件贯穿于容器编排的始终。运行工作负载需要一些真正的硬件配置,包括具有计算能力(CPU或核心)、内存和一些本地持久存储(机械硬盘或SSD)的实体物理机。此外,需要一些共享的持久存储,并使用网络连接所有物理机,以便于其互相查找和信息互通。此时,可在物理机上运行多个虚拟机或单纯保持裸金属状态。Kubernetes可部署在实体硬件或虚拟机集群上,同时也可以直接在实体硬件或虚拟机上管理容器。理论上,一个Kubernetes集群可以由物理机和虚拟机组合而成,但这并不常见。
容器是封装微服务的理想选择,因为它们不仅为微服务提供隔离,并且非常轻量,且在部署多个微服务时不会像使用虚拟机时那样产生大量开销。这使得容器非常适合于云部署,因为为每个微服务分配整个虚拟机的成本非常高。
现在主要的云提供商(如AWS、GCE和Azure)都提供容器托管服务,其中一些便是基于Kubernetes(如Google的GKE);另外诸如Microsoft Azure的容器服务,则是基于Apache Mesos等其他解决方案。此外,AWS将ECS(EC2上的容器服务)作为其自有的编排解决方案。Kubernetes的强大之处在于,它可以部署在上述这些云服务器上。Kubernetes有一个云提供商接口,允许任何云提供商执行并无缝集成Kubernetes。
过去系统规模很小,每个服务器都有一个名字。开发人员和用户确切地知道每台机器上运行的是什么软件。我工作过的许多公司都进行过数日讨论,来决定服务器的命名主题。例如,作曲家和希腊神话人物是受欢迎的选择。开发人员像对待自己挚爱的宠物一样对待服务器。如果一台服务器发生故障,这将是重大的危机,所有人都需投入全部精力完成这3件事情:更换一台新的服务器;确认发生故障的服务器上还运行着哪些数据;如何让这些数据在新服务器上运行。如果发生故障的服务器存储了一些重要的数据,那只能寄希望于备份数据和数据恢复。
显然,这种方法并不合适,当有几十个甚至上百个服务器时,必须像对待牲畜一样对待它们,此时需考虑的是集体而非个体。或许此时构建机器时仍需要像对待宠物一样处理,但对于网络服务器来讲,只能像对待牲畜一样去处理。
Kubernetes把这一方法推向极致,它承担了将容器分配给特定机器的全部任务。无须花费大量时间与各个机器(节点)交互。这对于无状态工作负载来说是最好的。对于有状态应用程序,情况稍有不同,但Kubernetes提供了一个名为StatefulSet的解决方案,我们接下来将对其进行讨论。
在本节中,讲述了容器编排的概念,讨论了主机(物理机或虚拟机)和容器之间的关系,以及在云端运行容器的优势,最后以牲畜和宠物作为类比探讨服务器的运行模式。1.2节将进入Kubernetes的世界,了解与之相关的概念和术语。
本节将简要介绍许多与Kubernetes相关的重要概念,并提供一些案例来说明这些概念的重要性和相互间的关系,以便熟悉这些术语和概念。接着,介绍如何将这些概念编排在一起以实现令人敬畏的效果。读者可将其中的许多概念视为构建块。一些概念被视作一个Kubernetes组件来执行,如节点和主节点。这些组件处于不同的抽象级,这将在1.5节中进行详细讨论。
图1.1是著名的Kubernetes架构。
图1.1 Kubernetes架构
集群是主机存储和网络资源的集合,Kubernetes使用集群来运行组成系统的各种工作负载。一个完整的系统可以由多个集群组成。之后会详细讨论集群联邦的高级用例。
节点是单个主机,它可以是物理机或虚拟机,职责是运行Pod。每个Kubernetes节点运行多个Kubernetes组件,如Kuberlet和Kube代理。节点由Kubernetes主控制器管理,这些节点类似于Kubernetes的工蜂,肩负重担,过去它们被称为下属(Minion)。如果读者曾阅读过以往的文献资料,请不要被混淆,下属即指节点。
主节点是Kubernetes的控制面板,由几个组件组成,包含API服务器、调度器和控制器管理器。主节点负责节点在全局、集群水平的调度和事件处理。通常,所有的主控制器组件都设置在一个单一主机上,在考虑到高可用性场景或大型集群时,会倾向于采用多个主节点。在第4章中将详细说明高可用性集群。
Pod是Kubernetes的工作单元,每个Pod包含一个或多个容器,Pod通常在同一台机器上运行并一起调度。Pod中的所有容器具有相同的IP地址和端口空间,它们可通过本地主机或标准进程进行通信。此外,Pod中的所有容器都可以访问承载于Pod的节点上的本地共享存储,共享存储会存在于每个容器上。Pod是Kubernetes的重要特征,通过作为运行于多个进程的主Docker应用程序的超级管理员,可以实现在单个Docker容器中运行多个应用程序,但出于以下几点,并不鼓励这种做法。
对于相互依赖且需要在同一主机上协作以实现其目标的容器组,Pod提供了很好的解决方案。切记,Pod被认为是临时、可替代的实体,需要的话可以被丢弃和替换,Pod可破坏任何Pod存储。每个Pod都有一个唯一的ID(UID),因此,区分它们仍是可实现的。
标签是用来组合对象集合(通常是Pod)的键值对。这对于其他几个概念非常重要,例如副本控制器、副本集以及需要在对象动态组上进行操作、标识组成员的服务。对象和标签之间存在N×N的关系,每个对象可以具有多个标签,并且每个标签可以应用于不同的对象。标签的设计有一定的限制,对象上的每个标签都必须且有唯一密钥,标签密钥必须遵守严格的语法。它的语法包含两个部分:前缀和名称。前缀是可选的,如果它存在,则通过前斜杠(/)与名称分离,并且必须是有效的DNS子域,前缀最多包含253个字符。名称是强制的,最多包含63个字符。名称必须以字母、数字、字符(a~z、A~Z、0~9)开头和结尾,并且只包含字母、数字、字符、点、破折号和下划线。值的规则与名称相同。需注意的是,标签只用于标识对象,而不会将任何元数据附加到对象中。这便是注解的目的(参见1.2.6节)。
注解可使任意元数据与Kubernetes对象关联。Kubernetes只存储注解并使其元数据可用。与标签不同的是,它对字符类型和大小没有严格要求。复杂的系统通常需要这样的元数据,而Kubernetes可识别这样的需求并提供开箱即用的元数据,这样用户则不必提取自己单独的元数据存储进行映射。
这里已涵盖了大部分Kubernetes的概念,也对其进行了简要概括。在1.2.7节中,将从其设计动机、内部结构与实现、源代码方面继续研究Kubernetes的体系结构。
标签选择器根据标签选择对象,基于相等的选择器指定键名和值。基于值的等式或不等式,它有两个运算符:=(或==)和!=,代码如下。
role = webserver
这将选择所有具有该标签键和值的对象。
标签选择器可以用多个逗号分隔,代码如下。
role = webserver, application != foo
基于集合的选择器扩展性能并允许基于多个值进行选择,代码如下。
role in (webserver, backend)
副本控制器和副本集管理由标签选择器标识的一组Pod,确保一定数量的Pod始终运行。它们之间的主要区别在于,副本控制器通过名称匹配来测试成员资格,副本集则通过基于集合的选择器。副本集更新,并被指定为下一代副本控制器。它还处于测试阶段,且在编写时不能被所有工具支持。但也许读者在读到这本书时,它已完全成熟。
Kubernetes会保证在副本控制器或副本集中保持相同数量的Pod运行。在因主机节点或Pod本身的问题而导致数量下滑时,Kubernetes将启动新的用例。需注意的是,如果人为启动Pod并超过指定数量,则副本控制器将结束多余Pod的进程。
副本控制器曾经是许多工作流的中心,例如滚动更新和运行一次性作业。随着Kubernetes的发展,它引入了对很多类似工作流的直接支持,例如Deployment、Job和DaemonSet等专用对象。这些将在下面的章节中提到。
服务向用户或其他服务暴露一些功能。它们通常包含一组Pod,由标签进行区分。服务可提供对外部资源的访问路径,或者直接控制虚拟IP的Pod。本地Kubernetes服务器通过便捷的端点暴露功能。需注意的是,服务在第3层(TCP/UDP)进行。Kubernetes 1.2添加了入口对象,该对象提供对HTTP对象的访问,后续会对这一部分展开详谈。服务可通过以下两种机制之一被发布或发现:DNS或环境变量。服务可以由Kubernetes均衡负载。但当服务使用外部资源或需要特殊处理时,开发人员可自行管理和均衡负载。
与IP地址、虚拟IP地址和端口空间相关的细节,都将在之后的章节中深入讨论。
Pod上的存储是临时的,会随Pod一起消失。如果只是在节点的容器间交换数据,这已经足够,但有时数据需要在Pod上存储更长的时间,或在Pod间传递数据,存储卷的概念便支持了这种需求。需注意的是,虽然Docker中也有存储卷的概念,但它仍比较有限(尽管功能越来越强大)。Kubernetes使用自有的存储卷,并且支持额外的容器类型(如rkt),因此在根本上它独立于Docker的存储卷。
存储卷类型有多种,Kubernetes目前直接支持所有类型。如果可添加间接层,则抽象存储卷插件也许会被开发。emptyDir存储卷类型会在每个容器上安装一个卷,该卷会默认在宿主机器的任意可用容器上备份。如果需要,可以请求存储介质。当Pod由于任何原因终止时,此存储会被删除。对于特定的云环境、各种联网的文件系统,甚至Git存储库,都有许多存储卷类型。一个比较有意思的存储卷类型是PersistentDiskClaim,它概括了部分细节,并在开发者的云提供商环境中使用默认的持久存储。
如果关注Pod上的数据,则可以使用持久化存储。但若需要Kubernetes管理诸如Kubernetes或MySQL Galera分布式数据存储库,便不能用常规的Pod和服务来模拟它,因为这些集群存储使数据分布在唯一的节点上。说回有状态服务集,前文讨论了宠物与牲畜的关系,以及牲畜是如何管理和执行的。有状态服务集介于二者之间。有状态服务集能够确保给定数量的具有唯一标识的宠物在任意给定时间运行(类似于复制控制器)。宠物具有以下特性。
有状态服务集可以帮助对等体发现、添加或移除宠物。
密钥对象是包含敏感信息的小型对象,如凭据和令牌。它们以明文的形式存储在etcd中,可通过Kubernetes API服务器访问,并在需要访问时作为文件装入Pod中(使用负载于常规容量上的专用密钥对象容量)。相同的密钥对象可被安装到多个Pod中。Kubernetes本身已为它的组件加密,开发者也可以创造自有密钥对象。另一种方法是使用密钥对象作为环境变量。需注意的是,为获得更好的安全性,在预制密钥对象的情况下,Pod中的密钥对象一般存储于tmpfs内存中。
Kubernetes中的每个对象都由UID和名称标识,该名称用于引用API调用中的对象。名称应不超过253个字符,并使用小写字母数字字符、下划线(_)和圆点(.)。如果删除对象,则可以创建与已删除对象具有相同名称的另一对象,但UID在集群生命周期中必须是唯一的。UID由Kubernetes生成,因此无须担心其重复。
命名空间是一个虚拟集群。由命名空间分隔的多个虚拟集群可组成一个单独的物理集群。每个虚拟集群与其他虚拟集群完全隔离,它们只能通过公共接口交换信息。需注意的是,节点对象和持久化存储卷不存在于命名空间中。Kubernetes可以调度来自不同命名空间的Pod在同一节点运行。同样,来自不同命名空间的Pod可以使用相同的持久存储。
在使用命名空间时,必须考虑网络策略和资源配额,以确保物理集群资源的正确访问和分配。
Kubernetes有非常宏大的目标,它致力于管理并简化跨环境和云提供商的分布式系统的编排、部署和管理。它提供了许多功能和服务,这些功能和服务应当适应于各种情境,并不断衍化和保持足够简单以供大部分用户使用。这是一个艰巨的任务。Kubernetes通过清晰的排布、高水平的设计和成熟的架构来实现这一点,该架构同时促进了系统的扩展性和灵活性。Kubernetes的许多部分仍是硬编码或环境敏感的,但它们会被逐渐分解为插件,并保持内核的通用性和概括性。在本节中,将对Kubernetes层层解剖,首先介绍各种分布式系统设计模式以及Kubernetes如何对其进行支持;然后介绍Kubernetes外层,即它的API集;接下来会介绍组成Kubernetes的实际组件;最后,对源代码树进行简要介绍,以对Kubernetes的结构进行进一步了解。
在本节的最后,读者将对Kubernetes的架构、执行以及其部分设计决策有深入的理解。
用托尔斯泰在《安娜·卡列尼娜》中的一句话来形容幸福的家庭(工作的分布式系统)都是相似的。这意味着,所有设计良好的分布式系统都必须遵循最佳实践和原则,以使其功能正常运行。Kubernetes不仅是一个管理系统,它同时还可应用这些最佳实践为开发者和管理员提供高水平的服务。下面将介绍几种设计模式。
边车模式除主应用容器之外,还在Pod中共同定位另一个容器。应用容器并不知道边车容器,只是单纯执行自己的任务。中央日志代理(Central Logging Agent)就是一个很好的例子。主容器只将日志记录到stdout,但边车容器会将所有日志发送到一个中央日志服务,这些日志将在此处聚合整个系统的日志。使用边车容器相较于将中央日志添加到主应用容器有巨大的优势,应用不再受到中央日志的负担,如果要升级或更改中央日志记录策略或切换到新的提供商,只需更新并部署边车容器,应用容器并没有任何改变,因此不会由于意外情况而遭到破坏。
外交官模式是指将远程服务当作本地服务,并使其强制执行部分策略。外交官模式的一个很好的例子是,如果有一个Redis集群,该集群中一个主机用于编写,其余副本用于读取,则本地外交官容器可作为代理,并将Redis暴露给本地主机上的主应用容器。主应用容器简单地连接到localhost:6379(Redis缺省端口)上的Redis,但是它其实只是连接到在相同Pod中运行的外交官容器,该容器过滤请求,将编写请求发送到真正的Redis主机,并将读取请求随机发送到其中一个读取副本上,与挎斗模式类似,主应用在这期间并不了解运行过程。当测试真正的本地Redis集群时,这会有很大的帮助。此外,如果Redis集群配置发生改变,则只需要修改外交官容器,主应用同样不了解这一运行过程。
适配器模式是关于主应用容器的标准化输出。逐步推出的服务可能会面临如下问题:服务可能会生成不符合先前版本的格式报表,而其他使用该输出的服务和应用还未升级。适配器容器可以与新的应用容器共同部署在同一Pod上,并将其输出与旧版本相匹配,直到所有的用户都被升级。适配器容器与主应用程序容器共享文件系统,以此监控本地文件系统,每当新应用写入某个文件时,适配器容器将立即进行适配。
单节点模式都是直接由Kubernetes通过Pod直接进行支持的。而多节点模式并不被直接支持,例如负责人选举、工作队列和分散收集等,但使用标准接口组合Pod可实现Kubernetes支持。
若想了解一个系统的功能及其提供的服务,需要关注它的API。API为使用该系统的用户提供了一个全局图。Kubernetes从多角度为开发者提供多组REST API。有些API需通过工具使用,有些则可以被开发者直接使用。API的一个重要方面在于它们也在不断地发展,Kubernetes开发者通过尝试扩展(向现有对象添加新对象和新字段),避免重命名或删除现有对象和字段来保持其可管理性。此外,所有API端点都是版本化的,通常也包含Alpha或Beta记法。代码如下。
/api/v1
/api/v2alpha1
通过基于客户端库的kubectl CLI,或者直接调用REST API,可以访问API。下面的章节会对认证和授权机制进行详细介绍。由此,读者可对API有初步的认识。
这是Kubernetes的主要API,它非常庞大。前文所讲的所有概念以及许多辅助概念,都有相应的API对象和运算。若有正确的权限,则可列出、获取、创建和更新对象。下面是一个常见操作的详细文档,可以得到所有的Pod列表。
GET /api/v1/pods
它支持各种可选参数。
自动伸缩API非常聚焦,允许控制同级别的Pod自动缩放器。该自动缩放器基于CPU利用率,甚至特定于应用的度量来管理一组Pod。它可以用/apis/autoscaling/v1端点来列出、查询、创建、更新和销毁自动缩放器对象。
批处理API用来管理作业。作业是执行和终止某些活动的Pod。与副本控制器管理的常规Pod不同,它们在作业完成时就应该终止。批处理API使用Pod模板指定作业,然后在大部分情况下,允许通过/apis/batch/v1端点列出、查询、创建和删除作业。
Kubernetes集群具有几个用于控制集群的主组件,以及在每个集群节点上运行的节点组件。这一部分将介绍这些组件,并解释它们是如何协同工作的。
主组件通常在一个节点上运行,但在高可用性集群或大型集群上,它们可以分布在多个节点上。
Kube API服务器(Kube-API Server)提供Kubernetes REST API。由于其具有无状态性,因此它可以很轻松地水平缩放。它的所有数据都存储在etcd集群中。API服务器是Kubernetes控制平面的体现。
etcd是一种非常可靠的分布式数据存储。Kubernetes使用它来存储整个集群状态。在小型的瞬态集群中,单个etcd可以与所有其他主组件在同一节点上运行。但考虑到冗余和高可用性,更大型的集群通常包含3个,甚至5个etcd集群。
控制器管理器是各种管理器的集合,这些管理器被打包成一个二进制文件。它包含副本控制器、Pod控制器、服务控制器和端点控制器等。所有这些控制器通过API监控集群状态,它们的任务是将集群控制在目标状态。
Kube调度器负责将Pod调度到节点中。这是一个非常复杂的任务,因为它需要考虑多个相互作用的因素,例如以下几点。
从Kubernetes 1.3开始,DNS服务便成为标准Kubernetes集群的一部分。它被调度成一个普通的Pod。除Headless服务外的每个服务都会接收DNS名称,Pod也可以接收DNS名称,这对于自动化探索非常有用。
集群中的节点需要几个组件与集群主组件交互,接收要执行的工作负载,并根据它们的状态更新集群。
Kube代理在每个节点上进行低水平的网络维护,它用于呈现本地Kubernetes服务,可以执行TCP及UDP转发,通过环境变量或DNS寻找集群IP。
Kubelet是节点上Kubernetes的代表。它负责监控与主组件的通信并管理运行中的Pod,包括以下几个方面的内容。
在本节中,我们通过Kubernetes的API以及用于控制管理集群的组件,深入研究了它的内在构成,从宏观的视角探讨了它的体系结构及其所支持的设计模式。1.6节将介绍Kubernetes支持的运行时。
Kubernetes最初只支持Docker作为容器运行时引擎,目前情况有所变化,Rkt成为另一被支持的运行时引擎,也通过Hypernet与Hyper.sh容器工作进行了一些有趣的尝试。一个较为重要的设计策略是,Kubernetes本身应与特定的运行时完全脱离。Kubernetes与运行时的交互是通过运行时引擎必须执行的一个相对通用的接口实现的。大多数信息交换会通过Pod、容器概念以及可在容器上执行的操作来实现,每个运行时引擎负责保证Kubernetes运行时接口是兼容的。
在本节中,将深入介绍运行时接口,并细化到单个运行时引擎。阅读完本节,读者将能选择适合实际用例的运行时引擎,并知晓在同一系统中切换或组合多个运行时的具体实用场景。
容器的运行时接口在GitHub的Kubernetes项目中有详细介绍。Kubernetes是开源的,可以查看相关网址。
下面的代码展示了该文件中没有详细注释的部分片段。即便是对Go语言一无所知的入门级程序员,也能够从Kubernetes的角度掌握运行时引擎的功能范围。
type Runtime interface {
Type() string
Version() (Version, error)
APIVersion() (Version, error)
Status() error
GetPods(all bool) ([]*Pod, error)
}
在此对Go语言进行简要介绍,以帮助读者更好地解析代码——首先是方法名,接下来是括号中的方法参数。每个参数都是一对,由名称和名称类型组成。最后,指定返回值。Go语言允许多个返回类型。除返回实际结果之外,返回错误对象也很常见,如果一切正常,错误对象将为nil。
事实上,这是一个意味着Kubernetes不执行任何操作的接口。第一组方法提供了运行时的基本信息:类型、版本、API版本和状态。通过下面的代码可以得到全部Pod。
SyncPod(pod *api.Pod, apiPodStatus api.PodStatus, podStatus
*PodStatus, pullSecrets []api.Secret, backOff
*flowcontrol.Backoff) PodSyncResult
KillPod(pod *api.Pod, runningPod Pod, gracePeriodOverride *int64)
error
GetPodStatus(uid types.UID, name, namespace string) (*PodStatus,
error)
GetNetNS(containerID ContainerID) (string, error)
GetPodContainerID(*Pod) (ContainerID, error)
GetContainerLogs(pod *api.Pod, containerID ContainerID, logOptions
*api.PodLogOptions, stdout, stderr io.Writer) (err error)
DeleteContainer(containerID ContainerID) error
下一组方法主要处理Pod,因为这是Kubernetes概念模型中的主要概念框架。然后是GetPodContainerID(),它将数据从容器传输到Pod。还有如下一些与容器相关的更多方法。
ContainerCommandRunner、ContainerAttacher和ImageService是运行时接口继承的接口。这意味着,任何需要执行运行时接口的人都需要执行这些接口方法。接口的定义存放在同一文件中,接口名称已经提供了很多接口功能的信息。显然,Kubernets需要在容器中执行命令,将容器附加到Pod并抽取容器映像。建议读者搜索这个文件并熟悉代码。
现在,读者已经在代码级别对作为运行时引擎的Kubernetes有了初步的认知,接下来将对各个运行时的引擎进行介绍。
当然,Docker是举足轻重的容器。Kubernetes在设计之初仅针对Docker容器,在Kubernetes 1.3中才首次加入多运行时功能。在此之前,Kubernetes只能管理Docker容器。
假定读者在阅读此书时对Docker非常熟悉并了解其功能,我们知道Docker饱受赞誉并历经发展,但也受到一些批判,对它的批判主要针对以下几个方面进行。
针对上述问题,Docker做出了一些改善,尤其针对Docker Swarm产品。Docker Swarm是一个对标Kubernetes的Docker本地编排解决方案,它使用起来比Kubernetes更简单,但没有Kubernetes强大和成熟。
从Docker 1.12开始,Docker Daemon进程中就自带群模式,但由于膨胀和范围蠕变使部分用户受挫。这反过来又使更多的人转而将CoreOS Rkt作为替代方案。
从2016年4月发布的Docker 1.11开始,Docker已经改变了运行容器的方式。运行时现在用containerd和Runc在容器中运行开放容器倡议(OCI)镜像,如图1.2所示。
图1.2 容器中运行开放容器倡议(OCI)镜像
本文系作者在时代Java发表,未经许可,不得转载。
如有侵权,请联系nowjava@qq.com删除。