Skip to content

Latest commit

 

History

History
769 lines (426 loc) · 75.4 KB

云原生:运用容器、函数计算和数据构建下一代应用.md

File metadata and controls

769 lines (426 loc) · 75.4 KB

云原生:运用容器、函数计算和数据构建下一代应用

鲍里斯·肖勒 特伦特·斯旺森 彼得·加索维奇

O'Reilly Media, Inc.介绍

O'Reilly以“分享创新知识、改变世界”为己任。40多年来我们一直向企业、个人提供成功所必需之技能及思想,激励他们创新并做得更好。

译者序

随着“云计算”的兴起,越来越多的公司开始“上云”。很多公司的上云之路是从传统应用的迁移开始的,但越来越多的创新企业开始直接基于云基础设施来设计、开发新一代应用。于是“云原生应用”这个概念这两年也变得愈发火热。这种“诞生在云上的公司”往往具备很多不同寻常的特质,这使其具有快速爆发的潜力。利用公有云丰富的IaaS、PaaS和SaaS产品,公司可以实现业务的高度弹性,也便于实现数据驱动的运营。毫无疑问,“云原生”正悄然改变着软件开发的传统思维和模式。

这个理念的背后是云计算、容器、函数计算等核心技术

前言

不同公司和行业的精神领袖常常会重述Watts Humphrey的观点:“任何企业最终都将变成一家软件企业。”他对形势的判断确实非常准确。软件正在冲击每个企业的现状,悄然改变着世界。Netflix彻底颠覆了我们收看电视和电影的习惯,Uber改变了运输业,而Airbnb正在挑战酒店业。

云计算可以带来很多好处,比如可以快速更新和修改、易于使用新技术,并利用云端资源的集群优势来降低成本,改善经济效益。

这本书不会手把手教你如何实现一个满足特定业务需求的云原生应用。但是在读完这本书之后,你应该会知道如何去设计、构建和运维一个成功的云原生应用。在你去实现一些业务需求的时候,操作指南固然很有用,然而系统地理解云原生应用的基本原理和构建方法,才能使你的团队掌握打造成功的云原生应用的能力。

1.1 分布式系统

分布式系统看上去就像是一台机器在工作,但其实它由一组独立的机器组成,它们之间通过网络相连接。这样的系统可以将计算任务分配到不同的机器上,这种分配任务的能力使得应用服务在具有可扩展性、可靠性的同时也更加经济。

网络是安全的 即使在云端,你也无法保证网络是安全的。各种服务通常会部署在不同的机器上,所以在开发这些服务时你需要把潜在的网络故障考虑进去。

· 避免频繁的网络调用和一些不必要的请求。 · 在设计云原生应用时,可以考虑采用缓存、内容分发网络(CDN)、多区域部署等技术或方法来使得数据离客户端更近。 · 采用“发布/订阅”模式,以通知有新数据到达,并将其存储在本地以便可以立即使用这些数据。

一个人完全搞明白整个应用是如何工作的几乎是件不可能的事情,更别说去修复问题了。所以你需要确保你的应用有完善的治理措施,使得排查故障变得相对容易。

其次,传输的数据与对象之间的转换是有成本的,例如序列化和反序列化除了会带来网络延迟外通常还会产生一些额外的昂贵开销。

CAP定理指出,任何一个通过网络连接的、共享数据的分布式系统最多只能同时满足以下三个需求中的两个: · 一致性(Consistency, C):指所有节点访问同一份最新的数据副本。 · 可用性(Availability, A):指系统提供的数据或服务必须一直处于可用状态。 · 分区容错性(Partition tolerance, P):指系统在遇到网络分区故障的时候,仍然能够对外提供服务。

1.2 十二要素应用

传统数据中心在需要扩容时经常采用纵向扩容的方式,即通过增加单台物理服务器的计算资源来进行扩容。而在云端,通常采用的是横向扩容的方式,即通过增加虚拟服务器的数量来分担负载。这种扩容方式要求应用程序是无状态的,而这个特点也是十二要素(12-factor)应用的宣言之一。

一份基准代码,多份部署。 一个应用只有一份基准代码,但是这份代码可以部署到多个环境中,如开发环境、测试环境和生产环境。在云原生架构中,这个原则可以解释成一个服务或者函数只有一份基准代码,它们各自拥有自己的持续集成(Continuous Integration, CI)和持续部署(Continuous Deployment, CD)工作流。

个比较推荐的做法是通过外部配置系统来获取这些服务的配置信息。这样做的好处是降低耦合度,这也是云原生应用的一个基本原则。

用一个或多个无状态的进程来运行应用。 如前所述,在云端的应用应该是无状态的,任何需要持久化的数据都应该存储在外部。这样做才能实现弹性,而弹性是云计算的目的之一。

每个服务管理自己的数据。 这是微服务架构的一个关键原则,同时也是云原生应用的一个常见模式。每个服务管理自己的数据,这些数据只有通过该服务的API才能获取。这意味着即使属于同一个应用,一个服务也无法直接访问其他服务中的数据。

通过快速启动和优雅退出来最大化应用的健壮性。 容器技术和函数已经能够满足第一点了,因为它们的启动速度都很快。但后一点常常被忽略,在设计一个服务时就应该考虑到程序崩溃和规模收缩问题,这种情况下容器或者函数的实例数量会减少,我们要特别注意这一点。

尽可能地保持开发环境、预发布环境和生产环境相同。

这一点的意思是你应该把管理任务当作是一个短期进程来执行。函数和容器都是执行这些任务的好工具。

1.3 可用性和服务等级协议

这个应用的综合服务等级协议(Service-Level Agreement, SLA)永远不可能达到单个服务中的最高SLA的水平。SLA通常是按年来衡量的,我们经常说某个应用达到了“多少个9”。表1-1列出了云服务常见的可用性百分比及其所对应的宕机时间。

第2章 云原生基础

原生应用本质上是分布式的,充分利用了云基础架构的特性。构建一个云原生应用有很多不同的技术和工具,从计算的角度来看主要有两个,一个是容器,另一个是函数计算。从架构的角度来看,微服务架构已经受到了很多人的欢迎。

理解了如何利用好函数计算和容器技术,配合事件通知和消息通信相关技术,开发者们就可以最有效和快速地构建下一代基于微服务架构的云原生应用

2.1 容器

此外,容器使用写时复制(copy-on-write)的文件系统策略,这就允许多个容器实例可以共享数据,因为只有当容器需要修改或者写入新数据时,操作系统才会复制一个数据的副本。所以从内存和磁盘空间使用的角度来看容器是非常轻量级的,这也是为什么容器可以非常快地启动,快速启动是容器带来的巨大好处之一

对于现代的云原生应用而言,容器镜像已经成为集应用服务代码的封装、运行环境、依赖项和系统库等于一体的部署单元。因为容器具有快速启动的特性,所以非常适合用在需要快速横向扩容的场景,比如云原生应用。

容器编排主要包含以下内容: · 在集群节点上创建和部署容器实例。 · 容器的资源管理,即把容器部署在有足够运行资源的节点上,或者当这个节点的资源超出限额时可以将容器转移到别的节点上。 · 监控容器以及集群节点的运行状况,以便在容器或者节点出现故障时进行重启或重新编排。 · 在集群内对容器进行扩容或收缩。 · 为容器提供网络映射服务。 · 在集群内为容器提供负载均衡服务。

Kubernetes是目前最流行的一款集群管理及容器编排工具。

Kubernetes(通常缩写为k8s)是一个用于运行和管理容器的开源项目。谷歌公司于2014年开源该项目,Kubernetes常常被视为容器平台、微服务平台以及为云计算提供可移植性保障的中间层。

Kubernetes主节点组件包括了以下组件:

API服务器(kube-apiserver) 该组件对外暴露了Kubernetes的API接口,是Kubernetes的前端控制层。

etcd 这是一个键值数据库,用来保存集群数据。

图2-3:Kubernetes主节点组件与工作节点组件

pod基本上可以认为是一个管理容器整个生命周期的一个封装。它里面包含了一个或者多个容器、存储资源、唯一的网络IP。

换句话说,通过边车容器来扩展或增强应用容器功能的这种模式非常受欢迎。像Istio这样的服务网格(service mesh)很大程度上依赖于边车容器,

服务(service) 在Kubernetes中,一个服务是对集群中运行的一组pod的抽象,它向外提供一个可以访问这组pod的稳定接入点。Kubernetes使用标签选择器(label selector)来识别服务所指向的那些pod。

副本集(replicaSet) 最简单的理解就是把副本集当作是一组服务的实例。你可以简单地定义一个pod需要多少个实例运行,Kubernetes会负责保证在任何情况下都有指定数量的实例在运行。

