云原生的代表技术

Posted by ni on December 6, 2024
本文共8792 字 | 大约需要29.31 分钟阅读

前言

通过对云原生的探索可以了解到,云原生的目标为:

  • 高可用
  • 适应不同规模
  • 敏捷
  • 成本

随着技术不断发展,诞生了许多不同技术成为云原生的代表技术

  • 容器技术
  • 微服务
  • 服务网格
  • 不可变基础设施
  • 声明式设计
  • DevOps

1. 容器技术

容器技术分为了多种阶段,随着技术的发展,容器技术变得越来越成熟

1.1 chroot阶段:隔离文件系统

  • 在 1979 年,贝尔实验室的工程师在开发 Unix V7 期间,发现当系统软件编译和安装完成后,整个测试环境的变量就会发生改变,如果要进行下一次构建、安装和测试,就必须重新搭建和配置测试环境
  • 为解决该问题chroot诞生了
  • 它能将进程的根目录重定向到某个新目录,复现某些特定环境,同时也将进程的文件读写权限限制在该目录内。
  • 通过 chroot 隔离出来的新环境有一个形象的命名“Jail”(监狱),这便是容器最重要的特性 —— 隔离

1.2 LXC阶段:封装系统

  • 2008 年,Linux 内核版本 2.6.24 刚开始提供 cgroups,社区开发者就将 cgroups 资源管理能力和 Linux namespace 资源隔离能力组合在一起,形成了完整的容器技术 LXC(Linux Container,Linux 容器)。
  • LXC 是如今被广泛应用的容器技术的实现基础,通过 LXC 可以在同一主机上运行多个相互隔离的 Linux 容器,每个容器都有自己的完整的文件系统、网络、进程和资源隔离环境,容器内的进程如同拥有一个完整、独享的操作系统

1.3 Docker阶段:封装应用

  • 从 2008 年 Google 推出的基于 LXC 技术的 GAE,到 2011 年开源的 Cloud Foundry,这些早期的 PaaS 平台一直在思考如何改善软件的交付方式。

  • 直到 Docker 的出现,大家才如梦方醒,原来不是方向不对,而是应用分发和交付的手段不行。

  • Docker 的核心创新“ 容器镜像(container image)”:

  • 容器镜像打包了整个容器运行依赖的环境,以避免依赖运行容器的服务器的操作系统,从而实现“build once,run anywhere”

  • 容器镜像一但构建完成,就变成只读状态,成为不可变基础设施的一份子

  • 与操作系统发行版无关,核心解决的是容器进程对操作系统包含的库、工具、配置的依赖(注意,容器镜像无法解决容器进程对内核特性的特殊依赖)。

开发者基于镜像打包应用所依赖的环境,而不是改造应用来适配 PaaS 定义的运行环境。如图 1-14 所示,Docker 的宣传口号“Run Any App”一举打破了 PaaS 行业面临的困境,创造出了无限的可能性。

1.4 OCI 阶段:容器标准化

2015 年 6 月,Linux 基金会联合 Docker 带头成立 OCI(Open Container Initiative,开放容器标准)项目,OCI 组织着力解决容器的构建、分发和运行标准问题,其宗旨是制定并维护 OCI Specifications(容器镜像格式和容器运行时的标准规范)

经过一系列的演进发展之后,OCI 有了三个主要的标准

  • OCI Runtime Spec(容器运行时标准):定义了运行一个容器,如何管理容器的状态和生命周期,如何使用操作系统的底层特性(namespace、cgroups、pivot_root 等)。
  • OCI Image Spec(容器镜像标准):定义了镜像的格式,配置(包括应用程序的参数、依赖的元数据格式、环境信息等),简单来说就是对镜像文件格式的描述。
  • OCI Distribution Spec(镜像分发标准):定义了镜像上传和下载的网络交互过程的规范。

Docker 把与内部负责管理容器执行、分发、监控、网络、构建、日志等功能的模块重构为 containerd 项目

