分布式系统架构的本质
参考:
- 21 分布式系统架构的冰与火 | 极客时间 (opens new window)
- 22 从亚马逊的实践,谈分布式系统的难点 | 极客时间 (opens new window)
- 23 分布式系统的技术栈 | 极客时间 (opens new window)
- 24 分布式系统关键技术:全栈监控 | 极客时间 (opens new window)
- 25 分布式系统关键技术:服务调度 | 极客时间 (opens new window)
- 26 分布式系统关键技术:流量与数据调度 | 极客时间 (opens new window)
- 27 洞悉PaaS平台的本质 | 极客时间 (opens new window)
# 1. 分布式系统架构的冰与火
# 1.1 为什么需要分布式系统
首先,我们需要阐述一下为什么需要分布式系统,而不是传统的单体架构。使用分布式系统主要有两方面原因:
- 增大系统容量。单台机器的性能无法满足了,需要垂直或水平拆分业务系统,让其变成分布式的架构。
- 加强系统可用。通过分布式架构来冗余系统以消除单点故障,从而提高系统的可用性。
当然,分布式系统还有一些其他优势,比如模块化、扩展性。但这个世界上不存在完美的技术方案,都存在一种 trade-off,下面这个表格比较了单体应用和分布式架构的优缺点:
分布式系统架构的难点在于系统设计,以及管理和运维,这需要我们不断地用各式各样的技术和手段来解决这些问题。
# 1.2 分布式系统的发展
起初随着编程的发展,很自然演化出 SOA —— 基于服务的架构,它将应用程序功能作为服务发送给最终用户或者其他服务。它采用开放标准与软件资源进行交互,并采用标准的表示方式。
但 IBM 搞出来的 SOA 非常重,因此对 SOA 的裁剪和优化从来没有停过,最终改成了 RESTful 和 JSON 这样的方式,而 ESB(Enterprise Service Bus,企业服务总线)这样非常重要的东西也被简化成了 Pub/Sub 的消息服务 …… 如今,SOA 的思想也一直延续着,到了今天的分布式服务架构。
下面是一个 SOA 架构的演化图:
我们可以看到,SOA 有以下三个阶段:
- 20 世纪 90 年代前,是单体架构,软件模块高度耦合。
- 2000 年左右出现了比较松耦合的 SOA 架构,这个架构需要一个标准的协议或是中间件来联动其它相关联的服务(如 ESB)。服务间不直接依赖,而是通过中间件或通讯框架来实现相互依赖,这其实是 IOC(控制反转)和 DIP(依赖倒置原则)设计思想在架构中的实践。
- 2010 年后,出现了微服务架构,这个架构更为松耦合。它和传统 SOA 的差别在于,服务间的整合需要一个服务编排或是服务整合的引擎来将这些微服务来组织起来。
一般来说,这个编排和组织引擎可以是工作流引擎,也可以是网关。当然还需要像 K8s 这样容器化调度的技术。
微服务的技术需要一套比较好的微服务 PaaS 平台,比如像 Spring Cloud 一样需要提供各种配置服务、服务发现、智能路由、控制总线等功能,还有像 K8s 提供的各式各样的部署和调度方式。没有这些 PaaS 层的支撑,微服务也是很难被管理和运维的。
# 2. 从亚马逊的实践,谈分布式系统的难点
# 2.1 亚马逊的实践
采用分布式系统架构后会出现很多的问题:
- 一个线上故障的工单会在不同的服务和不同的团队中转过来转过去。
- 每个团队都可能成为一个潜在的 DDoS 攻击者,除非每个服务都要做好配额和限流。
- 监控和查错变得更为复杂。
- 除非有非常强大的监控手段。服务发现和服务治理也变得非常复杂。
为了克服这些问题,亚马逊这么多年的实践让其可以运维和管理极其复杂的分布式服务架构。我觉得主要有以下几点:
- 分布式服务的架构需要分布式的团队架构。一个服务由一个小团队(Two Pizza Team)负责,按职责分工,而不是按技能分工。
- 分布式服务查错不容易。一旦出现比较严重的故障,需要整体查错。在故障发生的一开始,大家都在签到并自查自己的系统。如果没问题,也要在线待命(standby),等问题解决。
- 没有专职的测试人员,也没有专职的运维人员,开发人员做所有的事情。这就是著名的“Eat Your Own Dog Food”,自己写的代码自己维护自己养,会让开发人员明白,写代码容易维护代码复杂,从而使其考虑软件的长期维护性。
- 运维优先,崇尚简化和自动化。亚马逊十多年前就已经通过 DevOps 具备了强大的运维,并对系统进行简化和自动化,让亚马逊做到了可以轻松运维拥有上千万台虚机的 AWS 云平台。
- 内部服务和外部服务一致。亚马逊的内部系统都和外部系统一样对待。这样做的好处是,内部系统的服务随时都可以开放出来。而且,从第一天开始,服务提供方就有对外服务的能力。可以想象,以这样的标准运作的团队其能力会是什么样的。
分布式服务架构是需要从组织,到软件工程,再到技术上的一个大的改造,需要比较长的时间来磨合和改进,并不断地总结教训和成功经验。
# 2.2 分布式系统中需要注意的问题
# 问题 1:异构系统的不标准问题
这主要表现在:
- 软件和应用不标准
- 通讯协议不标准
- 数据格式不标准
- 开发和运维的过程和方法不标准
分布式系统架构需要有相应的规范。比如配置管理,一个好的配置管理,应该分成三层:底层和操作系统相关,中间层和中间件相关,最上面和业务应用相关。于是底层和中间层是不能让用户灵活修改的,而是只让用户选择。比如:操作系统的相关配置应该形成模板来让人选择,而不是让人乱配置的。只有配置系统形成了规范,我们才 hold 得住众多的系统。
这样的规范还有很多,但一定要提前制定好。
# 问题 2:系统架构中的服务依赖性问题
分布式架构下,服务是会有依赖的,一个服务依赖链上的某个服务挂掉了,可能会导致出现“多米诺骨牌”效应。所以,在分布式系统中,服务的依赖也会带来一些问题:
- 如果非关键业务被关键业务所依赖,会导致非关键业务变成一个关键业务。
- 服务依赖链中,出现“木桶短板效应”——整个 SLA 由最差的那个服务所决定。
这是服务治理的内容了。服务治理不但需要我们定义出服务的关键程度,还需要我们定义或是描述出关键业务或服务调用的主要路径。
这里需要注意的是,很多分布式架构在应用层上做到了业务隔离,然而,在数据库结点上并没有。如果一个非关键业务把数据库拖死,那么会导致全站不可用。所以,数据库方面也需要做相应的隔离。也就是说,最好一个业务线用一套自己的数据库。这就是亚马逊服务器的实践——系统间不能读取对方的数据库,只通过服务接口耦合。这也是微服务的要求。我们不但要拆分服务,还要为每个服务拆分相应的数据库。
# 问题 3:故障发生的概率更大
而分布式系统中,虽然故障的影响面可以被隔离,但是因为机器和服务多,出故障的频率也会多。
对分布式系统架构的运维,简直就是一场噩梦。我们会慢慢地明白下面这些道理:
- 出现故障不可怕,故障恢复时间过长才可怕。
- 出现故障不可怕,故障影响面过大才可怕。
运维团队在分布式系统下会非常忙,忙到每时每刻都要处理大大小小的故障。我看到,很多大公司,都在自己的系统里拼命地添加各种监控指标,有的能够添加出几万个监控指标。我觉得这完全是在“使蛮力”。一方面,信息太多等于没有信息,另一方面,SLA 要求我们定义出“Key Metrics”,也就是所谓的关键指标。然而,他们却没有。这其实是一种思维上的懒惰。
但是“防火胜于救火”,我们还要考虑如何防火。这就需要在设计之初就要 Design for Failure,考虑如何减轻故障或者减少故障影响面。
因为当机器和服务数量越来越多时,你会发现,人类的缺陷就成为了瓶颈。这个缺陷就是人类无法对复杂的事情做到事无巨细的管理,只有机器自动化才能帮助人类。也就是,人管代码,代码管机器,人不管机器!
# 问题 4:多层架构的运维复杂度更大
通常来说,我们可以把系统分成四层:基础层、平台层、应用层和接入层:
- 基础层就是我们的机器、网络和存储设备等。
- 平台层就是我们的中间件层,Tomcat、MySQL、Redis、Kafka 之类的软件。
- 应用层就是我们的业务软件,比如,各种功能的服务。
- 接入层就是接入用户请求的网关、负载均衡或是 CDN、DNS 这样的东西。
对于这四层,我们需要知道:任何一层的问题都会导致整体的问题;没有统一的视图和管理,导致运维被割裂开来,造成更大的复杂度。
但很多公司都是按技能分工,折让一个完整的技术系统被割裂开来,很多分工完全连不在一起,导致没有统一的运维视图,进而当出错时的查错过程变成噩梦。
分工不是问题,问题是分工后的协作是否统一和规范。这点,你一定要重视。
# 2.3 小结
这一节以亚马逊为例,讲了它是如何做分布式服务架构的,并遇到了哪些问题,以及是如何解决的。
构建分布式服务需要从组织,到软件工程,再到技术上的一次大的改造,需要比较长的时间来磨合和改进,并不断地总结教训和成功经验。
# 3. 分布式系统的技术栈
构建分布式系统的目的是增加系统容量,提高系统的可用性,转换成技术方面,也就是完成下面两件事:
- 提高整体架构的吞吐量,服务更多的并发和流量
- 提高系统的稳定性,让系统的可用性更高
# 3.1 提高架构的性能
- 缓存系统:提高快速访问能力最有效的手段。对于分布式系统下的缓存系统,需要的是一个缓存集群。这其中需要一个 Proxy 来做缓存的分片和路由。
- 负载均衡系统:是水平扩展的关键技术。
- 异步调用:主要通过消息队列来对请求做排队处理,这样可以把前端的请求的峰值给“削平”了,而后端通过自己能够处理的速度来处理请求。但这样降低了实时性,并引入消息丢失的问题,还会造成“有状态”的节点,从而增加了服务调度的难度。
- 数据分区:把数据按一定的方式分成多个区,可参考数据分区
- 数据镜像:把一个数据库镜像成多份一样的数据,可参考数据复制
对于一般公司来说,在初期,会使用读写分离的数据镜像方式,而后期会采用分库分表的方式。
# 3.2 提高架构的稳定性
- 服务拆分:拆分的目的主要有两个:隔离故障和模块复用。但拆分会引入服务依赖问题。
- 服务冗余:目的是去除单点故障,并支持服务的弹性伸缩,以及故障迁移。但对有状态的服务做冗余的话会引入更高的复杂性。
- 限流降级:当系统实在扛不住压力时,只能通过先停掉一部分服务,以确保整个架构不会挂掉。这些技术属于保护措施。
- 高可用架构:也是为了不出单点故障,技术手段主要也是从冗余的角度来考虑,比如多租户隔离、灾备多活、数据复制等。
- 高可用运维:主要指的是 DevOps 中的 CI/CD(持续集成 / 持续部署),一个良好的运维应该是一条很流畅的软件发布管线,其中做了足够的自动化测试,还可以做相应的灰度发布,以及对线上系统的自动化控制。这可以让宕机事件发生的时长最短。
# 3.3 分布式系统的关键技术
我们可以看到,引入分布式系统,会引入一堆技术问题,需要从以下几个方面来解决:
- 服务治理。包含服务拆分、服务调用、服务发现、服务依赖、服务的关键度定义……它的最大意义是需要把服务间的依赖关系、调用链和关键服务给梳理出来,并对这些服务进行性能和可用性方面的管理。
- 架构软件管理。主要考虑服务之间兼容性的问题。包括对整体服务所形成的的版本、生命周期进行管理,以及对服务的编排、聚合等的调度。
- DevOps。快速地更新服务对服务的测试和部署都是挑战,这需要 DevOps 的全流程,包括环境构建、持续集成、持续部署。
- 自动化运维。对服务进行自动伸缩、故障迁移、配置管理、状态管理等一系列运维的事情借助 DevOps 进行自动化运维。
- 资源调度管理。应用层的自动化运维需要基础层的调度支持,也就是云计算 IaaS 层的计算、存储、网络等资源调度、隔离和管理。
- 整体架构监控。监控系统是你的眼睛,没有眼睛,没有数据,就无法进行高效运维。我们要对三层系统(应用层、中间件层、基础层)进行监控。
- 流量控制:包含负载均衡、服务路由、熔断、降级、限流等和流量相关的调度。
幸好我们生在一个比较好的时代,有了 Docker 和 K8s 这样的技术,能帮助我们解决其中很多问题。
# 3.4 分布式系统的“纲”
总结上面内容,可以发现,分布式系统主要由五个关键技术:
- 全栈系统监控;
- 服务 / 资源调度;
- 流量调度;
- 状态 / 数据调度;
- 开发和运维的自动化。
只有把前面四项做到了,最后的开发与运维的自动化才有可能实现。所以,最为关键的就是下面这四项技术:应用整体监控、资源和服务调度、状态和数据调度及流量调度。
# 4. 分布式系统关键技术:全栈监控
全栈系统监控就像我们的眼睛,在分布式下,系统分成多层、服务各种关联,没有好的监控系统,我们将无法自动化运维和资源调度。
这个监控系统需要完成的功能为:
- 全栈监控
- 关联分析
- 跨系统调用的串联
- 实时报警和自动处置
- 系统性能分析
# 4.1 需要监控什么内容
所谓全栈监控,其实就是三层监控:
- 基础层:监控主机和底层资源。比如 CPU、内存、网络吞吐、磁盘使用情况等。
- 中间层:中间件层的监控。比如 Nginx、Redis、MQ、MySQL 等。
- 应用层:监控应用层的使用。比如 HTTP 访问的吞吐量、响应时间、调用链分析、性能瓶颈等。
这还需要一些监控的标准化:
- 日志数据结构化
- 监控数据格式标准化
- 统一的监控平台
- 统一的日志分析
# 4.2 什么才是好的监控系统
现在我们的很多监控系统都做得很不好,它们主要有两个很大的问题:
- 监控数据是隔离开来的。因为公司分工的问题,开发、应用运维、系统运维,各管各的,所以很多公司的监控系统之间都有一道墙,完全串不起来。
- 监控的数据项太多。信息太多等于没有信息,会抓不住重点,这完全是使蛮力的做法。
一个好的监控系统应该有以下几个特征:
- 关注于整体应用的 SLA。主要从为用户服务的 API 来监控整个系统。
- 关联指标聚合。把有关联的系统及其指标聚合展示,最重要的是把服务和相关的中间件以及主机关联在一起。服务可能运行在 Docker、Tomcat 等各种形式中运行,无论运行在哪里,我们都需要把服务的具体实例和主机关联在一起,否则,对于一个分布式系统来说,定位问题犹如大海捞针。
- 快速故障定位。故障总是会发生的,而且还会频繁发生。故障发生不可怕,可怕的是故障的恢复时间过长。所以,快速地定位故障就相当关键。快速定位问题需要对整个分布式系统做一个用户请求跟踪的 trace 监控,我们需要监控到所有的请求在分布式系统中的调用链,这个事最好是做成没有侵入性的。
换句话说,一个好的监控系统主要是为以下两个场景所设计的:
- 体检:
- 容量管理:提供一个全局的系统运行时数据的展示,可以让工程师团队知道是否需要增加机器或者其它资源。
- 性能管理:可以通过查看大盘,找到系统瓶颈,并有针对性地优化系统和相应代码。
- 急诊:
- 定位问题:可以快速地暴露并找到问题的发生点,帮助技术人员诊断问题。
- 性能分析:当出现非预期的流量提升时,可以快速地找到系统的瓶颈,并帮助开发人员深入代码。
只有做到了上述的这些关键点才能是一个好的监控系统。
# 4.3 如何做出一个好的监控系统
下面是我认为一个好的监控系统所应该事先的功能:
- 服务调用链跟踪:这个监控系统应该从对外的 API 开始,然后将后台的实际服务给关联起来,然后再进一步将这个服务的依赖服务关联起来,直到最后一个服务(如 MySQL 或 Redis),这样就可以把整个系统的服务全部都串连起来了。这个事情的最佳实践是 Google Dapper 系统,其对应于开源的实现是 Zipkin。对于 Java 类的服务,我们可以使用字节码技术进行字节码注入,做到代码无侵入式。一个示例如下图所示:
- 服务调用时长分布。使用 Zipkin,可以看到一个服务调用链上的时间分布,这样有助于我们知道最耗时的服务是什么。下图是 Zipkin 的服务调用时间分布:
- 服务的 Top N 视图:一般来说,这个排名会有三种排名的方法:a)按调用量排名,b) 按请求最耗时排名,c)按热点排名(一个时间段内的请求次数的响应时间和)。
- 数据库操作关联:对于 Java 应用,我们可以很方便地通过 JavaAgent 字节码注入技术拿到 JDBC 执行数据库操作的执行时间。对此,我们可以和相关的请求对应起来。
- 服务资源跟踪:把服务与机器节点的资源(CPU、MEM 等)进行关联
这样一来,我们就可以知道服务和基础层资源的关系。如果是 Java 应用,我们还要和 JVM 里的东西进行关联,这样我们才能知道服务所运行的 JVM 中的情况(比如 GC 的情况)。
有了以上数据的关联,当故障发生时,才能快速定位问题所在,并帮助我们了解故障影响面。
所以,一个分布式系统,或是一个自动化运维系统,或是一个 Cloud Native 的云化系统,最重要的事就是把监控系统做好。在把数据收集好的同时,更重要的是把数据关联好。这样,我们才可能很快地定位故障,进而才能进行自动化调度。
上图只是简单地展示了一个分布式系统的服务调用链接上都在报错,其根本原因是数据库链接过多,服务不过来。另外一个原因是,Java 在做 Full GC 导致处理过慢。于是,消息队列出现消息堆积堵塞。这个图只是一个示例,其形象地体现了在分布式系统中监控数据关联的重要性。
# 4.4 小结
这一大节的内容主要如下:
- 全栈监控应该监控什么?
- 什么才是好的监控系统?
- 如何做出好的监控?
一些监控系统的工具
# 5. 分布式系统关键技术:服务调度
我们把服务治理和流量调度分开来讲,这一大节只涉及服务治理上的一些关键技术,主要有以下几点:
- 服务关键程度
- 服务依赖关系
- 服务发现
- 整个架构的版本管理
- 服务应用生命周期全管理
# 5.1 服务关键程度和服务的依赖关系
关于服务关键程度,主要是要我们梳理和定义服务的重要程度。这不是技术可以完成的,需要对业务足够了解才能定义出架构中各个服务的重要程度。
我们还要梳理出服务间的依赖关系。依赖越多,系统越易碎,但要真正做到无依赖还是很困难的,我们只能是降低服务依赖的深度和广度,从而让管理更为简单和简洁。在这一点上,以 Spring Boot 为首的微服务开发框架就开了一个好头。
微服务是服务依赖最优解的上限,而服务依赖的下限是千万不要有依赖环。依赖环的存在表明你的架构设计是错误的,这是一种极强的耦合。
解决服务依赖环的方案一般是,依赖倒置的设计模式。在分布式架构上,你可以使用一个第三方的服务来解决这个事。比如,通过订阅或发布消息到一个消息中间件,或是把其中的依赖关系抽到一个第三方的服务中,然后由这个第三方的服务来调用这些原本循环依赖的服务。
服务的依赖关系是可以通过技术的手段来发现的,其中,Zipkin 是一个很不错的服务调用跟踪系统,它是通过 Google Dapper (opens new window) 这篇论文来实现的。这个工具可以帮你梳理服务的依赖关系,以及了解各个服务的性能。
在梳理完服务的重要程度和服务依赖关系之后,我们就相当于知道了整个架构的全局,相当于拿到了一张架构的地图。
# 5.2 服务状态和生命周期的管理
由于架构中的服务是动态进入和退出的,因此需要一个服务发现的中间件。服务注册中心就能让我们知道下面这些事:
- 整个架构中有多少种服务?
- 这些服务的版本是什么样的?
- 每个服务的实例数有多少个,它们的状态是什么样的?
- 每个服务的状态是什么样的?是在部署中,运行中,故障中,升级中,还是在回滚中,伸缩中,或者是在下线中……
有了这些服务的状态和运行情况之后,你就需要对这些服务的生命周期进行管理了。服务的生命周期通常会有以下几个状态:
- Provision,代表在供应一个新的服务;
- Ready,表示启动成功了;
- Run,表示通过了服务健康检查;
- Update,表示在升级中;
- Rollback,表示在回滚中;
- Scale,表示正在伸缩中(可以有 Scale-in 和 Scale-out 两种);
- Destroy,表示在销毁中;
- Failed,表示失败状态。
有了这些服务的状态和生命周期的管理,以及服务的重要程度和服务的依赖关系,再加上一个服务运行状态的拟合控制(后面会提到),你一下子就有了管理整个分布式服务的手段了。一个纷乱无比的世界从此就可以干干净净地管理起来了
# 5.3 整个架构的版本管理
对于整个架构的版本管理这个事,我只见到亚马逊有这个东西,叫 VersionSet,也就是由一堆服务的版本集所形成的整个架构的版本控制。
正如 Linux 软件包会有一个用于管理各个包兼容性的版本控制一样,在分布式架构中,我们也需要一个架构的版本,用来控制其中各个服务的版本兼容。比如,A 服务的 1.2 版本只能和 B 服务的 2.2 版本一起工作,A 服务的上个版本 1.1 只能和 B 服务的 2.0 一起工作。这就是版本兼容性。
有了这个版本管理,如果我们要回滚一个服务的版本,就可以把与之有版本依赖的服务也一起回滚掉。这样当出现版本依赖问题时,就需要在架构版本中记录下这个事,以便可以回滚到上一次相互兼容的版本。
要做到这个事,你需要一个架构的 manifest,一个服务清单,这个服务清单定义了所有服务的版本运行环境,其中包括但不限于:
- 服务的软件版本;
- 服务的运行环境——环境变量、CPU、内存、可以运行的节点、文件系统等;
- 服务运行的最大最小实例数。
每一次对这个清单的变更都需要被记录下来,算是一个架构的版本管理。而我们上面所说的那个集群控制系统需要能够解读并执行这个清单中的变更,以操作和管理整个集群中的相关变更。
# 5.4 资源 / 服务调度
正如操作系统调度计算机上的所有进程一样,我们也需要对分布式的服务和资源进行调度,主要有如下关键技术:
- 服务状态的维持和拟合
- 服务的弹性伸缩和故障迁移
- 作业和应用调度
- 作业工作流编排
- 服务编排
# 5.4.1 服务状态的维持和拟合
所谓服务状态不是服务中的数据状态,而是服务的运行状态,换句话说就是服务的 Status,而不是 State。也就是上述服务运行时生命周期中的状态——Provision,Ready,Run,Scale,Rollback,Update,Destroy,Failed……
服务运行时的状态是非常关键的。服务运行过程中,状态也是会有变化的,这样的变化有两种:
- 一种是没有预期的变化。比如服务突然挂掉。
- 一种是预期的变化。比如新版本的发布。
对于分布式系统的服务管理来说,当需要把一个状态变成另一个状态时,我们需要对集群进行一系列的操作。比如,当需要对集群进行 Scale 的时候,我们需要:
- 先扩展出几个结点;
- 再往上部署服务;
- 然后启动服务;
- 再检查服务的健康情况;
- 最后把新扩展出来的服务实例加入服务发现中提供服务。
可以看到,这是一个比较稳健和严谨的 Scale 过程,这需要集群控制器往生产集群中进行若干次操作。这个操作的过程一定是比较“慢”的。一方面,需要对其它操作排它;另一方面,在整个过程中,我们的控制系统需要努力地逼近最终状态,直到完全达到。此外,正在运行的服务可能也会出现问题,离开了我们想要的状态,而控制系统检测到后,会强行地维持服务的状态。
我们把这个过程就叫做“拟合”。基本上来说,集群控制系统都是要干这个事的。没有这种设计的控制系统都不能算作设计精良的控制系统,而且在运行时一定会有很多的坑和 bug。如果研究过 Kubernetes 这个调度控制系统,你就会看到它的思路就是这个样子的。
# 5.4.2 服务的弹性伸缩和故障迁移
有了上述的服务状态拟合的基础工作之后,我们就能很容易地管理服务的生命周期了,甚至可以通过底层的支持进行便利的服务弹性伸缩和故障迁移。
对于弹性伸缩,上面已经给出了一个服务伸缩的操作步骤,包括底层资源的伸缩、服务的自动化部署、服务的健康检查、服务发现的注册和服务流量的调度。
对于故障迁移,也就是服务的某个实例出现问题时,我们需要自动地恢复它。恢复方式有两种模式:
- 宠物模式:就是一定要救活,主要是对于 stateful 的服务。
- 奶牛模式:就是不用救活了,重新生成一个实例。
对于这两种模式,在运行中也是比较复杂的,其中涉及到了:
- 服务的健康监控(这可能需要一个 APM 的监控)
- 如果是宠物模式,需要:服务的重新启动和服务的监控报警(如果重试恢复不成功,需要人工介入)。
- 如果是奶牛模式,需要:服务的资源申请,服务的自动化部署,服务发现的注册,以及服务的流量调度。
这些工作很细节,好消息是,Docker 和 K8s 提供了这样的服务调度功能,有了它们,我们就可以做到一个完全自动化的运维架构了。
# 5.4.3 服务工作流和编排
在操作系统中,一个好的操作系统需要能够通过一定的机制把一堆独立工作的进程给协同起来。在分布式的服务调度中,这个工作叫做 Orchestration,国内把这个词翻译成“编排”。
传统的 SOA 的编排工作是通过 ESB(Enterprise Service Bus)——企业服务总线来完成的。ESB 的主要功能是服务通信路由、协议转换、服务编制和业务规则应用等。
而在微服务中,我们希望使用更为轻量的中间件来取代 ESB 的服务编排功能。
简单来说,这需要一个 API Gateway 或一个简单的消息队列来做相应的编排工作。在 Spring Cloud 中,所有的请求都统一通过 API Gateway(Zuul)来访问内部的服务。这个和 Kubernetes 中的 Ingress 相似。
左耳听风
我觉得,关于服务的编排会直接导致一个服务编排的工作流引擎中间件的产生,这可能是因为我受到了亚马逊的软件工程文化的影响所致——亚马逊是一家超级喜欢工作流引擎的公司。通过工作流引擎,可以非常快速地将若干个服务编排起来形成一个业务流程。(你可以看一下 AWS 上的 Simple Workflow 服务。)
这就是所谓的 Orchestration 中的 conductor 指挥了。
# 6. 分布式系统关键技术:流量与数据调度
关于流量调度,现在很多架构师都把这个事和服务治理混为一谈了。我觉得还是应该分开的。一方面,服务治理是内部系统的事,而流量调度可以是内部的,更是外部接入层的事。另一方面,服务治理是数据中心的事,而流量调度要做得好,应该是数据中心之外的事,也就是我们常说的边缘计算,是应该在类似于 CDN 上完成的事。
所以,流量调度和服务治理是在不同层面上的,不应该混在一起,所以在系统架构上应该把它们分开。
# 6.1 流量调度
# 6.1.1 流量调度的主要功能
对于一个流量调度系统来说,其应该具有的主要功能是:
- 依据系统运行的情况,自动地进行流量调度,在无需人工干预的情况下,提升整个系统的稳定性;
- 让系统应对爆品等突发事件时,在弹性计算扩缩容的较长时间窗口内或底层资源消耗殆尽的情况下,保护系统平稳运行。
这还是为了提高系统架构的稳定性和高可用性。
此外,这个流量调度系统还可以完成以下几方面的事情:
- 服务流控。服务发现、服务路由、服务降级、服务熔断、服务保护等。
- 流量控制。负载均衡、流量分配、流量控制、异地灾备(多活)等。
- 流量管理。协议转换、请求校验、数据缓存、数据计算等。
所有的这些都应该是一个 API Gateway 应该做的事。
# 6.1.2 流量调度的关键技术
作为一个 API Gateway 来说,因为要调度流量,首先需要扛住流量,而且还需要有一些比较轻量的业务逻辑,所以一个好的 API Gateway 需要具备以下的关键技术:
- 高性能
- 扛流量。要能扛流量,就需要使用集群技术。集群技术的关键点是在集群内的各个结点中共享数据。这就需要使用像 Paxos、Raft、Gossip 这样的通讯协议。因为 Gateway 需要部署在广域网上,所以还需要集群的分组技术。
- 业务逻辑。API Gateway 需要有简单的业务逻辑,所以,最好是像 AWS 的 Lambda 服务一样,可以让人注入不同语言的简单业务逻辑。
- 服务化。一个好的 API Gateway 需要能够通过 Admin API 来不停机地管理配置变更,而不是通过一个.conf 文件来人肉地修改配置。
可以参考 MegaEase (opens new window) 这样一个软件。
# 6.2 状态数据调度
对于服务调度来说,最难办的就是有状态的服务了。这里的状态是 State,也就是说,有些服务会保存一些数据,而这些数据是不能丢失的,所以,这些数据是需要随服务一起调度的。
服务的 State 和 Status 要区分开。
一般来说,我们会通过“转移问题”的方法来让服务变成“无状态的服务”。也就是说,会把这些有状态的东西存储到第三方服务上,比如 Redis、MySQL、ZooKeeper,或是 NFS、Ceph 的文件系统中。
这些“转移问题”的方式把问题转移到了第三方服务上,于是自己的 Java 或 PHP 服务中没有状态,但是 Redis 和 MySQL 上则有了状态。所以,我们可以看到,现在的分布式系统架构中出问题的基本都是这些存储状态的服务。
因为数据存储结点在 Scale 上比较困难,所以成了一个单点的瓶颈。
# 6.2.1 分布式事务一致性的问题
要解决数据结点的 Scale 问题,也就是让数据服务可以像无状态的服务一样在不同的机器上进行调度,这就会涉及数据的 replication 问题。而数据 replication 则会带来数据一致性的问题,进而对性能带来严重的影响。
要解决数据不丢失的问题,只能通过数据冗余的方法,就算是数据分区,每个区也需要进行数据冗余处理。这就是数据副本。当出现某个节点的数据丢失时,可以从副本读到。数据副本是分布式系统解决数据丢失异常的唯一手段。简单来说:
- 要想让数据有高可用性,就得写多份数据。
- 写多份会引起数据一致性的问题。
- 数据一致性的问题又会引发性能问题。
在解决数据副本间的一致性问题时,我们有一些技术方案:
- Master-Slave 方案。
- Master-Master 方案。
- 两阶段和三阶段提交方案。
- Paxos 方案。
下图展示了各种方案的对比:
现在,很多公司的分布式系统事务基本上都是两阶段提交的变种。比如:阿里推出的 TCC–Try–Confirm–Cancel,或是我在亚马逊见到的 Plan–Reserve–Confirm 的方式,等等。凡是通过业务补偿,或是在业务应用层上做的分布式事务的玩法,基本上都是两阶段提交,或是两阶段提交的变种。
换句话说,迄今为止,在应用层上解决事务问题,只有“两阶段提交”这样的方式,而在数据层解决事务问题,Paxos 算法则是不二之选。
# 6.2.2 数据结点的分布式方案
真正完整解决数据 Scale 问题的应该还是数据结点自身。只有数据结点自身解决了这个问题,才能做到对上层业务层的透明,业务层可以像操作单机数据库一样来操作分布式数据库,这样才能做到整个分布式服务架构的调度。
也就是说,这个问题应该解决在数据存储方。但是因为数据存储结果有太多不同的 Scheme,所以现在的数据存储也是多种多样的,有文件系统,有对象型的,有 Key-Value 式,有时序的,有搜索型的,有关系型的……
这就是为什么分布式数据存储系统比较难做,因为很难做出来一个放之四海皆准的方案。类比一下编程中的各种不同的数据结构你就会明白为什么会有这么多的数据存储方案了。但是我们可以看到,这个“数据存储的动物园”中,基本上都在解决数据副本、数据一致性和分布式事务的问题。
比如 AWS 的 Aurora,就是改写了 MySQL 的 InnoDB 引擎。为了承诺高可用的 SLA,所以需要写 6 个副本,但实现方式上,它不像 MySQL 通过 bin log 的数据复制方式,而是更为“惊艳”地复制 SQL 语句,然后拼命地使用各种 tricky 的方式来降低 latency。比如,使用多线程并行、使用 SQL 操作的 merge 等。
MySQL 官方也有 MySQL Cluster 的技术方案。此外,MongoDB、国内的 PingCAP 的 TiDB、国外的 CockroachDB,还有阿里的 OceanBase 都是为了解决大规模数据的写入和读取的问题而出现的数据库软件。所以,我觉得成熟的可以用到生产线上的分布式数据库这个事估计也不远了。
而对于一些需要文件存储的,则需要分布式文件系统的支持。试想,一个 Kafka 或 ZooKeeper 需要把它们的数据存储到文件系统上。当这个结点有问题时,我们需要再启动一个 Kafka 或 ZooKeeper 的实例,那么也需要把它们持久化的数据搬迁到另一台机器上。
注意,虽然 Kafka 和 ZooKeeper 是 HA 的,数据会在不同的结点中进行复制,但是我们也应该搬迁数据,这样有利用于新结点的快速启动。否则,新的结点需要等待数据同步,这个时间会比较长,可能会导致数据层的其它问题。
于是,我们就需要一个底层是分布式的文件系统,这样新的结点只需要做一个简单的远程文件系统的 mount 就可以把数据调度到另外一台机器上了。
所以,真正解决数据结点调度的方案应该是底层的数据结点。在它们上面做这个事才是真正有效和优雅的。而像阿里的用于分库分表的数据库中间件 TDDL 或是别的公司叫什么 DAL 之类的这样的中间件都会成为过渡技术。
# 6.2.3 状态数据调度小结
- 对于应用层上的分布式事务一致性,只有两阶段提交这样的方式。
- 而底层存储可以解决这个问题的方式是通过一些像 Paxos、Raft 或是 NWR 这样的算法和模型来解决。
- 状态数据调度应该是由分布式存储系统来解决的,这样会更为完美。但是因为数据存储的 Scheme 太多,所以,导致我们有各式各样的分布式存储系统,有文件对象的,有关系型数据库的,有 NoSQL 的,有时序数据的,有搜索数据的,有队列的……
总之,我相信状态数据调度应该是在 IaaS 层的数据存储解决的问题,而不是在 PaaS 层或者 SaaS 层来解决的。
在 IaaS 层上解决这个问题,一般来说有三种方案,一种是使用比较廉价的开源产品,如:NFS、Ceph、TiDB、CockroachDB、ElasticSearch、InfluxDB、MySQL Cluster 和 Redis Cluster 之类的;另一种是用云计算厂商的方案。当然,如果不差钱的话,可以使用更为昂贵的商业网络存储方案。
# 6.3 小结
首先,不要将流量调度和服务治理混为一谈(当然,服务治理是流量调度的前提)。
然后本节分别讲述了流量调度和状态数据调度,并给出了相应的一些关键技术。
# 7. 洞悉 PaaS 平台的本质
# 7.1 软件工程能力的体现
一家商业公司的软件工程能力主要体现在三个地方:
- 提高服务的 SLA。也就是我们能提供多少个 9 的系统可用性,表现在高可用的系统和自动化的运维。故障是常态,如果没有自动化的故障恢复,就很难提高服务的 SLA。
- 能力和资源重用或复用。需要我们有两个重要的能力:一个是“软件抽象的能力”,另一个是“软件标准化的能力”。这样能让整体软件开发运维的能力和资源得到最大程度的复用,从而增加效率。
- 过程的自动化。把软件生产和运维的过程自动化起来,这体现在两方面:软件生产流水线和软件运维自动化。
通过了解软件工程的这三个本质,你会发现,我们上面所说的那些分布式的技术点是高度一致的,也就是下面这三个方面的能力:
- 分布式多层的系统架构。
- 服务化的能力供应。
- 自动化的运维能力。
只有做到了这些,我们才能够真正拥有云计算的威力。这就是所谓的 Cloud Native。而这些目标都完美地体现在 PaaS 平台上。
前面讲述的分布式系统关键技术和软件工程的本质,都可以在 PaaS 平台上得到完全体现。所以,需要一个 PaaS 平台把那么多的东西给串联起来。下面将讲一下 PaaS 相关东西,并把前面的东西做一个总结。
# 7.2 PaaS 平台的本质
一个好的 PaaS 平台应该具有分布式、服务化、自动化部署、高可用、敏捷以及分层开放的特征,并可与 IaaS 实现良好的联动:
下面这三件事是 PaaS 跟传统中间件最大的差别:
- 服务化是 PaaS 的本质。软件模块重用,服务治理,对外提供能力是 PaaS 的本质。
- 分布式是 PaaS 的根本特性。多租户隔离、高可用、服务编排是 PaaS 的基本特性。
- 自动化是 PaaS 的灵魂。自动化部署安装运维,自动化伸缩调度是 PaaS 的关键。
# 7.3 PaaS 平台的总体架构
使用 Docker + K8s 来做一个技术缓冲层,可以帮助我们构建 PaaS 平台:
在 Docker+Kubernetes 层之上,我们看到了两个相关的 PaaS 层:
- 一个是 PaaS 调度层,很多人将其称为 iPaaS;
- 另一个是 PaaS 能力层,通常被称为 aPaaS。
没有 PaaS 调度层,PaaS 能力层很难被管理和运维,而没有 PaaS 能力层,PaaS 就失去了提供实际能力的业务价值。而本文更多的是在讲 PaaS 调度层上的东西。
在两个相关的 PaaS 层之上,有一个流量调度的接入模块,这也是 PaaS 中非常关键的东西。流控、路由、降级、灰度、聚合、串联等等都在这里,包括最新的 AWS Lambda Service 的小函数等也可以放在这里。这个模块应该是像 CDN 那样来部署的。
然后,在这个图的两边分别是与运营和运维相关的。运营这边主要是管理一些软件资源方面的东西(类似 Docker Hub 和 CMDB),以及外部接入和开放平台上的东西,这主要是对外提供能力的相关组件;而运维这边主要是对内的相关东西,主要就是 DevOps。
总结一下,一个完整的 PaaS 平台会包括以下几部分:
- PaaS 调度层 – 主要是 PaaS 的自动化和分布式对于高可用高性能的管理。
- PaaS 能力服务层 – 主要是 PaaS 真正提供给用户的服务和能力。
- PaaS 的流量调度 – 主要是与流量调度相关的东西,包括对高并发的管理。
- PaaS 的运营管理 – 软件资源库、软件接入、认证和开放平台门户。
- PaaS 的运维管理 – 主要是 DevOps 相关的东西。
实际上,其中的很多组件是可以根据自己的需求被简化和裁剪的,而且很多开源软件能帮你简化好多工作。虽然构建 PaaS 平台看上去很麻烦,但是其实并不是很复杂。
# 7.4 PaaS 平台的生产和运维
下面的图我给出了一个大概的软件生产、运维和服务接入的流程,它把之前的东西都串起来了:
从左上开始软件构建,进入软件资产库(Docker Registry+ 一些软件的定义),然后走 DevOps 的流程,通过整体架构控制器进入生产环境,生产环境通过控制器操作 Docker+Kubernetes 集群进行软件部署和生产变更。
其中,同步服务的运行状态,并通过生命周期管理来拟合状态,如图右侧部分所示。服务运行时的数据会进入到相关应用监控,应用监控中的一些监控事件会同步到生命周期管理中,再由生命周期管理器来做出决定,通过控制器来调度服务运行。当应用监控中心发现流量变化,要进行强制性伸缩时,它通过生命周期管理来通知控制系统进行伸缩。
左下是服务接入的相关组件,主要是网关服务,以及 API 聚合编排和流程处理。这对应于之前说过的流量调度和 API Gateway 的相关功能。
# 8. 小结
下面对这一大章做一下总结:
为了构建分布式系统,我们面临的主要问题如下:
- 分布式系统的硬件故障发生率更高,故障发生是常态,需要尽可能地将运维流程自动化。
- 需要良好地设计服务,避免某服务的单点故障对依赖它的其他服务造成大面积影响。
- 为了容量的可伸缩性,服务的拆分、自治和无状态变得更加重要,可能需要对老的软件逻辑做大的修改。
- 老的服务可能是异构的,此时需要让它们使用标准的协议,以便可以被调度、编排,且互相之间可以通信。
- 服务软件故障的处理也变得复杂,需要优化的流程,以加快故障的恢复。
- 为了管理各个服务的容量,让分布式系统发挥出最佳性能,需要有流量调度技术。
- 分布式存储会让事务处理变得复杂;在事务遇到故障无法被自动恢复的情况下,手动恢复流程也会变得复杂。
- 测试和查错的复杂度增大。
- 系统的吞吐量会变大,但响应时间会变长。
为了解决这些问题,我们深入了解了以下这些解决方案:
- 需要有完善的监控系统,以便对服务运行状态有全面的了解。
- 设计服务时要分析其依赖链;当非关键服务故障时,其他服务要自动降级功能,避免调用该服务。
- 重构老的软件,使其能被服务化;可以参考 SOA 和微服务的设计方式,目标是微服务化;使用 Docker 和 Kubernetes 来调度服务。
- 为老的服务编写接口逻辑来使用标准协议,或在必要时重构老的服务以使得它们有这些功能。
- 自动构建服务的依赖地图,并引入好的处理流程,让团队能以最快速度定位和恢复故障,详见《故障处理最佳实践:应对故障》一讲。
- 使用一个 API Gateway,它具备服务流向控制、流量控制和管理的功能。
- 事务处理建议在存储层实现;根据业务需求,或者降级使用更简单、吞吐量更大的最终一致性方案,或者通过二阶段提交、Paxos、Raft、NWR 等方案之一,使用吞吐量小的强一致性方案。
- 通过更真实地模拟生产环境,乃至在生产环境中做灰度发布,从而增加测试强度;同时做充分的单元测试和集成测试以发现和消除缺陷;最后,在服务故障发生时,相关的多个团队同时上线自查服务状态,以最快地定位故障原因。
- 通过异步调用来减少对短响应时间的依赖;对关键服务提供专属硬件资源,并优化软件逻辑以缩短响应时间。