部署(deployment) 一个“部署”指的是Kubernetes中的一个部署描述文件,你可以在该文件中描述你所期望的部署结果,部署控制器(deployment controller)会按指定的节奏来改变实际状态,直至达到期望状态。换句话说,你可以用部署文件来部署或监控服务的副本集、调整副本集的数量、更新服务、把服务回滚到之前的版本、清理旧的服务副本集,等等。

Kubernetes其实只是一个容器的编排平台,所以它需要一个容器运行时来管理容器的生命周期。Kubernetes从诞生开始就支持Docker运行时,但它并不是Kubernetes唯一支持的容器运行时。

事实证明,接口是一种很好的软件模式,可以协调两个不同的系统,因此,Kubernetes社区创建了容器运行时接口(Container Runtime Interface, CRI)。CRI的存在避免了把特定的容器运行时“硬编码”进Kubernetes的代码中,这样也就避免了一旦容器运行时发生变化时就需要修改Kubernetes代码的麻烦。

containerd 如果要说一个最受欢迎的容器运行时的话,那非containerd莫属。它是Docker和Kubernetes CRI所使用的一个行业标准级的容器运行时。它作为一个Linux和Windows系统中的守护进程出现,负责管理它所在的这个主机上所有容器的整个生命周期(包括容器镜像的管理、容器的运行、底层存储、网络配置,等等)。

2.2 无服务器架构

无服务器架构意味着服务的伸缩以及底层的基础架构都是由云服务提供商来管理的。也就是说,你的应用会自动被分配到或者释放计算资源,你完全不用操心底层的基础架构该怎么来管理。所有的这些管理和运维操作都被剥离了出来,交由云服务提供商来解决

从开发人员的角度看,无服务器架构通常会伴随着事件驱动的编程模型,而从经济角度看,无服务器架构意味着你只需要按执行所耗费的资源付费,比如消耗的CPU时间。

很多人把函数即服务(Function as a Service, FaaS)等同于无服务器架构。

2.3 函数计算

无服务器计算带来的快速启动和执行的优势,加上对应用程序的简化使得FaaS产品对开发人员产生了巨大的吸引力,因为他们只需要专注在写业务代码上,而不需要操心底层架构了。

开发者的角度看,一个函数就是一个可执行单元,这意味着这段代码有一个起始状态和一个结束状态。一个函数通常是由其他函数或平台服务发出的事件触发的。

第一种情况是你希望避免供应商锁定。因为你需要把你的函数部署在一个特定的FaaS产品上并使用供应商提供的高级别的云服务,这样做会使你的整个应用缺乏可移植性

任何FaaS产品的实施,无论是基于云服务提供商的无服务器架构的还是安装在你自己的集群上的,一个关键点在于启动时间。一般而言,你总是期望函数在被触发后立马就开始执行,这意味着它们所依赖的底层技术能够提供非常快的启动时间

2.4 从虚拟机到云原生

向云原生世界的演进主要有两种途径。第一种是“旧房改造”,意思是你有一个历史遗留应用,通过对这个应用的转变和提升,使其变得更现代化,这是一个程序优化的过程。另一种是“白手起家”,也就是从无到有创建一个云原生应用。

上云的主要优势还是在于经济原因。通过使用云服务器可以避免搭建自己的数据中心,也省去了很多基础设施的运维工作,因此可以节约成本。

容器除了提供超快的启动速度外,还彻底解决了缺少依赖项的问题,因为应用程序所需的所有依赖都会被一起打包进同一个容器中。因此,很快开发人员就纷纷投入容器的怀抱,把容器作为一个包装格式的想法实在太赞了。现在,几乎所有的新应用都在使用容器技术,越来越多的历史遗留下的巨石应用(monolithic application)正在被容器化。

尽管如此,你还是有以下理由去切分你的巨石应用: · 更快的部署速度。 · 同一个应用中,总有些组件的更新频率会高于另一些组件。 · 不同的组件对于扩容的需求也是不同的。 · 有些组件可能更适合用另一种技术或语言来开发。 · 代码库已经变得太大,太复杂了。

除了需要了解Kubernetes之外,开发人员还需要了解一些分布式系统的模式以处理诸如弹性、诊断和路由等问题。

像Istio或Linkerd之类的服务网格(service mesh)越来越受欢迎了,因为它们将一些分布式系统复杂性转移到平台层。第3章详细介绍了服务网格,你可以暂时将服务网格理解为处理服务与服务间通信的专用网络基础架构层。除此之外,服务网格还帮助你的应用实现弹性,提供了诸如重试、断路器、分布式追踪和路由等功能。

改进应用程序的下一个阶段就是用无服务器架构来承担容器的工作负载,比如像Azure Container Instances或者AWS Fargate这样的容器即服务(CaaS)产品。

函数计算在处理短任务方面是非常出色的,例如更新记录、发送电子邮件、消息转换,等等。学会利用函数计算的前提是,你需要首先从你的应用中鉴别出哪些功能可以作为短任务,然后把它们改造成一个个函数。

2.5 微服务

微服务这个术语常常代表两个意思,一个是指微服务这种架构风格,另一个是指微服务架构中的各个服务。微服务架构是一种面向服务的架构体系,其中应用程序按功能分解为小型的、松耦合的各种服务。其重点在于,单个服务被划分的足够小,相互间耦合度很低,并围绕业务功能进行分解。

微服务架构中,应用程序由多个较小的代码库组成,由独立团队开发和管理。每个服务都专注于一个特定的任务,由一个小团队负责开发和运营。这些服务在独立的进程中运行,相互之间通过基于同步或异步消息的API进行通信。 每个服务都可以被视为一个独立的应用,有独立的团队、测试、开发、数据和部署。图2-7以库存服务为例展示了微服务架构的概念。

敏捷性 对大型的巨石应用而言,快速、可靠地部署是一件具有挑战性的事情。一个模块中的改动可能会阻碍你部署另一个功能模块的小改动。随着应用的成长,测试的工作量也会随之增加,这就会导致可能需要相当长的时间才能使得新改动的价值得以体现。一个功能改动会需要整个应用的重新部署,而当出现问题时需要回滚整个应用。通过把一个应用拆分成多个小服务,那么改动所需的验证时间会被缩短,发布速度可以得到提高,同时也会更可靠。

服务架构可以帮助企业更容易和可靠地交付新功能和服务。小型的独立团队甚至可以在业务最繁忙的时候仍旧发布新的功能,同时进行A/B测试,以便提升转化率或改善用户体验。

通过把应用拆分成更小的服务,小的敏捷开发团队可以聚焦在更小的功能点上,并且快速地行动起来。对于新手而言,也更容易融入团队,因为他们只需要关注这个较小的服务。团队成员可以更轻松地维护这个服务并对他们构建的服务负责。

通过把这些功能拆分成独立的服务,开发人员就可以把服务放在最合适的环境中运行,以满足每个服务的扩容和资源使用的需求。

分布式系统本身就已经够复杂的了。当我们把一个应用程序拆分成多个独立的服务后,它们之间需要依靠网络去通信。网络调用会增加延迟,并且有时候会遇到闪断。此外,任务又是在不同的机器上执行的,它们的时钟可能不一致,当前时间多多少少都会存在一点细微差别。

随着服务数量的增加,单个服务遇到故障的可能性也在增加。这就需要服务在设计时考虑到弹性,或者在服务中断时采取功能降级措施。

3.1 云原生应用的基础

精益运营的意思是,你在设计这个应用的时候就需要考虑如何去运行你的程序,如何去监控它,并且持续去改进它。

云端自动化与基础设施即代码(Infrastructure as Code, IaC)密切相关。它可以帮助你在配置环境和部署应用时最大限度地减少错误,因为整个环境的管理是完全通过代码来实现的。

第7章还会简要介绍HashiCorp的Terraform,这个产品可以帮助你用IaC的方式来进行多云管理和部署。除了最大限度地减少犯错外,自动化还可以使你借助源代码版本控制系统来记录你的环境变化,并且可以快速地复制出相同的新环境。除了自动化地配置环境,你还需要自动化整个应用的部署过程。

监控一切 通过监控,你不仅可以了解应用程序和环境的状态,还可以了解程序的使用情况。根据监控数据,你可以采取相应的措施来降低运营成本、提高程序的性能以及增加新功能。从架构的角度来看,你应该确保整个应用的栈都处于监控下,从你运行应用的基础架构,到你使用的托管服务,以及你的应用的功能和特性。

