1.3 分布式系统开发技术组件
在分布式系统开发过程中,开发人员需要应用一组技术组件。在本书中,我们把这些技术组件分成3大类,即远程过程调用组件、微服务构建组件和通用技术组件,如图1-5所示。
图1-5 分布式系统开发技术组件一览
接下来,我们将对图1-5中的每一个技术组件的核心知识展开讨论,从而为后续内容的学习打好基础。
1.3.1 远程过程调用组件
远程过程调用是分布式系统最根本的实现技术,我们将从网络通信、远程调用、负载均衡、服务容错以及服务降级等5个维度分别进行讨论。
1.网络通信
网络通信是一切分布式操作的基础。我们以TCP协议的三次握手过程为例来讨论客户端和服务器端之间的请求处理过程,如图1-6所示。
图1-6 TCP协议的三次握手
可以看到,当客户端向服务器端发起连接请求之后,服务器端会接收该请求,然后双方就可以建立连接。为了响应来自客户端的请求,服务器端会完成一次读写过程,这时候双方都可以发起关闭操作。而如果双方都不发起关闭操作,那么客户端和服务器端之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。这样,当网络连接建立之后,通信的双方就可以连续发送多个数据包,从而节省资源并降低时延。
在网络通信的实际场景中,整个过程实现起来并没有像前面描述的那么简单。我们需要考虑网络通信的性能、可靠性以及数据传输的方式,这就涉及I/O模型、可靠性设计以及序列化方式等一系列技术主题。
2.远程调用
远程调用解决的问题是发布以及引用远程服务。其中服务发布的目的是暴露服务访问入口,这是一个构建网络连接并启动监听端口的过程。而服务引用的目的是完成对远程服务的调用,是一个向目标服务端口发起请求并获取响应结果的过程,如图1-7所示。
图1-7 服务发布和服务引用流程示意图
在远程调用过程中,我们需要考虑透明化的远程方法调用实现机制,确保开发人员能够像调用本地方法一样调用远程方法。同时,我们也需要考虑同步调用、异步调用以及同步转异步调用等一系列具体的调用实现策略。
3.负载均衡
所谓负载均衡,简单来讲就是将请求分摊到多个操作单元上执行。负载均衡建立在现有网络结构之上,提供了一种廉价、有效、透明的方法来加强对网络数据的处理能力,以及提高网络结构的灵活性。负载均衡的基本结构如图1-8所示。
图1-8 负载均衡示意图
在实现上,负载均衡可以使用硬件、软件或者两者均使用。而针对软件负载均衡的实现方式,也可以分成服务器端负载均衡和客户端负载均衡两大类。在分布式系统构建过程中,我们主要讨论的对象是客户端负载均衡机制。所谓客户端负载均衡,就是在客户端内部保存着各个服务实例信息,而客户端自己也会设定一个调度算法。通过这种实现方式,客户端在向服务器发起请求时,通过执行调度算法就可以计算出目标服务实例的地址。客户端负载均衡应用广泛,例如目前主流的微服务架构实现框架Spring Cloud、Dubbo等都内置了完整的客户端负载均衡机制,而老牌的分布式缓存Memcache同样也是这一负载均衡策略的典型应用框架。
无论是使用何种负载均衡,对请求的分发策略决定了负载均衡执行的结果。在软件类负载均衡中,分发策略的实现形式为一组调度算法,通常称为负载均衡算法。负载均衡算法同样可以分成两大类,即静态负载均衡算法和动态负载均衡算法,两者之间的区别在于后者依赖服务的运行时状态,这些状态信息通常包括服务在过去一段时间内的平均调用时延和所承接的连接数等。
4.服务容错
在分布式环境中,服务访问出错了该怎么办?这就是服务容错机制要解决的问题。
和单块系统相比,分布式系统中的各个服务会独立部署,而且服务与服务之间存在相互依赖关系,这就导致服务访问失败的原因和场景非常复杂。这时候,需要我们从服务可靠性的角度出发,对服务自身以及服务与服务之间的交互过程进行合理设计。服务可用是分布式架构的关键要素之一。为了确保服务可用,我们需要引入容错的思想和机制。
服务容错同时涉及服务的提供者和消费者。对服务提供者而言,要做的事情比较简单。一旦服务自身发生错误,那么应该快速返回合理的处理结果,也就是要做到快速失败。而对于服务消费者而言,一方面,我们可以采用超时和重试等常见的操作方法;另一方面,业界也存在一些专门针对消费者的容错机制来应对服务请求失败的场景。
消费者容错机制是我们讨论的重点。在客户端负载均衡基础上,我们可以在服务调用的过程中添加消费者容错机制,这些机制包括服务隔离、服务熔断和服务回退。图1-9展示了添加消费者容错机制后的系统架构。
图1-9 添加服务消费者容错机制后的系统架构图
5.服务降级
所谓服务降级,指的是在服务器压力剧增的情况下,根据业务的重要性及当前流量对一些服务进行有策略的快速失败处理,从而避免非核心的服务调用所消耗的资源影响核心服务的正常运行。
从概念上讲,分布式系统中的任何一个服务都是可以分等级的。具体的服务分级方法因业务场景和需求而异。一旦我们实现了对服务的合理分级,就可以对那些处于业务链路最外围、等级最低的服务执行降级操作。
在服务分级上,一种常见的方法是将服务分成3个不同的等级,如图1-10所示。
图1-10 服务三级分类示意图
在图1-10中,一级服务属于核心服务,需要确保高可用,不能执行降级操作;二级服务多采用异步方式进行系统交互,容忍暂时性的数据不一致性;而三级服务则可以随时降级。
1.3.2 微服务构建组件
在远程过程调用各个组件的基础上,本节将进一步讨论微服务构建组件,包括注册中心、服务网关、配置中心和消息通信。
1.注册中心
当一个分布式系统中存在大量服务时,服务之间的相互调用关系就会变得非常复杂,那么如何高效找到目标服务呢?这时候就需要引入注册中心。注册中心是微服务系统构建过程中的核心组件。
在微服务架构中,服务与服务之间通过远程调用完成业务链路的构建。而在调用服务之前,我们首先需要发现服务,即解决在分布式集群环境下如何找到目标服务实例这一问题。服务发现和调用构成了服务交互的基础,整体流程如图1-11所示,其中实线部分代表服务调用流程,而虚线部分则包含了服务的注册和发现过程。
图1-11 服务发现和调用流程图
在图1-11中存在三个服务,即服务A、服务B和服务C,每个服务都以多实例的方式部署在集群环境中。显然,服务B和服务C对于服务A而言是服务的提供者,而作为消费者的服务A需要通过负载均衡器分别找到服务B和服务C的实例并完成远程调用。另外,这三个服务都需要注册到注册中心,以便负载均衡器能够从注册中心中获取它们的服务实例信息。
2.服务网关
在微服务架构中,API网关或服务网关的出现有其必然性。通常,微服务提供的API粒度与客户端的请求粒度不一定完全匹配。相较于客户端请求,微服务提供的API粒度一般都会更加细化,这意味着客户端需要与多个服务进行交互才能完成某一个业务功能。更为重要的是,网关能够起到客户端与微服务之间的隔离作用。随着业务需求的变化和时间的推移,网关背后的各个微服务的划分和实现可能需要做相应的调整和升级,而通过网关就可以确保这种调整和升级对客户端透明。
另外,在服务网关中,1.3.1节介绍的负载均衡机制仍然是有用的,但是把负载均衡器直接放置在系统中所有服务的前面往往不是一个很好的解决方案,因为这会导致负载均衡器本身成为系统在运行时的瓶颈,更好的解决方案是在负载均衡器的前面再添加一层网关。
根据需要,我们可以在服务提供者和消费者之间架设这层服务网关,从而满足上文中提到的各种需求。在负载均衡器的基础上,添加了服务网关之后的系统架构如图1-12所示。当然,并不是所有的服务调用链路都需要添加这层网关。我们也可以根据具体场景直接通过负载均衡器进行服务调用。在实际应用过程中,这种混合式的服务调用管理方式也是一种常见的做法。
与负载均衡器一样,前置的服务网关同样可能面临成为系统瓶颈的风险,所以服务网关本身也应该具备高度可伸缩性。在设计上,首要的一点是确保网关本身的无状态性,即网关不应该保存任何与服务调用相关的状态数据。另外,网关本身所承载的处理逻辑应该是非常轻量级的,不应该把复杂的业务操作放在网关中。
图1-12 添加服务网关之后的系统架构图
3.配置中心
我们接下来要讨论的问题是如何统一管理多个服务中的配置信息。在单块系统中,配置文件比较少,更新频率也不会太高,所以我们倾向于把所有配置信息跟源代码一起放在代码仓库中。但随着技术架构的演变,分布式系统对配置信息的管理方式提出越来越高的要求。到了微服务架构,面对爆发式增长的应用数量和服务器数量,传统的配置信息管理方式就显得无能为力。为此,在微服务架构中,我们需要引入配置中心的设计思想和相关工具。
在前面各个技术组件的基础上,图1-13展示了添加配置中心之后的系统架构图。微服务系统中的各个服务都可能会接入配置中心,从而完成配置信息的统一管理。
图1-13 添加配置中心之后的系统架构图
配置中心是微服务架构中的一个基础组件,而业界关于如何实现配置中心也有一些基本的模型和工具。
4.消息通信
我们接下来将讨论事件驱动架构(Event-Driven Architecture,EDA)及其在微服务架构中的应用。事件驱动架构是一种设计和实现应用系统的架构风格,在这个架构风格中,事件可以以松耦合的方式在服务和服务之间进行传输。而在具体技术实现上,我们可以把事件处理成一种消息。
在微服务设计和开发过程中通常会存在这样的场景,即系统中的某个服务因为用户操作或内部行为发布了一个消息,该服务知道这个消息会在将来的某一个时间点被其他服务所消费,但是并不知道这个服务具体是谁、也不关心什么时候被消费。同样,消费该消息的服务也不一定需要知道该消息是由哪个服务所发布的。针对以上场景,我们可以引入消息通信机制来构建松耦合的事件驱动架构。
基于消息通信的服务交互过程见图1-14,包括消息的发布、订阅和消费等基本过程。
图1-14 基于消息通信的服务交互过程
当微服务系统中的某一个服务发布消息时,该服务可以广播一个或多个消息到消息中心,每一个对该消息感兴趣的服务都可以对其进行订阅。每当消息被分发时,系统将通知那些已经订阅了该消息的服务,而这些服务中的消息处理程序将被自动触发。每个消息订阅者自身都可以有一套独立的消息处理程序。
1.3.3 通用技术组件
在分布式系统构建过程中,也需要引入一组通用型的技术组件,这些技术组件在多个应用场景中都能发挥作用。本书梳理了5种通用技术组件,包括动态代理、应用缓存、资源管理、框架集成以及架构模式。在这组技术组件中,有些关注某一个具体的技术要点,有些则关注框架的应用以及架构设计的方法和实践。
1.动态代理
在日常开发过程中,代理机制可以说是一种通用性非常高的实现机制,它是面向切面编程的基础。在系统设计过程中,对象之间相互依赖会造成耦合度过高,我们需要引入一个中间类来消除或缓解直接访问目标对象带来的问题。但是对于发起访问的对象而言,它通常希望这个中间类的存在是无感知的,这时候我们就可以引入动态代理机制。
代理机制是在访问目标对象之前添加一层代理对象来达到上述目的。代理机制中存在3种不同的角色,即抽象角色、代理角色和真实角色,如图1-15所示。
其中,抽象角色通过接口或抽象类声明真实角色实现的业务方法;真实角色实现抽象角色,定义具体的业务逻辑,供代理角色调用;而代理角色则实现抽象角色,代理真实角色,并附加自己的操作。代理实际上是一种通用的实现机制,其设计理念和实现过程无处不在。
图1-15 代理机制中的3种角色
2.应用缓存
对于互联网应用而言,缓存可以说是一种基础技术组件。缓存的应用非常广泛,开发人员可以使用位于应用程序内部的本地缓存,也可以使用位于独立服务器上的分布式缓存。在日常开发过程中,缓存的应用通常都是分层级的,我们会综合使用多级缓存来提高访问目标对象的效率和性能。
分析缓存的实现技术,我们会发现可以抽象出通用的缓存结构,图1-16就是一种常见的缓存结构表现形式。
图1-16 缓存基本结构
在图1-16中,我们用Key-Value对来表示缓存数据,然后对Key施加一定的算法获取其HashCode值,再根据该HashCode对应的索引找到Value在内存中的位置,进而获取该Value值。
尽管各类缓存实现工具支持的数据结构以及数据在内存中的分配和查找方式不尽相同,但缓存的基本结构都与图1-16类似。从该图中,我们也认识到缓存本质上是一种用空间交换时间的实现方法。
3.资源管理
资源在软件开发过程中有很多表现形式,如数据库会话、网络连接、分布式服务和组件等,这些都可以认为是系统的资源,都需要进行管理。高性能、可伸缩、灵活则是资源管理的基本需求。资源管理包含一组模式,涵盖了资源从创建到销毁的整个过程,被认为是实现资源管理基本需求的有效方式,本书主要讨论其中的资源池模式。
说到资源池,相信你对线程池、数据库连接池、JDBC(Java数据库连接)等技术并不陌生。这里的“池”是对资源的一种抽象方法,资源池代表一组可以随时使用的资源,但这些资源的创建和释放过程则基于一定的管理策略来实现。虽然线程池、数据库连接池等技术组件的表现形式和使用方法有所不同,但其基本原理都是一致的。资源池的主要特点是节省了创建资源实例的开销,但其占用的存储空间会随着对象的增多而增大。
4.框架集成
这里所说的框架集成,指的是Dubbo、MyBatis、Spring Cloud等主流的分布式开发框架与Spring框架之间的集成。
到目前为止,Spring家族为我们提供了两种框架集成机制,一种是自定义标签机制,即通过扩展XML Schema来实现与Spring的集成;而另一种则是通过编写自定义的starter组件来完成与Spring Boot的集成。基于这些框架集成机制,开发人员无论采用哪一款Spring框架,对于使用Dubbo、Spring Cloud等开发框架而言都是零学习成本。
另外,Spring是一个容器框架,围绕注入该容器中的各个JavaBean的生命周期,开发人员也可以在InitializingBean、Aware和ApplicationListener等扩展点中嵌入处理逻辑,从而完成更为定制化的框架集成过程。
5.架构模式
软件开发过程包括设计并实现一系列技术组件,考虑的是这些组件的外部可见特性及其相互关系,本质是一种抽象工作。虽然该抽象工作的切入点很多,但其本身同样具备层次性,在不同层次上的抽象结果往往用于实现不同的目标。
架构模式被用来描述在某一特定应用领域中系统组织和表现的惯用方式。对软件架构模式的研究和实践完善了软件设计的抽象过程,一些经过实践考验的架构模式也可以可靠地解决新的问题。在分布式系统开发过程中,大量应用了诸如微内核架构、管道-过滤器架构等架构模式。