来源: 《从0开始学架构》(极客时间) ---李运华
硬件系统生产出来后不会再改变,而软件系统却需要一直扩展迭代。
可扩展基本方法
可扩展性架构的设计方法很多,但万变不离其宗,所有的可扩展性架构设计,背后的基本思想都可以总结为一个字:拆!
拆,就是将原本大一统的系统拆分成多个规模小的部分,扩展时只修改其中一部分即可,无须整个系统到处都改,通过这种方式来减少改动范围,降低改动风险。
按照不同的思路来拆分软件系统,就会得到不同的架构。常见的拆分思路有如下三种:
- 面向流程拆分:将整个业务流程拆分为几个阶段,每个阶段作为一部分。
- 面向服务拆分:将系统提供的服务拆分,每个服务作为一部分。
- 面向功能拆分:将系统提供的功能拆分,每个功能作为一部分。
以学生管理系统为例:
- 面向流程拆分,可分为展示层 → 业务层 → 数据层 → 存储层
- 面向服务拆分,可将系统拆分为注册、登录、信息管理、安全设置等服务
- 面向功能拆分,每个服务都可以拆分为更多细粒度的功能
不同的拆分方式,本质上决定了系统的扩展方式。典型的可扩展系统架构有:
- 面向流程拆分:分层架构。
- 面向服务拆分:SOA、微服务。
- 面向功能拆分:微内核架构。
相比于高性能、高可用架构模式在最近几十年的迅猛发展来说,可扩展架构模式的发展可以说是步履蹒跚,最近几年火热的微服务模式算是可扩展模式发展历史中为数不多的亮点,但这也导致了现在谈可扩展的时候必谈微服务,甚至微服务架构都成了架构设计的银弹,高性能也用微服务、高可用也用微服务,很多时候这样的架构设计看起来高大上,实际上是大炮打蚊子,违背了架构设计的“合适原则”和“简单原则”。
分层架构
分层架构是很常见的架构模式,它也叫 N 层架构,通常情况下,N 至少是 2 层。
- C/S 架构、B/S 架构
划分的对象是整个业务系统,划分的维度是用户交互,即将和用户交互的部分独立为一层,支撑用户交互的后台作为另外一层。
- MVC 架构、MVP 架构
划分的对象是单个业务子系统,划分的维度是职责,将不同的职责划分到独立层,但各层的依赖关系比较灵活。
- 逻辑分层架构
划分的对象可以是单个业务子系统,也可以是整个业务系统,划分的维度也是职责。虽然都是基于职责划分,但逻辑分层架构和 MVC 架构、MVP 架构的不同点在于,逻辑分层架构中的层是自顶向下依赖的。典型的有操作系统内核架构、TCP/IP 架构。
分层架构设计最核心的一点就是需要保证各层之间的差异足够清晰,边界足够明显,让人看到架构图后就能看懂整个架构,这也是分层不能分太多层的原因。
分层架构之所以能够较好地支撑系统扩展,本质在于隔离关注点,即每个层中的组件只会处理本层的逻辑。
分层结构的另外一个特点就是层层传递,也就是说一旦分层确定,整个业务流程是按照层进行依次传递的,不能在层之间进行跳跃。分层结构的这种约束,好处在于强制将分层依赖限定为两两依赖,降低了整体系统复杂度。
SOA
SOA 的全称是 Service Oriented Architecture,中文翻译为“面向服务的架构”。
SOA 出现 的背景是企业内部的 IT 系统重复建设且效率低下,为了应对传统 IT 系统存在的问题,SOA 提出了 3 个关键概念。
- 服务
所有业务功能都是一项服务,服务就意味着要对外提供开放的能力,当其他系统需要使用这项功能时,无须定制化开发。服务可大可小,可简单也可复杂。
- ESB
ESB 的全称是 Enterprise Service Bus,中文翻译为“企业服务总线”。从名字就可以看出,ESB 参考了计算机总线的概念。计算机中的总线将各个不同的设备连接在一起,ESB 将企业中各个不同的服务连接在一起。因为各个独立的服务是异构的,如果没有统一的标准,则各个异构系统对外提供的接口是各式各样的。SOA 使用 ESB 来屏蔽异构系统对外提供各种不同的接口方式,以此来达到服务间高效的互联互通。
- 松耦合
松耦合的目的是减少各个服务间的依赖和互相影响。但实际上真正做到松耦合并没有那么容易,要做到完全后向兼容,是一项复杂的任务。
SOA 架构是比较高层级的架构设计理念,一般情况下我们可以说某个企业采用了 SOA 的架构来构建 IT 系统,但不会说某个独立的系统采用了 SOA 架构。
SOA 最广为人诟病的就是 ESB,ESB 需要实现与各种系统间的协议转换、数据转换、透明的动态路由等功能。要完成这么多协议和数据格式的互相转换,工作量和复杂度都很大,而且这种转换是需要耗费大量计算性能的,当 ESB 承载的消息太多时,ESB 本身会成为整个系统的性能瓶颈。这种情况是因为 SOA 提出时,企业的各种异构的 IT 系统已经存在很多年了,完全重新的成本很大,只能通过 ESB 去适配已有的系统。
微服务
微服务与SOA的关系
- 服务粒度
整体上来说,SOA 的服务粒度要粗一些,而微服务的服务粒度要细一些。
- 服务通信
SOA 采用了 ESB 作为服务间通信的关键组件,负责服务定义、服务路由、消息转换、消息传递,总体上是重量级的实现。微服务推荐使用统一的协议和格式,例如,RESTful 协议、RPC 协议,无须 ESB 这样的重量级实现。
- 服务交付
SOA 对服务的交付并没有特殊要求,因为 SOA 更多考虑的是兼容已有的系统;微服务的架构理念要求“快速交付”,相应地要求采取自动化测试、持续集成、自动化部署等敏捷开发相关的最佳实践。如果没有这些基础能力支撑,微服务规模一旦变大(例如,超过 20 个微服务),整体就难以达到快速交付的要求,这也是很多企业在实行微服务时踩过的一个明显的坑,就是系统拆分为微服务后,部署的成本呈指数上升。
- 应用场景
SOA 更加适合于庞大、复杂、异构的企业级系统,这也是 SOA 诞生的背景。
微服务更加适合于快速、轻量级、基于 Web 的互联网系统,这类系统业务变化快,需要快速尝试、快速交付;同时基本都是基于 Web,虽然开发技术可能差异很大(例如,Java、C++、.NET 等),但对外接口基本都是提供 HTTP RESTful 风格的接口,无须考虑在接口层进行类似 SOA 的 ESB 那样的处理。
SOA 和微服务本质上是两种不同的架构设计理念,只是在“服务”这个点上有交集而已
微服务的陷阱
1. 服务划分过细,服务间关系复杂
服务划分过细,单个服务的复杂度确实下降了,但整个系统的复杂度却上升了,因为微服务将系统内的复杂度转移为系统间的复杂度了。
2. 服务数量太多,团队效率急剧下降
微服务的“微”字,本身就是一个陷阱,很多团队看到“微”字后,就想到必须将服务拆分得很细,这样做给工作效率带来了明显的影响,一个简单的需求开发就需要涉及多个微服务,光是微服务之间的接口就有 6 ~ 7 个,无论是设计、开发、测试、部署,都需要工程师不停地在不同的服务间切换。
3. 调用链太长,性能下降
由于微服务之间都是通过 HTTP 或者 RPC 调用的,每次调用必须经过网络。一般线上的业务接口之间的调用,平均响应时间大约为 50 毫秒,如果用户的一起请求需要经过 6 次微服务调用,则性能消耗就是 300 毫秒,这在很多高性能业务场景下是难以满足需求的。
4. 调用链太长,问题定位困难
系统拆分为微服务后,一次用户请求需要多个微服务协同处理,任意微服务的故障都将导致整个业务失败。然而由于微服务数量较多,且故障存在扩散现象,快速定位到底是哪个微服务故障是一件复杂的事情。
5. 没有自动化支撑,无法快速交付
如果没有相应的自动化系统进行支撑,都是靠人工去操作,那么微服务不但达不到快速交付的目的,甚至还不如一个大而全的系统效率高。
6. 没有服务治理,微服务数量多了后管理混乱
随着微服务种类和数量越来越多,如果没有服务治理系统进行支撑,微服务提倡的 lightweight 就会变成问题。
微服务的设计方法
服务粒度
针对微服务拆分过细导致的问题,建议基于团队规模进行拆分,类似贝索斯在定义团队规模时提出的“两个披萨”理论(每个团队的人数不能多到两张披萨都不够吃的地步),可以使用“三个火枪手”原则,即一个微服务三个人负责开发。
“三个火枪手”的原则主要应用于微服务设计和开发阶段,如果微服务经过一段时间发展后已经比较稳定,处于维护期了,无须太多的开发,那么平均 1 个人维护 1 个微服务甚至几个微服务都可以。
拆分方法
- 基于业务逻辑拆分
这是最常见的一种拆分方式,将系统中的业务模块按照职责范围识别出来,每个单独的业务模块拆分为一个独立的服务。
- 基于可扩展拆分
将系统中的业务模块按照稳定性排序,将已经成熟和改动不大的服务拆分为稳定服务,将经常变化和迭代的服务拆分为变动服务。稳定的服务粒度可以粗一些,即使逻辑上没有强关联的服务,也可以放在同一个子系统中;不稳定的服务粒度可以细一些,但也不要太细,始终记住要控制服务的总数量。
- 基于可靠性拆分
将系统中的业务模块按照优先级排序,将可靠性要求高的核心服务和可靠性要求低的非核心服务拆分开来,然后重点保证核心服务的高可用。
这样拆分带来下面几个好处:
- 避免非核心服务故障影响核心服务
- 核心服务高可用方案可以更简单
- 能够降低高可用成本
- 基于性能拆分
基于性能拆分和基于可靠性拆分类似,将性能要求高或者性能压力大的模块拆分出来,避免性能压力大的服务影响其他服务。常见的拆分方式和具体的性能瓶颈有关,可以拆分 Web 服务、数据库、缓存等。
基础设施
大部分人主要关注的是微服务的“small”和“lightweight”特性,但实际上真正决定微服务成败的,恰恰是那个被大部分人都忽略的“automated”。如果“automated”相关的基础设施不健全,那微服务就是焦油坑,让研发、测试、运维陷入各种陷阱中无法自拔。
常见的微服务基础设施搭建优先级为:
- 服务发现、服务路由、服务容错:这是最基本的微服务基础设施。
- 接口框架、API 网关:主要是为了提升开发效率,接口框架是提升内部服务的开发效率,API 网关是为了提升与外部服务对接的效率。
- 自动化部署、自动化测试、配置中心:主要是为了提升测试和运维效率。
- 服务监控、服务跟踪、服务安全:主要是为了进一步提升运维效率。
以上 3 和 4 两类基础设施,其重要性会随着微服务节点数量增加而越来越重要,但在微服务节点数量较少的时候,可以通过人工的方式支撑,虽然效率不高,但也基本能够顶住。
1. 自动化测试
自动化测试涵盖的范围包括代码级的单元测试、单个系统级的集成测试、系统间的接口测试,理想情况是每类测试都自动化。如果因为团队规模和人力的原因无法全面覆盖,至少要做到接口测试自动化。
2. 自动化部署
自动化部署系统包括版本管理、资源管理(例如,机器管理、虚拟机管理)、部署操作、回退操作等功能。
3. 配置中心
配置中心包括配置版本管理、增删改查配置、节点管理、配置同步、配置推送等功能。
4. 接口框架
微服务提倡轻量级的通信方式,一般采用 HTTP/REST 或者 RPC 方式统一接口协议。但在实践过程中,光统一接口协议还不够,还需要统一接口传递的数据格式。
5. API 网关
系统拆分为微服务后,内部的微服务之间是互联互通的,相互之间的访问都是点对点的。如果外部系统想调用系统的某个功能,也采取点对点的方式,则外部系统会非常“头大”。因为在外部系统看来,它不需要也没办法理解这么多微服务的职责分工和边界,它只会关注它需要的能力,而不会关注这个能力应该由哪个微服务提供。
API 网关是外部系统访问的接口,所有的外部系统接⼊系统都需要通过 API 网关,主要包括接入鉴权(是否允许接入)、权限控制(可以访问哪些功能)、传输加密、请求路由、流量控制等功能。服务发现
6. 服务发现
服务发现主要有两种实现方式:自理式和代理式。
自理式
自理式结构就是指每个微服务自己完成服务发现。
自理式服务发现实现比较简单,因为这部分的功能一般通过统一的程序库或者程序包提供给各个微服务调用,而不会每个微服务都自己来重复实现一遍;并且由于每个微服务都承担了服务发现的功能,访问压力分散到了各个微服务节点,性能和可用性上不存在明显的压力和风险。
代理式
代理式结构就是指微服务之间有一个负载均衡系统,由负载均衡系统来完成微服务之间的服务发现。
代理式的方式看起来更加清晰,微服务本身的实现也简单了很多,但实际上这个方案风险较大。第一个风险是可用性风险,第二个风险是性能风险。
不管是自理式还是代理式,服务发现的核心功能就是服务注册表,注册表记录了所有的服务节点的配置和状态,每个微服务启动后都需要将自己的信息注册到服务注册表,然后由微服务或者 LOAD BALANCER 系统到服务注册表查询可用服务。
7. 服务路由
有了服务发现后,微服务之间能够方便地获取相关配置信息,但具体进行某次调用请求时,我们还需要从所有符合条件的可用微服务节点中挑选出一个具体的节点发起请求,这就是服务路由需要完成的功能。
服务路由和服务发现紧密相关,服务路由一般不会设计成一个独立运行的系统,通常情况下是和服务发现放在一起实现的。对于自理式服务发现,服务路由是微服务内部实现的;对于代理式服务发现,服务路由是由 LOAD BALANCER 系统实现的。
8. 服务容错
微服务需要能够自动处理出错场景,否则,如果节点一故障就需要人工处理,投入人力大,处理速度慢;而一旦处理速度慢,则故障就很快扩散,所以我们需要服务容错的能力。
常见的服务容错包括请求重试、流控和服务隔离。通常情况下,服务容错会集成在服务发现和服务路由系统中。
9. 服务监控
服务监控的主要作用有:
- 实时搜集信息并进行分析,避免故障后再来分析,减少了处理时间。
- 服务监控可以在实时分析的基础上进行预警,在问题萌芽的阶段发觉并预警,降低了问题影响的范围和时间。
10. 服务跟踪
服务监控和服务跟踪的区别可以简单概括为宏观和微观的区别。例如,A 服务通过 HTTP 协议请求 B 服务 10 次,B 通过 HTTP 返回 JSON 对象,服务监控会记录请求次数、响应时间平均值、响应时间最高值、错误码分布这些信息;而服务跟踪会记录其中某次请求的发起时间、响应时间、响应错误码、请求参数、返回的 JSON 对象等信息。
11. 服务安全
服务安全主要分为三部分:接入安全、数据安全、传输安全。通常情况下,服务安全可以集成到配置中心系统中进行实现,即配置中心配置微服务的接入安全策略和数据安全策略,微服务节点从配置中心获取这些配置信息,然后在处理具体的微服务调用请求时根据安全策略进行处理。由于这些策略是通用的,一般会把策略封装成通用的库提供给各个微服务调用。
微内核
微内核架构(Microkernel Architecture),也被称为插件化架构(Plug-in Architecture),是一种面向功能进行拆分的可扩展性架构,通常用于实现基于产品的应用。
基本架构
微内核架构包含两类组件:核心系统(core system)和插件模块(plug-in modules)。核心系统负责和具体业务功能无关的通用功能,例如模块加载、模块间通信等;插件模块负责实现具体的业务逻辑。
设计关键点
微内核的核心系统设计的关键技术有:插件管理、插件连接和插件通信。
1. 插件管理
核心系统需要知道当前有哪些插件可用,如何加载这些插件,什么时候加载插件。常见的实现方法是插件注册表机制。
核心系统提供插件注册表(可以是配置文件,也可以是代码,还可以是数据库),插件注册表含有每个插件模块的信息,包括它的名字、位置、加载时机(启动就加载,还是按需加载)等。
2. 插件连接
插件连接指插件如何连接到核心系统。通常来说,核心系统必须制定插件和核心系统的连接规范,然后插件按照规范实现,核心系统按照规范加载即可。
常见的连接机制有 OSGi(Eclipse 使用)、消息模式、依赖注入(Spring 使用),甚至使用分布式的协议都是可以的,比如 RPC 或者 HTTP Web 的方式。
3. 插件通信
插件通信指插件间的通信。虽然设计的时候插件间是完全解耦的,但实际业务运行过程中,必然会出现某个业务流程需要多个插件协作,这就要求两个插件间进行通信。由于插件之间没有直接联系,通信必须通过核心系统,因此核心系统需要提供插件通信机制。
OSGi 架构简析
OSGi 是一个插件化的标准,而不是一个可运行的框架,Eclipse 采用的 OSGi 框架称为 Equinox,类似的实现还有 Apache 的 Felix、Spring 的 Spring DM。
OSGi 的基本架构为:
1. 模块层(Module 层)
模块层实现插件管理功能。OSGi 中,插件被称为 Bundle,每个 Bundle 是一个 Java 的 JAR 文件,每个 Bundle 里面都包含一个元数据文件 MANIFEST.MF,这个文件包含了 Bundle 的基本信息。OSGi 核心系统会将这些信息加载到系统中用于后续使用。
2. 生命周期层(Lifecycle 层)
生命周期层实现插件连接功能,提供了执行时模块管理、模块对底层 OSGi 框架的访问。生命周期层精确地定义了 Bundle 生命周期的操作(安装、更新、启动、停止、卸载),Bundle 必须按照规范实现各个操作。
3. 服务层(Service 层)
服务层实现插件通信的功能。OSGi 提供了一个服务注册的功能,用于各个插件将自己能提供的服务注册到 OSGi 核心的服务注册中心,如果某个服务想用其他服务,则直接在服务注册中心搜索可用服务中心就可以了。
规则引擎架构简析
规则引擎从结构上来看也属于微内核架构的一种具体实现,其中执行引擎可以看作是微内核,执行引擎解析配置好的业务流,执行其中的条件和规则,通过这种方式来支持业务的灵活多变。
规则引擎的基本架构为:
- 开发人员将业务功能分解提炼为多个规则,将规则保存在规则库中。
- 业务人员根据业务需要,通过将规则排列组合,配置成业务流程,保存在业务库中。
- 规则引擎执行业务流程实现业务功能。
规则引擎实现微内核架构的要点:
1. 插件管理
规则引擎中的规则就是微内核架构的插件,引擎就是微内核架构的内核。规则可以被引擎加载和执行。规则引擎架构中,规则一般保存在规则库中,通常使用数据库来存储。
2. 插件连接
规则引擎规定了规则开发的语言,业务人员需要基于规则语言来编写规则文件,然后由规则引擎加载执行规则文件来完成业务功能,因此,规则引擎的插件连接实现机制其实就是规则语言。
3. 插件通信
规则引擎的规则之间进行通信的方式就是数据流和事件流,由于单个规则并不需要依赖其他规则,因此规则之间没有主动的通信,规则只需要输出数据或者事件,由引擎将数据或者事件传递到下一个规则。
目前最常用的规则引擎是开源的 JBoss Drools,采用 Java 语言编写,基于 Rete 算法