containerd 的架构主要分为三个部分:生态系统(Ecosystem)、平台(Platform)和客户端(Client),每个部分在整个系统中扮演着不同的角色,协同工作以提供全面的容器管理功能。

根据拆分后的 Docker 架构图看 ,根据功能的不同,容器运行时被分成两类:

  • 只关注如 namespace、cgroups、镜像拆包等基础的容器运行时实现被称为“低层运行时”(low-level container runtime)。目前,应用最广泛的低层运行时是 runc;
  • 支持更多高级功能,如镜像管理、容器应用的管理等,被称为“高层运行时”(high-level container runtime)。目前,应用最广泛高层运行时是 containerd。

在 OCI 标准规范下,两类运行时履行各自的职责,协作完成整个容器生命周期的管理工作。

1.5 容器编排阶段:封装集群

Kubernetes 围绕容器抽象了一系列的“资源”概念能描述整个分布式集群的运行,还有可扩展的 API 接口、服务发现、容器网络及容器资源调度等关键特性,非常符合理想的分布式调度系统。

随着 Kubernetes 资源模型越来越广泛的传播,现在已经能够用一组 Kubernetes 资源来描述一整个软件定义计算环境。就像用 docker run 可以启动单个程序一样,现在用 kubectl apply -f 就能部署和运行一个分布式集群应用,而无需关心是在私有云还是公有云或者具体哪家云厂商上

1.6 云原生阶段:百花齐放

迄今为止在其 CNCF 公布的云原生全景图中,显示了近 30 个领域、数百个项目的繁荣发展,从数据存储、消息传递,到持续集成、服务编排乃至网络管理无所不包、无所不含。

其中与容器相关的最为重要的几个规范包括:CRI(Container Runtime Interface,容器运行时接口规范)、CNI(Container Network Interface,容器网络接口规范)、CSI(Container Storage Interface,容器存储接口规范)、OCI Distribution Spec、OCI Image Spec、OCI Runtime Spec

2. 微服务

微服务中的两个核心概念:

  • 松耦合(Loosely Coupled):意味着每个服务可以独立的更新,更新一个服务无需要求改变其他服务。
  • 限界上下文(Bounded Contexts):意味着每个服务要有明确的边界性,你可以只关注自身软件的发布,而无需考虑谁在依赖你的发布版本。微服务和它的消费者严格通过 API 进行交互,不共享数据结构、数据库等。基于契约的微服务规范要求服务接口是稳定的,而且向下兼容。

所以微服务综架构的特征是:服务之间独立部署,拥有各自的技术栈,各自界定上下文等。如下图所示一个是巨石应用和微服务进行对比

2.1 微服务带来的技术挑战

微服务架构首先是一个分布式的架构,分布式意味着复杂性的挑战。

软件架构从巨石应用向微服务架构转型的过程中带来了一系列的非功能性需求,例如:

  • 服务发现(Service Discovery)问题:解决“我想调用你,如何找到你”的问题。
  • 服务熔断(Circuit Breaker)问题:缓解服务之间依赖的不可靠问题。
  • 负载均衡(Load Balancing)问题:通过均匀分配流量,让请求处理更加及时。
  • 安全通讯问题:包括协议加密(TLS)、身份认证(证书/签名)、访问鉴权(RBAC)等。

解决这些问题需要编写和维护⼤量非功能性代码,这些代码与业务代码逻辑混在一起,动不动还会遇到点匪夷所思的分布式 bug。所以说,基础设施不完善的话,实施微服务很痛苦,服务越多越悲剧。

2.2 后微服务时代

微服务架构中,有一些必须解决的问题,如负载均衡、伸缩扩容、传输通讯等等,这些问题可以说只要是分布式架构的系统就无法完全避免。直接来看待这些问题与它们最常见的解决方法:

  • 如果某个系统需要解决负载均衡问题,通常会布置负载均衡器,选择恰当的均衡算法来分流;
  • 如果某个系统需要伸缩扩容,通常会购买新的服务器,多部署几套副本实例;
  • 如果要解决安全的传输通讯,通常要布置 TLS 链路,设置 CA 证书,保证中间不被窃听,等等。