文档应该是自动生成的而不是手工撰写的。比如说,你可以在开发时设计符合OpenAPI规范的API,这样你就可以通过注释和Swagger工具,在持续集成(CI)的某个步骤完成文档的自动生成。

增量更改 当你对环境或者应用进行更改时,应该确保这些修改是增量的和可逆的。这就回到了使用IaC的一个优势,就是可以通过源代码版本控制工具来管理你的环境描述和定义文件,因此你就可以很轻松地回滚任何更改。

为故障而设计 在云端,故障总是会时不时地出现。你需要考虑的不仅仅是当故障发生时你的应用该如何进行容错,更需要考虑当故障发生时应该进入什么样的流程来处理故障。很多测试框架可以帮助你模拟故障,使你知道相应的故障会带来什么影响以及如何减轻这种影响。

可靠性的意思是当故障发生了,应用程序仍然处于一个可接受的工作状态。而可用性指的是你的应用程序在一个时间窗口内可以正常提供服务的时间。

从可靠性的角度来看,你需要确保你的设计可以使应用从故障中恢复。如前面介绍的那样,微服务架构中每个服务都是独立的,当某个服务发生故障时,不会导致整个应用故障。对于服务本身而言,你应当考虑通过水平扩展来提高整个系统的可用性。比

我们已经通过前面的内容明白了网络不总是可靠的,所以你在设计时就应该把重试和断路器考虑进去。在3

3.2 云原生与传统架构的对比

在收缩的时候需要注意一点:大多数情况下,你需要确保在收缩前,该节点的所有请求都已经处理完毕。

原生架构处理故障的方式与传统应用有很大的不同。如前所述,云原生架构总是会对故障有所准备,并且有处理故障的机制。而传统架构总是试图规避故障,比如说,通过数据库集群来避免数据库故障等。

3.3 函数计算与服务

对于简单的、短期和独立的任务应该考虑使用FaaS。

· 许多物联网(IoT)场景使用函数来编排任务。比如发送消息到IoT中心,后者触发函数来对这个消息执行计算或者路由任务。

函数是无状态的且不直接对外暴露 因此,FaaS鼓励使用事件驱动的分布式编程模型或者使用API网关这样的方案来对外提供函数的功能。通常,函数间通过事件或者消息系统来相互传递数据。状态信息一般存储在外部云服务中,这意味着处理一个事件时需要把状态信息从存储中读出来,函数处理完再写回去。每个步骤都会增加网络延迟。通常来说,如果用FaaS来构建一个大型应用,那么总得忍受一部分性能损失,因为任何通信和数据都是要靠网络来传输的。

将函数计算和容器化服务结合在一起是一个很好的解决方案。这样既利用了FaaS的简便性,又利用了容器服务的灵活性。

即便对FaaS产品而言,虽然冷启动(启动一个函数或者容器所需的时间)也花不了多长时间,但你还是需要了解应用在伸缩时的行为。在突发场景下,许多函数是并行的,如果你的应用依赖于其他服务,比如RDBMS,你的数据库连接可能会超过最大连接数限制,最终导致的结果就是应用程序速度变慢。

3.4 API设计与版本控制

因为API是其他服务用来与你的服务进行通信的接口,因此正确地记录和版本化API至关重要。

使用全局版本控制,你可以对整个API进行版本控制。版本体现在API的路径中(例如,/api/v1/users)或二级域名中(例如,api-v1.example.com/users)。

3.5 服务间的通信

你可以把服务间的通信分为两类,一类是外部服务通信,另一类是内部服务通信。内部通信是指通信发生在同一个集群中(例如,同一个Kubernetes集群中的服务之间的通信),而外部通信是指与外部服务的相互通信,比如调用数据库服务(DBaaS)。从客户端到集群的外部通信通常称为南北通信,内部服务通信通常称为东西通信。在Kubernetes环境下,入口(ingress)控制器用于南北通信,出口(egress)控制器可用于访问外部服务。Kubernetes通过kube-proxy来提供原生具有负载均衡能力的东西通信,

大多数情况下,HTTP协议会被用来作为客户端和云原生应用程序之间的通信协议。但是,它并不是性能最高的协议。一个大型的微服务架构下的应用程序可能由数百甚至数千个微服务组成,服务越多,需要进行的通信和数据交换就越多。

HTTP/2的主要设计目的是实现通信的低延迟,并在单个TCP连接上通过流(stream)来实现多路复用请求,从而提高数据传输的效率。HTTP/2是二进制协议,而HTTP 1.x是文本协议。二进制协议的解析效率更高,因为只有一个代码路径,这使得它们在网络上非常高效。

gRPC是一个比较新的协议,其出色的性能和对开发人员的友好性使得它在微服务社区中迅速流行起来。gRPC是一个使用HTTP/2作为传输协议的高性能、轻量级的通信框架,它提供了诸如身份验证、双向通信、流控制、阻塞或非阻塞绑定以及取消和超时等功能。gRPC也使用了协议缓冲区(也称为protobuf),它提供了一种将结构化数据定义和序列化为高效的二进制格式的方法。由于它们是二进制格式的,所以它们的传输量相对更小,可以快速在线上传输。

云原生应用经常会和事件驱动和基于消息的架构结合起来,因此十分有必要来介绍一下消息协议。消息传递协议有很多,如STOMP、WAMP、AMQP和MQTT等。

高级消息队列协议(AMQP)也是一种二进制协议,它的设计目的主要是提供具有丰富功能集的消息传递,这些功能集包括可靠的队列、基于主题的发布者/订阅者模式、消息路由、安全性和事务。

幂等性 无论你是使用同步还是异步的通信方式,都需要确保如果一个相同的操作被重复执行了多次,目标系统中的结果仍将保持不变。能够多次执行同一个操作而不改变结果的特性被称为幂等性。正如你将在本章的后面看到的那样,由于接收方的故障、重试策略等原因,消息可能被重复接收和处理。理想情况下,接收方应以幂等方式处理消息,这样即便消息被重复也不会导致不同的结果。

确保幂等性操作的一种常见方法是在消息中添加唯一的标识符,并确保仅当标识符不重复时,服务才对消息进行处理。

发布者/订阅者(pub/sub)模式是云原生应用中最常见的异步通信模式之一。发布者将消息发布到一个主题,订阅该主题的所有订阅者将立即接收到该消息。

· 实现服务和函数之间的松耦合,因为它可以将发布者与订阅者分离。 · 实现事件驱动的设计,这是云原生应用领域中的一种非常流行的设计方法。

· 另一方面,有状态的应用程序有时确实需要消息按顺序被处理,这种情况下,你可以利用消息系统内置的排序功能或使用优先队列模式来解决这个问题。

同步的意思是客户端一直等待直到有响应。同步调用很直接,容易理解和使用,那为什么不以同步方式来实现整个服务间通信呢?

资源枯竭 同步是指线程在等待响应时被阻塞。这种行为在大规模场景下很容易导致资源枯竭。

响应延迟 例如,如果用户调用了服务A,服务A调用服务B,依此类推,则总响应时间是各个服务响应的总和。只要其中一项服务的响应速度很慢,它将阻止整个响应,并且导致应用程序的延迟增加,这通常会导致糟糕的用户体验

级联故障 与响应延迟类似,中间有一个服务故障就会导致级联故障,最终可能导致整个应用的崩溃。

与其想办法解决使用同步通信而遇到的潜在问题,不如考虑在服务之间使用异步通信。通过服务之间的异步通信,客户端可以发出请求,并不需要阻塞,直到获得响应。与此同时,它可以使用非阻塞请求节省下来的资源来做其他事情。在云原生世界中,基于事件和基于队列的异步消息传递是进程间通信最受欢迎的模式。

3.6 网关

网关 在微服务和函数计算的世界中,客户端所需的功能通常分布在多个服务和函数上。客户如何知道要请求的服务的接入点是什么呢?此外,如果将现有服务重新部署到不同的接入点或引入新的服务要怎么办

API网关可以帮助我们解决前面提到的那几个问题。在客户端和服务之间可以存在一个或多个API网关。它们的职责可能有所不同,从将传入请求路由到后端的服务,到通过公共接入点暴露业务API,又或者是负责SSL终结或身份验证之类的任务。另外,网关是可以分层的:你可以让一个网关负责SSL终结,下一个网关进行鉴权和授权,然后最后一个网关可以实际将请求路由到后端服务。

路由是网关最常见的功能之一。在这种情况下,网关充当一个反向代理,并将传入的请求路由到后端服务,

网关也可以充当聚合器:它从客户端接收一个请求,然后把这个请求拆分成多个子请求,发送到不同的后端服务。最后,它把这些子请求的响应聚合成单个响应返回给客户端

