语言中最危险的短语是:“我们一直都是这么做的。”
——格蕾丝·霍普(Grace Hopper),《Computerworld》(1976年1月)
如果你正在阅读这本书,那么你无疑至少听说过“云原生”这个术语。更有可能的是,你已经看过很多由供应商写的文章,这些文章充满了对云原生的热烈推崇,眼中闪烁着美元符号。如果你对这个术语的了解大部分来自这些文章,那么你或许会觉得它含糊不清,充满了流行语,只是又一个市场营销术语,可能最初是有用的,但现在已被一些推销产品的人所霸占。类似的术语还包括:敏捷(Agile)、DevOps。
出于类似的原因,网络搜索“云原生定义”可能会让你认为,要成为云原生应用程序,唯一需要做的就是使用“正确”的语言或框架,或者采用“正确”的技术。确实,选择合适的编程语言可以让你的工作变得轻松或困难,但它既不是成为云原生应用的必要条件,也不是充分条件。
那么,云原生到底只是应用程序运行的地方吗?这个术语确实暗示了这一点。你只需要将一个老旧的应用程序放入容器中,并在 Kubernetes 中运行,就可以称之为云原生了,是吗?不对。你所做的只是让你的应用程序变得更难部署和管理。一个不规范的应用程序在 Kubernetes 中依然是不规范的。
那么,什么才是云原生应用程序?在这一章中,我们将具体回答这个问题。首先,我们将回顾计算服务范式的历史,尤其是当前的计算服务,并讨论推动技术发展的持续压力,尤其是在大规模操作中如何要求高度的可靠性。最后,我们将识别与云原生应用程序相关的具体特征。
到目前为止的故事
网络应用程序的故事就是扩展压力的故事。
1950年代末,主机计算机的出现标志着计算机时代的开始。那时,所有的程序和数据都存储在一台巨大的机器中,用户通过没有计算能力的哑终端进行访问。所有的逻辑和数据都聚集在一起,形成了一个庞大的整体。这是一个更简单的时代。
到了1980年代,随着廉价网络连接的个人计算机的出现,一切发生了变化。与哑终端不同,个人计算机能够进行一些计算,允许将应用程序的部分逻辑卸载到这些计算机上。这种新的多层架构——将展示逻辑、业务逻辑和数据分离(见图1-1)——第一次使得网络应用程序的组件可以独立于其他组件进行修改或替换。
在1990年代,随着万维网的普及以及随之而来的互联网泡沫热潮,软件即服务(SaaS)开始进入公众视野。整个行业都基于SaaS模型建立,推动了更复杂、更消耗资源的应用程序的开发,而这些应用程序又变得更难以开发、维护和部署。突然间,经典的多层架构不再足够应对需求。为了应对这一挑战,业务逻辑开始被拆解成子组件,这些子组件可以独立开发、维护和部署,开启了微服务的时代。
2006年,亚马逊推出了亚马逊云服务(AWS),其中包括弹性计算云(EC2)服务。虽然AWS不是第一个基础设施即服务(IaaS)产品,但它彻底改变了按需提供数据存储和计算资源的方式,使得云计算——以及快速扩展的能力——走向大众,催生了大量资源向“云端”的迁移。
不幸的是,组织很快发现,面对大规模运作并不容易。坏事总会发生,当你同时处理成百上千个资源(甚至更多)时,坏事发生的频率就更高。流量会剧烈波动,关键硬件会发生故障,下游依赖关系可能突然变得不可访问。即使一段时间内没有问题,你仍然需要部署和管理这些资源。在这种规模下,人工处理这些问题几乎是不可能的(或至少是极不实际的)。
上游和下游依赖
在本书中,我们有时会使用“上游依赖”和“下游依赖”来描述两个资源在依赖关系中的相对位置。业界对这些术语的方向性并没有统一的共识,因此本书将按如下方式使用这些术语:
假设我们有三个服务:A、B 和 C,如下图所示:
在这种情况下,服务 A 向服务 B 发出请求(因此依赖于服务 B),而服务 B 又依赖于服务 C。
由于服务 B 依赖于服务 C,我们可以说服务 C 是服务 B 的下游依赖。进一步来说,因为服务 A 依赖于服务 B,而服务 B 又依赖于服务 C,所以服务 C 也是服务 A 的传递性下游依赖。
反过来,由于服务 C 被服务 B 依赖,我们可以说服务 B 是服务 C 的上游依赖,而服务 A 是服务 C 的传递性上游依赖。
什么是云原生?
从根本上说,真正的云原生应用程序融合了过去60年我们关于在大规模下运行网络应用程序的所有经验。它能够在面对剧烈变化的负载时扩展,在面对环境不确定性时具有弹性,并且能够在不断变化的需求下进行管理。换句话说,云原生应用程序是为在一个残酷且不确定的宇宙中生存而构建的。
那么,我们如何定义“云原生”这个术语呢?幸运的是,我们不需要自己去定义。云原生计算基金会(Cloud Native Computing Foundation,CNCF)——作为知名的Linux基金会的子基金会,并且在这一领域具有公认的权威性——已经为我们定义了云原生:
云原生技术使组织能够在现代动态环境(如公有云、私有云和混合云)中构建和运行可扩展的应用程序……
这些技术使得松耦合的系统成为可能,这些系统具备弹性、可管理性和可观察性。结合强大的自动化,它们允许工程师频繁且可预测地进行高影响的更改,且付出的努力最小。
——云原生计算基金会(CNCF)云原生定义 v1.0
根据这一定义,云原生应用程序不仅仅是“恰好运行在云中的应用程序”。它们还应该具备可扩展性、松耦合、弹性、可管理性和可观察性。综合起来,这些“云原生特性”可以说构成了系统云原生的基础。
事实证明,这些词每个都有相当具体的含义,所以让我们来逐一看看。
可扩展性
在云计算的上下文中,可扩展性可以定义为系统在面对需求显著增加或减少时,仍能按照预期行为运行的能力。如果一个系统在需求急剧增加期间或之后,不需要重构就能完成预定功能,那么可以认为它是可扩展的。
由于不可扩展的服务在初始条件下可能看起来完全正常,服务设计时并不总是把可扩展性作为首要考虑因素。虽然这在短期内可能没问题,但那些不能超越最初预期进行扩展的服务,可能在生命周期的后期会有较大限制。而且,重新设计一个服务以具备可扩展性通常非常困难,因此从一开始就考虑可扩展性,长远来看可以节省时间和金钱。
服务可以通过两种方式进行扩展,每种方式都有各自的优缺点:
垂直扩展
系统可以通过增加(或减少)已经分配给它的硬件资源来进行垂直扩展(或向上扩展)。例如,通过增加运行在专用计算实例上的数据库的内存或CPU。垂直扩展的好处是技术上相对简单,但任何给定的实例只能扩展到一定程度。
水平扩展
系统可以通过添加(或移除)服务实例来进行水平扩展(或向外扩展)。例如,可以通过增加负载均衡器后面的服务节点数量,或在 Kubernetes 或其他容器编排系统中增加容器的数量来实现。这个策略有许多优点,包括冗余和摆脱可用实例大小的限制。然而,更多的副本意味着更大的设计和管理复杂性,并且并非所有服务都可以水平扩展。
既然服务可以通过两种方式进行扩展——向上或向外——这是否意味着任何硬件可以扩展的服务(并且能够利用增加的硬件资源)就是“可扩展的”?如果你想较为严谨地分析,那么可以说是的,但这能扩展到多大程度呢?垂直扩展本质上受限于可用计算资源的大小,因此只能向上扩展的服务并不是很具备可扩展性。如果你希望能够扩展十倍、百倍或千倍,你的服务必须具备水平扩展能力。
那么,水平可扩展的服务与不可水平扩展的服务有什么区别?这归结为一件事:状态。一个不维护任何应用状态的服务——或者已经非常仔细地设计成将状态分布在服务副本之间的服务——会相对容易进行水平扩展。对于其他应用而言,这将非常困难。就是这么简单。
可扩展性、状态和冗余的概念将在第7章中更深入地讨论。
松耦合
松耦合是一种系统属性和设计策略,在这种策略中,系统的各个组件对其他组件的了解尽可能少。当一个组件的变化通常不需要改变另一个组件时,可以认为这两个系统是松耦合的。
例如,Web服务器和Web浏览器可以被认为是松耦合的:服务器可以被更新,甚至完全替换,而不影响浏览器的正常使用。这之所以可能,是因为标准的Web服务器已经约定使用一套标准协议进行通信。换句话说,它们提供了一种服务契约。想象一下,如果每次NGINX或httpd有新版本时,世界上所有的Web浏览器都必须进行更新,那将会是多么混乱的局面!
可以说,“松耦合”只是微服务架构的一个表述:将组件划分开来,使得一个组件的变化不必影响到另一个组件。这也许是对的。然而,这一原则常常被忽视,需要反复强调。松耦合的好处和如果忽视它的后果不能被低估。很容易创建出一个“最坏的世界”系统,它将多个服务的管理和复杂性开销与单体系统的依赖和纠缠结合在一起:令人害怕的分布式单体。
不幸的是,没有任何魔法技术或协议能够使你的服务保持松耦合。任何数据交换格式都可能被滥用。然而,有一些技术确实有助于实现松耦合,并且——当与声明性API和良好的版本控制实践结合使用时——可以用来创建既松耦合又可修改的服务。
这些技术和实践将在第8章中详细讨论和演示。
弹性
弹性(大致与容错同义)是衡量系统在面对错误和故障时的承受和恢复能力的标准。如果一个系统在某个部分失败时,能够继续正常运行——可能在降低的性能下——而不是完全失败,那么可以认为该系统具有弹性。
当我们讨论弹性(以及其他云原生属性时,尤其是在讨论弹性时),我们会经常使用“系统”这个词。根据系统的使用方式,“系统”可以指从复杂的互联服务网络(例如整个分布式应用程序)到一组紧密相关的组件(例如单个功能或服务实例的副本),甚至是运行在单台机器上的单个进程。每个系统都由多个子系统组成,而每个子系统又由更小的子子系统组成,依此类推。可以说是“从上到下,层层相叠”。
在系统工程的语言中,任何系统都可能包含缺陷或故障,在软件领域我们通常把这些称作“bug”。我们都知道,在某些条件下,任何故障都可能导致错误,错误是我们用来描述系统预期行为与实际行为之间差异的术语。错误有可能导致系统未能执行其要求的功能,进而造成失败。然而,这还不是全部:子系统或组件的失败会成为更大系统中的故障;任何未得到妥善隔离的故障都有可能向上传播,最终导致整个系统的失败。
在理想的世界里,每个系统都会被精心设计,以防止故障的发生,但这是一个不现实的目标。你无法防止每一个可能的故障,尝试去做是浪费且无效的。然而,通过假设系统的所有组件都会失败——它们确实会失败——并设计它们以响应潜在的故障并限制故障的影响,你可以构建出一个即使部分组件失败仍然能正常运行的系统。
有许多方法可以设计一个具有弹性的系统。部署冗余组件可能是最常见的方法,但这也假设一个故障不会影响同类组件的所有实例。可以包含断路器和重试逻辑,以防止故障在组件之间传播。甚至可以让故障组件被“清除”——或故意让它们失败——以便使整个系统受益。
我们将在第9章中深入讨论所有这些方法(以及更多)。
弹性与可靠性不是同一回事
“弹性”和“可靠性”这两个术语描述的是密切相关的概念,且常常被混淆。但正如我们将在第9章中讨论的那样,它们并不完全相同:
系统的弹性是指系统在面对错误和故障时,继续正确运行的能力。弹性,和其他四个云原生特性一样,只是对系统可靠性贡献的一个因素。
系统的可靠性是指系统在给定时间间隔内按照预期行为运行的能力。可靠性与可用性、可维护性等属性一起,构成了系统整体的可靠性。
可管理性
系统的可管理性是指修改其行为以保持系统安全、平稳运行,并符合不断变化的需求的难易程度(或缺乏易性)。如果可以在不修改代码的情况下充分改变系统的行为,那么该系统可以被认为是可管理的。
作为系统的一个属性,可管理性通常比一些更引人注目的特性(如可扩展性或可观察性)受到的关注要少。然而,它同样至关重要,特别是在复杂的分布式系统中。
例如,假设有一个假设的系统,其中包括一个服务和一个数据库,且服务通过一个 URL 引用数据库。如果你需要更新该服务以引用另一个数据库,怎么办?如果 URL 是硬编码的,你可能不得不更新代码并重新部署,这对于某些系统来说,可能会因为各种原因而显得很麻烦。当然,你也可以更新 DNS 记录指向新的位置,但如果你需要重新部署服务的开发版本,并且该版本使用的是它自己的开发数据库,怎么办?
一个更易管理的系统可能会将这个值表示为一个容易修改的环境变量;如果该服务部署在 Kubernetes 中,调整其行为可能只是更新 ConfigMap 中的一个值。更复杂的系统甚至可能提供一个声明式 API,供开发者用来告诉系统她期望的行为是什么。没有单一的正确答案。
可管理性不仅限于配置更改。它包括系统行为的所有可能维度,例如激活功能标志、轮换凭证或 TLS 证书,甚至(可能特别是)部署或升级(或降级)系统组件。
可管理的系统设计上更具适应性,能够轻松调整以适应功能、环境或安全需求的变化。另一方面,不可管理的系统往往更加脆弱,通常需要临时——往往是手动——的更改。管理此类系统的开销对其可扩展性、可用性和可靠性施加了基本的限制。
可管理性的概念以及在 Go 中实现它的一些最佳实践将在第10章中深入讨论。
可管理性与可维护性不是一回事
可以说,可管理性和可维护性有一些“任务重叠”,因为它们都涉及系统修改的难易程度,但实际上它们是完全不同的概念:
可管理性描述的是对正在运行的系统的行为进行修改的难易程度,包括部署(和重新部署)该系统的组件。这是指从外部进行更改的难易程度。
可维护性描述的是对系统底层功能进行修改的难易程度,通常是指其代码。这是指从内部进行更改的难易程度。
可观察性
系统的可观察性是指通过了解系统的外部输出,推断其内部状态的能力。当能够在最少的先验知识下,快速而一致地提出关于系统的新问题,而不需要重新编写代码或重新部署监控工具时,可以认为该系统是可观察的。
乍一看,这似乎很简单:只需要添加一些日志和展示几个仪表盘,系统就变得可观察了,对吧?几乎可以肯定不行。对于现代复杂系统来说,几乎任何问题都是多个故障同时发生的表现。LAMP栈的时代已经过去;现在的事情变得更复杂了。
这并不是说度量、日志记录和追踪不重要。恰恰相反:它们是可观察性的基石。但它们的存在本身并不足够:数据不是信息。它们需要以正确的方式使用,必须富有表现力。它们必须能够回答你从未想到要问的问题。
检测和调试问题的能力是维护和发展一个强健系统的基本要求。但在分布式系统中,单纯找出问题所在就已经足够困难了。复杂的系统实在是太复杂了。任何给定系统的可能故障状态数量,与其各个组件的部分故障和完全故障状态的数量之积成正比,且无法预测所有可能的状态。仅仅关注我们预计会失败的部分已不再足够。
可观察性的出现可以看作是监控的演变。多年的复杂系统设计、构建和维护经验教会我们,传统的仪器化方法——包括但不限于仪表盘、非结构化日志、或对各种“已知未知”的告警——无法应对现代分布式系统所带来的挑战。
可观察性是一个复杂而微妙的主题,但从根本上说,它归结为这一点:对系统进行足够丰富的仪器化,并在足够真实的场景下进行监控,以便在未来能够回答那些你甚至未曾想到要问的问题。
可观察性的概念以及一些实现建议将在第11章中更深入地讨论。
为什么云原生成为了一个热门话题?
向“云原生”的转变是架构和技术适应的一个例子,源于环境压力和自然选择的驱动。这是进化——适者生存。请耐心听我讲讲,我的专业背景是生物学。
很久以前,在时间的黎明,应用程序通常是构建并部署到一台或少数几台服务器上(通常是手动操作),这些服务器被精心维护和管理。如果应用程序出现问题,它们会被细心地修复。如果某个服务挂掉,通常通过重启就能解决问题。可观察性就是登录到服务器,运行 top 命令并查看日志。那是一个更简单的时代。
1997年,工业化国家中只有11%的人和全球只有2%的人是常规的互联网用户。然而,接下来的几年,互联网接入和普及呈指数增长,到2017年,这一比例在工业化国家达到了81%,在全球达到了48%,并且这个比例仍在继续增长。
所有这些用户——以及他们的钱——给服务带来了压力,促使了对扩展的巨大需求。更重要的是,随着用户的技术水平和对网络服务的依赖增长,他们对自己最喜欢的网络应用程序的期望也随之提高,希望这些应用程序既功能丰富,又始终可用。
结果是——且仍然是——朝着规模、复杂性和可靠性三个方向的显著进化压力。然而,这三者并不容易兼容,传统方法无法、也不能跟上发展步伐。必须发明新的技术和实践。
幸运的是,公有云和基础设施即服务(IaaS)的引入使得扩展基础设施变得相对简单。可依赖性的不足往往可以通过数量来弥补。但这也带来了新的问题。如何管理一百台服务器?一千台?一万台?如何将应用程序安装到这些服务器上,或进行升级?当应用程序出现问题时,如何调试?如何知道它是否健康?在小规模下只是令人烦恼的问题,到了大规模后就变得非常棘手。
云原生之所以成为一个重要概念,是因为规模是我们所有问题的根源(也是解决方案)。这不是魔法,也不是特别的东西。抛开华丽的词藻不谈,云原生技术和实践的存在,唯一的目的是使得能够利用“云”的好处(数量),同时弥补其缺点(缺乏可靠性)。
总结
在本章中,我们讨论了计算机发展的历史,以及我们现在称之为云原生的概念并不是一种新现象,而是技术需求驱动创新再驱动更多需求的良性循环的必然结果。
然而,最终,所有这些华丽的词语归结为一点:今天的应用程序必须可靠地服务于大量用户。我们所称为云原生的技术和实践代表了构建一个可扩展、适应性强且具有弹性服务的最佳当前实践,使其能够胜任这一任务。
那么,这与 Go 有什么关系呢?事实证明,云原生基础设施需要云原生工具。在第二章中,我们将开始详细讨论这究竟意味着什么。