云原生概要

Posted on 2021-08-12,19 min read

什么是云原生

设计目的

云原生软件的设计目的是预测故障,并且即使当它所依赖的基础设施出现故障,或者发生其他变化时,它也依然能够保持稳定运行。

将变化或者失败视为正常规律,让面向失败的设计成为它们构建、交付和管理软件过程中的一个组成部分。

云计算时代应用需求

  1. 零停机时间
    软件开发人员或者架构师对设计和开发一个松耦合、组件化的系统同样负有责任,应该通过部署冗余组件来应对不可避免的故障,并设置隔离机制来防止故障在整个系统中引起连锁反应。而且,还必须把软件设计成能够在不停机的情况下完成计划事件(例如,升级)。

  2. 缩短反馈周期
    频道的软件发布可以缩短反馈周期,降低风险

  3. 多设备支持
    用户越来越希望他们的应用体验随时可以从一个设备无缝切换到另一个设备上。

  4. 互联设备(物联网)
    数据量和基础架构之间的差异,需要新的软件设计和实践来解决。

  5. 数据驱动
    数据量正在不断增加,数据源分布得更加广泛,而软件的交付周期正在缩短
    这些应用程序需要的不是单一的共享数据库,而是一个由更小的、本地化数据库组成的网络,以及能够在多个数据库管理系统之间管理数据关系的软件。

定义

云原生软件是高度分布式的,必须在一个不断变化的环境中运行,而且自身也在不断地发生变化

不适合使用云原生架构的情形

  1. 不需要云计算的软件,例如嵌入到家电中的软件。
  2. 云原生提供的是最终一致性,但如果需要数据强一致性的话,云原生架构就不适用了。
  3. 用云原生架构重写软件时并没有提供新的价值

云原生的价值

云原生的绝妙之处在于它最终是由许多不同组件组成的,即使其中一些组件的模式不是最新的,云原生组件也可以与他们进行交互。即使你的软件使用了旧的模式,应用云原生模式依然可以带来立竿见影的效果。

云原生与持续交付

生产环境运维面临的困难

1. 碎片化的变化

  • 环境发生变化
  • 部署的构件发生变化

2. 部署是有风险的
部署通常充满危险。在升级期间需要停机或者部署时引起意外停机都是很正常的,而停机的代价是昂贵的。

3. 传统运维认为变化是例外
开发人员通常会在初次部署之后退出,然后由运维人员接管。运维团队手里只有一本运维手册。虽然运维手册详细描述了可能的失败场景及其解决方案,但是更加深入思考一下不难了解,手册设定了一个假设,就是失败场景是已知的。但是,绝大多数情况不是这样的!

4. 生产环境的不稳定性

云原生架构运维的解决办法

目标

  • 简单且可以频繁地在生产环境中进行发布。
  • 运维具有稳定性和可预测性。

持续交付(Continuous delivery,CD)

1. 什么是持续交付
持续交付的每个开发/测试流程都不会进行部署,但每个流程都会生成可以交付的软件。之后,是否部署就看商业决策了,而传统的软件交付则是在创建可以发布到生产环境中的构件之前,会提前进行大量的开发工作和一个长时间的测试流程

2. 持续交付的优势

  • 何时进行下一次软件发布依赖于商业决策,而不是由一个复杂、不可预测的软件开发过程决定的
  • 有机会收集反馈,并用来改进产品的后续版本

可重复部署

部署可重复有助于系统部署和保证系统的稳定性,并且通过在每次开发/测试的迭代过程中控制变化风险,可以缩短整个新功能的交付时间。

达到部署可重复手段包括:

1. 控制环境

你必须从标准化的机器镜像开始。例如,从一个基础Ubuntu镜像开始,并且软件需要Java开发工具包(JDK),那么可以通过脚本将JDK安装到基础镜像中。此模式也经常被称为基础设施即代码(Infrastructure as Code)。当需要一个环境的新实例时,可以从基础镜像开始并执行脚本,这样就可以保证每次都拥有相同的环境。

2. 控制可部署构建
一旦你为软件开发生命周期的不同阶段构建了不同的构件,可重复性就可能受到影响。为了实现高效、安全和可重复的生产环境运维,不要把环境相关的配置包含到代码中。

3. 控制流程
将各个部分组合起来并确保一致性的唯一方法就是自动化。

