Skip to content

分布式系统

单体系统的核心问题

单体系统的核心问题表现在三个方面,即业务扩展性,性能伸缩性和代码复杂度

业务扩展性
所谓扩展性,指的是当系统的业务需求发生变化时,我们对现在系统的改动程度的一种控制能力。
改动程度越大,扩展性就越差。显然,对于单体系统而言,任何改动都会导致整个系统进行重新构建和发布,所以它的扩展性是比较低的

性能伸缩性
和扩展性关注业务变化的角度不同,伸缩性关注的是性能指标。
如果通过简单扩容就能确保系统的性能得到等比例的提升,那么我们就认为该系统具备较好的伸缩性。
对于单体系统而言,由于内存密集型和 CPU 密集型的代码都位于同一个服务器上,所以很难做到对资源的充分利用
例如系统中 A, B, C 组件的资源利用率分别是 75%, 10%, 15%。因为我们无法对单体系统中的这些组件进行拆分,所以也就无法单独对 B, C 组件进行资源利用率的提升

代码复杂度
由于我们无法对单块系统中的代码进行物理上的拆分,所以不同组件之间的代码边界往往很难清晰划分,
这就无法有效控制代码结构的复杂度。久而久之,因为代码复杂度引起的系统缺陷就会难以维护

分布式系统的本质特性

什么是分布式系统? 所谓分布式系统,区别于单体系统,会将整个系统拆分成多个能够独立运行的服务,这些服务在物理上是隔离的,相互之间通过网络进行通信和协调

分布式系统相较于单体系统而言具备优势的同时,也存在一些我们不得不考虑的问题

网络传输的三态性

我们知道对于单体系统中的函数式方法调用而言,只有“成功”或“失败”这两种状态。
但是分布式系统则不同,因为远程请求是通过网络进行传输的,而网络在处理请求时还会出现“超时”这个状态,这样就相当于有三个状态。

显然,网络传输的三态性为系统开发带来新的挑战。面对超时状态,我们不能简单把它处理成是一种成功或失败,而是要具体场景具体分析,避免出现请求丢失或请求重复发送现象。
在分布式系统设计过程中,我们需要考虑这种由于网络通信所导致的用户体验问题

请求的容错性

从错误发生的几率而言,分布式系统显然比单体系统更加容易出错,因为系统的调用链路变得更长、更复杂。
每个分布式服务自身可能会发生异常,而这种异常在整个调用链路上会进行扩散,最终可能导致整个系统都不可用。

在分布式系统设计过程中,一大挑战就是需要确保部分服务的异常情况不会影响到整个系统的可用性。

系统的异构性

分布式系统的异构性很好理解,原则上,每个服务都可以采用一套完全不同的技术体系来进行实现,只要它们对外暴露接口是统一的。
但是,因为技术异构性的存在,会增加分布式系统的开发难度和维护成本。

数据的一致性

在分布式系统中,各个服务通常都会构建属于自身的数据库,这样就会导致业务数据无法进行集中管理,也就无法通过传统的事务机制确保它们之间的一致性。
如何实现数据的一致性是分布式系统构建过程的一大难点

以上几点是分布式系统的基本特性,我们无法避免,只能想办法进行利用和管理,这就给我们设计和实现分布式系统提出了挑战。

技术体系

在分布式服务的开发过程中,开发人员需要应用到一批技术组件。
把它们分成三大类,即远程过程调用组件、微服务构建组件和通用技术组件,如下图所示:

远程过程调用组件

远程过程调用是分布式服务最基础的实现技术,开发人员需要从网络通信、远程调用、负载均衡、服务容错以及服务降级这五个维度来进行系统的理解。

第一个,网络通信。 网络通信是一切分布式操作的基础。
当客户端和服务器端建立网络连接之后就可以相互发送消息。
但围绕网络通信整个过程,事情并没有那么简单。
我们需要考虑网络通信的性能、可靠性以及在通信过程中实现数据传输的方式,这就涉及到 IO 模型、可靠性设计以及序列化方式等一系列技术主题。

第二个,远程调用。 远程调用解决的问题是如何发布远程服务以及如何引用远程服务。
一旦服务发布成功,就相当于构建了一个有效的网络连接,并通过启动监听端口来对外暴露服务访问的入口;
而服务引用则是一个向目标监听端点发起请求并获取响应结果的执行过程

在服务调用过程中,远程调用本地化是基本要求,即远程调用过程的实现对于开发人员而言应该是透明化的。
同时,我们也需要考虑同步调用、异步调用以及同步转异步调用等一系列具体的调用实现策略。

第三个,负载均衡。 所谓负载均衡,简单讲就是将请求按照一定的策略分摊到多个服务实例上进行执行
负载均衡在实现上可以使用硬件、软件或者两者兼有。
而针对软件负载均衡,也可以分成服务器端负载均衡和客户端负载均衡两大类。
在分布式服务构建过程中,我们主要的讨论对象是基于软件的客户端负载均衡机制。
例如,目前主流的微服务架构实现框架 Spring Cloud、Dubbo 等都内置了完整的客户端负载均衡模块。

另一方面,负载均衡算法决定了对请求进行分发的效果。
负载均衡算法有很多,可以分成静态和动态两个大类,它们之间的区别在于动态算法依赖于当前服务的运行时状态,这些状态信息通常包括服务过去一段时间的平均调用时延和所承接的连接数等。

