随着以Dubbo、Spring Cloud等框架为代表的分布式服务调用和治理工具的大行其道,以及以Docker、Kubernetes等容器技术的日渐成熟,微服务架构(Microservices Architecture)毫无疑问是近年来最热门的一种服务化架构模式。所谓微服务,就是一些具有足够小的粒度、能够相互协作且自治的服务体系。正因为每个微服务都比较简单,仅关注于完成一个业务功能,所以具备技术、业务和组织上的优势[1]。
另一方面,随着Spring 5的正式发布,我们引来了响应式编程(Reactive Programming)的全新发展时期。Spring 5中内嵌了响应式Web框架、响应式数据访问、响应式消息通信等多种响应式组件,从而极大地简化了响应式应用程序的开发过程和难度。
本章作为全书的开篇,将对微服务架构和响应式系统(Reactive System)的核心概念做简要介绍,同时给出两者之间的整合点,即如何构建响应式微服务架构。在本章最后,我们也会给出全书的组织架构以总领全书。
响应式系统核心概念
在本节中,我们将带领大家进入响应式系统的世界。为了让大家更好地理解响应式编程和响应式系统的核心概念,我们将先从传统编程方法出发逐步引出响应式编程方法。同时,我们还将通过响应式宣言(Reactive Manifesto)了解响应式系统的基本特性和设计理念。
从传统编程方法到响应式编程方法
在电商系统中,订单查询是一个典型的业务场景。用户可以通过多种维度获取自己已下订单的列表信息和各个订单的明细信息。我们就通过订单查询这一特定场景来分析传统编程方法和响应式编程方法之间的区别。
1.订单查询场景的传统方法
在典型的三层架构中,图1-1展示了基于传统实现方法的订单查询场景时序图。一般用户会使用前端组件所提供的操作入口进行订单查询,然后该操作入口会调用后台系统的服务层,服务层再调用数据访问层,进而访问数据库,数据从数据库中获取之后逐层返回,最后显示在包括前端服务或用户操作界面在内的前端组件上。
图1-1订单查询场景的传统实现方法时序图
显然,在图1-1所展示的整个过程中,前端组件通过主动拉取的方式从数据库中获取数据。如果用户不触发前端操作,那么就无法获取数据库中的数据状态。也就是说,前端组件对数据库中的任何数据变更一无所知。
2.订单查询场景的响应式方法
主动拉取数据的方式在某些场景下可以运作得很好,但如果我们希望数据库中的数据一有变化就通知到前端组件,这种方式就不是很合理。这种场景下,我们希望前端组件通过注册机制获取数据变更的事件,图1-2展示了这一过程。
在图1-2中,我们并不是直接访问数据库来获取数据,而是订阅了OrderChangedEvent事件。当订单数据发生任何变化时,系统就会生成这一事件,然后通过一定的方式传播出来。而订阅了该事件的服务就会捕获该事件,从而通过前端组件响应该事件。事件处理的基本步骤涉及对某个特定事件进行订阅,然后等待事件的发生。如果不需要再对该事件做出响应,我们就可以取消对事件的订阅。
图1-2订单查询场景的响应式实现方法时序图
图1-2体现的是响应式系统中一种变化传递(Propagation Of Change)思想,即当数据变化之后,会像多米诺骨牌一样,导致直接和间接引用它的其他数据均发生相应变化。一般而言,生产者只负责生成并发出事件,然后消费者来监听并负责定义如何处理事件的变化传递方式。
显然,这些事件连起来会形成一串数据流(Data Stream),如果我们能够及时对数据流的每一个事件做出响应,就会有效提高系统的响应能力。基于数据流是响应式系统的另一个核心特点。
我们再次回到图1-1,如果从底层数据库驱动,经过数据访问层到服务层,最后到前端组件的这个服务访问链路全部都采用响应式的编程方式,从而搭建一条能够传递变化的管道,这样一旦数据库中的数据有更新,系统的前端组件上就能相应地发生变化。而且,当这种变化发生时,我们不需要通过各种传统调用方式来传递这种变化,而是由搭建好的数据流自动进行传递。
3.传统方法与响应式方法的对比
图1-1展示的传统方法和图1-2展示的响应式方法具有明显的差异性,我们分别从处理过程、线程管理和伸缩性角度做简要对比。
(1)处理过程
传统开发方式下,我们拉取(Pull)数据的变化,这意味着整个过程是一种间歇性、互不相关的处理过程。前端组件不关心数据库中的数据是否有变化。
在响应式开发方式下,一旦对事件进行注册,处理过程只有在数据变化时才会被触发,类似一种推(Push)的工作方式。
(2)线程管理
在传统开发方式下,线程的生命周期比较长。在线程存活的状态下,该线程所使用的资源都会被锁住。当服务器在同时处理多个线程时,就会存在资源的竞争问题。
在响应式开发方式下,生成事件和消费事件的线程的存活时间都很短,所以资源之间存在较少的竞争关系。
(3)伸缩性
传统开发方式下,系统伸缩性涉及数据库和应用服务器的伸缩,一般我们需要专门采用一些服务器架构和资源来应对伸缩性需求。
在响应式开发方式下,因为线程的生命周期很短,同样的基础设施可以处理更多的用户请求。同时,响应式开发方式同样支持传统开发方式下的各种伸缩性实现机制,并提供了更多的分布式实现选择。图1-3展示了事件处理与系统伸缩性之间的关系。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mcct5KeY-1655541704294)(https://upload-images.jianshu.io/upload_images/27867710-6144b92c1660b6c1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
图1-3 事件处理与系统伸缩性示意图
在图1-3中,显然Web应用程序和事件处理程序可以分别进行伸缩,这为伸缩性实现机制提供更多的选型余地。
响应式宣言与响应式系统
如同业界的其他宣言一样,响应式宣言是一组设计原则,符合这些原则的系统可以认为是响应式系统。同时,响应式宣言也是一种架构风格,是一种关于分布式环境下系统设计的思考方式,响应式系统也是具备这一架构风格的系统。
1.响应式系统特性
响应式宣言给出了响应式系统所应该具备的特性,包括即时响应性(Responsive)、回弹性(Resilient)、弹性(Elastic)以及消息驱动(Message Driven)。具备这些特性的系统可以称为响应式系统。图1-4给出了响应式宣言的图形化描述。
图1-4响应式宣言(来自响应式宣言官网)
在图1-4中,响应式宣言认为,响应式系统的价值在于提供了即时响应性、可维护(Maintainable)和扩展性(E意味着可以快速地检测到问题并且有效地对其进行处理。即时响应的系统专注于提供快速而一致的响应时间,确立可靠的反馈上限,以提供一致的服务质量。这种一致的行为转而将简化错误处理、建立最终用户的信任,并促使用户与系统做进一步互动。
(2)回弹性
回弹性指的是系统在出现失败时依然保持即时响应性。这不仅适用于高可用的、任务关键型系统——任何不具备回弹性的系统都将会在发生失败之后丢失即时响应性。回弹性是通过复制、遏制、隔离以及委托来实现的。失败的扩散被遏制在了每个组件内部,与其他组件相互隔离,从而确保系统某部分的失败不会危及整个系统,并能独立恢复。每个组件的恢复都被委托给了另一个内部或外部组件。此外,在必要时可以通过复制来保证高可用性。因此,组件的客户端不再承担组件失败的处理。
(3)弹性
弹性指的是系统在不断变化的工作负载之下依然保持即时响应性。响应式系统可以对输入的速率变化做出反应,比如,通过增加或者减少被分配用于服务这些输入的资源。这意味着设计上并没有竞争点和中央瓶颈,系统得以进行组件的分片或者复制,并在它们之间分布输入。通过提供相关的实时性能指标,响应式系统能支持预测式以及响应式的伸缩算法。这些系统可以在常规的硬件以及软件平台上实现高效的弹性。
(4)消息驱动
消息驱动指的是响应式系统依赖异步的消息传递,从而确保松耦合、隔离、位置透明的组件之间有着明确边界。这一边界还提供了将失败作为消息委托出去的手段。使用显式的消息传递,可以通过在系统中塑造并监视消息流队列