作者介绍:Saad Ali 是来自Google的高级软件工程师,致力于开源Kubernetes项目。他于2014年12加入该项目并负责Kubernetes存储和volume子系统的开发。他还担任Kubernetes存储兴趣小组的负责人,同时也是CSI(容器存储接口)的协同开发者和maintainer。在加入Google之前,Saad Ali于微软负责IMAP原型开发项目。
Kubernetes正在迅速成为在分布式系统上部署工作负载的事实标准。在这篇文章中,笔者将通过揭示其设计的一些原则,帮助你更深入地了解Kubernetes。
1、要声明式而不要命令式
一旦你学会在Kubernetes开源编排引擎上部署第一个工作负载(pod),就会遇到Kubernetes的第一个原则:Kubernetes API是声明性的而不是命令式的。
在命令式API中,你可以直接发出服务器要执行的命令,例如: “运行容器”、“停止容器”等。在声明性API中,你声明系统要执行的操作,系统将不断向该状态驱动。
可以想象成手动驾驶和自动驾驶系统。
因此,在Kubernetes中,你创建一个API对象(使用CLI或REST API)来表示你希望系统执行的操作。系统中的所有组件都会向该状态发展,直到删除该对象。
例如,如果要调度容器化工作负载,不是发出“运行容器”命令,而是创建描述所需状态的API对象、pod:
创建后,此对象在API服务器上保留:
如果容器由于某种原因崩溃,系统将重新启动容器。
要终止容器,请删除pod对象:
为什么要声明式而不要命令式?
声明式API使系统更加健壮。
在分布式系统中,任何组件都可能随时出现故障。当组件恢复时,需要弄清楚要做什么。
使用命令式API时,崩溃的组件可能在它关闭时错过了一个调用,并且需要一些外部组件在它恢复时“赶上”。但是使用声明式API,组件只需查看API服务器的当前状态,即可确定它需要执行的操作(“啊,我需要确保此容器正在运行”)。
这也被描述为“水平触发”而不是“边缘触发”。在边缘触发系统中,如果系统错过“事件”(“边缘”),则必须重放事件以便系统恢复。在水平触发系统中,即使系统错过了“事件”(可能是因为它已经关闭),当它恢复时,它可以查看信号的当前状态并相应地做出响应。
因此,声明式API使Kubernetes系统对组件故障更加健壮。
- 2、
如果你了解各种Kubernetes组件的工作原理,你会遇到Kubernetes的下一个原则:控制平面是透明的,因为没有隐藏的内部API。
这意味着Kubernetes组件用来交互的API和你用来与Kubernetes交互的API相同。结合第一个原则(Kubernetes API是声明式的),这意味着Kubernetes组件只能通过监控和修改Kubernetes API来交互。
让我们通过一个简单的例子来说明这一点。为了启动容器化工作负载,你可以在Kubernetes API服务器上创建一个pod对象,如上所示。
Kubernetes调度程序根据可用资源确定运行pod的最佳节点。调度程序通过为新pod对象监控Kubernetes API服务器来完成此操作。当新的未调度的pod被创建时,调度程序将运行其算法为这个pod查找最佳节点。在调度pod之后(已为pod选择了最佳节点),调度程序不会去告诉所选节点要启动pod。请记住,Kubernetes API是声明式的,内部组件使用相同的API。因此,调度程序更新pod对象中的NodeName字段以指示已经调度该pod。
kubelet(在节点上运行的Kubernetes代理)监控Kubernetes API(就像其他Kubernetes组件一样)。当kubelet看到具有与自身对应的NodeName字段的pod时,它知道改pod已经被调度且必须启动它。一旦kubelet启动了pod,它就会继续为pod监控容器状态,并且只要相应的pod对象继续存在于API服务器中,就会一直让容器保持运行。
删除pod对象后,Kubelet会明白不再需要容器,并终止它。
为什么没有隐藏的内部API?
让Kubernetes组件使用相同的外部API使得Kubernetes可组合和可扩展。
如果由于某种原因,Kubernetes的默认组件(例如,调度程序)不足以满足你的需求,你可以将其关闭并将其替换为自己的使用相同API的组件。
此外,如果你需要的功能尚不可用,则可以使用公共API轻松编写组件以扩展Kubernetes功能。
- 3、
Kubernetes API允许存储工作负载可能感兴趣的信息。例如,Kubernetes API可用于存储秘密或配置映射。秘密可以是你在容器镜像中不想要的任何敏感数据,包括密码、证书等。配置映射可以包含应独立于容器镜像的配置信息,例如应用程序启动和其他类似参数。
由于上面定义的没有隐藏的内部API的第二个原则,可以修改在Kubernetes上运行的应用程序以直接从Kubernetes API服务器获取秘密或配置映射信息。但这意味着你需要修改应用程序以意识到它是在Kubernetes中运行。
这就是Kubernetes的第三个原则:在用户所在的位置满足他们。也就是说,Kubernetes不应要求重写要在Kubernetes上运行的应用程序。
例如,许多应用程序接受秘密和配置信息作为文件或环境变量。因此,Kubernetes支持将秘密和配置映射作为文件或环境变量注入pod。
为什么要这么做?
通过进行设计选择,最大限度地减少在Kubernetes上部署工作负载的障碍,Kubernetes可以轻松地运行现有工作负载,而无需重写或大幅改变它们。
- 4、
一旦在Kubernetes上运行无状态工作负载,下一步自然就是尝试在Kubernetes上运行有状态工作负载。Kubernetes提供了一个功能强大的卷插件系统,可以将许多不同类型的持久存储系统与Kubernetes工作负载一起使用。
例如,用户可以轻松地请求将Google Cloud Persistent Disk安装到特定路径的pod中:
创建此pod时,Kubernetes将自动负责将指定的GCE PD挂载到pod被调度到的节点,并将其挂载到指定的容器中。然后,容器可以在容器或pod的生命周期之外写入GCE PD被挂载到持久数据的路径。
此方法的问题在于pod定义(pod YAML)直接引用Google Cloud Persistent Disk。如果此pod已部署在非Google Cloud Kubernetes集群上,则无法启动(因为GCE PD无法使用)。
这是另一个Kubernetes原则的用武之地:工作负载定义应该可以跨集群移植。用户应该能够使用相同的工作负载定义文件(例如相同的pod yaml)来跨不同的集群可以部署工作负载。
理想情况下,上面指定的pod甚至应该可以在没有GCE PD的集群上运行。为了实现这一目标,Kubernetes引入了PersistentVolumeClaim(PVC)和PersistentVolume(PV)API对象。这些对象将存储实现与存储使用分离。
PersistentVolumeClaim对象用作用户以与实现无关的方式请求存储的方法。例如,用户可以创建PVC对象来请求100 GB的ReadWrite存储,而不是请求特定的GCE PD:
Kubernetes系统将此请求与来自包含PersistentVolume对象的可用磁盘池中的卷匹配,或自动配置新卷以满足请求。无论哪种方式,用于Kubernetes集群部署工作负载的对象都可以跨集群实现移植。
为什么要有工作负载可移植性?
这种工作负载可移植性原则突出了Kubernetes的核心优势:与操作系统使应用程序开发人员不必担心底层硬件的细节一样,Kubernetes将分布式系统应用程序开发人员从底层集群的细节中解放出来。使用Kubernetes,分布式系统应用程序开发人员不必被锁定到特定的集群环境。针对Kubernetes部署的应用程序可以轻松地部署到本地和云环境中的各种集群,而无需对应用程序或部署脚本进行特定于环境的更改(Kubernetes端点除外)。
- 5、
由于这些原则,Kubernetes更强大,可扩展,可移植且易于迁移。这就是Kubernetes迅速成为在分布式系统上部署工作负载的行业标准的原因。
本文转移K8S技术社区-Kubernetes设计和开发四大基本原则