网关最常见的功能之一是为独立的服务减负,将一些后端服务的功能卸载到网关中实现。例如,你可以把SSL终结的任务放到网关中进行,而不是让后端服务去做这些事。在网关终结SSL还可以将安全资产(如证书)独立保管,更安全。

以下是一些可以从单个服务中卸载到网关级别处理的功能示例: · 鉴权和授权 · 速率限制、重试策略、断路 · 缓存 · 压缩 · SSL终结 · 日志和监控

实现网关有很多种技术和方法。最受欢迎的网关代理是NGINX、HAProxy和Envoy。所有这些都是反向代理,提供负载平衡、SSL和路由等功能。这些产品都在许多生产环境中经过了实战检验。

3.7 出口网关

上一节我们了解了网关(入口网关),入口网关处理的是进入系统的流量,并且可以承担各种任务,例如路由或卸载部分后端服务的功能。与之类似的是,在内网运行的出口网关(egress gateway)可以帮助引导和控制所有离开内网的流量。

将出口网关用作Istio等服务网格的一部分,可以对出站流量进行更细粒度的控制,并提供更丰富的功能,例如安全传输层协议(TLS)创建。你可以配置Istio为需要访问外部服务的请求创建TLS连接,出口网关负责接收未加密的内部HTTP连接,对请求进行加密,然后将其转发给外部服务。另外,你还可以使用通配符来限制可访问主机的范围,从而将流量定向到公共域中的某一组特定主机。

如果你需要监控或者控制对外部服务的访问,那么你应该考虑使用出口网关。

3.8 服务网格

在云原生的世界中,每个服务都是独立构建和部署的,并且每个服务都可能与其他微服务通信。随着业务的发展,你会开发越来越多的微服务,这也意味着服务之间的通信会增加,并且也会变得更加复杂。

服务网格背后的想法之一是通过将通用功能从每个服务抽出来移入到服务网格中,从而提高开发人员的生产力。这样做的另一个好处是实现了服务的业务功能和服务网格的通用功能之间的分离,使得开发人员能专注在业务上。如果将功能移到网格上,你就不再需要维护多个库,最终状态如图3-17所示。

(sidecar)与服务本身在一个相同的pod中运行,并且它们共享相同的网络。代理的工作是监听所有进出服务的请求。每个代理都可以有其自己的配置,该配置定义了如何处理流入或流出的流量。除了处理流量和请求外,代理还可以向网格服务控制器发送各种数据。除了边车代理这种模式,你也可以在每个主机上运行一个代理,在Kubernetes中,你可以使用DaemonSet来实现。

使用边车模式的代理非常简单,不需要太多配置。但是,这种做法会增加资源消耗,因为你需要在每个pod中运行一个额外的容器。如果你运行服务实例太多,这种做法就可能会出现问题。你可以通过在每个主机上运行代理来降低资源消耗。

Istio服务网格在服务的每个实例旁运行Envoy代理,

我们可以将服务网格的主要功能分成以下几类: · 流量管理 · 故障处理 · 安全性 · 追踪和监控

以下是一些最常见的可以用来控制流量的筛选条件:

请求标头 你可以根据传入的请求所使用的HTTP标头,URI地址或HTTP方法来确定是否要应用路由规则。

故障处理 在分布式系统中,你应该始终假设服务通信可能会由于各种原因的故障而中断。这些错误不一定是由于服务代码中的缺陷造成的。例如,网络或基础设施的问题也可能会导致故障。故障有两种类型:瞬时故障和非瞬时故障。瞬时故障随时可能发生,并且在大多数情况下,多试几次就能恢复。非瞬时性故障更为持久。例如,访问一个已删除的文件

除流量管理功能外,服务网格还支持通过定义请求超时、重试和断路器来处理请求失败。超时和重试的默认值是针对每个服务和服务版本独立设置的。

断路器是使服务更具弹性的另一项功能。断路器模式通过对故障服务的访问限制来防止其进一步导致其他服务的故障,甚至导致对整个系统的压力。如果断路器跳闸,它将阻止出现故障的服务被继续访问。

每个接收流量的接入点都需要定义其自己的断路器。Envoy代理中的断路器实现是通过监控每个主机的状态,一旦有主机达到预定义的阈值,它将被排除在可用主机池外。

安全性 概括而言,可以将服务网格中的安全性分解为鉴权和授权两部分。鉴权是搞清楚你是谁,你的身份是什么,你可以在系统内执行什么操作(授权)或访问什么内容。

以Istio服务网格为例,有多个组件共同为运行在服务网格内的服务提供安全保障: · Citadel组件提供密钥和证书管理。 · 作为边车服务运行的Envoy代理和出入口代理负责实现服务之间的通信安全。 · Pilot组件为Envoy代理提供身份验证策略和安全的命名信息。 · Mixer组件提供授权管理和审计功能。

同鉴权策略类似,Istio将授权策略保存在配置中心内,pilot组件会看这些策略有没有变化,如果有变化则会及时更新到代理上。Envoy代理会根据策略来评估访问请求,并返回结果ALLOW或DENY。

追踪和监控 事实上,网格内的所有服务的进出流量都会经过代理,这使得服务网格可以自动收集各种数据和指标,比如请求数、请求耗时、请求大小、返回的状态码,等等。收集的指标会被发送到另一个组件(比如Istio中的Mixer组件),在这个组件内再对这些指标做聚合。

Mixer组件中内置了一个Prometheus的适配器,并提供一个访问接口。Prometheus可以通过Mixer上的这个接口收集从代理发送来的各种指标数据。最终,你可以通过Grafana来看到可视化的数据

Envoy代理在配置后也可以将链路追踪信息自动发送到Jaeger组件。作为一个服务的开发人员,你需要确保任何对下游服务的请求都带上了与trace和span相关的请求标头,这样Jaeger就可以知道如何去关联这些追踪信息了。

每当请求进入系统时,都会为其设置一个ID标头值。这个ID值(有时候也叫CID,即前面提到过的关联ID)可以用来追踪请求,因为它会贯穿整个系统。当出现故障时,你可以把这个ID值返回给客户端,这样就可以用它来追踪失败的请求,并定位故障。图3-21展示了请求ID如何生成并贯穿整个系统。  图3-21:带x-request-id标头的请求

3.9 架构示例

用户可以在任何能够联网的地方,通过单页应用(SPA)或移动应用来管理设备。用户还能收到设备发出的告警,或者云端服务发现的设备告警。此外,他们还同意设备发送匿名数据给这个服务,以便进行数据分析。这个服务还需要能够满足日益壮大的对集成应用和云端服务感兴趣的开发者社区和家居自动化爱好者的需求。

云原生的一个原则是尽量多的使用已有的云服务。

这个智能家居设备管理服务包括一个后端API,供有兴趣与该服务集成的开发人员使用,也会由客户端(移动设备和单页应用)使用。

API网关用于减轻一些API管理任务。API网关负责对请求进行鉴权并限制发送过多请求的用户,以维护所有使用该服务的用户的服务质量。

图3-25显示了一个单页应用(SPA),这个应用通过内容分发网络(CDN)为用户提供服务,其背后的数据源来自块存储服务。SPA通常由静态资源组成,这些静态资源可以通过块存储进行存储并提供给用户。CDN可以使客户端快速加载这些静态资源,因为它们被缓存在靠近客户端的地方。SPA必须具备缓存清除的技术,例如如果资源已更改,那么需要更新哈希值,或者在将更新推送到存储时使CDN缓存中对应的资源失效。这些任务可以在持续交付的流程中实现。

3.10 本章小结

每个应用的架构都是不同的,并且没有一种架构适合所有的应用。

第4章 数据处理

微服务架构的一个趋势是鼓励数据去中心化,将数据分散到多个服务中,每个服务都有自己的数据存储。数据副本和数据分区也是很常见的一种扩展系统的办法。

通过使用托管数据库,团队可以专注于使用数据库构建应用程序,而不必花费时间配置和管理底层的数据系统。

无服务器数据库这个术语是指基于使用情况计费的托管数据库,其根据存储和处理的数据量向客户收费。这意味着,如果不访问数据库,用户只需为存储的数据量付费。当需要操作数据库时,用户为这个操作付费,或者在操作时再创建和缩放数据库。

4.1 数据存储系统

你应该首选对象存储来存储文件数据。对象存储相对便宜,持久性强且可用性高。所有主流的云服务商都提供了不同层次的存储服务,可以根据数据访问的需求选择合适的存储以节省成本。

· 应用需要调用云服务商的API来使用对象存储。在第7章我们会介绍如果应用需要可迁移性该怎么办。

数据库通常用来存储结构化数据,这些数据有明确定义的格式。