这些问题一定要由分布式系统自己来解决吗?

微服务时代,之所以选择在应用服务层面,而非基础设施层面去解决这些分布式问题,主要是因为硬件构建的基础设施无法追赶上软件构成的应用服务的灵活性

Kubernetes 的出现就很好的解决这个问题。直接来看 Kubernetes 在基础设施层面,解决分布式系统问题的方案:

  • Kubernetes 用 CoreDNS 替代 Spring Cloud 服务发现组件 Eureka。
  • Kubernetes 用 Service/Load Balancer 替代 Spring Cloud 中的负载均衡组件 Ribbon。
  • Kubernetes 用 ConfigMap 替代 Spring Cloud 的配置中心 Config。
  • Kubernetes 用 Ingress 代替 Spring Cloud 的网关组件 Zuul。

传统微服务框架解决的问题,已完全可以用基础设施 Kubernetes 的方案去解决。虽然出发点不同,导致它们解决问题的方式和效果存在差异,但无疑为我们提供了一种全新且更具前景的解决问题的思路。

当虚拟化的基础设施从单个服务的容器扩展至由多个容器构成的服务集群,并开始解决分布式的问题。那么,软件与硬件的界限便开始模糊。一旦虚拟化的硬件能够跟上软件的灵活性,那些与业务无关的技术性问题便有可能从软件层面剥离,并悄无声息地解决于硬件基础设施之内。

此即为“后微服务时代”。

2.3 后微服务时代的二次进化

Kubernetes 的崛起标志着微服务时代的新篇章,但它并未能完全解决所有的分布式问题。就功能的灵活性和强大性而言,Kubernetes 还比不上之前的 Spring Cloud 方案,原因在于某些问题位于应用系统与基础设施的交界处,而微观的服务管理(如单个请求的治理)并不能完全在基础设施层面得到解决。

举个例子,假设微服务 A 调用了微服务 B 的两个服务,即 B1 和 B2。若 B1 正常运行,而 B2 持续出现 500 错误,那么在达到一定阈值后,就应对 B2 进行熔断,以避免引发雪崩效应。如果仅在基础设施层面处理这个问题,那就会陷入两难境地“切断 A 到 B 的网络通路会影响到 B1 的正常运作,不切断则会持续受到 B2 错误的影响”。

上述问题在使用 Spring Cloud 等方案中比较容易处理,既然是使用程序代码来解决问题,只要合乎逻辑,想要实现什么功能就实现什么功能。但对于 Kubernetes,由于基础设施粒度更粗糙,通常只能管理到容器层面,对单个远程服务的有效管理就相对困难。类似的情况不仅仅在断路器上出现,服务的监控、认证、授权、安全、负载均衡等都有可能面临细化管理的需求。

为了解决这一类问题,微服务基础设施很快进行了第二次进化,引入了今天被称为 服务网格(Service Mesh) 的模式。

3. 服务网格

服务网格的定义

服务网格(ServiceMesh)是一个基础设施层,用于处理服务间通信。云原生应用有着复杂的服务拓扑,服务网格保证请求在这些拓扑中可靠地穿梭。在实际应用当中,服务网格通常是由一系列轻量级的网络代理组成的,它们与应用程序部署在一起,但对应用程序透明

ServiceMesh 之所以称为“服务网格”,是因为每台节点同时运行着业务逻辑和具备通信治理能力的网络代理(如 Envoy、Linkerd-proxy)。这个代理被形象地称为“网络边车代理”(Sidecar),其中业务逻辑相当于主驾驶,处理辅助功能的网络代理相当于边车。

具有通信治理能力的网络代理以边车形式部署,服务之间通过边车发现和调用目标服务。如果我们把节点和业务逻辑从视图剥离,边车之间呈现网络状依赖关系,服务网格由此得名。