安全部署

  1. 并行部署和版本化的服务
    安全部署的核心是并行部署。与用新版本完全替换一个正在运行的软件版本不同,你可以在部署新版本的同时让已有的版本继续运行。一开始,只有一小部分流量被路由到新的版本,然后你可以观察会发生什么。你可以根据各种条件来控制哪些流量被路由到新的版本,例如请求来自何处(例如,来自某个地理位置或者引用页)或者用户是谁。
  2. 进行必要的远程监控
  3. 灵活的路由
  • 软件构件必须实现版本控制,并且版本必须对路由可见,以便恰当地切分流量
  • 用于分析新版本工作情况的数据可以有多种形式
  • 路由是并行部署的一个关键因素,而路由算法属于软件的一部分
  • 创建更小的部署单元

变化是一定的

在不断变化的情况下保持系统功能的完整性,是我们设计软件的最终目标,而变化对系统稳定性和可靠性的影响也是显而易见的。一个能够自我修复的系统,其正常运行的时间比每次出故障都需要人工干预的系统要长得多。将部署作为一种新的期望状态,可以极大地简化部署过程并降低风险。坚持“变化是一定的”的思维模式,可以从根本上改变在生产环境中管理软件的方式。

云原生平台

云原生平台的发展

  • AWS:软件架构、开发和运维并没有太多的改变。优势是成本降低,但故障率偏高,API中引入了AZ的概念,继而影响软件架构
  • GAE以及Azure:限制了用户代码直接对计算、存储、网络资源的访问,提供了安全性及弹性的保证
  • 云原生平台:IaaS平台是一个提供访问基础设施(例如,主机、存储和网络)的接口,而云原生平台则是一个让应用程序成为开发人员或运维人员最主要的交互对象的接口

云原生平台的核心原则

  • 使用比虚拟机更加轻量级的容器技术
  • 支持不断变化
    系统管理是通过不断监控系统的实际状态(不断变化的),将其与期望状态进行比较,并在必要时进行调整来实现预期的。
  • 支持高度分布式
    应用程序自己的环境中运行多个微服务,同时支持独立开发并降低级联失败的影响。它们的确能带来这些好处,但是随之产生的是一个由分布式组件组成的系统,而非原来单个组件或者单个进程的架构,因此要面临的复杂性也是之前不存在的(或者很少存在)。
    支持分布式的平台需要提供如下功能:
  1. 服务发现:单独的服务运行在不同的容器和不同的主机上,为了让一个服务能够调用另一个服务,它必须首先能够找到另一个服务
  2. 服务配置
  3. 分布式跟踪机制,允许自动将跟踪器嵌入请求中,以诊断多个微服务调用之间的问题。
  4. 断路器,防止意外产生的内部DDoS攻击,例如,网络中断时可能产生的重试风暴。

事件驱动模式

顺序式的编程模型会促使你以请求/响应的方式进行思考。
如果请求只有在犹如树状的所有级联请求都成功的情况下才能成功,那么微服务的可用性会降低很多。

  • “请求/响应”微服务:接收到请求时执行的代码,会直接决定向请求者提供某种响应。
  • 事件驱动的微服务:代码执行的结果与触发它的事件没有直接关系。

事件驱动的核心思想在于,因为事件导致代码被执行,然后可能会产生更多的事件。

对于请求/响应的方式,聚合发生在用户发出请求的时候。而对于事件驱动的方式,聚合发生在系统中数据发生变化的时候,并且这是异步的。

在请求/响应的微服务架构中,重试是补偿网络分区的一个关键模式。在事件驱动的系统中,事件存储是对网络不稳定性的一个关键补充

命令查询责任分离(CQRS)模式

将写逻辑(命令)与读逻辑(查询)分离开来。这就是命令查询责任分离(CQRS)模式的核心思想。CQRS的核心就是将这两个关注点分离开来。其优点在于,

  1. 写少量的数据,读取时可以通过计算得到更多的数据
  2. 读取可以采用请求与响应模式,但更新数据可以采用事件驱动模式

水平伸缩与无状态

水平伸缩

灵活的伸缩性并不是采用多个实例的唯一动机,高可用、可靠性和运维效率也同样是考虑因素,例如避免单点故障、在线升级等

无状态

对于云原生应用,相同的输入应该产生相同的结果,无论有多少实例,也无论请求被路由到哪个实例。