通常,只需要使用主键甚至是部分键来检索应用程序的数据。键/值数据库可以被看作一个非常大的哈希表,该表在唯一的键下存储了一些值。

不强制要求定义模式的数据库通常被称为“读时模式(schema on read)”,因为尽管数据库未强制要求模式,但是在使用数据的应用中存在固有的模式,并且需要知道如何转化读到的数据。

关系型数据库将数据组织到称为表的二维结构中,该结构由列和行组成。一张表中的数据可以与另一表中的数据有关联,数据库系统可以保证这种关联。关系型数据库通常强制执行严格的模式,也称为“写时模式(schema on write)”,在该模式中,向数据库写入的数据必须符合数据库中定义的结构。

图数据库存储两种类型的信息:边和节点。边定义了节点之间的关系,你可以把节点看作实体。节点和边都具有属性,其中存储了该节点或边的一些信息。

时序数据库是针对时间进行优化的数据库,可根据时间来存储值。这些数据库通常需要支持大量的写操作。它们通常被用于从大量数据源实时收集大量数据。这些数据很少更新,删除操作通常是批量进行的。写入时序数据库的记录通常很小,但记录的量很多。时序数据库非常适合存储遥测数据。流行的用途包括物联网(IoT)传感器或应用程序/系统的计数器。

搜索引擎数据库通常用于搜索保存在其他存储和服务中的数据。搜索引擎数据库可以对大量的数据建立索引,并提供近实时的索引查询。除了搜索像网页这样的非结构化的数据,许多应用程序还使用它为其他数据库中的数据提供结构化和即时搜索功能。

流和队列是存储事件和消息的数据存储系统。尽管有时它们被用于相同的目的,但它们是非常不同的系统类型。在事件流中,事件被存储为不可变的数据流。用户能够在特定位置读取流中的事件,但无法修改事件或数据流,也无法从流中删除单个事件

消息队列或主题用来存储可以变更的消息,并且可以从队列中删除单个消息。流非常适合记录一系列的事件,流系统通常能够存储和处理大量的数据。队列或主题非常适合用来在不同服务之间进行消息传递,这些系统通常被用作可以被更改和随机删除的消息的短期存储。

主题是一个在发布者/订阅者消息模型中使用的概念。主题和队列唯一的区别是,队列中的消息将发送给一个订阅者,而主题中的消息将发送给多个订阅者。你可以将队列视为有且仅有一个订阅者的主题。

部署一个简单的数据库可能很容易,但打补丁、升级、性能调优、备份和高可用性配置等任务都会增加运维工作量

4.2 多数据存储下的数据

捕获数据更改如今,许多数据库都提供了数据更改事件(更改日志)流,并通过易于使用的API供用户调用。这样就可以基于事件去执行某些操作,例如在文档更改或更新实例化视图时触发一个函数。

在微服务架构中,常常会遇到一个服务希望收到另一个服务中数据更改的通知的情况。为此,你可以使用Webhook或订阅来发布事件通知给其他服务。

缓存对于提高系统的扩展性和性能非常有用,但是当后端数据发生更改时如何使缓存中的数据无效是个挑战。除了使用有效时间(Time-To-Live, TTL)外,你还可以使用更改事件来删除或更新缓存中的数据。

当一个操作跨越多个数据库时,另一种办法是将一组更改写入更改日志,然后将更改日志应用到不同的数据库上。可以将一组更改写入一个流中以维护这些更改的顺序,如果应用更改时发生故障,可以很容易地重试或恢复操作

另一种方法可能是先保存订单或购物车的处理状态,然后调用支付网关处理信用卡付款,最后更新订单状态

数据的移动和转换是实现商业智能(BI)的一个非常普遍的需求。长期以来,企业一直在使用提取、转换和加载(ETL)平台将数据从一个系统迁移至另一个系统。不管企业的规模大小,数据分析正成为每个企业的重要组成部分,因此ETL平台变得越来越重要也就不足为奇了。

让所有服务使用一个共享的数据库或公共的数据库会违反微服务的原则,并可能使服务之间产生耦合。解决此问题的常用方法是将数据迁移和汇总到一个地方,以供数据报告或分析团队使用。在图4-8中,来自多个微服务的数据被汇总到一个集中式数据库中,从而满足必要的报告和分析需求。

各个服务团队可以给数据分析团队提供对数据库的只读访问权限,并允许他们复制数据,如图4-9所示。这将是一种非常快捷、简便的方法,但是服务团队无法控制数据提取将在何时发生或者对存储会造成多大的负载,这有可能导致潜在的性能问题。

可以通过让数据分析团队访问只读的数据副本而不是主数据源,来解决数据库上的ETL负载对服务性能产生不利影响的问题。还可以通过让数据分析团队访问数据视图,而不是原始文档或数据库表来减轻一些耦合问题。

4.3 客户端访问数据

一个简单地以数据为中心的应用程序通常需要你构建和运行一个服务,该服务执行数据的鉴权、授权、日志记录、转换和验证。它需要控制哪些人可以访问数据存储中的内容以及验证所写的内容。

服务可以创建一个令牌并将令牌返回给调用者,这个令牌限制了使用范围。实际上,这可以通过使用OAuth服务或者一个自定义的加密签名策略来实现。

除了通过服务来上传文件,一种更高效的方法是将令牌和一个存储地址返回给客户端,客户端可以用这个令牌来往这个存储地址上传文件,或者是从这个地址读取文件

另外,一个最佳做法是同时设置令牌的有效期限,使得在一定时间后无法再使用该令牌。令牌应遵循最小特权原则,只授予完成任务所需的最小权限。

GraphQL既不是一个数据库查询语言,也不是一个存储模型。它是一个API服务,可以根据定义的模式返回应用所需的数据,并且完全与数据的存储方式无关。

GraphQL非常适合作为以数据为中心的后端服务,偶尔需要调用服务方法。像GitHub这样的服务实际上正在将其整个API迁移至GraphQL,因为这为API的用户提供了更大的灵活性。GraphQL有助于解决有时会出现在REST API上的过度获取和过度烦琐的问题。

GraphQL的一个好处是,它可以轻松定义你所需的数据,并且仅限于所需的数据,而无须进行多次调用或获取多余的数据。该规范支持授权、分页、缓存等功能。这样可以轻松快捷地创建一个后端服务,该后端服务可以处理以数据为中心的应用程序所需的大多数功能。有

4.4 可快速伸缩的数据

数据分片是指将数据存储划分为水平分区。每个分片都包含相同的数据模式,但包含不同数据集。分片通常用于通过在多个数据存储系统间分摊负载来达到扩展系统的目的。

以下是一些使缓存失效和更新的常用方法: · 可以通过设置TTL值来使得缓存项在时间到期后自动删除。然后,当应用程序或服务层在缓存中找不到该项时,重新从数据源将该数据加载到缓存中。 · 使用CDC来更新缓存项或者使之失效。一个订阅了数据存储更改事件的进程可以对缓存数据进行更新。 · 应用程序可以实现一个业务逻辑,该业务逻辑负责在更新源数据的时候同时更新缓存中的数据或者使之失效。

· 运行一个后端服务,以一个配置好的时间间隔去更新缓存。 · 使用数据库或者其他服务的数据复制功能来把数据同步到缓存中。 · 缓存层根据访问请求和可用的缓存资源来更新缓存项。

以下是一些常见的CDN用例: · 通过使数据离用户更近来减少网页的加载时间。 · 通过在更靠近用户的地方终结API请求来提高应用程序的性能。 · 加速软件的下载和更新。 · 增加数据可用性和冗余。 · 通过像Amazon CloudFront这样的CDN服务来加快文件上传速度。

以下是一些管理CDN缓存的考虑因素: · 通过使内容过期来对内容定期更新。 · 在资源名中增加哈希值或者版本号,来改变资源名。 · 通过管理控制台或者API来显式地删除过期缓存。

尽量多地使用CDN,把尽可能多的内容放到CDN上去。

4.6 Kubernetes中的数据库

Kubernetes具有诸如状态集和支持存储卷这样的功能,这些可以帮助你在Kubernetes集群中部署和操作数据库。大多数持久数据存储系统都需要以磁盘卷为基础的持久存储机制,因此,在Kubernetes上部署数据库时,了解如何将存储绑定到pod上以及存储卷是如何工作的就非常重要。

Kubernetes通过将存储卷、存储卷的声明和底层存储映射到pod中来提供持久化存储。以下是一些基本的有关存储卷的术语和概念:

存储卷(persistent volume) 存储卷是Kubernetes的一种资源,它代表着实际的物理存储服务,例如云服务商提供的存储磁盘。

