3.2 云原生平台的核心原则
在深入讨论云原生平台的一些功能以及由此带来的好处之前,你必须理解所有这些内容基于的哲学基础和根本原则。毫无疑问,这个基础就是让那些高度分布式的应用程序可以在不断变化的环境中运行。但是在我更详细地介绍这两点之前,让我们先来讨论一下这种平台必须具备的技术。
3.2.1 先聊聊容器
实际上,容器是云原生软件的重要推动者。好吧,虽然这种关系并不像我说的这么轻松,但这其实是一个“鸡生蛋还是蛋生鸡”的问题:容器的流行无疑是被对云原生应用程序的支持需求驱动的,而容器的出现也同样推动了云原生软件的发展。
当我提到容器这个术语时,很可能你会立即想到“Docker”。但是我想做的,是把容器中的关键概念也抽象出来,这样你可以更容易地将这些能力与云原生软件联系起来。
从基本层面上说,容器其实是一个使用底层机器功能(例如,底层的操作系统)的计算环境。通常,多个容器会运行在一台机器(例如,服务器)上,机器可以是实际的也可以是虚拟的。多个容器之间是彼此隔离的。从较高层面上说,它们有点像虚拟机(VM),即一个运行在共享资源上的独立计算环境。但是,容器比虚拟机更加轻量级,创建容器的时间比创建虚拟机的时间低几个数量级,消耗的资源也更少。
前面已经提到,在一台主机上运行的多个容器会共享主机的操作系统,但仅此而已。应用程序所需的其他运行时环境都运行在容器中(没错,你的应用程序会在容器中运行)。
图3.4显示了在主机和容器中同时运行的应用程序和运行时环境。主机只提供操作系统内核。在容器中,首先会有操作系统的根文件系统,包括openssh或apt get等操作系统命令。应用程序所需的运行时环境也在容器中运行,例如,Java运行时环境(JRE)或者.NET框架。最后,你的应用程序也会在容器中运行。
图3.4 一台主机上通常会运行多个容器,这些容器会共享主机的操作系统内核,但是每个容器都有自己的根文件系统、运行时环境和应用程序代码
当你运行一个应用程序的实例时,首先会在主机上创建一个容器。所有应用程序运行所需的东西,包括操作系统的文件系统、应用程序运行时环境和应用程序本身,都将被安装到该容器中,并启动相应的进程。云原生平台将容器作为核心,为你的软件提供了大量的功能,而创建应用程序的实例只是功能之一。其他的功能还包括:
■ 监视应用程序的健康状况
■ 在基础设施上分布应用程序的实例
■ 为容器分配IP地址
■ 动态路由到应用程序的实例
■ 配置注入
■ 其他更多功能
当你学习什么是云原生平台时,我希望你能记得与容器有关的几个知识点:1.基础设施上会有多台主机;2.主机上运行着多个容器;3.应用程序会使用已安装在容器中的操作系统和运行时环境。在本章接下来的许多插图中,我都将使用图3.5所示的图标来表示容器。
图3.5 在研究云原生平台时,有时我们会认为容器是一个黑盒,应用程序在其中运行。稍后,我们将深入讨论应用程序在容器中运行的细节
在基本了解容器之后,我们现在来了解一下云原生平台的一些关键原则。
3.2.2 支持“不断变化”
在本书的开头,我讲述了一个亚马逊宕机的事件,这个事件演示了一个应用程序如何在其所在平台遇到故障时依然保持稳定运行。尽管在设计系统的弹性方面,开发人员起着关键的作用,但他们不需要实现保障稳定性的每个功能,云原生平台会提供这部分重要功能。
以可用区为例。为了支持可靠性,亚马逊允许EC2用户访问多个可用区,允许他们将应用程序部署到多个可用区中,这样即使某个可用区发生故障,应用程序依然可以运行,不过一些用户的应用程序仍然会出现短时间无法访问的情况。
虽然原因可能各不相同,但一般来说,无法做到跨可用区部署应用程序是因为这本来就不是一件容易的事情。你必须跟踪所使用的各个可用区,在每个可用区上启动机器实例,配置可用区之间的网络,并确定如何在多个可用区的虚拟机中部署应用程序实例(容器)。当你在进行任何运维操作时(例如,升级操作系统),必须决定是否一次只对一个可用区操作,还是采用其他方式。当AWS不断下线你运行应用程序的主机时是否需要转移流量?你必须考虑整个网络拓扑,包括应该将流量转移到哪个可用区,这绝对是一件非常复杂的事情。
虽然AWS向使用EC2服务的用户公开了可用区的概念,但不需要向云原生平台的用户公开。平台可以被配置为使用多个可用区,然后由平台来编排跨可用区的所有应用程序实例。应用程序的开发团队只需要请求部署应用程序的多个实例(比如,4个实例,如图3.6所示),平台就会自动将它们均匀地分布到所有可用区。平台实现了所有的编排和管理工作,将人们从这些工作中解放出来。当以后发生变化时(例如,可用区出现故障),应用程序还可以继续工作。
图3.6 云原生平台负责管理多个可用区之间的流量
前面提到的另一个概念是最终一致性,这是云计算中的一个关键模式,因为事物都是在不断变化的。我们都知道,部署和管理任务必须是自动化的,但是设计时的预期是永远不会实现的。系统管理是通过不断监控系统的实际状态(不断变化的),将其与期望状态进行比较,并在必要时进行调整来实现预期的。这种技术很容易描述,但很难实现,因此通过云原生平台来提供这种功能是很有必要的。
有少数几个云原生平台实现了此基本模式,包括Cloud Foundry和Kubernetes。尽管实现的细节略有不同,但基本思路是相同的。图3.7描述了其中的关键模块以及它们之间的基本流程。
图3.7 对于在平台上运行的应用程序,对其状态的管理是通过不断将期望状态与实际状态进行比较,然后在必要时进行调整来实现的
1 用户通过与平台API的交互来表达期望状态。例如,用户可能要求运行某个应用程序的4个实例。
2 平台API不断将期望状态的变化广播到一个可容错、分布式的数据存储或者消息结构中。
3 每台工作的主机负责将运行的当前状态广播到一个可容错、分布式的数据存储或消息结构中。
4 其中一个模块,这里称之为比较器(Comparator),负责从状态存储中获取信息,维护一个期望状态和实际状态的模型,并对两者进行比较。
5 如果期望状态与实际状态不匹配,比较器会将其差异通知给系统中的另一个组件。
6 这里将该组件称为调度器(Scheduler),它会确定应该在何处创建新的实例,或者应该关闭哪些实例,并通过与主机之间的调用来实现该任务。
这里的复杂性在于系统的分布式特点。坦率地说,在分布式系统上实现此功能是有难度的。在平台中实现的算法,必须考虑API或者主机的消息丢失、短暂但可能会中断流量的网络分区现象,以及由于这类网络抖动而导致的状态变化。
当状态存储等组件的输入发生冲突时,必须有能够维护状态的方法(Paxos和Raft协议是目前使用最广泛的两个协议)。正如应用程序开发团队不需要考虑如何管理多个可用区的实例一样,他们也不需要对实现最终一致性的系统负责,这个功能也由云原生平台来提供。
云原生平台是一个复杂的分布式系统,它需要像分布式应用程序一样具有弹性。如果比较器宕机,无论是由于故障还是计划中的升级,平台都必须是可以自我修复的。之前描述的在平台上运行应用程序的模式,也同样适用于对平台的管理。我们的期望状态可能包括100台运行应用程序的主机,以及5个分布式的状态存储节点。如果系统当前的拓扑结构与期望不同,那么平台会将其自动调整为期望状态。
本节中介绍的内容非常复杂,远远超出了以前可以手动执行、简单的自动化步骤。这些都是云原生平台为了支持不断变化所提供的功能。
3.2.3 支持“高度分布式”
所有对自治的讨论都包含两部分:团队自治,可以让团队避免烦冗的流程和大量协调工作,快速地迭代和部署应用程序;应用程序自治,即在应用程序自己的环境中运行多个微服务,同时支持独立开发并降低级联失败的影响。它们的确能带来这些好处,但是随之产生的是一个由分布式组件组成的系统,而非原来单个组件或者单个进程的架构,因此要面临的复杂性也是之前不存在的(或者很少存在)。
好消息是,在整个行业中,我们已经花了很多时间来研究这些问题的解决方案,模式也已经相当成熟。当一个组件需要与另一个组件通信时,它需要知道在哪里可以找到另一个组件。当一个应用程序水平扩展到数百个实例时,需要一种办法能够更改所有实例的配置,同时不需要大量、集中地重启。如果用户的某个请求需要通过十几个微服务来响应,而且性能很差,你需要在复杂的网络结构中找到问题所在。你需要避免让重试机制(这是云原生软件架构的一种基本模式,客户端服务会在服务端没有响应时,重复向服务端发送请求)变成对系统的DDoS[2]攻击。
但是请记住,开发人员并不负责实现云原生软件的所有模式,这些模式需要由平台来实现。本节接下来会简单介绍一些云原生平台在这方面提供的功能。
我想以一个具体的食谱分享网站为例,来说明一些模式。示例网站提供的服务之一是推荐食谱列表,为了做到这一点,推荐服务会调用收藏服务,来获得用户以前所点赞的食谱列表,然后用它们来计算推荐结果。你有多个应用程序,每个应用程序都部署了多个实例,这些应用程序的功能以及它们之间的交互决定了软件的行为,因此你的系统是分布式的。那么一个平台应该提供哪些功能来支持这个分布式的系统呢?
服务发现
单独的服务运行在不同的容器和不同的主机上,为了让一个服务能够调用另一个服务,它必须首先能够找到另一个服务。其中一种方式是通过众所周知的万维网(World Wide Web)模式,即DNS和路由。推荐服务会调用收藏服务的URL地址,而URL会被DNS解析为一个IP地址,该IP地址指向一个路由器,该路由器随后会将请求发送给收藏服务的一个实例,流程如图3.8所示。
图3.8 推荐服务通过DNS查询和路由来找到收藏服务
另一种方法是让推荐服务通过IP地址直接访问收藏服务的实例,但是因为后者有很多实例,所以必须像以前一样对请求进行负载均衡。图3.9展示了将路由功能直接集成到调用服务中,从而让其自身支持分布式路由的功能。
无论路由功能在逻辑上是中心化的(如图3.8所示)还是高度分布式的(如图3.9所示),保证路由表始终是最新的都是一个重要的过程。为了完全实现这个过程的自动化,平台需要实现一些功能,例如,收集新启动或者刚恢复的微服务实例的IP地址信息,并将这些数据分发给路由组件。
图3.9 推荐服务通过IP地址直接访问收藏服务,路由功能是分布式的
服务配置
假设我们的数据科学家已经进行了更多分析,因此想改变推荐算法的一些参数。由于推荐服务已经部署了数百个实例,每个实例都必须采用新的值。当推荐引擎被部署为单个进程时,你可以直接为该实例提供一个新的配置文件,并重新启动应用程序。但是,由于现在的软件架构是高度分布式的,所以没有人知道某个时间点各个实例都在哪里运行,但是云原生平台知道。
为了在云原生平台上提供这个功能,需要使用到配置服务。该服务与平台的其他部分一起协同工作,从而实现图3.10中演示的功能。具体流程如下所示:
1 运维人员向配置服务提供新的配置值(可能会将配置信息提交到一个源代码控制系统中)。
2 服务实例知道如何访问配置服务,在需要时会通过该服务获取配置值。当然,服务实例在启动时也会获取配置值,但是在配置值发生变更,或者某些生命周期事件发生时,也需要获取配置值。
3 当配置值变更时,需要让每个服务实例刷新自己的配置。云原生平台知道所有的服务实例在哪里。实际状态保存在状态存储中。
4 由云原生平台通知每个服务实例有新的值可以使用,然后所有服务实例更新为新的值。
图3.10 云原生平台的配置服务,为部署基于微服务的应用程序提供了重要的配置功能
同样,无论是开发人员还是应用程序的运维人员,都不需要实现这个功能,这由云原生平台自动为部署的应用程序提供。
服务发现和服务配置,只是云原生平台提供的众多功能中的两个,但它们为云原生应用程序实现模块化和高度分布式提供了支持。云原生平台提供的其他服务还包括:
■ 分布式跟踪机制,允许自动将跟踪器嵌入请求中,以诊断多个微服务调用之间的问题。
■ 断路器,防止意外产生的内部DDoS攻击,例如,网络中断时可能产生的重试风暴。
这些功能以及更多服务都是云原生平台的优势,极大地降低了我们构建现代软件的难度,减轻了开发人员和运维人员的负担。这样的平台对于以功能为主的IT组织是至关重要的。