响应式编程与响应式系统
在恒久的迷惑与过多期待的海洋中,登上一组简单响应式设计原则的小岛。
下载 Konrad Malawski 的免费电子书《为什么选择响应式?企业应用中的基本原则》,深入了解更多响应式技术的知识与好处。
自从 2013 年一起合作写了《响应式宣言》之后,我们看着响应式从一种几乎无人知晓的软件构建技术——当时只有少数几个公司的边缘项目使用了这一技术——最后成为 中间件领域 大佬们全平台战略中的一部分。本文旨在定义和澄清响应式各个方面的概念,方法是比较在响应式编程风格下和把响应式系统视作一个紧密整体的设计方法下编写代码的不同之处。
响应式是一组设计原则
响应式技术目前成功的标志之一是“ 响应式 ”成为了一个热词,并且跟一些不同的事物与人联系在了一起——常常伴随着像“ 流 ”、“ 轻量级 ”和“ 实时 ”这样的词。
举个例子:当我们看到一支运动队时(像棒球队或者篮球队),我们一般会把他们看成一个个单独个体的组合,但是当他们之间碰撞不出火花,无法像一个团队一样高效地协作时,他们就会输给一个“更差劲”的队伍。从这篇文章的角度来看,响应式是一组设计原则,一种关于系统架构与设计的思考方式,一种关于在一个分布式环境下,当 实现技术 、工具和设计模式都只是一个更大系统的一部分时如何设计的思考方式。
这个例子展示了不经考虑地将一堆软件拼揍在一起——尽管单独来看,这些软件都很优秀——和响应式系统之间的不同。在一个响应式系统中,正是不同 组件
一个响应式系统 是一种 架构风格
以响应式的风格(或者说,通过响应式编程)写一个软件是可能的;然而,那也不过是拼图中的一块罢了。虽然在上面的提到的各个方面似乎都足以称其为“响应式的”,但仅就其它们自身而言,还不足以让一个系统成为响应式的。
当人们在软件开发与设计的语境下谈论“响应式”时,他们的意思通常是以下三者之一:
- 响应式系统(架构与设计)
- 响应式编程(基于声明的事件的)
- 函数响应式编程(FRP)
我们将调查这些做法与技术的意思,特别是前两个。更明确地说,我们会在使用它们的时候讨论它们,例如它们是怎么联系在一起的,从它们身上又能到什么样的好处——特别是在为多核、云或移动架构搭建系统的情境下。
让我们先来说一说函数响应式编程吧,以及我们在本文后面不再讨论它的原因。
函数响应式编程(FRP)
函数响应式编程 ,通常被称作 FRP,是最常被误解的。FRP 在二十年前就被 Conal Elliott 精确地定义过了了。但是最近这个术语却被错误地 脚注1 用来描述一些像 Elm、Bacon.js 的技术以及其它技术中的响应式插件(RxJava、Rx.NET、 RxJS)。许多的 库
响应式编程
响应式编程 ,不要把它跟函数响应式编程混淆了,它是异步编程下的一个子集,也是一种范式,在这种范式下,由新信息的 有效性
它能够把问题分解为多个独立的步骤,这些独立的步骤可以以异步且 非阻塞
“异步地”
响应式编程一般是 事件驱动
响应式编程库的应用程序接口(API)一般是以下二者之一:
- 基于回调的
( —匿名的 间接作用( 回调函数被绑定在 事件源( 上,当事件被放入 数据流( 中时,回调函数被调用。 - 声明式的
( ——通过函数的组合,通常是使用一些固定的函数,像 map、 filter、 fold 等等。
大部分的库会混合这两种风格,一般还带有 基于流
说响应式编程跟 数据流编程
举几个为这种编程技术提供支持的的编程抽象概念:
- Futures/Promises——一个值的容器,具有 读共享/写独占
( 的语义,即使变量尚不可用也能够添加异步的值转换操作。 - 流
( - 响应式流——无限制的数据处理流,支持异步,非阻塞式,支持多个源与目的的 反压转换管道( 。 - 数据流变量——依赖于输入、 过程
( 或者其它单元的 单赋值变量( (存储单元),它能够自动更新值的改变。其中一个应用例子是表格软件——一个单元的值的改变会像涟漪一样荡开,影响到所有依赖于它的函数,顺流而下地使它们产生新的值。
在 JVM 中,支持响应式编程的流行库有 Akka Streams、Ratpack、Reactor、RxJava 和 Vert.x 等等。这些库实现了响应式编程的规范,成为 JVM 上响应式编程库之间的 互通标准
响应式编程的基本好处是:提高多核和多 CPU 硬件的计算资源利用率;根据阿姆达尔定律以及引申的 Günther 的通用可伸缩性定律
另一个好处是开发者生产效率,传统的编程范式都尽力想提供一个简单直接的可持续的方法来处理异步非阻塞式计算和 I/O。在响应式编程中,因活动(active)组件之间通常不需要明确的协作,从而也就解决了其中大部分的挑战。
响应式编程真正的发光点在于组件的创建跟工作流的组合。为了在异步执行上取得最大的优势,把 反压
尽管如此,响应式编程在搭建现代软件上仍然非常有用,为了在更高层次上 理解
事件驱动 vs. 消息驱动
如上面提到的,响应式编程——专注于短时间的数据流链条上的计算——因此倾向于事件驱动,而响应式系统——关注于通过分布式系统的通信和协作所得到的弹性和韧性——则是消息驱动的 脚注4(或者称之为 消息式
一个拥有 长期存活的可寻址
响应式宣言中的术语表定义了两者之间概念上的不同:
一条消息就是一则被送往一个明确目的地的数据。一个事件则是达到某个给定状态的组件发出的一个信号。在一个消息驱动系统中,可寻址到的接收者等待消息的到来然后响应它,否则保持休眠状态。在一个事件驱动系统中,通知的监听者被绑定到消息源上,这样当消息被发出时它就会被调用。这意味着一个事件驱动系统专注于可寻址的事件源而消息驱动系统专注于可寻址的接收者。
分布式系统需要通过消息在网络上传输进行交流,以实现其沟通基础,与之相反,事件的发出则是本地的。在底层通过发送包裹着事件的消息来搭建跨网络的事件驱动系统的做法很常见。这样能够维持在分布式环境下事件驱动编程模型的相对简易性,并且在某些特殊的和合理的范围内的使用案例上工作得很好。
然而,这是有利有弊的:在编程模型的抽象性和简易性上得一分,在控制上就减一分。消息强迫我们去拥抱分布式系统的真实性和一致性——像 局部错误
这些在语义学和适用性上的不同在应用设计中有着深刻的含义,包括分布式系统的 复杂性
在一个响应式系统中,特别是使用了响应式编程技术的,这样的系统中就即有事件也有消息——一个是用于沟通的强大工具(消息),而另一个则呈现现实(事件)。
响应式系统和架构
响应式系统 —— 如同在《响应式宣言》中定义的那样——是一组用于搭建现代系统——已充分准备好满足如今应用程序所面对的不断增长的需求的现代系统——的架构设计原则。
响应式系统的原则决对不是什么新东西,它可以被追溯到 70 和 80 年代 Jim Gray 和 Pat Helland 在 串级系统
响应式系统的基石是 消息传递
从程序到系统
这个世界的连通性正在变得越来越高。我们不再构建 程序 ——为单个操作子来计算某些东西的端到端逻辑——而更多地在构建 系统 了。
系统从定义上来说是复杂的——每一部分都包含多个组件,每个组件的自身或其子组件也可以是一个系统——这意味着软件要正常工作已经越来越依赖于其它软件。
我们今天构建的系统会在多个计算机上操作,小型的或大型的,或少或多,相近的或远隔半个地球的。同时,由于人们的生活正变得越来越依赖于系统顺畅运行的有效性,用户的期望也变得越得越来越难以满足。
为了实现用户——和企业——能够依赖的系统,这些系统必须是 灵敏的
响应式系统的弹性
弹性是与 错误下 的 灵敏性
因此构建一个弹性的、 自愈
响应式系统的韧性
韧性
系统必须能够在不重写甚至不重新设置的情况下,适应性地——即无需介入自动伸缩——响应状态及行为,沟通负载均衡, 故障转移
如同《响应式宣言》所述:
一个极大地简化问题的关键洞见在于意识到我们都在使用分布式计算。无论我们的操作系统是运行在一个单一结点上(拥有多个独立的 CPU,并通过 QPI 链接进行交流),还是在一个 节点集群
( (独立的机器,通过网络进行交流)上。拥抱这个事实意味着在垂直方向上多核的伸缩与在水平方面上集群的伸缩并无概念上的差异。在空间上的解耦 […],是通过异步消息传送以及运行时实例与其引用解耦从而实现的,这就是我们所说的位置透明性。
因此,不论接收者在哪里,我们都以同样的方式与它交流。唯一能够在语义上等同实现的方式是消息传送。
响应式系统的生产效率
既然大多数的系统生来即是复杂的,那么其中一个最重要的点即是保证一个系统架构在开发和维护组件时,最小程度地减低生产效率,同时将操作的 偶发复杂性
这一点很重要,因为在一个系统的生命周期中——如果系统的设计不正确——系统的维护会变得越来越困难,理解、定位和解决问题所需要花费时间和精力会不断地上涨。
响应式系统是我们所知的最具 生产效率 的系统架构(在多核、云及移动架构的背景下):
- 错误的隔离为组件与组件之间裹上舱壁(LCTT 译注:当船遭到损坏进水时,舱壁能够防止水从损坏的船舱流入其他船舱),防止引发连锁错误,从而限制住错误的波及范围以及严重性。
- 监管者的层级制度提供了多个等级的防护,搭配以自我修复能力,避免了许多曾经在侦查(inverstigate)时引发的操作 代价
( ——大量的 瞬时故障( 。 - 消息传送和位置透明性允许组件被卸载下线、代替或 重新布线
( 同时不影响终端用户的使用体验,并降低中断的代价、它们的相对紧迫性以及诊断和修正所需的资源。 - 复制减少了数据丢失的风险,减轻了数据 检索
( 和存储的有效性错误的影响。 - 韧性允许在使用率波动时保存资源,允许在负载很低时,最小化操作开销,并且允许在负载增加时,最小化 运行中断
( 或 紧急投入( 伸缩性的风险。
因此,响应式系统使 生成系统
响应式编程与响应式系统的关联
响应式编程是一种管理 内部逻辑
只使用响应式编程常遇到的一个问题,是一个事件驱动的基于回调的或声明式的程序中两个计算阶段的 高度耦合
这意味着,它通常在内部处理成功与错误的状态而不会向外界发送相应的信号。这种寻址能力的缺失导致单个 阶段
另一个与响应式系统方法的不同之处在于单纯的响应式编程允许 时间 上的 解耦
位置透明性的缺失使得很难以韧性方式对一个基于适应性响应式编程技术的程序进行向外扩展,因为这样就需要分附加工具,例如 消息总线
对于基于回调的编程,常会被提及的一个问题是写这样的程序或许相对来说会比较简单,但最终会引发一些真正的后果。
例如,对于基于匿名回调的系统,当你想理解它们,维护它们或最重要的是在 生产供应中断
为响应式系统设计的库与平台(例如 Akka 项目和 Erlang 平台)学到了这一点,它们依赖于那些更容易理解的长期存活的可寻址组件。当错误发生时,根据导致错误的消息可以找到唯一的组件。当可寻址的概念存在组件模型的核心中时, 监控方案
一个好的编程范式的选择,一个选择实现像可寻址能力和错误管理这些东西的范式,已经被证明在生产中是无价的,因它在设计中承认了现实并非一帆风顺,接受并拥抱错误的出现 而不是毫无希望地去尝试避免错误。
总而言之,响应式编程是一个非常有用的实现技术,可以用在响应式架构当中。但是记住这只能帮助管理一部分:异步且非阻塞执行下的数据流管理——通常只在单个结点或服务中。当有多个结点时,就需要开始认真地考虑像 数据一致性
因此,要最大化响应式编程的价值,就把它作为构建响应式系统的工具来使用。构建一个响应式系统需要的不仅是在一个已存在的遗留下来的 软件栈
总结
企业和中间件供应商在目睹了应用响应式所带来的企业利润增长后,同样开始拥抱响应式。在本文中,我们把响应式系统做为企业最终目标进行描述——假设了多核、云和移动架构的背景——而响应式编程则从中担任重要工具的角色。
响应式编程在内部逻辑及数据流转换的组件层次上为开发者提高了生产率——通过性能与资源的有效利用实现。而响应式系统在构建 原生云
脚注
via: https://www.oreilly.com/ideas/reactive-programming-vs-reactive-systems
作者:Jonas Bonér, Viktor Klang 译者:XLCYun 校对:wxy