从演化的角度看云原生,其实并不抽象

从单体到微服务

在分析单体架构之前,我们有必要先搞清楚一个思维误区,那就是单体架构是落后的系统架构风格,最终会被微服务所取代。因为在许多微服务的研究资料里,单体系统往往是以“反派角色”的身份登场的

对于小型系统来说,这样的单体不仅易于开发、易于测试、易于部署,而且因为各个功能、模块、方法的调用过程,都是在进程内调用的,不会发生进程间通讯,所以程序的运行效率也要比分布式系统更高,完全不应该被贴上“反派角色”的标签

所以,当我们在讨论单体系统的缺陷的时候,并不是泛指所有的单体系统,而是要基于满足一定条件的大型单体系统,这样才有讨论的价值

随着业务的增长,单体中的功能模块越来越多,应用程序越来越复杂,开发、测试、部署和扩展变得更加困难、系统也变得越来越不稳定:

  • 开发速度缓慢:从打开ide开始,开发者就开始进入"慢"的节奏了,构建和运行非常缓慢,从编辑代码,到构建代码,再到运行、测试,这个周期花费的时间越来越长,严重影响了团队的工作效率
  • 测试速度缓慢:过高的复杂性导致任何一个更改都可能带来很多未知性,为此要执行所有的自动化测试用例,甚至需要手工测试,小小的变更也要耗费许多时间
  • 横向扩展困难:应用的不同模块对资源的需求往往不同,有对内存需求较大的、有对CPU需求较大的,但由于这些模块在一个应用程序内,选用服务器时必须同时满足所有模块的需求,也没有办法做到单独扩展
  • 隔离能力欠缺导致系统频繁故障:在单体架构中,所有的代码都运行在同一个进程空间之内,如果任何一部分的代码出现了缺陷,过度消耗进程空间内的公共资源(如内存泄漏、线程爆炸、阻塞、死循环),那所造成的影响就是全局性的、难以隔离的

我们很自然的想到了"拆",围绕着业务能力,将单体拆分为多个高内聚、低耦合、可以独立升级、部署、扩展、互相协作的微服务。原来的本地方法调用,变成了远程调用,这会引入很多复杂度:比如说,远程的服务在哪里(服务发现)、要调哪一个(路由、负载均衡)、方法的输入输出如何表示(序列化协议)、如何传输(传输协议)、如何保证通信安全(网络安全层)、服务权限如何管理(认证、授权)、调用超时、异常怎么办(熔断、降级)、如何控制调用速度(限流)、如何准确定位服务调用问题(链路追踪)

于是应用程序中引入了一系列技术组件,包括RPC框架、注册中心、配置中心、熔断框架、限流框架、安全框架、序列化协议、链路追踪框架等。如果使用的是Spring Cloud技术套件,就不得不花好长时间了解Config、Eureka、Zuul、Hystrix、Ribbon、Feign 等组件的运作原理。对于团队的开发人员、架构人员来说,这都并不轻松

我们不禁想到:为什么架构进化了,应用的复杂度反而增加了?为什么应用需要关注大量非功能性问题?有没有更好的架构方式?

从微服务到云原生

应用程序只应该关注业务逻辑,这是架构演进的主要方向之一,我们希望让开发变得简单的同时,又不用舍弃微服务带来的任何好处。我们希望可以裁剪应用程序的技术栈深度,将微服务相关的技术组件全部从应用程序中剥离,回归单体架构的开发模式。

从架构演化的角度看,如何使用基础设施剥离微服务架构的复杂度,解决微服务架构的痛点,这是云原生时代要重点解决的问题之一

容器技术的诞生,使得基础设施有了飞速发展的立足点。容器是云原生时代的技术基石,其初衷是将软件的部署过程,从原来的安装、配置应用运行时环境、然后部署应用的过程,变成直接部署包含了整套运行环境的虚拟镜像。早期的容器只是被简单地视为一种可快速启动的服务运行环境,使用它的目的是方便程序的分发部署。所以,早期阶段针对单个服务的容器,并没有真正参与到分布式问题的解决之中