业内绝大部分服务网格产品通常由“数据平面”和“控制平面”两部分组成,以服务网格的代表实现 Istio 架构为例

  • 数据平面(Data plane):通常采用轻量级的网络代理(如 Envoy)作为 Sidecar,网络代理负责协调和控制服务之间的通信和流量处理,解决微服务之间服务熔断、负载均衡、安全通讯等问题。
  • 控制平面(Control plane):包含多个控制组件,它们负责配置和管理 Sidecar ,并提供服务发现(Discovery)、配置管理(Configuration)、安全控制(Certificates)等功能。

值得注意的是,尽管服务网格的特点是 Sidecar 模式,但 Sidecar 模式并非服务网格专有。

Sidecar 是一种常见的容器设计模式,Kubernetes 的工作负载 Pod 内可配置多个容器,业务容器之外的其他所有容器均可称为“边车容器”(Sidecar container)。如日志收集 Sidecar、请求代理 Sidecar 和链路追踪 Sidecar 等等。

app-container 是一个主业务容器,logging-agent 是一个日志收集容器。主业务容器完全感知不到 logging-agent 的存在,它只负责输出日志,无需关心后续日志该怎么处理。你思考这样开发一个高内聚、低耦合的系统是否更加容易?

至此,相信大家已经清楚服务网格不是什么黑科技,也没有什么耀眼的新技术。

服务网格本质是通过 iptables 劫持发送到应用容器的流量,将原本在业务层处理的分布式通信治理相关的技术问题,下沉到网络代理型边车中处理,实现业务与非业务逻辑解耦的目的。

4. 不可变基础设施

如果你对一位开发工程师说:“你的软件有 bug”,他大概率这样回:“我本地跑好好的,怎么到你那就不行?”,或者你是个运维工程师,维护线上系统时,肯定吐槽过:“谁又改了配置文件…”。

相信大家在工作肯定遇到过这些问题:joy:

4.1 可变的基础设施

从管理基础设施的层面看:“可变”的基础设施与传统运维操作相关。例如,有一台服务器部署的是 Apache,现在想换成 Nginx。传统手段是先卸载掉 Apache,重新安装一个 Nginx,再重启系统让这次变更生效。

上述的过程中,装有 Apache 的 Linux 操作系统为了满足业务需求,进行了一次或多次变更,该 Linux 操作系统就是一个可变的基础设施。可变的基础设施会导致以下问题:

  • 重大故障时,难以快速重新构建服务:持续过多的手动操作并且缺乏记录,会导致很难由标准初始化的服务器来重新构建起等效的服务;

  • 不一致风险:类似于程序变量因并发修改而带来的状态不一致风险。服务运行过程中,频繁的修改基础设施配置,同样会引入中间状态,导致出现无法预知的问题。

    可变的基础设施带来的运维之痛,引得业内技术专家 Chad Fowler 这样吐槽:

    要把一个不知道打过多少个升级补丁,不知道经历了多少任管理员的系统迁移到其他机器上,毫无疑问会是一场灾难。

4.2 不可变基础设施

不可变基础设施的核心思想是任何基础设施的运行实例一旦创建之后就变成只读状态。如需修改或升级,应该先修改基础设施的配置模版(例如 yaml、Dockerfile 配置),之后再使用新的运行实例替换。例如上面提到的 Nginx 升级案例,应该准备一个新的装有 Nginx 的 Linux 操作系统,而不是在 Linux 操作系统上原地更新。

此刻,灵光一现想起前面介绍的容器技术。构建镜像运行容器之后,如果出现问题,我们不会在容器内修改解决,而是修改 Dockerfile 在容器构建阶段去解决。

从容器的角度看,镜像就是一个不可变基础设施。工程师交付的产物从有着各种依赖条件的安装包变成一个不依赖任何环境的镜像文件,当软件需要升级或者修改配置时,我们修改镜像文件,新起一个容器实例替换,而不是在运行容器内修改。有了镜像之后,本地与测试环境不一致、测试环境与正式环境不一致问题消失殆尽了。

