文章目录
云原生概念
概念发展
云原生没有一个十分确切的定义,因为它一直在发展变化
- 云原生的概念最早开始于2010年,在当时 Paul Fremantle 的一篇博客中被提及,他主要将其描述为一种和云一样的系统行为的应用的编写,比如分布式的、松散的、自服务的、持续部署与测试的。当时提出云原生是为了能构建一种符合云计算特性的标准来指导云计算应用的编写。
- 2013年 Matt Stine在推特上迅速推广云原生概念,并在2015年《迁移到云原生架构》一书中定义了符合云原生架构的特征:12因素、微服务、自服务、基于API协作、扛脆弱性。而由于这本书的推广畅销,这也成了很多人对云原生的早期印象,同时这时云原生也被12要素变成了一个抽象的概念。
- 2015年由Linux基金会发起了一个 The Cloud Native Computing Foundation(CNCF) 基金组织,CNCF基金会的成立标志着云原生正式进入高速发展轨道,google、Cisco、Docker各大厂纷纷加入,并逐步构建出围绕 Cloud Native 的具体工具,而云原生这个的概念也逐渐变得更具体化。因此,CNCF基金最初对云原生定义是也是深窄的,当时把云原生定位为容器化封装+自动化管理+面向微服务
- 2017年, 云原生应用的提出者之一的Pivotal在其官网上将云原生的定义概况为DevOps、持续交付、微服务、容器这四大特征
- 2018年,随着Service Mesh的加入,CNCF对云原生的定义发生了改变,基于容器、服务网格、微服务、不可变基础设施和声明式API构建的可弹性扩展的应用;
基于自动化技术构建具备高容错性、易管理和便于观察的松耦合系统;
构建一个统一的开源云技术生态,能和云厂商提供的服务解耦,
云计算与云原生
- 云计算是一种模型。它可以实现随时随地、便捷地、随需应变地从可配置计算资源共享池中获取所需的资源(例如网络、服务器、存储、应用及服务),资源能够快速供应并释放,使管理资源的工作量和与服务提供商的交互减小到最低限度。
- 云原生(CloudNative)是一个组合词,Cloud+Native。Cloud表示应用程序位于云中,而不是传统的数据中心;Native表示应用程序从设计之初即考虑到云的环境,原生为云而设计,在云上以最佳姿势运行,充分利用和发挥云平台的弹性+分布式优势。CloudNative也就是云计算和土著的意思——云计算上的原生居民,即天生具备云计算的亲和力。
总结来说,云原生是基于云计算技术,与普通的云概念区别在于,云与“哪里”有关,云原生则是“如何应用”,当然“如何应用”不是说简单将服务部署到云上,而是考虑如何将我们的服务设计为更符合任意云部署的架构
云原生软件架构设计
在了解了云原生基本概念后,那软件如何才能做到“云原生”呢?在实现云原生之前,我们不妨先了解一下云原生的上下文,这有助于我们明白如何去实现
云原生上下文
云原生软件简介
云原生软件产生背景
在互联网时代,我们总是希望自己的软件可以:
- 终处于运行状态
- 频繁发布新版本
- 承受住大量的请求和大幅波动的数据量,能够动态伸缩以及持续提供服务的软件
云原生软件特征
因此云原生软件有以下特征:
- 高度分布式,原先的单体软件拆分为一组独立的组件,冗余地部署多个实例(平均部署在基础设施上,降低地区设施宕机影响)
- 必须在不断变化的环境中运行,并且软件本身也在不断地发展、变化(即使遇到基础设施不断变化甚至发生故障的情况,云原生应用程序依然可以保持稳定)
总结云原生软件核心特征就是:高度分布式和不断变化
云原生软件三大关键实体
云原生应用程序
依然是由你编写的代码构成的,实现了软件的业务逻辑,在云原生中你的应用程序应该关注以下问题
- 应用程序可以通过添加或删除实例来进行容量伸缩。即水平扩容或水平缩容,为应用程序在不稳定的环境中提供一定弹性
- 应用某个实例故障时,补救措施(故障实例隔离、新实例运行)
- 应用多实例部署时程序配置问题
- 基于云环境的动态特性要求你改变管理应用程序生命周期的方式(不是软件交付的生命周期,而是应用程序启动和关闭的过程)。你必须重新审视如何在新的上下文中启动、配置、重新配置和关闭应用程序。
云原生数据
云原生软件将代码分解成许多更小的模块(应用程序),同样数据库也被拆分成多个且分布式化。
云原生交互
交互关注问题:
- 应用多实例访问问题,这势必会使用到同步的请求/响应模式,以及异步的事件驱动模式
- 访问失败后补救措施,重试及断路器等
- 由于云原生软件是多个服务的组合,所以单个用户请求会涉及调用大量相关服务,为保证用户体验应用服务之间交互响应指标及日志必须加以调整
- 模块化系统最大的优点之一是其各个部分能更容易独立地演化。但是,因为这些独立的部分最终会组合成一个更大的整体,所以它们之间的底层交互协议必须适合云原生环境
云平台
云平台是云原生软件开发的基础,目前大型的云服务商,可选的有Google App Engine、AWS ElasticBeanstalk和Azure App Service(它们都没有得到特别广泛的使用)。CloudFoundry是一个开源的云原生平台,在全球范围内的大型企业中拥有众多的客户
想了解云平台可自行搜索相关知识,这里就不展开了
云原生模式
好了,到了最重要的如何实现云原生了,即如何设计才能让我们的软件更符合部署云平台,充分发挥云平台的优势
注:接下来的模式介绍都基于一个假定项目——我的全球食谱,同时环境也假设是在云平台下
网站将会列出用户最喜欢的美食博主的帖子,它们是由用户所关注的人和这些人所发布的帖子聚合而成的
如图:项目由web页面+相关帖子服务+关系服务+帖子服务构成
1、事件驱动微服务
云原生软件的主要实现之一是微服务,必须小心避免将各个部分连接得太紧或者太早,导致又出现一个庞大的单体系统,所以软件架构中的微服务交互方式很重要,请求/响应的方式or事件驱动的方式or二者结合。请求/响应的方式,客户端会发出一个请求并期待一个响应。尽管请求者可以允许该响应异步到达,但对响应到达的预期本身就在一个响应和另一个响应之间建立了直接的依赖关系。事件驱动的方式,事件的消费方可以完全独立于事件的生成方。不同的自治性是这两种交互方式之间差异的核心。
使用了请求/响应的方式我的食谱项目:
这样虽然能达到项目期望目标(列出我所关注的人发表的文章),不过它相当脆弱。为了产生最终的结果,需要启动和运行关系微服务和帖子微服务。网络也必须足够稳定,从而保证所有的请求和响应都可以被接收。相关帖子微服务的正常运行,在很大程度上依赖于许多其他的因素,因此它并不能真正掌控自己的命运。
事件驱动架构在很大程度上是为了解决系统过于紧耦合的问题而设计的。现在让我们来看一个这方面的实现,它既满足了相关帖子微服务的需求,又具有不同的架构和弹性。
使用事件驱动后的项目:
由图可以看出,以相关服务帖子和帖子为例,这两个服务之间是松耦合的,每个服务都可以自动执行。帖子微服务在收到一个新帖子时会执行相关任务,并产生另一个事件。相关帖子微服务在处理请求时,只需要查询其本地的数据存储。
对于请求/响应的方式,聚合发生在用户发出请求的时候。而对于事件驱动的方式,聚合发生在系统中数据发生变化的时候
2、应用程序冗余
云原生软件核心是冗余——即水平伸缩 应用多实例部署,因为是同个应用,所以需要让多个实例从逻辑上看上去是一个实例,以保证不同实例处理同个请求会输出同样结果,虽然这看起来很简单,但在实际场景中可能会有点棘手,因为每个应用程序实例运行的环境都是不同的。调用应用程序的输入(我在这里没有指明使用何种模式,因此请求/响应或者事件驱动都有可能)并不是影响结果的唯一因素。每个应用程序实例运行在自己的容器中,这可能是一个JVM、一台主机(虚拟机或者物理机),或者一个Docker(或者类似技术的)容器,并且环境参数也会影响应用程序的执行。应用程序的配置也会影响每个应用程序的实例。这一小节将注重如何消除请求历史对结果的影响
即使可能受到不同外部因素的影响,云原生应用程序必须保证具有一致的结果
水平伸缩
在云计算环境中,增加或减少应用程序的容量来应对变化的流量,我们将之称为水平伸缩。这里指的不是增加或者减少单个应用程序实例的容量(垂直伸缩),而是通过添加或者删除应用程序实例,来增加或者减少能够处理的请求数量。同时会极大降低基础设施故障导致整个项目宕机的风险
有状态服务无状态化
有状态服务非常常见,如平电商平台一些购物车列表访问前需要提供身份认证。这里也以【我的世界食谱】为例,要求连接相关帖子服务的客户端,但在提供任何内容之前先进行身份验证,即需先登录。
假设现在现在相关帖子服务存在两个实例1和2,用通过登录,将登录信息保存在相关帖子服务1中,在后面访问中当请求被发送到实例1时,能正常获取信息,但是当请求被路由到实例2时,缺无法获取信息,因为实例2并没有用户登录相关信息
解决方案一:粘性会话
这也是目前相对广泛的“解决方案”,黏性会话是一种实现模式,在这种模式中,应用程序在响应用户的第一个请求时会包含一个会话ID,也可以称为该用户的指纹。然后,通常在用户后续的所有请求中,都会在cookie中包含该会话ID。这使得负载均衡器可以跟踪与应用程序交互的个人用户。负责分发请求的负载均衡器,会记得第一次访问的是哪个实例,并且会“尽力”将携带相同会话ID的请求发给这个实例。如果应用程序实例有一个本地状态,那么不断路由到该实例的请求也可以使用这个本地状态。
注意:这里的“尽力”,尽管尽最大努力,路由器可能还是无法将请求发送到“正确的”实例。因为该实例可能已经消失,或者由于网络异常而无法访问
而且在云原生中,这种“尽力”带来的弊端越来越明显,因为在云原生中应用程序实例的回收已经变得越来越普遍,因为基础设施会面对很多意外或者人为的更改,而且这样通过粘性去访问同个实例,会增加该实例的访问压力,也符合负载均衡的初衷
解决方案二:将会话状态持久化到一个共享的存储中
解决这个问题的正确方法是,让应用程序变成“无状态”,最终是让整个服务有状态,而应用程序无状态。以【我的世界食谱】为例,可以引入一个键/值存储,用来验证登录令牌是否有效,并将应用程序绑定到一个有状态的服务。
访问:
架构图:
3、应用程序配置
知道了如何消除请求历史对输出结果的影响后,现在看看如何消除系统环境和应用程序配置的影响。
注:这里说的配置是基于云原生环境,和常见编程框架中的属性文件有所不同。云原生应用程序比以前更加分布式。例如,我们倾向于通过启动应用程序的多个实例,来满足不断增加的流量,而不是将更多的资源分配给单个应用程序
在云原生十二要素中,第三点便是“在环境中存储配置”。这种方法的部分依据是,实际上所有的操作系统都支持环境变量的概念,并且所有编程语言都提供了访问它们的方法。这不仅有助于利用应用程序的可移植性,而且还可以形成统一的运维基础,而无须在意应用程序运行在哪种系统上。
- 可以使用Kubernetes等云原生平台,将环境配置的值传递给应用程序
- 应该像对待源代码一样对待应用程序的配置:在源代码仓库中管理配置、进行版本控制和访问控制。
- 配置服务器(例如,Spring Cloud Configuration Server)可以用来注入应用程序的配置值。
4、应用程序生命周期
应用程序的生命周期看起来非常简单:应用程序被部署、启动、运行一段时间,最后关闭。但应用程序的生命周期指的是应用程序已经准备好进行生产部署之后,要经历的所有阶段。它的核心关注点不是软件开发或者管理,而是应用程序本身的状态。应用程序是否已经部署?是否还在运行?是否停止(因为故障或人为停止)?
单个应用服务升级
以【我的世界食谱】为例,给服务加一个用于验证身份的密码。初始密码为theFirstSecret,现在需要升级为theSecondSecret,因为不想关掉整个服务再重启的方式升级,即想零停机实现升级,我们很容易会想到用spring框架(假设这个项目是用spring框架实现)中的启用/refresh端点,当它被调用时会刷新应用程序的上下文,而不需要完全重新启动应用程序。但是问题是当运行一个应用程序的多个实例时,云原生应用程序架构会让这些实例作为一个逻辑上的应用程序来响应结果,所以这个命令请求最终只会升级其中一个实例,这里假设是实例2.
如图,只升级成功了实例2,因此请求的结果会有很大的不同,这取决于请求是被路由到第一个实例还是第二个实例
现在服务版本零停机升级比较常见的有:
- 蓝绿升级:
- 滚动升级:
任何允许滚动升级的应用程序都可以通过蓝/绿升级的方式部署。但是,反过来则不成立。要使用滚动升级,应用程序的设计必须允许同时运行不同版本的实例和配置,而对于蓝/绿升级,一次只需提供一个版本或者配置,因此不需要这种特殊处理。
明显上述升级问题可以使用蓝绿升级解决
协调多个不同的应用程序升级
虽然上述的问题解决了,但是在云原生中服务之间往往都是互相影响的,应用程序生命周期事件不仅影响所在的应用程序,还影响其他依赖于它们的应用程序。例如,因为相关帖子服务是帖子服务的一个客户端,所以你可以看到一个应用程序的生命周期事件,会如何影响其他相关的应用程序。在这个特定的场景中,如果改变帖子服务的密码,还需要改变相关帖子服务的密码。
所以需要精心设计应用程序,以便让不同应用程序的生命周期事件可以独立进行,同时保持软件功能正常且没有停机时间。
如果你首先更新了帖子服务,那么相关帖子服务发送的请求会失败,因为它依然使用的是旧的密码。如果你首先更新了相关帖子服务,那么它会将新的密码发送给帖子服务,请求依然会失败。我们已经确定,你不可能在同一时间更新它们,而不导致停机。
简单解决方案:密码轮换模式,增加一个密码列表,实现一个分阶段更新密码的方法
5、服务、路由和服务发现
服务发现
在没有使用服务发现时,须通过硬编码指定服务ipUrl,如下图中的左边
Kubernetes提供了一种简单及优雅的方法,来指定一个服务的实例,即标签(tag)和选择器(selector)。帖子服务的每个实例都使用了键/值对app:posts作为标签。服务是通过一个选择器来定义的,指定该服务代表了一组由app:posts标签所标记的实例列表。如上图右边
右部分的元素:如何保持应用程序实例列表的最新状态,以及如何将流量路由到这些实例呢?常见的有两种方法:服务端负载均衡和客户端负载均衡
- 服务端负载均衡
在该模式的部署实现中,有一个组件会接收传入的请求,然后将这些请求发送到一个相应的实例中
- 客户端负载均衡
将负载均衡组件放在客户端中,客户端的每个实例都内置了自己的负载均衡功能
路由刷新
上图中的路由表如何时刻保持最新呢,答案是控制循环,它的工作是不断地评估部署的实际状态,并确保路由表反映了这一事实。你的云原生平台会提供所需的核心功能,而你的工作是确保应用程序提供平台所需要的信息。对于路由刷新,这意味着两件事情:(1)提供信息,使平台能够建立系统实际状态的一个准确模型,以及(2)提供一种方法,让平台可以识别哪些是服务的实例。
6、交互冗余
请求重试
从定义上讲,云原生软件就是一个分布式的系统。在以前的代码中,调用函数只是一个方法调用,并且所有操作都在同一进程中进行。如今,这些调用都是通过在网络上发送的请求来实现的,但是网络并不总是可靠的。即使网络正常,也无法保证进程启动和运行时所调用的所有服务都是健康的。传统的方法可能都聚焦在请求持久性上—确保请求永不丢失。但这类似升级服务器配置和存储设备的传统方法:使其变得越来越强大,这样它们就不会出现故障。另一种是更现代化的方法,即本书中的内容,接受组件终将发生故障的事实,通过适应不可避免的中断来实现弹性。这就是本章对请求的处理。通过请求冗余来实现弹性,而不是让每个请求都不可丢失。
传统重试:
在分布式架构中使用传统重试可能导致重试风暴问题,因为重试是一直请求到返回正确结果,并确保请求永不丢失,试想在分布式架构下,有一个服务故障了,其他依赖他的服务不停对他进行重试,在多达数百上千的服务情况下,这将是一种灾难,即使该服务很快恢复,但故障时积累下来的成千上万请求处理完也要很长一段时间
解决方式:
- 我们可以在客户端程序中限制重试的次数和降低重试的速度
- 缓存上次成功的响应数据,在请求重试失败次数一定时,返回缓存数据
7、断路器和API网关
上面介绍的都是关注客户端对应请求失效时的措施,接下来介绍的事服务端对请求失效时的措施。
断路器
与电路中的断路器类似,在软件中,断路器的运行方式基本相同。当负载过高时,断路器会打开并阻止流量通过。但是它有两个不同之处。首先,用来检测何时应打开断路器的机制是基于实际的故障,而不是对可能的故障的预测(你肯定不希望电路在检测到小火苗后才会跳闸)。其次,软件中的断路器通常具有内置的自我修复机制(这与让人类在黑暗的房屋中找到配电板,并手动翻转断路器的方式不同)。
断路器的基本思想是:如果服务开始出现故障但是次数不多,先停止该服务的所有流量一段时间,希望给它一段时间,让它能够从故障中恢复。过一段时间后,让单个请求通过,查看其运行情况。如果请求失败,则继续维持保护措施,不允许后续的流量通过。如果请求成功,则视为服务恢复正常,并允许流量通过。
断路器工作:
当服务不可用时(由于网络中断、服务本身出现问题或者其他问题),断路器的主要优点之一是可以大大减少等待响应所浪费的时间
API网关
API网关在软件架构始终位于实现的最前面,并且提供了大量的服务。这些服务可能包括以下内容:
- 身份验证和授权:控制对API网关后面服务的访问。这种访问控制的机制各不相同,可以是基于密码的方式(例如,使用密码或者令牌),也可以是基于网络、集成或者实现防火墙类型的服务。
- 传输中的数据加密:API网关可以处理解密,因此也必须负责管理证书。
- 保护服务免受负载高峰的影响:正确配置后,API网关会成为客户端访问服务的唯一方式。因此,网关实现的限流机制可以提供重要的保护。你可能会以为这和我们刚才讨论的断路器很像,没错,就是这样。
-访问日志:由于进入服务的所有流量都通过API网关,因此你可以记录所有的访问日志。这些日志可以用在许多场景,包括审核和运维的可观察性。
15年前,API网关通常被部署为集中式(虽然是集群)组件,但是在云原生架构中这已经发生了变化。
可以将网关视为一个逻辑实体,是管理所需要的。但是对云原生架构来说,网关的实现最好是分布式的
挎斗
为了避免限制语言组件的注入弊端来提供一个分布式的API网关—答案就是挎斗(Sidecar)。简单来看,挎斗是一个与主服务一起运行的进程。可以想象网关服务可以与服务一起运行,而不必是嵌入式的。为了满足不将其编译到服务二进制文件中的要求,这意味着网关挎斗需要作为一个单独的进程,与主服务进程一起运行。Kubernetes提供了实现该功能的完美抽象:Kubernetes pod。pod是Kubernetes中部署的最小单元,其包含一个或多个容器。你可以将主要服务托管在一个容器中,而将网关服务托管在另一个容器中,两者都运行在同一个pod中。
分布式网关作为每个服务的挎斗运行。在Kubernetes中,这是通过在单个pod中运行两个容器来实现的,一个是主服务,另一个是网关挎斗
Envoy是当今最受欢迎的挎斗实现之一。Envoy最初由Lyft公司开发,因为是用C++编写的分布式代理,所以其效率极高。它可以用在各种部署拓扑中,尽管最常见的用法是将其作为一个实例部署到服务的单个实例前面,Envoy在这些交互的边缘实现了许多模式,包括重试、断路器、限速、负载均衡、服务发现、可观察性等,所以我们可以很简单使用它来处理服务交互的重试行为
添加了管理这些代理的控制平面后成果:
8、故障排除
在云原生软件中:
- 必须主动将度量指标和日志从执行服务的运行时环境中取出(stdout和stderr),因为在服务遇到故障或者升级后,这些执行环境通常会变得不可用。服务的执行环境应被视为短暂的。
- 聚合来自多个服务实例的日志对于可观察性很重要。通常首选按时间顺序来处理不同服务的日志。
- 可观察性信息、日志、指标和跟踪数据的收集可以放在挎斗代理中实现,这使得应用程序可以专注于业务逻辑,并将运维需求集中到服务网格中。
- 完善的分布式跟踪技术及其实现,为深入了解分布式应用程序的运行状况和性能提供了宝贵的洞察能力。
以上操作都有助于我们在项目出问题时快速定位及解决
9、云原生数据
在云原生软件中,避免将所有数据都存在一个共享数据库中。
- 当为微服务提供一个数据库来存储其所需的数据时,能显著提高自治性。这会让系统整体具有更好的弹性。
- 通过事件主动将数据变更推送到本地的数据存储,是一种更好的方法。
- 将事件日志作为数据的唯一真实来源,所有服务的本地数据库仅保留投射数据,这样可以让运行在高度分布式、不断变化的环境中的云原生软件,实现数据上的一致性。相比1,这样更符合云原生特点
Serverless
就像无线互联网实际有的地方也需要用到有线连接一样,无服务器架构仍然在某处有服务器。Serverless(无服务器架构)指的是由开发者实现的服务端逻辑运行在无状态的计算容器中,它由事件触发, 完全被第三方管理,其业务层面的状态则被开发者使用的数据库和存储资源所记录
认识Serverless架构
说到severless,很多人第一个想到的也许就是FaaS(Function as a Service)函数即服务,这并不完整,因为Serverless通常包含两个领域BaaS(Backend as a Service)和FaaS(Function as a Service)
- BaaS(Backend as a Service)后端即服务,一般是一个个的API调用后端或别人已经实现好的程序逻辑,比如身份验证服务Auth0,这些BaaS通常会用来管理数据,还有很多公有云上提供的我们常用的开源软件的商用服务,比如亚马逊的RDS可以替代我们自己部署的MySQL,还有各种其它数据库和存储服务。
- FaaS(Functions as a Service)函数即服务,FaaS是无服务器计算的一种形式,当前使用最广泛的是AWS的Lambada。FaaS本质上是一种事件驱动的由消息触发的服务,FaaS供应商一般会集成各种同步和异步的事件源,通过订阅这些事件源,可以突发或者定期的触发函数运行。传统的服务器端软件不同是经应用程序部署到拥有操作系统的虚拟机或者容器中,一般需要长时间驻留在操作系统中运行,而FaaS是直接将程序部署上到平台上即可,当有事件到来时触发执行,执行完了就可以卸载掉。
无服务器计算只有在主动处理某个事件并产生响应时,每次调用都会经历从配置环境到销毁环境的所有阶段,这样不仅节省了服务在不工作时的资源,它可以放大云原生软件的模式,例如,如果每次调用都会完全重新创建一个运行时环境,那么应用程序永远无法依赖以前调用的内部状态。因此,再不会出现像黏性会话这样的做法。
在severless平台中,程序的部署那些由平台提供优化及完成,但是作为一名开发人员,你只需负责并控制应用程序的启动和实际执行。你必须集中精力尽快实现它。
注意并不是所有应用都适合使用无服务架构,一个程序,运行时间很短,但是不断会被请求,尤其是如果程序启动的成本比执行功能的成本还高,那么可能更应该启动应用程序的一个或多个实例,让客户端请求那些已经运行的实例。如果程序的运行频率较低,并且运行所需的时间远远高于配置、部署和启动应用程序所需的时间,那么无服务器环境可能是理想的。
目前市场上的大多数无服务器平台都进行了各种优化,以便减少对应用程序生命周期原始形态的影响。例如,对于请求较为频繁的场景,应用程序环境通常会被保留下来并重复使用
总结
云原生软件要求我们面向“失败”编程,即时时刻刻要考虑到失败后的补救措施,因为在云原生中,变化是常态,而不是例外
注:本文介绍的只是云原生的一些皮毛
后记:上面便是我初次调研云原生的总结,第一次接触到这个概念,许多见解都是参考云原生模式 作者CorneliaDavis(这本书对小白很友好,推荐)一书,如有错请各位指正,这篇博客既是对这段时间的学习总结以供日后温习,也希望能给其他像我一样的云小白能对云原生有一定启示
参考