直到kubernetes赢得容器编排框架战争的胜利,局面开始有了新的变化。kubernetes使得虚拟化的基础设施,从单个服务的容器发展到由多个容器构成的服务集群,以及集群所需的所有通讯、存储设施。那么原来只能从软件层面解决的微服务架构问题,就有了另外一种解法:应用程序负责业务逻辑,其他的交给基础设施。此时,云原生时代已来!!

阿里云在2020年发布的《云原生架构白皮书》中给出了比较明确的云原生架构定义:云原生架构是基于云原生技术的一组架构原则和设计模式的集合,旨在将云应用中的非业务代码部分进行最大化的剥离,从而让云设施接管应用中原有的大量非功能特性

此外,该书还补充道:不能说云原生解决了所有非功能性问题,但确实大量非功能性特性,特别是分布式环境下复杂非功能性问题,被云原生产品处理掉了

不过相对于Spring Cloud这种微服务架构解决方案来说,kubernetes提供的对应解决方案并没有完美的解决问题,尤其在功能的灵活度上反而不如前者,比如kubernetes只能做到服务粒度的熔断、负载均衡、安全控制,而无法做到接口粒度,因为kubernetes的最小控制粒度是容器,没法做到像Spring Cloud这种精细化的控制程度

为了解决这一类问题,微服务基础设施很快就进行了第二次进化,服务网格(Service Mesh)正式登场,通过kubernetes向应用程序容器所在pod,自动注入一个sidecar代理服务,它会在应用毫无感知的情况下,接管掉应用的所有对外通讯,执行服务发现、负载均衡、限流、熔断、认证、追踪等各项工作。到此为止,微服务架构下,为了解决分布式问题而在应用程序中引入的诸多非功能性复杂度,就全部被剥离到基础设施中了。而这正是云原生的真正意义,接下来我们通过介绍云原生的几个代表技术,来深入理解这一点

第一个是云原生的理论基础:不可变基础设施

2013年6月,Chad 在自己的博客中撰写一篇 Trash Your Servers and Burn Your Code: Immutable Infrastructure and Disposable Components》 的文章,提出了 Immutable Infrastructure(不可变基础设施) 的概念。这一前瞻性的构想,在当今的云原生时代,得到了事实上的检验

大家可能经常会干这样一件事情,比如需要发布或者更新一个软件,那么流程大致是这样的,先通过 SSH 连到服务器,然后手动升级或者降级软件包,逐个调整服务器上的配置文件,并且将新代码直接都部署到现有服务器上。诸如此类的手工操作使基础设施不断地被更改,会给服务运行态引入过多的中间态,增加了不可预知的风险,导致迁移、扩展等工作难以流畅的进行

如果是不可变基础设施,上述更新过程会这么做:通过容器将应用及其运行环境一并打包为镜像,一旦应用部署完成之后,这套应用基础设施就不会再修改了。如果需要更新,就要构建另一个版本的容器镜像,然后重新发布替换旧的服务。之所以能够实现直接替换,就是因为容器提供了自包含的环境,同一个容器镜像,不论是在美国打开,在中国打开,还是在印度打开都是一样的,对于应用而言,它就不需要关心容器跑在哪里

基于这个前提,应用本身也可以更好地扩容,从 1 个实例变成 100 个实例、1000个实例,这个过程对于容器化后的应用没有任何特殊的。也就是提供了简单、可预测的部署运维能力

最后,我们也能够通过不可变的基础设施来地部署应用程序外围的管控系统和支撑组件。毕竟,接管微服务中的非功能性复杂度是云原生的一大重任

第二个是云原生技术的核心底盘:容器

从概念上来看,容器就是一个具有独立文件系统、独立访问视图、独立资源配额的进程集合

独立文件系统:依赖于chroot技术,它所具备的功能是当某个进程经过chroot操作之后,它的根目录就会被锁定在命令参数所指定的位置,以后它或者它的子进程就不能再访问和操作该目录之外的其他文件