云原生不能继续使用黏性会话来处理有状态的服务,因为黏性会话指定的实例可能已经消失,或者由于网络异常而无法访问

云原生应用程序有存在状态的地方,同样重要的是,也有不存在状态的地方。应用程序应该是无状态的,状态应该存在于数据服务中。

无状态的优势

  1. 当你的应用程序处于无状态时,云原生应用程序平台可以在旧的实例丢失的时候,轻松创建新的实例
  2. 可以有效地管理一个应用程序的多个版本,所有这些版本都可以同时部署和运行

应用程序配置

  • 创建合适的抽象,将应用程序的部署参数化,从而在正确的时间、以合理的方式将不同环境中的参数值注入应用程序
  • 比环境变量更好的方法是在应用程序中使用一个特定的配置层,可以在这里查看某个应用程序的所有配置选项。
  • 可以将环境变量映射到配置文件中
  • 也可以利用应用程序框架将配置注入到配置文件中

使用配置层的优点

  1. 所有配置参数都定义在同一个地方,开发人员或者运维人员可以轻松地查看和理解应用程序的配置参数。
  2. 属性文件会被编译成一个单独的可部署构件,并且可以被实例化到任何环境中。

应用程序升级

  • 蓝绿升级:建立另一组实例,所有这些实例都拥有新的配置,然后将所有流量从第一组实例切换到第二组实例。运行中的版本称为“蓝色”版本,希望部署的新版本称为“绿色”版本。
  • 滚动升级:滚动部署应用程序的实例,用一组新实例替换一组旧实例的子集,然后继续下一个子集。
  1. 蓝/绿升级比滚动升级需要更多的资源。

  2. 如果你构建的应用程序可以同时运行多个版本,那么你可以让一些消费者仍然使用旧的版本,而让另一些消费者使用新的版本。我们称之为并行部署。

  3. 除了支持滚动升级,并行部署的另一个用途是支持灰度发布。

  4. 密码更新策略:服务端同时支持新旧密码以保证零停机更新

服务发现

服务

  • 单个服务代表了多个应用程序实例。
  • 在云原生软件中,应用程序被部署为多个实例。为了让软件可以正常运行,你会希望每一组实例都作为一个单独的逻辑实体来运行
  • 软件被定义为多个服务的组合。每个服务都是由一组服务实例来实现的

动态路由

服务如何与它们所代表的应用程序实例关联

  1. 更新实例列表

  2. 负载均衡

  • 禁止使用粘性会话
  • 使用集中式的负载均衡器而不是分布式的

集中式负载均衡器的优势:

  • 技术成熟
  • 实现易于理解
  • 配置容易
  • 为避免单点故障应该部署多个实例

客户端负载均衡是在客户端配置负载均衡器,这样http请求就会直接发送给服务的某个实例,减少了一次网络跳转,性能更好

服务发现

服务的客户端如何发现和找到服务

  1. Kubernetes集群会提供一个名为CoreDNS的DNS服务。
  2. 在启动时,Kubernetes会将服务的名称和地址添加到CoreDNS服务中。
  3. 在Kubernetes环境中运行的所有pod(应用程序),都会配置CoreDNS服务的地址。
  4. 任何访问DNS的操作,例如,对包含某个名称的URL发出HTTP请求,都需要访问CoreDNS服务来解析地址。

交互冗余

重试

应用程序向远程服务发出请求,如果在合理的时间内没有接收到响应,将再次尝试。

重试风暴:使用重试时,系统需要15分钟才能从3分钟的网络中断中恢复。如果不使用重试,则可以立即从3分钟的网络中断中恢复。

重试既会造成灾难性的影响,但是又有巨大的好处,尤其是在调用只是间歇性失败的情况下。

友好的客户端会:

  1. 限制重试的次数
  2. 降低重试的频率

安全的方法:一个安全的方法是指可以被调用零次或者多次,而且效果相同。该方法不应该有其他任何副作用。如GET、HEAD、OPTIONS和TRACE

幂等的方法:一个幂等的方法是指可以被调用一次或者多次,而且效果相同。它可以有副作用,但是所有重复调用的副作用必须与第一次相同。如,PUT

重试安全的方法,而不是重试幂等的方法

回退逻辑

面向失败设计最基本的模式之一,是实现回退的方法,即当主逻辑失败时执行的代码。

控制循环

控制循环永远不会期望达到完成的状态。它的目的就是不断地寻找不可避免的变化,并做出适当的响应。

