1.2 云原生软件简介
你的软件需要7×24小时提供服务。你需要频繁地发布新版本,以便快速满足用户的新需求。用户的移动应用需求和始终处于连接的状态,促使你的软件需要处理比以前更多、数量波动更大的请求。而连接设备(物联网)形成了一个空前庞大的分布式数据结构,需要新的存储和处理方法。这些需求,以及对一个运行这些软件的新平台的需求,直接导致了一种新的软件架构风格的出现,即云原生软件。
1.2.1 定义“云原生”
云原生软件的特点是什么?让我们先进一步分析一下前面的需求,看看它们会导致什么样的结果。参见图1.4,顶部列出了各个需求,接着展示了与各结果之间的关系。以下是详细内容:
■ 如果想让软件始终处于运行状态,必须对基础设施的故障出现和需求变更具有更好的弹性,无论这些故障和变更是计划内的还是计划外的。当所运行的环境经历了一些不可避免的变化时,软件必须能够适应这些变化。如果能够正确地构建、部署和管理软件的独立模块,它们的组合可以降低任何故障的影响范围,这会促使你采用模块化的设计。因为我们清楚没有一个实体可以保证永远不会失败,所以会在整个设计中都考虑冗余的情况。
图1.4 用户对软件的需求推动云原生架构和相应管理方式的发展
■ 你的目标是频繁发布新版本,但是单体软件做不到这一点,太多相互依赖的模块需要耗费大量时间和复杂的协作。近年来,各种实践已经充分证明,由更小、更松耦合、可独立部署和发布的组件(通常称为微服务)组成的软件,可以支持更敏捷的发布模型。
■ 用户不再局限于只能坐在桌子前使用计算机,他们要求使用随身携带的移动设备。非人体设备(nonhuman entity),例如,传感器和设备控制装置,同样是连接在一起的。这两种情况都会导致请求和数据量大幅波动,因此需要能够动态伸缩以及持续提供服务的软件。
其中一些属性表明了架构,即这样的软件应该由可冗余部署的独立组件组成,其他属性说明了交付软件的管理实践,即部署必须能够适应不断变化的基础设施,以及数量大幅波动的请求。如果我们把属性集合作为一个整体,然后对它进行分析,会得到如图1.5所示的结论:
■ 将软件拆分成一组独立的组件,冗余地部署多个实例,这就意味着分布式。如果冗余副本彼此部署得很近,那么受到本地故障影响的风险就会更大。为了能够高效地利用基础设施资源,当你想通过部署某个应用程序的多个实例来满足不断增加的请求数量时,必须能够将它们平均部署在可用的基础设施上,例如均匀部署在AWS、Google Cloud Platform(GCP)和微软Azure等云平台上。只有这样,才可以分布式地部署软件模块。
■ 可适应性软件的定义是“能够适应新的条件”,这里的“条件”指的是基础设施和相关的软件模块。它们本质上是联系在一起的,随着基础设施的变化,软件也在变化,反之亦然。频繁地发布意味着频繁地更改,而通过水平伸缩的运维方法来适应波动的请求,也意味着需要不断调整。很明显,你的软件和它运行的环境在不断地变化着。
图1.5 从架构和管理方面我们理解了云原生软件的核心特征:它是高度分布式的,必须在不断变化的环境中运行,并且软件本身也在不断地发展、变化
定义 云原生软件是高度分布式的,必须在一个不断变化的环境中运行,而且自身也在不断地发生变化。
云原生软件的开发涉及更多的细节(具体内容将在本书各章节中介绍),但最终,它们都会回归到这些核心特征:高度分布式和不断变化。这将是你在学习本书过程中的“口头禅”,我将不断向你重复高度分布式和不断变化的概念。
1.2.2 云原生软件的思维模型
Adrian Cockcroft曾经是Netflix的首席架构师,现在是AWS云架构战略(Cloud Architecture Strategy)的副总裁,他曾经谈到过操作一辆汽车的复杂性:作为一名司机,你必须控制汽车在街道上穿行,并且确保不与执行同样复杂任务的其他司机相接触。[7]之所以能够做到这一点,是因为你已经形成了一个思维模型。你在了解世界的同时,还可以在不断变化的环境中控制你的工具(在这里指的是一辆汽车)。
大多数人在开车时用脚来控制速度,用手来控制方向,两者协作决定了汽车的正常行驶。为了改善交通状况,城市规划者在街道布局上下了很大功夫。交通标志和信号灯等工具,再加上交通法规,共同提供了一个框架,你的旅程就是在这样的框架中从起点走到终点的。
开发云原生软件也是一件很复杂的事。在本节中,我将介绍一个模型来帮助你梳理开发云原生软件时涉及的大量问题。我希望这个框架能够帮助你理解关键的概念和技术,从而使你成为熟练的云原生软件设计和开发人员。
我将从简单的部分开始,使用一些你肯定已经熟悉的核心技术来开发云原生软件,如图1.6所示。
图1.6 一个基本的软件架构的常见元素
一个应用程序(App)会实现关键的业务逻辑,你将为此编写大部分代码。例如,你的代码将接收一个客户订单,验证库存中是否有可用的商品,并向账单部门发送一个通知。
当然,这个应用程序需要调用其他组件来获取信息,或者执行操作,我将它们称为服务(Service)。某些服务会存储状态—例如,库存状态服务,而其他一些服务可能是实现了系统中另一部分业务逻辑的应用程序,如客户账单等。
在了解这些简单的概念之后,现在让我们去构建一个表示云原生软件的拓扑图,如图1.7所示。你有一组分布式的模块,其中大多数模块都部署了多个实例。你可以看到大多数应用程序也都被当作服务,并且有些服务是明显有状态的。箭头表示了组件之间的依赖关系。
图1.7说明了几个有趣的地方。首先,请注意这些部分(方框,以及表示数据库或存储的图标)总是带有两个名称:方框表示应用程序和服务,而圆柱形存储图标表示服务和状态。图1.7中所示的简单概念,可以看作软件中各种组件所承担的角色。
图1.7 云原生软件在熟悉的概念基础上增加了极度的分布式、无处不在的冗余,而且处于不断的变化中
你会注意到,任何具有指向它的箭头的实体(表示组件是被另一个组件所依赖的)都是服务。没错,几乎所有的对象都是服务。即使拓扑图中的根应用程序也依赖于软件的消费者。当然,应用程序是指你需要编写代码的地方。我特别喜欢服务和状态的组合,这清晰表明了一些服务是没有状态的(你肯定听说过无状态服务,这里用“应用程序”表示),而其他服务都会涉及状态管理。
因此,我定义了云原生软件包含的三个部分,如图1.8所示。
■ 云原生应用程序—它依然是由你编写的代码构成的,实现了软件的业务逻辑。我们通过使用合理的模式,能够让这些应用程序之间形成良好的合力,因为很少有单个应用程序能够提供完整的数字化解决方案。应用程序位于箭头的起点或终点(或者两者都有),因此必须通过实现某些行为让它们参与到这种关系中。它们还必须能够支持云原生的运维能力,例如,水平伸缩和升级等。
■ 云原生数据—这是在云原生软件中存储状态的地方。图1.8也显示了云原生架构与以往架构的明显差异,过去的架构通常会使用一个集中的数据库来存储软件的大部分状态。例如,你可能在同一个数据库中存储用户配置、账户详情、评论、订单历史记录、付款信息等。云原生软件将代码分解成许多更小的模块(应用程序),同样数据库也被拆分成多个且分布式化。
■ 云原生交互—云原生软件是云原生应用程序和云原生数据的组合,这些实体之间的交互方式最终决定了数字化解决方案的功能和质量。由于我们的系统具有极度分布式和不断变化的特点,所以在许多情况下,这些交互很大程度上区别于以前的软件架构,甚至一些交互模式都是全新的。
图1.8 云原生软件模型中的关键实体:应用程序、数据和交互
请注意,尽管一开始我就谈到了服务,但最终它并不是这个思维模型中的三个实体之一。在很大程度上,这是因为几乎所有对象都是服务,包括应用程序和数据。但更重要的是,我认为服务之间的交互比服务本身更加重要。服务在整个云原生软件模型中无处不在。
建立了这个模型之后,让我们回到1.1节介绍的现代应用程序的需求,并考虑它们对云原生软件的应用程序、数据和交互的影响。
云原生应用程序
与云原生应用程序有关的问题包括以下几点:
■ 应用程序可以通过添加或删除实例来进行容量伸缩。我们将其称为水平扩容或水平缩容,它与以前架构的纵向伸缩模型有很大的不同。如果部署正确,应用程序的多个实例也可以在不稳定的环境中提供一定的弹性。
■ 一旦一个应用程序有了多个实例,当一个实例出现某种程度的故障时,将故障实例与整个应用程序集群隔离开,就可以让你更容易地执行恢复操作。你可以简单地创建一个新的应用程序实例,并将其与所依赖的有状态服务连接起来。
■ 当应用程序部署了许多实例,并且它们所运行的环境不断变化时,云原生应用程序的配置会面临独特的挑战。例如,如果一个应用程序有100个实例,那么将一个新的配置放到一个已知的文件系统位置,然后重新启动应用程序的日子就一去不复返了。我们无法用这种传统的方式将配置更新到实例中,因为它们在分布式环境中会到处移动。
■ 基于云环境的动态特性要求你改变管理应用程序生命周期的方式(不是软件交付的生命周期,而是应用程序启动和关闭的过程)。你必须重新审视如何在新的上下文中启动、配置、重新配置和关闭应用程序。
云原生数据
虽然应用程序是无状态的,但是处理状态也是软件解决方案中重要的一部分,并且在极度分布式和不断变化的环境中也需要解决数据处理问题。因为在流量波动的时候也需要持久化数据,所以在云环境中处理数据会面临独特的挑战。云原生数据问题包括以下几个方面:
■ 需要打破数据统一管理的观念。在过去的几十年里,各类企业投入了大量的时间、精力和技术力量来管理大型、统一的数据模型。因为在许多领域存在类似的概念,所以许多软件系统也按照管理单个实体来实现。例如,在医院中,“患者”这一概念与许多场景相关,包括临床/护理、收费、服务调查等,所以开发人员通常会创建一个模型(一个数据库)来管理患者信息。但是,这种方法不适用于现代化的软件,它发展缓慢而且脆弱,并且缺少松耦合架构的敏捷性和健壮性。你需要创建一个分布式的数据架构,以创建一个分布式的应用程序架构一样的方式。
■ 分布式的数据架构由一些独立、适用于特定场景的数据库(支持多种持久化方式),以及一些数据源来自其他地方、仅用于数据分析的数据库组成。缓存就是云原生软件中的一种关键模式和技术。
■ 当多个数据库中存在多个实体(例如,前面提到的“患者”)时,必须解决公共信息在多个不同实例之间的同步问题。
■ 最终,分布式数据架构的核心就是将状态作为一系列事件的结果来处理。事件源模式会捕获状态更改的事件,将事件收集并制成统一日志分发给其他数据成员。
云原生交互
最后,当你将所有这些部分组合在一起时,又会产生一组新的云原生交互问题:
■ 当一个应用程序有多个实例时,需要某种路由系统才能够访问它们。这势必会使用到同步的请求/响应模式,以及异步的事件驱动模式。
■ 在高度分布式、不断变化的环境中,必须考虑访问失败的情况。自动重试是云原生软件中的一种常用模式,但如果管理不当,这可能会对系统造成严重破坏。当应用自动重试模式时,断路器是必不可少的。
■ 由于云原生软件是多个服务的组合,所以单个用户请求会涉及调用大量相关服务。如果想合理地管理云原生软件,确保提供良好的用户体验,就必须管理好组合中每个服务及它们之间的交互。像指标、日志这种我们已经开发了几十年的东西,必须针对新的架构加以调整。
■ 模块化系统最大的优点之一是其各个部分能更容易独立地演化。但是,因为这些独立的部分最终会组合成一个更大的整体,所以它们之间的底层交互协议必须适合云原生环境,例如,一个支持并行部署的路由系统。
本书介绍了满足这些需求的新的模式和实践。
让我们通过一个具体的例子,来更好地理解这里提出的问题,也希望各位读者能够了解我的思路。
1.2.3 云原生软件实战
让我们从一个熟悉的场景开始。假设你在Wizard银行有一个账户。你偶尔会去当地的分行办理业务,同时你也是该银行的网上银行注册用户。在过去一两年的大部分时间里,你都只能通过家里的固定电话接听来电(还是假设)。有一天你终于决定改变这一情况。因此,你需要更新在银行(以及许多其他地方)登记的电话号码。
网上银行允许你编辑自己的用户信息,其中包括主要电话号码和任何备用电话号码。登录站点后,你将被导航到“用户信息”页面,输入新的电话号码,然后单击“提交”按钮。随后你会收到修改完成的确认信息,于是这次用户体验到此为止。
让我们来看看,如果这个网上银行应用程序采用云原生的架构会是什么样子。图1.9描述了如下关键问题:
■ 因为还没有登录,所以当你访问“用户信息”(User profile)应用程序时,它会将你重定向到“身份验证”(Auth)应用程序。注意,每个应用程序都部署了多个实例,所以用户请求会通过一个路由器被发送到其中一个实例。
■ 作为登录的一部分,身份验证应用程序会在一个有状态的服务中创建和存储一个新的“身份令牌”(Auth token)。
■ 系统使用新的身份验证令牌将用户重定向回“用户信息”(User profile)应用程序。这一次,路由器会将用户请求发送到“用户信息”(User profile)应用程序的另一个实例。[提醒:在云原生软件中不要使用黏性会话(sticky session)!]
■ “用户信息”(User profile)应用程序会通过调用“认证API”(Auth API)服务来验证身份令牌。同样,因为有多个实例,所以请求会被路由发送到其中一个实例。回想一下,有效的身份令牌会被存储在一个有状态的“身份令牌”(Auth token)服务中,不仅“身份验证”(Auth)应用程序可以访问,“认证API”(Auth API)服务的任何实例都可以访问。
■ 这些应用程序(“用户信息”和“身份验证”)的实例可能会由于各种原因而发生改变,因此必须有一个协议能够不断用新的IP地址来更新路由器。
■ “用户信息”(User profile)应用程序向“用户API”(User API)服务发出一个下游请求,来获取当前用户的个人信息数据,包括电话号码。然后,“用户信息”(User profile)应用程序会向用户的有状态服务发出一个请求。
■ 在用户更新了电话号码并单击了“提交”按钮之后,“用户信息”(User profile)应用程序会将新数据发送到一个“事件日志”(Event log)中。
■ 最后,“用户API”(User API)服务的一个实例会接收并处理这个更改事件,并向Users数据库发送一个写请求。
图1.9 网上银行软件由应用程序和数据服务组成。其中用到了多种交互协议
虽然上面已经列出很多内容了,但我还想再增加一些。
当你再次来到银行分行,在银行柜员核实你的当前联系方式时,你会希望柜员有你更新后的电话号码。但是网上银行软件和柜员使用的软件是两个不同的系统。这个设计是没问题的,它提供了敏捷性、弹性和许多我认为对现代系统很重要的优点。图1.10展示了完整的产品架构。
银行柜员软件的结构与网上银行软件并没有明显的不同,它也是云原生应用程序和数据的组合。但是,你可以想象到,每个数字解决方案都要处理甚至存储用户的数据,或者说是客户的数据。在云原生软件中,即使是数据处理也会更倾向于松耦合。这体现在网上银行软件中的用户(User)状态服务和银行柜员软件中的客户(Customer)状态服务。
图1.10 虽然Wizard银行的各个软件是独立开发和管理的,但是在用户看来就是一个系统
因此,我们的问题是,如何协调这些不同存储位置中的公共数据?你的新电话号码将如何显示在银行柜员的软件上?
在图1.11中,我向模型中添加了另一个概念,我将其称为“分布式数据协调”。这里的描述并不意味着任何实现细节。我并不是建议使用一个规范化的数据模型、一主多从式的数据管理技术,或者任何其他解决方案。目前,这只是对问题的一个描述,我们很快会来研究如何解决问题。
图1.11 分散的、松耦合的数据架构需要将数据聚合管理
前面已经介绍了很多知识,图1.9、图1.10和图1.11中的内容都很多,你可能还不能很好地理解。不过没关系,我希望你了解的是云原生软件的一些关键概念:
■ 云原生软件架构包含很多组件。
■ 有一些专门用来处理系统变更的协议。
我们将在接下来的章节中深入讨论所有的细节。