独立访问视图:依赖于Linux Namespaces(Linux 名称空间)技术,进程在一个独立的 Linux 名称空间中朝系统看去,会觉得自己仿佛就是这方天地的主人,不仅文件系统是独立的,还有着独立的 PID 编号(比如拥有自己的 0 号进程,即系统初始化的进程)、UID/GID 编号(比如拥有自己独立的 root 用户)、网络(比如完全独立的 IP 地址、网络栈、防火墙等设置)

独立资源配额:依赖于Linux cgroups技术,cgroups用于隔离或者说分配并限制某个进程组能够使用的资源配额。这里的资源配额包括了处理器时间、内存大小、磁盘 I/O 速度,等等

隔离是为了给进程提供一个独立的运行环境,这也是容器技术的起源,但如果仅仅是隔离,这和虚拟机并没有本质区别。在 2013 年宣布开源的 Docker,毫无疑问是容器发展历史上里程碑式的发明,Docker定义了一种将应用及其所有的环境依赖都打包到一起的格式,使得应用具有了一种“自包含”的定义方式。如此,应用才有可能以敏捷的、以可扩展可复制的方式发布在云上,发挥出云的能力。这也就是容器技术对云发挥出的革命性影响所在,所以说,容器技术正是云原生技术的核心底盘

彼时,云计算模式依然停滞在传统的 IDC(Internet Data Center)时代,它们仅仅是用云端的虚拟机代替了传统的物理机而已。起初容器也只是被简单地视为一种可快速启动的服务运行环境,使用它的目的是方便程序的分发部署,并没有改变云计算的这一现状。无法充分利用云的强大能力,这让云计算厂商们有些焦虑

第三个是工业级的容器编排平台:Kubernetes(基于云原生另一个理论基础-云应用编排理论)

直到 Kubernetes 横空出世,大家才终于等到了破局的希望,认准了这就是云原生时代的操作系统,是让复杂软件在云计算下获得韧性、弹性、可观测性的最佳路径,也是为厂商们推动云计算时代加速到来的关键引擎之一

如果说以 Docker 为代表的容器引擎,是把软件的发布流程从分发二进制安装包,转变为了直接分发虚拟化后的整个运行环境,让应用得以实现跨机器的绿色部署;那以 Kubernetes 为代表的容器编排框架,就是把大型软件系统运行所依赖的集群环境也进行了虚拟化,让集群得以实现跨数据中心的绿色部署,并能够根据实际情况自动扩缩。这很符合所有云计算大厂的切身利益,有着业界巨头不遗余力地广泛支持,所以Kubernetes的成功便是一种必然

分布式系统里对于应用的概念已经不再等同于进程了,此时的应用需要多个进程共同协作,通过集群的形式对外提供服务,那么以虚拟化方法实现这个目标的过程,就被称为容器编排(Container Orchestration)

如何调度容器,如何分配资源,如何弹性伸缩、充分利用云的能力,如何最大限度地接管系统中的非功能特性,让业务系统尽可能地免受分布式复杂性的困扰,都是容器编排框架必须考虑的问题,只有恰当解决了这一系列问题,云原生应用才有可能获得比传统应用更高的生产力。

而到今天,Kubernetes 已经成为容器编排的事实标准,被广泛用于自动部署,扩展和管理容器化应用,那么接下来就让我们看看,Kubernetes是如何实现上述这些特性的

先来理解下Kubernetes中的一个最重要的概念:Pod,Pod是对容器组的抽象,一个Pod由一个或多个需要亲密协作的容器组成(比如应用容器和filebeat容器,应用容器和Envoy容器),比如说现在有四个职责不同、相互协作的进程,需要放在容器里去运行,在 Kubernetes 里面并不会把它们放到一个容器里,而是把四个独立的进程分别用四个独立的容器启动起来,然后把它们定义在一个 Pod 里面。Pod具有共享名称空间以及原子调度两大特性:

共享名称空间:同处于一个 Pod 内的多个容器,相互之间会以超亲密的方式协作。超亲密具体指什么呢?

  • 对于普通非亲密的容器来说,它们一般以网络交互方式(其他的如共享分布式存储来交换信息,也算跨网络)协作
  • 对于亲密协作的容器来说,是指它们被调度到同一个集群节点上,可以通过共享本地磁盘等方式协作
  • 而超亲密的协作,是特指多个容器位于同一个 Pod 这种特殊关系,Pod 这个抽象给这些容器提供了一个共享的运行环境,pod中的所有容器共享一些信息,包括主机名和域名、网卡、网络栈、IP 地址等等,可以用 localhost 来进行直接的连接。比如后面要介绍的service mesh,其数据平面中的代理,和应用就是超亲密关系,它们就会组成一个pod

原子调度:这点很好理解,如果以容器为单位来调度的话,不同容器就有可能被分配到不同机器上。而两台机器之间本来就是物理隔离,依靠网络连接的,所以这时候谈什么名称空间共享、亲密协作都没有意义了。因此一个pod中的多个容器必须以一个整体来调度,要嘛同时成功、要嘛同时失败

总结来说,Pod满足的是隔离与协作的需求,容器要让它管理的进程相互隔离,使用独立的资源与配额;容器编排系统要让它管理的各个容器相互协作,共同维持一个分布式系统的运作

再来看看Kubernetes的核心功能:

一、调度:为新创建出来的 Pod,通过一系列的过滤和打分的算法,选出一台最合适的Node(对应于集群中的单台机器,可以是生产环境中的物理机,或者云计算环境中的虚拟节点,节点是处理器和内存等资源的资源池,是硬件单元的最小单位),这里有个关键字“合适”,什么是合适呢?

  1. 满足pod的资源要求,可以分为四大类的基础资源,分别是:CPU资源;memory;ephemeral-storage,一种临时存储;通用的扩展资源,比如说像 GPU。用户要为容器声明合理的资源需求,pod的资源需求就等于它包含的所有容器的资源需求之和
  2. 满足pod的一些特殊关系的要求,包括怎么去亲和一个 pod,怎么去互斥一个 pod,针对这样的场景:比如说一个Pod必须要和另外一个Pod放在一起,或者不能和另外一个Pod放在一起
  3. 满足node的一些限制条件的要求,包括必须调度到某一类Node上,优先调度到某一类 Node 上、限制 Pod 调度到某些 Node 上
  4. 保证整个集群资源的合理利用,尤其在资源不够的场景下,要优先保证高服务质量等级的业务,剔除低服务质量等级业务的pod

二、应用部署与管理:支持应用的自动发布与应用的回滚,以及与应用相关的配置的管理

三、韧性:在一个集群中,无论是软件缺陷、意外操作或者硬件故障,都可能导致某个容器出现异常,Kubernetes 可以会监测这个集群中所有的宿主机,当宿主机或者 OS 出现故障,节点健康检查 会自动进行应用迁移;K8s 也支持应用的自愈,极大简化了运维管理的复杂性

四、弹性:Kubernetes可以监测业务上所承担的负载,当服务集群遇到压力时,能够自动部署新的Pod,实现水平扩展,当压力减小时,能够释放部分Pod。从而让集群、应用更富有弹性

这些功能的实现依赖于Kubernetes的资源模型及控制器模型,Kubernetes的用户如果想使用各种资源来实现某种需求,并不能像平常编程那样,去调用某个或某一组方法来达成目的。而是要通过描述清楚这些资源的期望状态,由 Kubernetes 中对应监视这些资源的控制器,来驱动资源的实际状态逐渐向期望状态靠拢,才能够达成自己的目的。而这种交互风格就被叫做声明式API

声明式API与期望状态:从 high-level上看,Kubernetes API是由HTTP+JSON组成的,用户访问的方式是 HTTP,访问的API中的content是JSON格式的(可以使用JSON或yaml来表达),在content中比较重要的一个部分叫做Spec,我们通过Spec描述希望资源达到的预期状态(比如声明应用实例副本数保持在3个)