断路器和API网关

断路器

如果服务开始出现故障但是次数不多,先停止该服务的所有流量一段时间,希望给它一段时间,让它能够从故障中恢复。过一段时间后,让单个请求通过,查看其运行情况。如果请求失败,则继续维持保护措施,不允许后续的流量通过。如果请求成功,看看则视为服务恢复正常,并允许流量通过。

断路器状态:打开、关闭、半开

API网关 服务网格实现

API网关始终位于实现的最前面,并且提供了大量的服务。这些服务可能包括以下内容。

  1. 身份验证和授权
  2. 加密解密
  3. 限流
  4. 访问日志

API网关模式的目标之一是将服务开发人员的关注问题与运维的关注问题分开。希望让后者能够统一控制正在运行的服务,并且为他们提供一个易于管理的控制平面。

服务网格

挎斗Sidecar

简单来看,挎斗是一个与主服务一起运行的进程,或者是与主服务容器运行于同一个pod中的容器。

Kubernetes pod中运行的所有服务都可以托管在相同的IP地址上,这意味着它们可以通过localhost互相寻址,因此网络开销将变得很小。

打破数据单体

微服务需要缓存

  1. 提升性能
  2. 当下游服务出现故障时,由于缓存的存在,请求也有可能成功

数据一致性

请求/响应模式 转变为 事件驱动的方式

问题在于事件通知机制失败时,缓存中的数据可能是损坏的,而且可能会无限期的保持下去。解决方法为:

使用异步的消息传递系统,而不是直接发送消息给需要的微服务

  1. 事件载荷的规则1 一个被发布到事件日志中的事件,应该完整地进行描述。
  2. 事件载荷的规则2 对于事件日志来说没有标准的事件模型。生产者可以控制所传递事件的数据格式,而消费者应该适配该格式。
  3. 事件载荷的规则3 所有发布到事件日志的事件都必须有一个相关的结构(schema),供所有相关方访问,并且必须对该结构进行版本控制。

事件消费者,规则1 尽可能让事件消费者的操作是幂等的。

Envoy在这些交互的边缘实现了许多模式,包括重试、断路器、限速、负载均衡、服务发现、可观察性等。

Istio的口号是“连接、安全、控制和观测服务”,支持自动注入挎斗并提供Envoy代理配置、证书处理和策略执行的组件。控制平面API提供了与此管理控制平面相关的接口。

日志、监控与跟踪

应用程序日志

3日志出现的位置应该完全由应用程序部署来控制,而不是由应用程序本身来决定。日志消息是出现在指定文件中还是出现在控制台中,或者出现在其他地方,都应该在部署时确定。

对云原生应用程序来说,该部署配置应该将日志行发送到stdout和stderr。这是因为:

  1. 禁止将日志直接写入文件。本地文件系统与容器的生命周期是一致的。
  2. stdout和stderr与供应商无关
  3. stdout和stderr与操作系统无关
  4. stdout和stderr是流式API,日志本身也是流。

日志需要进行大规模提取和存储,并且接口必须支持对这些海量数据的搜索和分析。ELK技术栈(详情参见链接46)汇集了三个开源项目—Elasticsearch、Logstash和Kibana—来满足这些要求。

应用程序监控

在基于拉的方法中,度量指标聚合器会作为一个收集器,从每个应用程序实例中收集请求指标数据,并将这些指标存储在时序数据库中。问题在于,
有多个经过负载均衡的应用程序实例,你只能从其中一个实例获取指标,并且不知道具体是哪个实例。这。

基于推的模式,其中,每个应用实例负责按固定时间间隔将指标发给指标聚合器。一个挎斗代理不仅可以提供指标值,还可以代理应用程序发来的指标推送,因为即使应用程序没有安装代理(Agent),挎斗依然可以提供一定程度的可观察性。因为它代理了进出应用程序的流量,所以可以代替应用程序来生成许多指标

应用程序跟踪

分布式跟踪解决的是如何跨多个分布式组件来跟踪程序流的问题。

Zipkin是当今使用的一种流行技术,核心包括以下几方面内容:

  • 使用跟踪器(tracer)在请求和响应中插入唯一标识符,以便找到相关的应用程序调用。
  • 一个控制平面(control plane)使用这些跟踪器将一组调用组装成调用图(或者是特意设计的独立调用)。

下一篇: Javascript中的异步编程→