存储卷声明(persistent volume claim) 存储卷声明是一个对存储的请求,Kubernetes将为其分配并关联持久化的存储卷。

如果创建的pod用了一个存储卷,那么当这个pod被删除后又在另一个节点上被创建了,此时数据仍然会在那里。在创建这样的pod之前,会先创建一个存储卷声明,指定工作负载的存储需求。创建存储卷声明并引用特定的存储类时,将使用该存储类中定义的存储供应商和参数来创建满足存储卷声明请求的存储卷。然后引用这个存储卷声明的pod会被创建,并将该卷映射到该pod指定的路径上

StatefulSet旨在解决在Kubernetes上运行有状态服务(例如数据存储系统)的问题。StatefulSet根据容器的规范管理一组pod的部署和扩展。StatefulSet会保证相关pod的顺序和唯一性。根据规范创建的pod每个都有一个永久性标识符,该标识符在所有该pod的重建过程中都会存在。这个唯一的pod标识符包含StatefulSet的名称和以零开头的序数

亲和性策略(affinity)和反亲和性策略(anti-affinity)是Kubernetes的一项功能,它可以让你决定将pod限制在哪些节点上运行。pod的反亲和性策略可以用来改善运行在Kubernetes上的数据库的可用性,因为它可以使得数据库的所有实例不在同一个节点上运行。

可以利用Kubernetes的节点选择策略和污点(taint)与容忍(toleration)调度策略,两者搭配在一起使用,以确保将数据存储系统安排在存储优化节点池上,而不会安排其他服务。

5.1 什么是DevOps

DevOps的目标是改进开发和运维团队之间的协作,这种协作的改进需要通过整个软件开发流程的改善来实现,流程涵盖了从计划到交付的整个过程。最终的效果体现在部署频率的提升、产品上市时间的缩短、新版本故障率的降低、故障修复间隔的缩短及故障恢复平均等待时间的缩短上。

作为一个企业组织,你应该更看重人的健康状态,而不是会使人筋疲力尽并最终迫使他们离职的流程。作为文化的一部分,你应该拥抱失败——给员工失败的空间,更重要的是,你能从失败中学习。在这种文化中,每个人的想法都应受到欣赏。你不应该对一小部分人有倾向。员工的职级和职称并不重要,每个人都应该参与到系统的设计中来。

需要自动化的关键元素是基础设施、持续集成(CI)的流程、构建代码后的测试、持续交付(CD)流程以及部署后的测试

回顾过去,基础设施的搭建是一个手动过程。它要求人们搭建好服务器,对其进行配置,然后再在这上面部署应用程序,等等。手动做这些事有很多缺点,比如买硬件、安装和管理硬件都很花钱,而且很慢。这使得它很难去对流量高峰做出及时响应,因为部署新服务和应用所需的时间太长了。

上云的主要好处之一是基础设施可以自动化。基础设施即代码(Infrastructure as Code, IaC)是一种用代码而不是通过手动流程来配置和管理基础设施的方法。所有的基础设施,如服务器、网络和数据库,都用代码来处理。

然后,只要通过运行这个脚本,你就可以在完全不同的可用区中部署一个高度一致的基础设施堆栈。通常这样的任务需要数周才能完成,而通过脚本自动化,只需几个小时即可完成这个工作。

精益的要旨是消除流程中的任何浪费。

Prometheus为你提供了一个通用的检测点,并使开发人员可以轻松地检测代码。你无须担心如何收集数据,因为可以通过一个接入点来收集服务中的数据。你唯一需要操心的是从服务和函数中检测和发布指标数据。可以想象,分布式系统中的指标数量可能非常大,因此你还需要Jaeger或OpenTracing这样的分布式追踪工具,这些工具可以使整个服务中的指标和事件具有关联性。使用这些工具,你可以分解服务之间的请求,并更好地了解系统,从而可以快速发现所有瓶颈、故障根源或潜在的优化点。

还有其他一些第三方工具可以帮助你进行度量,比如New Relic、Splunk和Sumo Logic。有些云平台也会提供内置的度量指标和追踪功能,例如Amazon CloudWatch和AWS X-Ray,以及Microsoft Azure Monitor,它可以用来收集活动日志、诊断日志和各种度量指标。

5.1.5 分享 积极地分享经验和最佳实践,无论是在你的组织内部,还是在公司中的不同组织之间,又或者是与外面同行之间。积极分享的目的是实现共同进步,改善整个行业。

SRE职位背后的想法是在传统的研发团队(该团队负责编写代码并将其部署到生产中)与运维团队(该团队试图保持生产环境正常运行)之间搭建一座桥梁。

SRE的观念和原则与DevOps的非常一致,DevOps可以视为SRE的超集。它在更高和更广的层面上提供了更通用的建议,而SRE针对的范围更小,主要是面向服务的。

5.2 测试

你需要将所有计划运行的测试都实现自动化,因为只有可靠且自动化的测试才能使你实现高频发布的需求,并对部署和发布的质量充满信心。

测试任务是CD工作流的一部分,你可以在其中自动化地测试代码,将其部署到环境中,然后发布。

桩指的是不包含任何业务逻辑,并且仅返回预设的值的一种测试替身。如果你需要某些对象总是返回某个特定的值并且处于某个特定的状态,那么桩是非常有用的。

图5-1:自动化测试金字塔

金字塔的最重要部分,即底部,是单元测试。单元测试应该是测试的基础,并且与其他类型的测试相比,单元测试是你应该拥有的数量最多的测试。

在做单元测试时,通常需要模拟和伪造来解决依赖关系,以便为运行单元测试用例创造条件。比如你现在要为登录服务编写单元测试,这时候你不需要使用真实的授权服务。你可能还想测试在授权服务不可用时的情形,或者你想测试无法登录或用户不存在的情况,类似这些情况下你都需要用到模拟和伪造。

最后是UI测试,它位于金字塔的顶部。UI测试应该代表所有金字塔测试中最少的测试数量。这些测试的编写和维护成本通常很高。但是,它们在测试可用性和可访问性时很有用。让

负载测试 负载测试是性能测试的一种,可用于确定某些特定情况下系统的性能。比如说,这些特定情况可能是你期望的系统在大多数时间下的负载水平,或者是在极端或峰值这种不典型情况下的负载。通过负载测试,你可以确定系统最大能承受的负载是多少,以及崩溃点在哪里。负载测试的结果可以帮助你有计划地去设置监控系统中警报的阈值。

安全和渗透测试的目的是确定你的系统是否可能受到不同类型攻击的影响,如果存在这种可能,那么何种方式更易受攻击。这种类型的测试还涉及对系统体系结构进行安全性审查,以发现容易被入侵的点和重点安全区域。审查过程还应确保服务对所访问的资源没有不必要的权限,因为在发生安全漏洞时,这可能会导致更严重的后果。

A/B测试 A/B测试通常是在生产环境中运行的服务上执行的。A/B测试的目的是确定服务的一个版本(A)是否比另一版本(B)更好。如果打算进行任何A/B测试,请确保你有一个定义明确的目标以及所有可以用来衡量结果的指标。例如,你可以创建一项A/B测试,以确定是使用绿色按钮还是黄色按钮更能鼓动用户点击,从而增加销售。你可以部署两个版本的服务,并在两个版本之间平均分配流量。请注意,平均分配流量并不一定是必需的,你还可以根据一些因素来划分用户,决定哪些用户访问版本A,哪些访问版本B。

集成测试通常指的是对多个服务以及这些服务之间的交互进行的测试。在测试金字塔中,这类测试位于服务测试之上、UI测试之下。

混沌测试 顾名思义,混沌测试的目的是对系统造成破坏,并向系统中随机引入故障。你需要在环境中运行一组名为捣蛋猴子(chaos monkey)的特殊服务,这类服务的作用就是捣蛋,比如随机关闭某个服务、下线服务、随机切断网络,等等,然后看你的系统在这种混乱情况下的表现如何。

混沌测试背后的想法是希望能够主动发现你的系统在故障发生时的表现如何,以便可以在问题变为实际事故并对客户造成影响之前就将其识别出来,然后及时进行修复。

你应当定期去做安全测试、模糊测试、负载测试以及性能测试,但是可能并不需要对每次构建或每个代码改动做这些测试,除非这个改动会影响系统的安全或者性能。

但是,每次部署前你都应当去运行配置测试,从而确保服务的配置是正确的。同时,你也应当在配置发生修改时选择性地做这类测试。

部署前 代码被编译、打包,然后打上标签后上传到容器镜像仓库中,如Docker registry。我们认为此时服务就处于部署前阶段。