资源与控制器:Kubernetes内置了很多资源对象,其中与韧性与弹性相关的资源主要是指用于创建、销毁、更新、扩缩 Pod的资源。只要是实际状态有可能发生变化的资源对象,就通常都会由对应的控制器进行追踪,每个控制器至少会追踪一种类型的资源。控制器将根据通过API声明的期望状态,异步的控制系统向设置的终态驱近,这些控制器是自主运行的,使得系统的自动化和无人值守成为可能

  • Pod作为最小的的资源单元,由一个或多个容器组成,Pod为这些容器提供共享的运行环境(网络、进程空间等)
  • ReplicaSet:维持指定数量的Pod副本,可以在ReplicaSet资源的Spec中,描述期望的Pod副本数量。当ReplicaSet成功创建之后,ReplicaSet控制器就会持续跟踪该资源,一旦有 Pod 发生崩溃退出或者状态异常,ReplicaSet都会自动创建新的Pod来替代异常的 Pod;如果因异常情况出现了额外数量的Pod,也会被ReplicaSet自动回收掉
  • Deployment:负责管理不同版本的 ReplicaSet,实现包括滚动发布与自动回滚,当我们更新了 Deployment中的信息以后(比如更新了镜像的版本),Deployment控制器就会跟踪到新的期望状态,自动地创建新 ReplicaSet,并逐渐缩减旧的 ReplicaSet 的副本数,直到升级完成后,彻底删除掉旧ReplicaSet。这个过程如下:从演化的角度看云原生,其实并不抽象_第1张图片如果发现当前的业务版本是有问题的,要做回滚的话,其实就是上述过程的逆过程,逐渐减少version2 Replicaset中的pod数,增加version1Replicaset中的pod数。                Pod、ReplicaSet、Deployment三者关系如下:从演化的角度看云原生,其实并不抽象_第2张图片
  • Horizontal Pod Autoscaler:为了让集群、应用更富有弹性,Kubernetes 也支持水平的伸缩,可以基于 CPU 利用率自动扩缩Pod 数量,除了CPU利用率,也可以基于其他应程序自定义的度量指标来执行自动扩缩。控制器会周期性地调整Pod数量,以使得类似 Pod 平均 CPU 利用率、平均内存利用率这类观测到的度量值与用户所设定的目标值匹配

声明式与命令式

  • 声明式表达的是要什么,是目的
  • 命令式表达的是怎么做,是过程

在生活中,常见的命令式的交互方式是家长和孩子交流方式,因为孩子欠缺目标意识,无法理解家长期望,家长往往通过一些命令,教孩子一些明确的动作,比如说:家长不会直接说让孩子保持生活规律,而是下达吃饭、睡觉类似的命令

常见的声明式交互方式,就是老板对自己员工的交流方式。老板一般不会给自己的员工下很明确的决定,实际上可能老板对于要操作的事情本身,还不如员工清楚。因此,老板通过给员工设置可量化的业务目标的方式,来发挥员工自身的主观能动性。比如说,老板会要求某个产品的市场占有率达到 80%,而不会指出要达到这个市场占有率,要做的具体操作细节

比如在Kubernetes中,我们可以声明应用实例副本数保持在3个,而不用明确的去扩容Pod 或是删除已有的 Pod,来保证副本数在三个

注意,声明式与命令式并非0和1的关系,而是渐进关系。越接近计算机语言就越命令式,也就是条件、分支、循环的执行流程。如果一款技术组件提供的API越声明式,那它要屏蔽的细节就越多,要干的事情越多,对用户来说越友好简单

第四个是服务间通讯基础设施、下一代微服务框架:Service Mesh