相比可变基础设施,不可变基础设施通过标准化描述文件(如 yaml、dockerfile 等)统一定义,同样的配置拉起的服务,绝对不可能出现不一致的情况。从此,我们可以快速拉起成千上万一模一样的服务,服务的版本升级、回滚也成为常态。

5. 声明式设计

声明式设计是指一种软件设计理念:“我们描述一个事物的目标状态,而非达成目标状态的流程”。至于目标状态如何达成,则由相应的工具在其内部实现。

和声明式设计相对的是命令式设计(又叫过程式设计),两者的区别是:

  1. 命令式设计:命令“机器”如何去做事情(how),这样不管你想要的是什么(what),它都会按照你的命令实现;
  2. 声明式设计:告诉“机器”你想要的是什么(what),让机器想出如何去做(how)。

很多常用的编程语言都是命令式。例如,有一批图书的列表,你会编写下面类似的代码来查询列表中名为“深入高可用原理与设计”的书籍:

function getBooks() {
  var results = []
  for( var i=0; i< books.length; i++) {
    if(books[i].name == "深入高可用原理与设计") {
      results.push(books)
    }
  }
  return results
}

命令式语言告诉计算机以特定的顺序执行某些操作,实现最终目标:“查询名为《深入高可用原理与设计》的书籍”,必须完全推理整个过程。

再来看声明式的查询语言(如 SQL)是如何处理的呢?

使用 SQL,只需要指定所需的数据、结果满足什么条件以及如何转换数据(如排序、分组和聚合),数据库直接返回我们想要的结果。这远比自行编写处理过程去获取数据容易的多。

SELECT * FROM books WHERE author = 'xiaoming' AND name LIKE '深入高可用原理与设计%';

接下来看以声明式设计为核心的 Kubernetes。

下面的 YAML 文件中定义了一个名为 nginx-deployment 的 Deployment 资源。其中 spec 部分声明了部署后的具体状态(以 3 个副本的形式运行)。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80

该 YAML 文件提交给 Kubernetes 之后,Kubernetes 创建拥有 3 个副本的 nginx 服务实例,将持续保证我们所期望的状态。

通过编写 YAML 文件表达我们的需求和意图,资源如何创建、服务如何关联,至于具体怎么实现,我们完全不需要关心,全部甩手给 Kubernetes。

只描述想要什么,中间流程、细节不需关心。工程师们专注于 what,正是我们开发软件真正的目标。

6. DevOps

关于DevOps在之前已经发过文章详细的介绍了DevOps,这里就不再重复描述了。有需要的小伙伴可以 点此跳转»

云原生架构技术栈

探索完云原生,可以体会出云原生架构是优雅的、灵活的、弹性的…,但不能否认这些优势的背后是它的学习曲线相当陡峭。

如果你有志投入云原生领域,希望构建一个高可用(高研发效率、低资源成本,且兼具稳定可靠)的云原生架构,对能力要求已提升到史无前例的程度。总结来说,除了掌握基础的 Docker 和 Kubernetes 知识外,熟知图 1-34 所示的几个领域也是必备要求。

  1. 容器运行时:Docker、Containerd、CRI-O、Kata Containers。
  2. 镜像和仓库:Harbor、Dragonfly、Nydus。
  3. 应用封装:Kustomize、Helm。
  4. 持续集成:Gitlab、Tekton。
  5. 持续部署:ArgoCD、FluxCD。
  6. 容器编排:Kubernetes。
  7. 服务网格: Istio、Envoy、Linkerd。
  8. 网关:Ingress-Nginx、Kong、APISIX。
  9. 日志:Grafana Loki、Elastic Stack、ClickHouse。
  10. 监控:Prometheus、Grafana。
  11. 可观测:OpenTelemetry。
  12. 机器学习/离在线业务混合部署:Volcano、Koordinator…。

对于各种技术方案肯定是不完全的,并且多少会有点区别,但不同的技术会有不同的优点和缺点,根据业务进行匹配才能达到最好的效果。当然这是需要长远且广泛的学习并且累积足够多的经验才能优雅的进行实现