容器化服务有个重要的注意事项,尽管你的代码现在已经在生产环境里了,但可能没有流量能够到达这些服务。你需要做一系列的配置、集成,有可能还需要负载测试,然后才能把流量导入服务。在所有的测试达到你的标准线后,才可以进入发布阶段。

发布 服务的发布涉及逐渐增加定向到已部署服务的实际流量。如果是容器化的应用程序,则可以使用服务网格(例如Istio)快速执行此过程。

部署后,你首先将10%!的(MISSING)流量重定向到新版本的服务上。同时,你需要持续监控新服务以确保没有问题。除了监控之外,你还可以运行针对此新服务的其他测试。当测试结果给你足够的信心后,你可以逐步增加流量,从10%!到(MISSING)20%!、(MISSING)50%!,(MISSING)直到最后达到100%!。(MISSING)增加流量后的过程也是相同的,监控并观察新服务,如果一切看起来不错,那就继续增加百分比。如果发现任何问题,那么可以回滚新版本(即将流量切换回0%!)(MISSING),解决问题,然后重复整个过程。另外,有时候尽管发现了问题你也可以继续操作,前提是问题的优先级较低,并且不会对你的服务造成太大影响。

发布后这个阶段也可以称之为服务的运维阶段。除了测试,这个阶段还涉及异常响应、日常维护等工作,你的团队需要24小时待命,随时准备好处理突发情况。

5.3 开发环境和工具

· MiniKube可以在一个VM中运行一个单节点的Kubernetes集群,通常在本地开发环境中使用。如果你想在本地环境中尝试Kubernetes,或者想搭建一个更接近测试和生产环境的本地环境,那么MiniKube是很有帮助的。

· Docker Compose是一个用来定义和运行多个容器的工具。该工具通过一个YAML文件来定义一组容器,你可以控制这组容器的启动、停止和删除。

5.4 持续集成/持续交付

持续集成(CI)是一种自动化构建、测试和集成新代码与已有代码的实践,其最终目的是为了发布。从实践角度讲,这个过程通常是指在功能分支上构建代码,然后运行单元测试,如果通过了就合并代码,最终创建一个代码包,比如二进制包,或者是一个容器镜像,也可能是个压缩文件,这取决于你的服务类型。

打包代码,贴上标签,然后推送到容器注册服务器(如Docker Registry)这个过程已成了CI流程的一部分,你不需要在不同阶段间移动代码,你只需要提供容器镜像的信息(例如,容器注册服务器地址、容器镜像的名称及对应的标签),这将显著地加速整个流程。

持续交付(CD),看作是CI阶段的延伸。在这个阶段,你会运行更多的测试,目标是使代码能够随时准备好发布到生产环境中。从实践角度讲,若你的代码通过了CD阶段,那么就不应该再有稳定性或质量的问题了,任何工程师都能够很容易地将代码部署到生产环境中。

代码库(mono-repo)的想法是将所有代码(所有服务、工具、应用,等等)保存在一个代码库中。与之相反的是多代码库(multi-repo),它指的是代码分别放在多个代码库中。

所以如果你选择单代码库,一定要仔细考虑如何管理依赖项,如何做好服务的隔离,尽量避免紧耦合的问题。

构建阶段(CI)的目标都是把你提交到代码库的所有改动都进行构建,并且确保其中没有出现任何错误。如果构建成功了,你就可以进入到下一个阶段,即测试阶段。如果构建失败了,那么整个流程将停止,代码的提交将被拒绝,并且会通知开发者。

如果你像这样构建,那么你最终会得到一个大约8MB大小的容器镜像。而在第一步构建的那个容器镜像的大小大概是在800MB左右。这100倍的大小差距很显著,你可以想象到当它在不同的镜像仓库或不同主机间迁移时的速度差异

大多数在Docker Hub注册服务器中流行的Docker镜像都有一个完整的镜像,同时也会有一个更小的或者说是瘦身版的镜像,这种镜像通常会带上“slim”标记。

站在安全性的角度看,更小的镜像意味着更小的攻击目标,因为镜像中除了二进制代码包就没有别的东西了。如果你用的是一个完整的操作系统镜像(例如,Ubuntu),在这之上安装你的包,那么潜在的攻击者除了访问你代码包外也会去瞄准Ubuntu系统自带的那些工具。

给容器镜像打标签的一个最佳实践是使用Git commit的哈希值缩写加上一个内部版本号。按照此命名规范,容器镜像名称的一个示例可以是这样的:myimage:ed3ee93-1.0.0。使用这种命名格式,你可以快速发现镜像包含哪些更改

另外有一种测试方案很流行,它被称之为流量复制、影子流量或者暗流量。这种方案允许你复制或引用真实生产环境中的流量,并把它发送给部署好的服务。注意一点,你不是把生产环境中的流量直接路由到新部署的服务上,真实流量仍然导向已发布的服务,只是这部分流量会被复制一份然后再发送到新部署的服务上。

如果你将Istio作为服务网格的方案,那么可以通过在Istio虚拟服务资源中增加镜像密钥来启用流量镜像功能。

有了流量复制以后,你就可以运行更多的测试了,并且使用生产环境中的数据来测试和监控新部署的服务。对于无服务器应用,

发布的过程是一个缓慢将生产环境中的流量导向新部署服务的过程,

在一个完美、理想的世界中,对新版本流量的增加应该是自动决策的。应该有一个系统来智能地判断是否应该继续发布,这个决策应该根据从服务中获取的数据得出。类似这样一个全自动的工作流程应该是一个处于成熟阶段的DevOps的一部分,这个工作流程包含了持续集成、持续交付和持续部署。然而,现实状况是这往往是个手动的流程,需要人来判断是否应该切换到新版本的服务。

所有已发布的应用都处于发布后阶段,这个阶段包括了持续的服务监控、事故处理,以及对从用户或监控告警系统中得到的故障报告的响应,还包括了做一些额外的测试,比如混沌测试。

5.5 监控

故障率 这个指标告诉你请求的失败率(例如,HTTP 500的数量)。

利用率 利用率指标告诉你系统的不同组成部分的使用情况。例如,你可以监控Kubernetes节点的利用率,确保内存、磁盘和CPU的使用都在正常范围内。

为了让Prometheus起作用,你需要定义一个数据卷(用来存储抓取的数据)以及一个配置文件(包含了数据抓取的时间间隔、超时设定和一些不同的规则及告警设定)。当然,你也需要在服务中添加检测代码,否则Prometheus也是巧妇难为无米之炊。

几乎每种流行的语言都有Prometheus的客户端库,这些库允许通过一个HTTP接入点来定义和暴露指标。Prometheus会调用这个HTTP接入点来获取服务发送的监控指标数据,然后保存下来。它同时也支持被称为“推送网关”的方式,假如你的组件没有办法被抓取数据,你可以使用推送网关来将数据推送到一个可以被Prometheus抓取数据的组件

当定义告警时,不要忘了加上一个链接,这个链接应该指向一个网页或文档,上面解释了告警详情、是什么触发了告警以及该如何解决。

如果你正在使用Istio服务网格,那么Jaeger可以作为Istio的一部分来安装和配置。Jager安装后,很容易就可以开始分布式链路追踪了。尽管Istio的Envoy代理会自动发送所有链路追踪数据,但你还是有必要在服务请求中告诉Jaeger如何正确地关联请求

5.6 配置管理

服务或函数的配置包含了服务或函数启动和运行所需的全部内容。以下是一些常见的app会用到的配置: · 数据库/队列/消息系统的连接字符串 · 凭据(用户名、密码、API密钥、证书) · 超时、端口、依赖服务的名称

5.6.2 多环境变量 当你的ConfigMap定义了多个值时,你可以通过名为envForm的关键字来声明所有ConfigMap里的值都需要挂载到pod的环境变量中。

如果你是以文件或目录为来源创建了ConfigMap,那么使用一个卷,你就可以将ConfigMap中的所有数据添加到pod中你指定的目录下:

把ConfigMap挂载到pod中的一个好处是,当它们被更新时,pod中的值也会自动更新。如果你只需要更新配置,那你完全可以这样做,并且Kubernetes会确保你的pod中的值也会被更新。

像Helm这样的工具还能够帮助你很容易地在持续部署流程中自动创建部署文件。另一个很有用的Helm CLI命令可以允许你在不用真正部署的前提下,把真实值插入模板文件中,生成最终的文件。除了Helm内置的用来验证图表的命令外,这种生成最终配置文件的方法也可以在必要时作为配置测试的一种输入来源

6.1 迈向云原生

“永远不要更改一个正常运行的系统”是软件开发中广泛接受的一种说法,

6.2 确保弹性