Service Mesh 是分布式应用在微服务软件架构之上发展起来的新技术,旨在将那些微服务间的连接、安全、流量控制和可观测等通用功能下沉到基础设施,实现应用与基础设施的解耦。这个解耦意味着开发者无需再关注微服务架构带来的非功能性问题,而聚焦于业务逻辑本身,提升应用开发效率并加速业务探索和创新。下图展示了Service Mesh的典型架构:

从演化的角度看云原生,其实并不抽象_第3张图片

在图中,Service A 调用 Service B 的所有请求,都被其下的 Proxy(以应用的Sidecar形式部署)截获, 代理Service A 完成到 Service B 的服务发现、负载均衡、熔断、限流、追踪等策略(数据平面)。而在其背后,由控制平面默默地完成配置下发和策略下发,指导Proxy工作。对应用程序来说,真正实现了透明通讯

并且Service Mesh提供了更加通用和标准化的能力,它屏蔽了不同语言、不同平台的差异性,带来了一致的服务治理体验,减少了多业务之间由于服务治理标准不一致带来的沟通和转换成本,提升全局服务治理的效率

Service Mesh的发展:

在Service Mesh发展早期,它仅以Sidecar的形式存在,代表有早期的Linkerd和Envoy。通过Kubernetes将包含网络代理的容器,以Sidecar的形式注入到应用容器所在的Pod,自动劫持应用的网络流量,让通信的可靠性由专门的通信基础设施来保障。由于通讯和治理的所有功能全部在Sidecar中,导致其承担了过多的特性和功能,使得其更新比较频繁,而作为所有应用流量的出入口,对Sidecar的稳定性要求实际上是极高的

为了协调频繁的更新与稳定性要求之间的矛盾,第二代Service Mesh(也称服务网格)应运而生,其重要标志就是将数据平面与控制平面分离开来,从总体架构看,服务网格包括两大块内容,分别是数据平面和控制平面:

数据平面:

由一系列与微服务共同部署的Proxy组成,它的核心职责是转发应用的入站和出站数据包,因此数据平面也有个别名叫转发平面。此外,数据平面还需要根据控制平面下发策略的指导,在应用无感知的情况下自动完成服务路由、健康检查、负载均衡、认证鉴权、产生监控数据等一系列工作

如何将Proxy注入到应用程序中呢?由于Proxy的定义就是一个与应用共享网络名称空间的辅助容器,这天然就契合了 Pod 的设定。因此只要借助于Kubernetes,问题就演变成为Pod增加一个额外容器而已。这种在Pod 里通过定义一些专门的容器,来执行主业务容器所需要的一些辅助工作的搞法,就是非常经典的容器设计模式:Sidecar

Proxy是如何劫持应用程序的通信流量的呢?最典型的方式是基于 iptables 进行的数据转发,在注入Proxy后,会通过修改容器的 iptables,让边车代理拦截所有进出 Pod 的流量

控制平面:如果说数据平面是行驶中的车辆,那控制平面就是车辆上的导航系统。控制平面的特点是不直接参与程序间通信,只会与数据平面中的代理通信。在程序不可见的背后,默默地完成下发配置和策略,指导数据平面工作。包括:请求路由管理,熔断、超时、限流等流量治理工作,通信中的加密、凭证、认证、授权等功能,以及日志收集、链路追踪、指标度量等可观测性方面的功能

小结:云原生其实是很具体的,只是由于宣传等原因,各种场合都往上蹭热点,以至于目前有点混乱,不知道看完本文,你有没有对云原生有更清晰的认识了呢?

参考文献:

《凤凰架构-构建可靠的大型分布式系统》,作者:周志明。书是开源的,点击链接可以直接阅读(业界良心啊。),本书是周老师第4本豆瓣评分超过9分的作品了,周老师一如既往的高屋建瓴、深度与广度并存,并且表达能力极佳,阅读体验非常良好,推荐大家阅读

《云原生技术公开课》,阿里巴巴和CNCF联合开设

《云原生架构白皮书》,阿里云

《微服务架构设计模式》,作者:[美]Chris Richardson

你可能感兴趣的:(架构,云原生,云原生架构,云原生意义,云原生含义)