本文要点
- 我们更倾向于将Kubernetes视为一个基本的范式,它在多个维度上都有意义,而不仅仅是一个能够与之交互的API。
- Kubernetes为基于语言的构建块增加了一个全新的维度,它提供了一组新的分布式原语和运行时环境,用于创建分布于多个进程和节点上的分布式系统。
- 创建容器化应用程序的核心原则是将容器镜像当做基本的原语,把容器编排平台当做目标容器的运行时环境。其中的原则包括单一关注(single concern)、自我控制(self-containment)、镜像不变性(image immutability)、高可观测性(high observability)、生命周期一致性(lifecycle conformance)、过程一次性(process disposability)以及运行时限制(runtime confinement)。
- 容器设计模式的重点是构建容器和其它分布式原语,要使用最佳的方式来解决当前面临的挑战。这些模式包括跨斗模式(sidecar)、大使模式(ambassador)、适配器模式(adapter)、初始化器模式(initializer)、工作队列模式(work queue)、自定义控制器模式(custom controller)以及自感知模式(self awareness)。
- 容器的最佳实践与镜像大小、端口规格、容量使用、镜像元数据等等都有关。
Kubernetes(k8s)在很短的一段时间内走过了很长的一段路。仅仅两年以前,它还需要与CoreOS的Fleet、Docker Swarm、Cloud Foundry Diego、HashiCorp的Nomad、Kontena、Rancher的Cattle、Apache Mesos、Amazon ECS等进行竞争,来证明自己比那些产品都要优秀。而现如今已经是完全不同的一幅景象了。其中的一些公司公开宣布了项目的终止并且开始加入到Kubernetes阵营中,还有一些公司没有公开宣布自己项目的失败,而是在战略上宣布了对Kubernetes的部分支持或者完全整合,这也就意味着他们的容器编排工具将会安静而缓慢地死掉。不论是哪一种情况,k8s都是最后一个活下来的平台。除此之外,不仅仅是用户和白金赞助商们,越来越多的大公司都将继续加入到Kubernetes的生态系统中,将自己的业务完全押注于Kubernetes的成功。我们首先能想到的有Google的Kubernetes Engine、Red Hat的OpenShift、Microsoft的Azure Container Service、IBM的 Cloud Container Service、Oracle的Container Engine。
但是这些意味着什么呢?首先,这意味着开发人员必须要掌握一个与90%的容器工作相关的容器编排平台。这是一个学习Kubernetes很好的理由。同时这还意味着我们已经深深地依赖于Kubernetes,Kubernetes就像容器领域中的Amazon。在Kubernetes上进行设计、实现和运行应用程序可以让你在不同的云提供商、Kubernetes发行版和服务提供商之间自由地对应用程序进行移动。它能让你有机会找到 Kubernetes认证的开发人员,让他们来开发一个项目并且在以后的运行过程中持续提供支持。Kubernetes不是VM,也不是JVM,它是全新的应用程序可移植层,它是大家共同的选择。
对Kubernetes的深度依赖
下图是一个容器化服务的图表,其中表明了该服务对Kubernetes的依赖程度:
图1 – Kubernetes上的应用程序依赖
请注意,我并没有写到Kubernetes是应用程序可移植性API,它是一个层。上图向我们展示了只有显式创建的k8s对象才能够调用k8s API。但事实上,我们与平台之间的关系更加紧密。k8s提供了一组完整的分布式原语(如Pod、服务、控制器等),它们能够满足我们的需求并且驱动我们对应用程序的设计。这些新的原语和平台容量决定了我们用来实现未来所有服务的指导设计原则和设计模式。反过来,它们也会影响我们用来应对日常挑战的技术,甚至会影响到我们所谓的“最佳实践”。因此,我们更倾向于将Kubernetes视为一个基本的范式,它在多个维度上都有意义,而不仅仅是一个能够与之交互的API。
Kubernetes效应
容器和容器编排工具特性提供了一组新的抽象和原语。为了获得这些原语的最佳价值并衡量它们的效果,我们需要一套新的设计原则来指导我们的实践。带来的结果就是,随着我们更多的使用这些新原语,我们就能更好地解决重复性的问题,能够重新发明轮子。这就是设计模式的用武之地。设计模式为我们提供了如何构建新原语以便更快地解决问题的方法。虽然原则更加抽象、更加基础、变化也更少,但是设计模式可能会受到原语行为变化的影响。平台上的一个新特性可能会使得一个设计模式成为反模式或者不那么相关。除此之外,还有我们每天使用的实践和技术也是如此。这些技术涵盖了从非常细微的技术技巧,到更有效地执行任务,再到更广泛的工作方法和实践方法。当我们发现了一种稍微好用一点的方法能够更快或者更简单地完成任务时,我们就会改进当前的技术和时间。这就是我们和平台互相促进的发展模式。我们为什么要这么做呢?因为,我们这样做就能够利用这个新平台的优势和价值来满足我们自己的需求。从某种意义上来说。Kubernetes效应是自我强化的,也是多方面的:
图2 – 软件开发生命周期中的Kubernetes效应
在本文的剩余部分,我们将探寻地更深,并且将对每个类别的示例进行研究。
分布式抽象和原语
为了能够阐释我所说的新的抽象和原语,我将把它们与大家众所周知的面向对象世界和Java进行比较。在面向对象编程(OOP)的世界里,我们有如下的概念:类、对象、包、继承、封装、多态等等。Java运行时环境提供了一些特性并且它对对象的生命周期和整个应用程序进行管理。Java语言和JVM运行时环境为应用程序的创建提供了本地的、进程内的构建块。Kubernetes为这种众所周知的思维模式又添加了一个新的维度,它提供了一组新的分布式原语和运行时环境,用以创建分布于多个进程之间和不同节点上的分布式系统。有了Kubernetes,我就不用依赖于Java原语来实现整个应用程序的行为了。我仍然需要使用面向对象的构建块来创建分布式应用程序的组件,但是我也可以使用Kubernetes原语来实现一些应用程序的行为。 一些Kubernetes分布式抽象和原语的例子如下:
- Pod:相关容器集合的部署单元
- Service:服务发现和负载均衡原语
- Job:异步调度工作的原子单元
- CronJob:在未来进行调度或者定期进行调度的工作原子单元
- ConfigMap:一种跨服务实例的分发配置数据的机制
- Secret:一种管理敏感配置数据的机制
- Deployment:一种声明式应用程序发布机制
- Namespace:用于隔离资源池的控制单元
例如,我可以依赖于Kubernetes的健康监测(如预备检测或活性检测)来获取我的应用程序的一些可靠性信息。我可以使用Kubernetes Service来进行服务发现,而不是在应用程序中进行客户端服务的发现。我能使用Kubernetes Jobs来对异步原子工作进行调度。我也能使用ConfigMap来对配置进行管理。我还能使用Kubernetes CronJob对任务进行周期性调度,这样就不用使用基于Java的Quartz库或者去实现ExecutorService接口了。
图3 – 分布式系统中本地原语和分布式原语部分
进程原语和分布式原语有着相同之处,但是它们之间没有直接的可比性和可替换性。它们运行于不同的抽象级别,具有不同的先决条件和保证条件。一些原语可以在一起使用。例如,我们仍然需要使用类来创建对象并且将其放入容器镜像中。但是Kubernetes中一些像CronJob这样的原语就能对Java中的ExecutorService行为进行完全的替代。下边有一些概念的图表,是我在JVM和Kubernetes之间找到的共同点,但是并不是很深入。
图4- 本地原语和分布式原语分类
我之前发布过关于分布式抽象和原语的博文。这里的要点是,作为一名开发人员,你可以使用一组更丰富的本地和全局原语来设计和实现分布式解决方案。随着时间的推移,这些新的原语产生了新的解决问题的方案,其中一些复杂的解决方案变成了设计模式。这就是我们将进一步进行探讨的问题。
容器设计原则
设计原则是编写高质量软件的基本规则和抽象准则。这些原则没有指定具体的规则,但是它们代表了许多开发人员所理解并且经常引用的一种语言和通用的智慧。和Robert C. Martin所引入的SOLID原则(该原则提供了如何能够更好地编写面向对象软件的指导)类似,也有一些设计原则能够用于创建更好的容器化应用程序。SOLID原则使用了面向对象的原语和概念(例如,类、接口以及继承等等)来阐释面向对象设计。类似地,下面列出的这些创建容器化应用程序的原则使用了容器镜像来作为基础原语,使用容器编排工具平台作为目标容器的运行时环境。基于容器的应用程序设计原则如下:
- 构建时
- 单一关注原则(Single Concern Principle):每一个容器都应该解决一个问题,并且把这一个问题解决好。
- 自我控制原则(Self-Containment Principle):容器应该只依赖于Linux内核,在容器构建时才添加其它外部的库。
- 镜像不变性原则(Image Immutability Principle):容器化的应用程序是不可变的,一旦构建完成,在不同的环境下也是不会产生变化的。
- 运行时
- 高可观测性原则(High Observability Principle):每个容器都必须要实现所有必要的API,用于帮助平台以最好的方式对应用程序进行监测和管理。
- 生命周期一致性原则(Lifecycle Conformance Principle):容器应该有一种方法来读取来自平台的事件,并通过对这些事件的响应来保持一致。
- 过程一次性原则(Process Disposability Principle):应用程序容器化的过程应该尽可能的短暂,它们需要时刻准备着被另一个容器实例所替换。
- 运行时限制原则(Runtime Confinement Principle):每个容器都应该对其资源需求进行声明,并且保证应用程序受限制于指定的资源需求也是很重要的。
遵循这些原则能够帮助我们创建更适合于Kubernetes等云平台的容器化应用程序。
图5 – 容器设计原则
这些原则都已经编纂成文,可以点击这里免费下载这份白皮书。上述图片就是这些原则的预览。
容器设计模式
新的原语需要新的原则来解释原语之间的作用。我们使用的原语越多,就能解决更复杂的问题,这就使得我们意识到可重用的解决方案,即设计模式。容器设计模式的重点是用能够解决当前面临挑战的最佳方式来构建容器和其它分布式原语。以下简短的列表是与容器相关的设计模式:
- 跨斗模式(Sidecar Pattern):跨斗模式的容器能够拓展和增强先前存在的容器的功能而不对其进行改变。
- 大使模式(Ambassador Pattern):这种模式能够隐藏复杂性,并为容器提供一个统一的外部接口。
- 适配器模式(Adapter Pattern):适配器模式是大使模式的反模式,它为来自外部世界的pod提供了一个统一的接口。
- 初始化器模式(Initializer Pattern):初始化器模式的容器能够将初始化相关的任务与主应用程序逻辑相分离。
- 工作队列模式(Work Queue Pattern):采用工作队列模式的容器能够将任意处理代码打包成容器或数据,并且构建成一个完整的工作队列系统。
- 自定义控制器模式(Custom Controller Pattern):通过这种模式,控制器就能观察对象的变化,并对这些变化采取活动,从而将集群驱动至所需的状态。这种调解模式可用于实现自定义逻辑并拓展平台的功能。
- 自感知模式(Self Awareness Pattern):这种模式描述了应用程序需要进行自身审查并且获得关于自身和运行环境元数据的情况。
这一领域的基础工作是由Brendan Burns和Oppenheimer在他们的容器设计模式的论文中完成的。之后,Brendan出版了一本关于分布式系统设计模式相关主题的书。 Roland Huß和我也写了一本书叫做《Kubernetes Patterns》,里面涵盖了所有这些设计模式和基于容器的应用程序的使用用例。下图是其中一些模式的可视化。
图6 – 容器设计模式
原语需要原则和精心设计的模式。接下来让我们看看使用Kubernetes的最佳实践和一些好处。
实践和技巧
除了原则和模式之外,创建良好的容器化应用程序还需要熟悉其他与容器相关的最佳实践和技巧。原则和模式是抽象的、基础的观点,它们的变化很少。最佳实践和相关的技巧则更加具体,可能会频繁地发生变化。以下是一些常见的与容器相关的最佳实践:
- 专注于小镜像:这能减小容器的大小、构建时间和复制容器镜像时的网络时间。
- 支持任意用户id:避免使用sudo命令或需要一个特定的userid来运行您的容器。
- 标记重要端口:使用EXPOSE命令指定端口,这能使得其他人和软件能够更简单地使用你的镜像。
- 为持久数据使用卷(Volume):在容器被销毁后,需要被保存的数据必须被写入到卷中。
- 设置映像元数据:镜像元数据以标记、标签和注释的形式出现,这能使您能够能容易发现自己的容器镜像。
- 同步主机和镜像:一些容器化的应用程序要求容器在某些属性上与主机进行同步,例如时间和机器ID。
- STDOUT和STDERR日志:将日志记录到这些系统流中而不是文件里能够确保日志被正确地收集和聚合。
以下的这些链接是容器相关最佳实践的一些资源:
Kubernetes优势
正如您从本文中所看到的,Kubernetes以一种基本的方式来指导分布式系统的设计、开发和操作。Kubernetes的学习路线并不短,跨越Kubernetes技术鸿沟需要时间和耐心。这就是为什么我想在这里和读者谈谈Kubernetes给开发者带来的一系列好处。我希望这将有助于证明为什么学习Kubernetes是值得的,并且为什么要使用它来指导你的IT战略。
- 自服务环境:它使得团队和团队成员能够立即从集群中分离出隔离的环境,以便进行CICD和用于实验目的。
- 动态放置应用程序:它允许基于应用程序需求、可用资源和指导策略,将应用程序以可预测的方式放置到集群中。
- 声明式服务部署:这个抽象封装了一组容器的升级和回滚过程,并能够将其执行为可重复和可自动化的活动。
图7 – Kubernetes部署和发布示例
- 应用程序弹性:容器和管理平台可以通过多种方式提高应用程序的弹性,例如:
- 无限循环:CPU共享和配额
- 内存泄露:自己内存不足
- 磁盘占用:配额
- Fork炸弹:进程限制
- 断路器、超时、以跨斗模式重启
- 以跨斗模式进行故障转移和服务发现
- 使用容器进行线程隔离
- 通过调度器进行硬件隔离
- 容量自动伸缩和自我恢复
- 服务发现、负载均衡和断路器:平台允许服务在不使用应用程序代理的情况下发现和使用其他服务。此外,跨斗模式的容器和Istio框架等工具的使用,可以完全将应用程序之外的相关网络职责转移到平台级别之外。
- 声明性应用程序拓扑:使用Kubernetes API对象允许我们对如何部署我们的服务进行描述以及说明它们对其他服务和资源的依赖。将所有这些信息以可执行的格式提供给我们,使我们能够在开发的早期阶段对应用程序的部署进行测试,并将其作为可编程的应用程序基础架构。
图8 – 声明性应用程序拓扑对Kubernetes资源描述符文件的使用
关于Kubernetes,还有更多我们为之感到兴奋的理由。我上边列出的是我感觉很有用的东西,因为它们来源于有开发背景的人们。
资源
我希望能向您描述我是如何看待Kubernetes对开发人员日常生活的影响的。如果你想了解更多关于这方面的信息,换句话说想从开发者的视角来了解Kubernetes的话,你可以看一下我的书并且关注下我的Twitter账号@bibryam。
下面的这些链接是从开发人员的角度看待Kubernetes的一些资源:
- 基于容器的分布式系统设计模式(白皮书)
- 基于容器的应用程序设计原则(白皮书)
- 分布式系统设计(电子书)
- Kubernetes模式(电子书)
- Kubernetes实战(电子书)
关于作者
Bilgin Ibryam (@bibryam) 是Red Hat的首席架构师,也是ASF的成员和委员会成员。他是一位开源的布道者、博客作者,是《Camel Design Patterns》和《Kubernetes Patterns》的作者。在他的日常工作中,Bilgin喜欢指导、编码和领导开发人员成功地构建云解决方案。他目前的工作重点是应用程序集成、分布式系统、消息传递、微服务、DevOps和云原生的挑战。你可以在Twitter、 Linkedin或者他的博客找到他。
查看英文原文:The Kubernetes Effect