弹性是指系统从故障中恢复并继续提供服务的能力。弹性并不是说一定要避免故障的发生,而是说当故障发生后,能够快速响应,避免长时间的宕机或者数据丢失。

请求可能由于多种原因而失败,例如网络延迟、连接断开或下游服务繁忙而导致的超时。如果你重试提交该请求,可以避免大多数此类失败。重试还可以提高应用程序的稳定性。

线性增长 每次重试之间的延迟是逐渐增长的。例如,一开始间隔是1秒,然后是3秒、5秒,以此类推。

指数增长 每次重试之间的延迟呈指数级增长。例如,一开始间隔是3秒,然后是12秒、30秒,以此类推。

如果你使用的是诸如Istio这样的服务网格的话,也可以在架构层来实现重试

断路器会监视故障的数量,并根据这个信息来确定请求是应该继续还是应该返回错误响应,从而避免继续调用下游服务。如果断路器跳闸,即故障数量已经超过了预定义的值,那么将在一段时间内自动回复错误响应。在经过预设的一段时间后,它将重置故障计数器并再次允许请求调用下游服务。

服务的降级应该是优雅的,即使它们出现了故障,也应该提供一个合理的、可接受的用户体验。例如,如果无法从服务中获取数据了,那么应当显示一份缓存中的数据,当数据源恢复后,再立即显示最新的数据。

活动性探针用于确定何时应该重启容器,而就绪性探针用于确定pod是否应开始接收流量。

6.3 确保安全性

6.3.7 传输数据加密 当数据在组件之间传输时,应当经过加密,这样当通信被劫持时你的数据才可以得到保护。为了实现这种保护,你需要先对数据进行加密,然后再进行传输,对接入点进行身份验证,最后在到达接入点后对数据进行解密和验证。传输层安全性(TLS)用于加密传输中的数据,以实现传输安全性。如果使用服务网格,那么可能已经在网格中的代理之间实现了TLS。

例如,Kubernetes RBAC可以控制对Kubernetes API的访问权限。使用RBAC,你可以允许或拒绝特定用户创建部署或列出pod信息等操作。最好是通过命名空间而不是集群角色来划分Kubernetes RBAC权限。

使用Kubernetes中的NetworkPolicy资源,你可以定义pod选择器以及详细的入口(ingress)和出口(egress)策略。

6.4 处理数据

尽可能使用托管数据库。尽管在虚拟机(VM)或Kubernetes集群中配置数据库通常是一项快速而轻松的任务,但是需要备份和复制的生产数据库会迅速增加维护数据存储系统的时间和负担。通过减轻部署和管理数据库的运维负担,团队可以更加专注于开发。

6.4.3 将数据保存在多个地域或可用区中

6.4.4 使用数据分区和复制以提高扩展性 云原生应用在设计时考虑更多的是如何做横向扩容而不是纵向扩容。

6.5 性能和伸缩性

在可能的情况下,请在实现自己的服务伸缩功能之前优先考虑使用平台内置的自动伸缩功能。Kubernetes提供了Horizontal Pod Autoscaler(HPA), HPA可以根据CPU、内存或自定义指标来缩放pod。

6.6 函数计算

6.6.1 编写单一用途的函数 遵循单一用途原则,让一个函数只负责一件事情。这将使你的函数更容易实现、测试,以及在必要时进行调试。

通常,函数应当在需要的时候,通过往队列或者数据存储中推送消息或数据来触发其他函数。在函数中调用其他函数通常被认为是一种反模式,它会增加你的使用成本,也会让你的调试变得更困难。

6.6.3 函数应保持轻量和简单 一个函数只应该做一件事,并且应该依赖很少的外部库。函数中任何额外的或者多余的代码都会使函数的体积变大,影响启动时间。

6.6.4 实现无状态函数 不要在函数中保存任何数据,因为函数实例通常运行在其自身的隔离环境中,函数之间不要共享任何东西,哪怕是同一个函数的不同调用也不要共享数据。

6.6.7 用队列解决跨函数通信问题 函数间的通信应该靠消息队列来发送消息,而不是直接通过函数相互调用来传递信息。函数应当通过队列中的事件(消息插入、删除、更新,等等)来触发和执行。

6.7 运维

6.7.1 部署和发布是两项独立的活动 区分部署和发布这两件事情是很重要的。部署是将已构建好的组件放到一个环境中的过程,这个组件已经配置好并且随时可以运行了。但是,这时候还没有流量发送过去。而发布的过程是我们开始允许流量重定向到部署好的组件上

6.8 日志、监控及告警

应用程序和基础架构的日志记录不仅可以提供故障原因的分析,还可以提供更多的价值。一个适当的日志记录解决方案可以提供对应用和系统的有价值的洞见。此外,对应用程序健康状况的监控以及重要事件的告警是非常有必要的。随着云原生应用分布式程度的加深,日志记录和指标检测也变得越来越具有挑战性,也愈发重要了。

每条日志记录都应当包含额外的上下文信息,这将帮助你进行问题的分析。例如,在日志中加入所有异常处理信息、重试次数、服务名称或ID、镜像版本、代码包的版本,等等。

数量庞大的指标使得设置告警以及告警内容变得很困难。如果你发出的告警过多,最终的结果就是大家都不再重视告警,最终停止关注它们

6.9 服务通信

6.9.1 设计时考虑前后兼容性 通过向后兼容,你可以确保添加到服务或组件的新功能不会破坏任何现有服务。例如,在图6-3中,服务A v1.0与服务B v1.0一起使用。向后兼容意味着服务B v1.1的发行不会破坏服务A的功能

为了确保向后兼容,添加到API的任何新字段都应该是可选的或具有合理的默认值。任何现有字段都绝不能重命名,因为这将破坏向后兼容性。

HTML是向前兼容的一个很好的例子:当遇到未知的标记或属性时,它不会失败,它只会跳过它。

构建在微服务架构上的分布式应用程序在很大程度上依赖于服务之间的通信和消息传递。数据的序列化和反序列化会在服务通信中增加大量开销。

队列或者流的作用是在组件之间充当缓冲区并存储消息,直到消息被使用为止。使用队列可以使组件以自己的节奏处理消息,而不管传入的数量或负载如何。因此,这有助于最大化服务的可用性和可伸缩性。

队列可以用来批量处理多个请求,并且只执行一次操作。例如,将1000条记录一次写入数据库会比一次写一条然后写1000次更高效。

6.10 容器

以特权模式运行容器会导致该容器有权限访问主机上的任何内容。可以通过在pod上使用安全策略来防止容器用特权模式运行。如果容器出于某种原因确实需要特权模式来更改主机环境,请考虑不要将此功能放在容器中,而是应该在基础架构的创建过程中做此操作。

切勿在生产环境中使用latest标签,因为它可能导致不一致的行为,从而难以进行故障排查。

6.10.6 单个容器只运行一个应用 一个容器中永远只运行一个应用。设计容器的目标就是运行单个应用,容器的生命周期应该与运行在容器中的应用保持一致。在同一个容器中运行多个应用程序会使这个容器很难维护,最终你可能得到一个容器,其中有一个进程已经退出了或者是不能正常响应。

6.10.9 不要将数据保存在容器中 容器应当是临时的,可以被停止、销毁或替换,而不会丢失任何数据。如果在容器中运行的服务需要存储数据,请挂载存储卷来保存数据。

如果你的容器需要使用任何密钥,你应该在环境变量中定义它们或者是从挂载到容器的存储卷中的文件读取。

7.3 何时及如何实现可移植性

基础设施 每个云服务商都提供了一组用于管理基础设施的API。对于需要管理多个云的开发者而言,一般会利用各平台的API来创建一个抽象层,然后使用这个抽象层来管理各个云平台上的基础设施。

现如今,几乎所有主流云服务商都提供了托管Kubernetes服务。这些托管Kubernetes服务可以使用户非常便捷地快速搭建起一个Kubernetes集群。云服务商会负责Kubernetes控制层的创建和维护,然后通过特定云平台的插件在底层基础设施之上创建集群节点

Virtual Kubelet是一个开源项目,该项目可以把API伪装成一个Kubelet(Kubernetes集群中的节点)。这样就可以通过Kubernetes使用云服务商的容器即服务(CaaS)产品。开发人员和管理员可以继续使用Kubernetes接口来运行其应用,同时仍然可以受益于云服务商提供的计算资源服务。

作者简介

作为Full Scale 180的联合创始人和顾问,他与Microsoft的一些大客户合作,帮助他们将应用迁移上云,或在云中构建应用。他一直致力于利用Docker、无服务器技术和微服务架构来设计、构建和运行大型应用程序