如果你稍微留意观察就会发现,最近几年很多新锐公司其实都是“诞生在云上的公司”,这些公司也就是所谓的“云上原住民”。他们的技术和业务都是构建在云上,并借助云原生的优势快速成长。而这些业务就是由一个个的云原生应用组成的,因此要讲明白云原生,就得弄清楚什么是云原生应用(application)。
一句话来说,云原生应用的核心便是容器、函数和数据。
云原生应用首先是一个分布式系统,也就是说这些应用服务往往运行在不同的机器上,将计算任务分配到不同的机器,更加高效,可扩展。
分布式系统首要考虑的应当是网络间的通信,因为网络是不安全的,传输也是有成本的,并且存在延迟。
正如CAP定理指出,任何一个通过网络连接的、共享数据的分布式系统最多只能满足以下三个需求中的两个:
现实情况,在分布式系统中,分区故障比较常见。我们设计应用时需要在追求一致性还是高可用之间做出取舍,例如很多NoSQL数据库会选择高可用,而关系型数据库会为了ACID原则追求一致性。
本文主要讲解云原生应用的原理、组成,以及如何去设计、构建和运维一个成功的云原生应用程序。
一、云原生原理和组成
容器
容器最初的想法是想将操作系统分割成几块互相不干扰,可以安全运行程序的区域。Linux内核空间就提供了命名空间(namespace)和控制组(control group)来实现这个功能。
但是直接调用系统内核对于开发者来说并不方便,因此便引入了Linux容器技术(LXC),它极大地简化了应用程序与系统内核交互的复杂性,也是现在人们所熟知的容器的底层技术。
而Docker正是将复杂的内核功能封装成了对开发者非常友好的组件,才使得容器真正流行开来。在Docker看来,容器是经过封装的,可以独立部署的一个组件,这个组件通过系统级别的虚拟技术使其可以作为一个独立的实例来运行并和其他实例共享一个系统内核。
容器使用写时复制(copy-on-write)的文件系统策略,允许多个容器共享数据,只有当容器需要修改或者写入新数据时,操作系统才会复制一个数据的副本。
从内存和空间使用上来看,容器是非常轻量级的,这也是为什么容器可以快速地启动。而快速启动非常适合需要横向扩容的场景,比如云原生应用。
容器是基于操作系统虚拟化技术,共享一个操作系统内核,尽管与虚拟机提供的基于硬件虚拟化技术的隔离相比还有一定差距,但是这种系统级别的隔离在大部分情况下已经足够了。
动态地管理容器的生命周期需要一个容器编排工具,毫无疑问,Kubernetes是目前最流行的一款集群管理及容器编排工具,关于Kubernetes我们会专门详细介绍。
无服务器架构意味着服务的伸缩以及底层的基础架构都是由云服务提供商来管理的。所有管理和运维操作都被剥离了出来,交由云服务提供商来解决。
从开发者的角度来看,无服务器架构通常会伴随着事件驱动的编程模型,而从经济成本角度看,无服务器架构意味着你只需要按耗费的资源付费,比如消耗的CPU时间。
函数计算通常指函数即服务产品,比如AWS Lambda。一个函数就是一个可执行单元,这意味着这段代码有一个起始状态和一个结束状态。一个函数通常是由其他函数或平台服务发出的事件触发的。举个例子,当数据库或者事件服务增加了一条记录时可以触发一个函数。
微服务架构是一种面向服务的架构体系,其中应用程序按功能分解为小型的、松耦合的各种服务。其重点在于,单个服务被划分的足够小,相互间耦合度很低,并围绕业务功能进行分解。
微服务架构经常与巨石架构相比较。在巨石架构中,通常只有一份代码库,共享一个数据库和数据结构,而在微服务架构中,应用程序由多个较小的代码库组成,由独立团队开发和管理。
每个服务都专注于一个特定的任务,由一个小团队负责开发和运营。这些服务在独立的进程中运行,相互之间通过基于同步或异步消息的API进行通信。
每个服务都可以被视为一个独立的应用,有独立的团队、测试、开发、数据和部署。
二、云原生应用的设计
云原生应用从上层架构来看,包括精益运营、安全性、可靠性和可用性、可扩展性和成本这个五大支柱,我们主要从以下几个技术实现层面来讲讲。
因为API是其他服务用来与你的服务进行通信的接口,因此正确地记录和版本化API至关重要
三种策略:
API只有一个版本,API的调用者永远只调用最新的API。当API接口发生更改时,所有使用者也需要跟着改。对于调用者而言,这是最昂贵的方法,因为每次发布新的API版本时,他们都必须升级。
所有版本的API都在正常运行,每个调用者都使用他们需要的版本。调用者可以根据需要迁移到新版本。与无版本相比,这对调用者来说是一个更好的策略,但是对于API开发人员而言,维护较旧的API版本成本很高。
所有调用者都使用最新的API版本。旧版本的API会被舍弃,但是最新版本的API是向后兼容的。
研究结果表明,兼容性版本控制策略是最高效的。尽管对API的开发者而言,它确实会带来一些额外的工作,因为需要保持向后兼容性。
REST本身不提供任何特定的版本控制约定,但有三种方法来实现版本控制:全局版本控制、资源版本控制和基于mime的方法。这些方法中的每一种都有其优点和缺点,这里没有明确的最佳方法。
迄今为止,使用语义版本号几乎已经是一种标准做法了。语义版本号(major.minor. patch)可以清晰地告诉你应该增加版本号的哪一部分:
网络和服务通信是分布式系统最基础的话题,它们对一个应用的性能起着至关重要的作用。因此,理解各种服务间的通信方案对于你设计和构建云原生应用是非常有帮助的。从大致上来讲,你可以把服务间的通信分为两类,一类是外部服务通信,另一类是内部服务通信
大多数情况下,HTTP协议会被用来作为客户端和云原生应用程序之间的通信协议。但是,它并不是性能最高的协议。
一个大型的微服务架构下的应用程序可能由数百甚至数千个微服务组成,服务越多,需要进行的通信和数据交换就越多。因此,所选择的协议成为影响性能的重要因素,并且更改生产环境下的服务通信协议的代价可能会相当大。
其中websocket、http2/2、gRpc等几个常用的协议,它们已经被证明可以为云原生应用带来更好的性能。
云原生应用经常会和事件驱动和基于消息的架构结合起来,消息传递协议有很多,如STOMP、WAMP、AMQP和MQTT等,我们就不在此一一介绍了。
除了协议外,数据的序列化和反序列化也会对整体性能产生影响,在最糟糕的情况下,它甚至可能会成为瓶颈。
JSON可能是目前使用最广泛的格式。JSON可读性强、自包含且易于扩展,但是它同样会占据相当可观的内存空间,并且在大数据量的情况下,序列化和反序列化的操作可能会很产生很高的开销。
无论你是使用同步还是异步的通信方式,都需要确保如果一个相同的操作被重复执行了多次,目标系统中的结果仍将保持不变。能够多次执行同一个操作而不改变结果的特性被称为幂等性。
由于接收方的故障、重试策略等原因,消息可能被重复接收和处理。理想情况下,接收方应以幂等方式处理消息,这样即便消息被重复也不会导致不同的结果。
假设有一个可穿戴设备,它将一些健康数据发送到一个队列中,然后服务端会从队列中读取数据,将其添加到个人健康计分卡中。下面是该设备提交的一个消息的示例:
{ "heartrate": { "time": "2020020307300", "bpm": "89" }}
我们进一步假设由于某些网络故障该操作失败了,接收方无法接收该消息,因此基于重试机制,该设备再次发送了相同的消息。最终,你得到两个相同的消息。如果接收者现在收到这两个消息并同时处理它们,则心率将显示为178bpm,这可能会使大多数人感到担忧。
为避免这样的情况发生,你需要把这样的操作变成是幂等的。确保幂等性操作的一种常见方法是在消息中添加唯一的标识符,并确保仅当标识符不重复时,服务才对消息进行处理。以下是相同消息的示例,但添加了标识符:
{ "heartrate": { "heartrateID": "124e456-e89b-12d3-a456-42665544000" "time": "89" }}
现在,接收方可以在处理该消息之前先检查消息是否已经被处理过。这通常也被称为去重操作。这个原则同样适用于数据更新的场景。这里最核心的意思是你应该将操作设计为幂等的,以便可以重复执行每个操作而不会导致系统的异常。
在微服务和函数计算的世界中,客户端所需的功能通常分布在多个服务和函数上。客户如何知道要请求的服务的接入点是什么呢?此外,如果将现有服务重新部署到不同的接入点或引入新的服务要怎么办?
概括而言,网关可以分为两大类:API网关和应用程序网关。后者不一定与API有任何关系,它们通常用于安全套接字层(SSL)的终结,路由静态资源(HTML、CSS文件等),或路由到对象存储。
路由是网关最常见的功能之一。在这种情况下,网关充当一个反向代理,并将传入的请求路由到后端服务,反向代理通常位于内网中,负责管理用户请求,将其导向正确的后端服务
当客户端需要通过一个统一的接入点进行通信时,该模式很有用。网关负责根据IP、端口、标头或URL将请求路由到对应的各种服务上。因为只需要使用单个接入点,这样就简化了客户端需要实现的逻辑。
实现网关有很多种技术和方法。最受欢迎的网关代理是NGINX、HAProxy和Envoy。所有这些都是反向代理,提供负载平衡、SSL和路由等功能。这些产品都在许多生产环境中经过了实战检验。
在云原生的世界中,每个服务都是独立构建和部署的,并且每个服务都可能与其他微服务通信。随着业务的发展,你会开发越来越多的微服务,这也意味着服务之间的通信会增加,并且也会变得更加复杂。
服务间的通信对云原生应用而言很重要,因此你的每个服务都需要具有弹性,并且能够不受任何网络问题的影响。
你需要用一套方法来实现请求重试、超时定义、断路器等机制。写一个具有完善通信功能的库来实现这些是一种方法,但是如果服务是用不同的编程语言来实现的,那这种办法可能对你没有太大帮助。
你可以选择分别为每种语言重写一遍这个库,你最终将得到的是一堆服务,这些服务包含着一部分相同的功能,这些功能是用不同的语言重复实现的。
三、架构示例
在这个示例中,用户可以管理和查看他们家中各种类型设备的信息。这个服务必须能够支持大量并且还在持续增长的房屋、用户和设备。设备的类型也在持续增多,而且家中的设备也会随着用户增加或者更替智能设备而发生变化。
用户可以在任何能够联网的地方,通过单页应用(SPA)或移动应用来管理设备。用户还能收到设备发出的告警,或者云端服务发现的设备告警。
此外,他们还同意设备发送匿名数据给这个服务,以便进行数据分析。这个服务还需要能够满足日益壮大的对集成应用和云端服务感兴趣的开发者社区和家居自动化爱好者的需求。
如下图中展示的那样,设备与云端的服务相连接。设备会以一定的时间间隔往云端服务持续发送大量的遥测数据,同时它们也会从云端获取用户或者其他事件发送来的指令。
用户也可以通过移动应用或者网页连接到云端服务,然后管理和查看家中的设备信息。数据被发送到云端后通常会被存储下来,然后被批量处理和分析。
进一步看下图中有关设备遥测数据存储和分析的服务,可以发现,数据会通过不同的路径传输然后被处理。这种把数据流通过热、温、冷的路径分开处理的架构被称之为lambda架构。
云服务提供商的设备管理服务可以用来让设备连接到云端,AWS IoT Core和Google Cloud IoT Core都是这样的服务
设备也可以通过Web API连接到云后端,但是这样会导致后端服务构建和运维的服务不太理想。这会提高服务的总体成本,也可能造成发布延迟。云原生的一个原则是尽量多使用已有的云服务。
设备通过云服务供应商的设备管理服务来发送遥测数据。遥测数据被写入数据流,它们被不同的订阅者获取。每个订阅者都可以用不同的频率按自己的方式来处理数据流,订阅者之间都是相互独立的。
如下图所示,云服务供应商配置的服务将收到的数据流处理后存储到对象存储中,这有时被称为冷路径。
对象存储价格便宜,并且可以用最少的基础设施和运营成本长时间保留大量设备和用户的数据。然后可以晚点再分析这些数据,并且可以通过长的时间跨度或者根据大量设备来做一些趋势性分析。
另一个订阅者将数数据处理后保存到时序数据库服务中,如Amazon Timestream或者Google BigTable。这些数据被用于更近乎实时的批处理分析,并显示过去一小时到数天内的设备遥测数据。
然后,随着时间的流逝,这个服务中的数据将自动移至速度较慢但是价格比较便宜的数据存储中,并对数据进行下采样,因为随着时间的推移,数据保真度在该数据存储中不那么重要了。
在某个时间点,数据会过期,并且不用再保留在数据存储中。如果需要查询超出时间范围的历史信息,那么系统需要从冷存储中加载它。
可以设计一个从冷存储中获取数据然后重新注入时序存储中的流程,这样可以简化应用读取数据的过程。另一个订阅者负责处理流中的数据,以执行复杂的事件处理或者流分析。
该热路径可以在接收到数据后的很短的一段时间内判断设备状况,时间通常从毫秒到分钟不等。当设备状态接近临界点时,它可以用来生成警报,并发送给用户。
这个智能家居设备管理服务包括一个后端API,供有兴趣与该服务集成的开发人员使用,也会由客户端(移动设备和单页应用)使用。
下图说明了API如何由多种服务组成,其中一些是在Kubernetes集群中运行的容器,而另一些是在云服务供应商的FaaS平台上运行的函数。
团队首选的计算模型是FaaS(函数即服务),但是有一些工作任务是长期运行的或具有复杂的环境要求,有些团队更喜欢容器。应该鼓励各个团队使用最适合实现其需求的计算模型。一些服务通过Kubernetes的Kubelet使用CaaS计算模型来运行某些Kubernetes任务。
API网关用于减轻一些API管理任务。API网关负责对请求进行鉴权并限制发送过多请求的用户,以维护所有使用该服务的用户的服务质量。
下图显示了一个单页应用(SPA),这个应用通过内容分发网络(CDN)为用户提供服务,其背后的数据源来自块存储服务。
SPA通常由静态资源组成,这些静态资源可以通过块存储进行存储并提供给用户。CDN可以使客户端快速加载这些静态资源,因为它们被缓存在靠近客户端的地方。
SPA必须具备缓存清除的技术,例如如果资源已更改,那么需要更新哈希值,或者在将更新推送到存储时使CDN缓存中对应的资源失效。这些任务可以在持续交付的流程中实现。
参考书籍《Cloud Native——Using Containers, Functions, and Data to Build Next-Generation Applications》
文丨Soundhearer
图丨来源于网络