第四个,服务容错。 在分布式环境中,服务访问出错了该怎么办?这就涉及到服务可靠性问题。
服务可靠性是分布式服务构建过程中的一项关键要素,我们需要引入容错思想和机制。
常见的服务容错技术包括集群容错、服务熔断(Circuit Breaker)和服务回退(Fallback)等。

第五个,服务降级。 从概念上讲,任何一个服务都是可以分等级的。具体的服务分级方法因业务场景和需求而定。
一旦我们实现了对服务的针对性分级,那么就可以对那些处于业务链路最外围、等级最低的服务开始执行降级。
至于如何对服务进行分级,可以按照需求采取一定的策略,例如常见的三级分类策略,如下图所示:

在上图中,一级服务属于核心服务,需要确保高可用,不能执行降级操作;
二级服务通常采用的是异步交互方式,容忍暂时的数据不一致性;
而三级服务则可以按需对整个服务实行降级操作。

微服务构建组件

在远程过程调用组件的基础上,我们继续讨论微服务构建组件,包括注册中心、服务网关、配置中心、消息通信和链路跟踪。
这些组件扩展了分布式技术能力,为构建大规模分布式系统提供了技术保障。

第一个,注册中心。 在分布式服务构建过程中,服务与服务之间通过远程调用完成业务链路的构建。
而在服务调用之前,我们首先需要发现服务,即解决在分布式集群环境下如何找到目标服务实例这一问题。
服务发现和调用构成了服务交互的基础,整体流程下图所示,其中实线部分代表服务调用流程,而虚线部分则包含了服务的注册(Registration)和发现(Discovery)过程。

可以看到,图中的这三个服务都需要注册到注册中心以确保负载均衡器能够从注册中心获取各个服务的定义信息。

第二个,服务网关。 在分布式系统中,API 网关(Gateway)或服务网关(Service Gateway)的出现有其必然性。
我们可以根据需要在服务提供者和消费者之间架设这层服务网关。
在注册中心和负载均衡的基础上,添加了服务网关之后的系统架构如下图所示:

当然,并不是所有的服务调用链路上都需要添加这层网关,我们也可以根据具体场景直接通过负载均衡器进行服务访问。
在实际应用过程中,这种混合式的服务调用管理方式也是一种常见的做法。

第三个,配置中心。 面对不断增长的服务实例数量,传统的配置信息管理方式就显得无能为力。
为此,在分布式服务构建过程中,一般都需要引入配置中心(Configuration Center)的设计思想和相关工具。
下图展示了在前面各个组件的基础上添加配置中心之后的系统架构图,分布式系统中的各个服务都可能会依赖配置中心,从而完成配置信息的统一管理

第四个,消息通信。 降低服务与服务之间的耦合度是分布式系统设计的一大目标,为此,我们可以引入事件驱动架构,基本组成如下图所示:

基于事件驱动架构,每一个服务既可以作为事件的发布者也可以作为事件的消费者,或者两者兼之。
而事件也可以在不同的服务之间进行传播,从而满足各种特定的应用场景。

第五个,链路跟踪。 服务之间的调用不可避免会出现各种问题,这时候就需要引入分布式链路跟踪体系来定位和解决这些问题。
基于每一次分布式请求,我们都可以捕获该请求的一系列跟踪数据,下图展示了基于 TraceId 和 SpanId 所构建的一次服务调用的完整链路。

服务调用链路跟踪是分布式系统的基础需求之一,业界关于分布式链路跟踪也有统一的规范以及代表性的实现框架。

通用技术组件

在分布式服务构建过程中,也需要引入一组通用型的技术组件,这些技术组件在多个场景中(不仅限于分布式系统)都能发挥作用。
这里有五种通用技术组件,包括动态代理、应用缓存、资源管理、框架集成以及架构模式。
这种技术组件有些关注于具体某一个技术实现要点,有些则关注于框架的应用以及架构设计的方法和实践。

第一个,动态代理。 在日常开发过程中,动态代理可以说是一种通用性非常高的实现机制,它是面向切面编程的基础,也在主流的分布式服务开源框架中得到了广泛的应用。
通过代理机制,一个对象就可以在承接另一个对象功能的同时添加新的功能。
相比直接在原有对象中嵌入代码,代理机制为我们提供了更为优雅的解决方案。

第二个,应用缓存。 对于分布式服务而言,缓存应用非常广泛,开发人员可以使用位于应用程序内部的本地缓存,也可以使用位于独立服务器上的分布式缓存。
在日常开发中,缓存的应用通常都是分层级的,我们会综合使用多级缓存来提高目标对象访问的效率和性能。

第三个,资源管理。 相信你对线程池、数据库连接池等技术并不陌生。
这里的池(Pool)是一种对资源的抽象方法,代表一组可以随时使用的资源,但这些资源的创建和释放过程则基于一定的管理策略。
资源池的应用非常广泛,存在多种具体的池化组件。

第四个,框架集成。 这里所说的框架集成,指的是 Dubbo、MyBatis、Spring Cloud 等主流的分布式开发框架与 Spring 框架之间的集成。
我们可以基于命名空间以及自定义 starter 等机制完成与 Spring 之间的有效集成。
理解框架集成的实现过程有利于掌握主流的分布式服务框架的运行原理。

第五个,架构模式。 架构模式描述某一特定应用领域中系统组织和表现的惯用方式。
对软件体系架构模式的研究和实践促进了对软件设计的重用。
在分布式系统开发过程中,也大量应用了诸如微内核架构、管道-过滤器架构等架构模式,这些模式能够为开发人员提供具有高度扩展性的技术组件。