简介
预备几个问题
- 为什么消息驱动是 反应式架构的底层实现?
书的整体思路,反应式系统的理念体现在 这两个步骤的各个方面
- 如何划分模块?
- 模块之间以异步消息流交互。 一个重要原则是:减少系统中发生交换的消息数量。
CompletableFuture原理与实践-外卖商家端API的异步化
- 可组合:可以将多个依赖操作通过不同的方式进行编排,例如CompletableFuture提供thenCompose、thenCombine等各种then开头的方法,这些方法就是对“可组合”特性的支持。
- 操作融合:将数据流中使用的多个操作符以某种方式结合起来,进而降低开销(时间、内存)。
- 延迟执行:操作不会立即执行,当收到明确指示时操作才会触发。例如Reactor只有当有订阅者订阅时,才会触发操作。
- 回压:某些异步阶段的处理速度跟不上,直接失败会导致大量数据的丢失,对业务来说是不能接受的,这时需要反馈上游生产者降低调用量。
为什么需要反应式
我们的初衷是构建一个对用户即时响应的系统,无论什么情况下都能响应用户的输入。由于任何单台计算机在任何时刻都可能宕机,因此我们需要将系统分布到多台计算机上。引入分布式结构这个额外的基础需求使我们意识到:构建这样的系统需要新的架构模式(或者重新发现旧模式)。过去我们建立了各种方法来维持某种表象:单线程的本地运算能够魔法般的扩展运行在多个处理器核心或网络节点上(PS:比如hadoop,写mapreduce就像在写单机程序一样)。然而虚实之间的沟壑已经大到难以为继。解决方法是让我们应用程序中所具有的分布和并发的本质明明白白的反映到编程模型上来,并使其变成我们的优势。
与很多成功想法一样,反应式的多项原则被过度使用和重新诠释。这不纯粹是坏事;思想需要不断进化,从而保持相关性。但这也会引起混淆,导致原始意图被淡化。例如一种不幸的流行误解是:反应式编程无非就是使用回调或面向流的组合 进行异步和非阻塞的编程。PS: 我们学习很多技术,都误解了初衷, 进而迷惑在细节中,价值结构图要多画一画。
反应式宣言解读
对用户做出反应
传统方法,本质上 一个用户请求的处理是单线程 接力完成。或者说,或许经过多个线程,但某个时刻只有单个线程在处理请求。对用户做出反应,用户请求体现在连接数,干活的是线程,性能调优就是连接数与线程数的纠葛。
性能调优 | |
---|---|
连接数>线程数 | 无意义 |
连接数=线程数 | 新的请求等待 |
连接池<线程数 | 服务能力不足 |
顺序执行模型,调用方、被调用方、调用方对结果的处理在同一个线程上同步进行,紧密耦合,所以失败会以同样的方式影响两者。
并行执行通常需要额外的思考和库的支持
- 任务并行执行, 调用方等着耗时最长的任务结束再进行下一步处理
- 进阶——异步结果组合。难道通过并行来降低响应延迟还不够吗?假设一个任务由ABC三个子任务组成,ABC均返回Future,任务可能是一个更高层级的子任务,也理应返回Future。并行执行必须与异步的、面向任务的结果聚合配合使用。
阻塞API ==> 大量线程等待 ==> 除了线程切换的成本,内核调度程序很难找出哪些线程是可运行的,哪些线程是正在等待的,也很难选择其中一个线程,使得每个线程都能获得公平的CPU时间片。操作系统内核负责线程切换,却难以业务友好的进行切换 ==> 使用消息作为沟通媒介(正是内核的运行机制——中断),不是挂起线程,只是挂起计算。
在并行计算中,一个使用多个处理器的程序的速度提升受限于程序中顺序执行部分的占比。PS:消息传递天然的可以提高程序的并行度。
挂起线程 vs 挂起计算:不管是同步还是异步,只要被调用方处理能力不足、网络通信存在耗时、竞争资源有限,排队是一定要排队的,只是排队的是线程还是消息罢了(就好比没有邮箱时你要邮局排队等窗口,有了邮箱你信放邮箱里,人等着vs信等着)。线程排队
对失败作出反应
只有一种通用的方法来防止你的系统在部分失败时牵连整个系统:distribute(不要把鸡蛋放在一个篮子里) 和 compartmentalize(保护你的篮子,避免它们互相影响)
回弹和容错
- 容错,比如备份儿,坏了一个还有另一个
- 回弹,回弹性更多的谈论出错的事情的性质, 比如断路器。PS:”弹“指的是一次断路器断开、闭合无需人工干预,系统具有根据一定策略自适应能力。
反应式设计模式
提升抽象水平已被证明是提高程序猿生产效率的最有效措施。复杂性有两种,以一个容器平台为例
- 固有的复杂性,是问题领域所固有的。配置cpu/mem/健康检查接口是 pass领域固有的,服务运行在物理机上也要配相关参数。
- 附带的复杂性,仅由解决方案所引入的。docker命令和Dockerfile 是docker 这个解决方案本身引入的。
一个适当的解决方案是暴露问题领域中的所有固有复杂性, 使其可以根据具体的使用情况进行处理,并且避免了由于所选择的抽象和底层机制之间的不匹配而导致的附加复杂性给用户带来的负担(系统设计的精辟之言)。 这意味着,随着你对问题领域理解的不断深化评估现有的抽象,考虑它们是否抓住了固有复杂性以及又增加了多少附带复杂性。
如何划分模块
以邮箱系统为例, 开始这样一个项目,首要任务是描绘出部署的架构图,并草拟出需要开发的软件模块清单。这也许不会是最终架构,但是你需要描绘问题空间,并探寻潜在的难点。层次化拆分的结果是一套层次分明的组件,每个组件就其功能而言可能是复杂的,例如搜索算法的实现。也可能其部署和编排过程是复杂的,例如为几十亿用户提供邮件存储。但在描述组件的个体职责时,总是应该保持简洁。
将一个庞大的任务分解为多个小任务,并在每个子任务上重复这个过程,直至我们可以得到相互协作的模块, 这些模块间的协作支撑起整个应用程序。子模块负责其父模块的一部分功能,而外部功能则只通过引用的方式引入。
模块和其直接的后裔模块之间不仅仅是依赖关系,父模块还定义了子模块的职责范围,定义了要解决问题空间的边界。在层级结构的底部是具体的实现细节,沿着层级越往上,组件变得越抽象,越接近你想要实现的逻辑上的概括性功能,越可能仅特定于具体用例。
消息流设计:对于由任意后端服务之间无限制的交互所产生的混乱问题,解决方法是专注于整个应用内的通信链路,并专门设计它们。
消息传递 vs 方法调用
在最底层,计算机之间的交互是以消息形式进行的,消息传递是任何种类的独立对象之间最天然的通信形式(解耦协作对象)。
消息传递 的对立面是方法调用,方法调用 使得调用方和被调用方是紧耦合的,体现在
- 方法调用不管是同步还是异步,调用方都要按要求输入参数处理返回结果,有时需处理异常,必要时还得将同步方法异步化(如果不想被“被调用方”阻塞住的话)
- 通信是同步的,接收方对如何及何时处理请求没有发言权,如果被调用方过载,就只能把调用方“阻塞”住
基于事件的系统通常建立在一个事件循环上。任何时刻只要发生了事情, 对应的事件就会被追加到一个队列中。事件循环持续的从队列中拉取事件,并执行绑定在事件上的回调函数。每一个回调函数通常都是一段微小的、匿名的、响应特定事件(例如鼠标点击)的过程。回调函数也可能产生新事件,这些事件随后也会被追加到队列里面等待处理。
基于消息的系统,不管是生产者还是消息系统本身都不需要考虑如何对消息产生正确的响应,由当前所配置的消费者来做这个决定
因为消息传递解耦了调用方和被调用方,使之转变为发送者和接收者,这种变化带来了垂直伸缩性,因为接收者现在可以自由的使用不同于发送者的处理资源,两者不在同一个调用栈上执行。位置透明性(无论接收者在哪里处理消息,发送消息的源代码看起来都一样)为消息传递添加了水平伸缩性,你可以通过向网络中添加更多机器来提高性能
在类C语言中常见的直接方法调用(direct method invocation),自身带有一种特定的流量控制:请求的发送者被接收者阻塞,直到处理过程结束。当多个发送者竞争同一个接收者的资源时,通常利用锁或信号量等同步方式,来串行化处理过程。 这种处理额外的阻塞了每个发送者,使其必须等待,直到前一个发送者的消息被处理完毕。这种隐式的回压一开始看起来好像很方便,但是当系统增长以及非功能性(吞吐量等)要求变得越来越重要时,它就会变成一种阻碍。你会发现自己花费很多时间调试性能瓶颈,而不是实现业务逻辑。(PS:以消息传递的视角来描述并发问题,很陶醉)消息传递为流量控制方案提供了更大的选择范围,因为它引入了队列概念(其实就是锁、信号量队列的显式化)。
从本质上讲,消息传递把普通的面向对象语言中隐含的流量控制机制分拆出来,并允许定制化的解决方案。只是这种选择并非没有成本,比如你必须选择在哪种粒度上应用消息传递,以及这种粒度下消息传递和直接方法调用哪个更好。举个例子,假设有一个拥有异步接口的面向服务架构,服务本身可能以传统同步方式实现,而服务之间的通信则通过消息传递执行。
消息驱动的业务原因
- 网站时代,两层架构:业务逻辑和页面展示混写;每个用户看到的都是一样的信息。
- 社交时代,社交对大数据处理有着天然的需求
- 中间件时代,社交网站最大的特点就是频繁的用户搜索、推荐,当用户上亿的时候,这就是前面传统的两层架构无法处理的问题了。
- 云和大数据时代,大数据的实时处理、数据挖掘、推荐系统、Docker化,包括A/B测试,这些都是很多企业还正在努力全面解决的问题。
- AI时代
PS:另一种划分方式是PC时代、互联网时代、移动互联网时代、人工智能时代。
对数据进行实时(近实时)处理的需求带动了后端体系的大发展,Kafka/Spark等等流处理大行其道。这时候的后端体系就渐渐引入了消息驱动的模式,所谓消息驱动,就是对新的生产数据会有多个消费者,有的是满足实时计算的需求(比如司机信息需要立刻能够被快速检索到,又不能每次都做全量indexing,就需要用到Spark),有的只是为了数据分析,写入类似Cassandra这些数据库里,还有的可能是为了生成定时报表,写入到MySQL。
无论如何,数据,数据的跟踪Tracking,数据的流向,是现代后台系统的核心问题,只有Dataflow和Data Pipeline清晰了,整个后台架构才会清楚。
其它
对用户做出反应 ==> 并行 ==> 复制(消息传递)替掉共享,无副作用 ==> 函数式编程
全面异步化:淘宝反应式架构升级探索 消息驱动强调无阻塞、无 callback,所以不会有线程挂在那里,不会有持续的资源消耗。同时,事件驱动或消息驱动都是异步化,而异步化会将操作系统中的队列情况显式地提升到了应用层,使得应用层可以显式根据队列的情况来进行压力负载的感知
操作系统的内存分配、进程/线程调度、队列等 显式到 应用层,看起来是一个趋势。