"reactive programming"的概念
下面的内容大多是翻译来的。
What is Reactive Programming?
为了了解Reactive——从编程范式至其背后的动机,有必要了解现在的开发者和公司在十年前不曾面对的挑战。
游戏的改变主要有两大方面:
- 硬件的提升
- 因特网
Why things are different now?
(原文对比了一遍2005年和2014年因特用户的增长:10亿至29.5亿;Facebook用户的增长:550万至13亿; twitter从无至有,0至2.7亿用户)
时代改变了,现在一个网站就需要处理十年前整个因特网的流量。实下,容易发现软件对于日常生活越来越重要。也容易发现,以前的一些范式并不再适用于现在,更不适用于将来。
The four Reactive principles
Reactive applications 构建于四个指导性的原则。
- 目标是一个responsive的程序
- 一个responsive的程序一定既scalable又resilent.responsiveness离开了scalability和resilence是不可能实现的
- 一个message-driven的架构是scalale, resilent以及最终实现responsive系统的基础
让我们在高层了解下每一个原则,理解为了开发一个适用于现在环境的软件,为什么它们是缺一不可的。
Responsive
当我们说一个程序是"responsive"的时候,是在说什么?
A responsive system is quick to react to all users — under blue skies and grey skies—in order to ensure a consistently positive user experience.
一个responsive的系统是对所有用户都能快速反应的——不管晴天阴天——来保证一个始终积极的用户体验。
在各种条件下都“快速”而“积级”的用户体验,比如外部系统出问题或者流量激增,依赖于Reactive application的两个特质:resilence,以及scalability。一个消息驱动架构提供了一个responsive系统的基础。
为什么消息驱动架构对于responsiveness如此重要?
这个世界是异步的。
假如你现在来杯咖啡,却发现糖和奶油没了。
一种做法是:
- 开始煮咖啡
- 去商店买糖和奶没
- 回家
- 啥都齐了,开始喝咖啡
- 享受生活
另一种做法是:
- 去商店买糖和奶油
- 回家
- 煮咖啡
- 等着咖啡煮好
- 忍受咖啡因短缺
- 崩溃
就像你看到的,一个消息驱动的架构提供给你异步边界(asynchronous boundary),使你从时间和空间中解放出来。在这个文章的其余部分我们将继续深入异步边界的概念。(这段举的例子挺好,可是看不出这个例子跟消息驱动有啥关系呀)
Consistency at Walmart Canada 一致性之于加拿大亚马逊
(译注:这个一致性和分布式系统的一致性不一样……)
在加入Typesafe公司之前,我是负责构建亚马逊加拿大的新电商平台的Play和Scala团队的技术leader。
我们的目标是提供一致的积极的用户体验,不管:
- 无论什么类型的物理设备被用来访问walmart.ca,不设它是桌面电脑,平板还是移动设备
- 当前的流量峰值,不管它是由于激增才出来的,还是持久的
- 一个主要的基础设施不可用,比如一整个数据中心。
响应时间和整体的用户体验必须保持一致,不管上面的情况是否发生。一致性对于你的网站是至关重要的,因为当下,这就是你的品牌。一个不好的用户体验是难以被忘记或者忽略的,不管是发生在线上还是在一个线下的商店。
Responsive retail at Gilt
Gilt的Responsive零售。电商领域的一致性(的重要性)并不是碰巧出现的。比如Gilt也是一样,Gilt是一个闪购网站,每天中午当他们发布了当日的销售商品时流量就会激增。
让我考虑下一个闪购网站的用户体验。如果你在11:58访问了Gilt,12:01又访问了它。那么你希望两次访问都有积极的体验,不管是不是Gilt在12:01那时候流量突增。
Gilt提供始终积极、responsive的用户体验,并且通过Reactive来实现这一切。通过这篇ReadWrite interview with Eric Bowman of Gilt 了解Gilt向基于Scala的微服务架构(Scala-based microservices architecture)的迁移。
Resilient
大部分的程序都被设计和开发成适用于“晴天”,但是事情可能往坏的方面发展。每隔几天就会有报告说一个主要的应用不可用,或者由于黑客的攻击造成服务不可用、数据丢失、或者储誉受损。
A resilient system applies proper design and architecture principles in order to ensure responsiveness under grey skies as well as blue.
一个弹性的系统采用了一些设计和架构的原则,使得不管外部情况的好坏,都是responsive的
Java和JVM都是用来把一个程序无缝地布署到多个操作系统,类似的是,201x年的内部互联的应用程序都是关于应用程序层级的组合、连接和安全。(注:这句有点绕……忽略,看下边的)
现在的程序大都由一定数量的其它程序组装而成,通过web service和其它网络协议整合在一起。现在一个应用可能依赖于大量的其它服务——10,20或者更多,它所依赖的这些服务在自己的信赖防火墙(trust firewall)之外。它可能同时在为大量外部用户提供服务——既包含人,也包括其它系统。
考虑到整合的复杂性,有多少开发人员:
- 分析和建模所有外在的依赖
- 把整合进的每个服务的理想响应时间整理成文档,进行性能测试——在峰值和平常——来全面评估最初的期望
- 把所有的预期,失败情况和其它非功能的需求编码到系统的核心逻辑中
- 分析和测试每个服务的失败情形的影响
- 分析每个外部依赖的安全,识别是否整合这个外部依赖是否给系统带来最的安全隐患
Resiliency is one of the weakest links of even the most sophisticated application, 但是Resilency作为一种事后的思考的情况很快就会终结。现代的程序必须在其核心就是resilent的,以保持在现实世界多变的环境下,而不只是理想情况下,保持responsive。 性能、持久性、安全都是resilency的方面。你的应用必须在各个层次都是resilent的。
Message-driven resiliency
在一个消息驱动的内核之上构建应用的美妙之处在于你自然地得到了很多有价值的构造模块。
Isolation 隔离性是一个自愈(self-heal)的系统所需要的。当隔离性在被恰当地使用,我们可以依据一些因素把不同类型的工作进行分离,比如失效的风险、不同的性能、CPU和内存的使用,等。一个isolation的组件的失效并不会影响整个系统的responsiveness, 并且给予了失效的那个系统一个被治愈的机会。
Location transparency 位置透明性使得我们与不同集群结点上的不同进程间的通信就像与同一个VM的进程内通信一样。
A dedicated separate error channel 一个精心设计的分离的错误信息通道使得我们可以把错误信号重定向到其它地方,而不是抛回到调用者面前。(注:这个作用应该类型于Akka中的event stream这种东西,使得某些失败的信息可以被订阅,使得不光在当前是调用了某个失败组件的组件可以知道它失败了,而是所有相关的组件都可以了解到这个情况,便于他们即是作出响应)
这些因素帮助我们把健壮的错误处理和容错合并进我们的系统中。Akka的supervisor hierarchies的实现展示了这点。
消息驱动架构提供的这些核心的构建模块有助于resiliency,进而有助于responsiveness——不仅在理想情况下,而且在各种并非理想的、现实世界的情况下。
The 440 million dollar resiliency mistake
来见识下骑士资本集团在2012年发生软件故障的经历(software glitch experienced by Knight Capital Group)。在一次软件升级中,另一个休眠的、被整合在一起程序被不经意地唤醒,并且开始放大交易量。
接下来的45分钟发生的事情是一场恶梦。
骑士资本的自动交易系统洪水般地向NASDAQ(注:纳斯达克)进行错误交易,使得公司进入了一个需要付出数十亿美元代价的位置。事后,这家公司花了4.4亿美元来扭转局势。在这个故障中,骑士资本不能停止泛水般的错误交易,NASDAQ不得不终止骑士资本来停止这一切。骑士资本的股票在一天跌了63%, 几乎不能做为一个公司存活了下来,只能等着它的股票恢复了一些价值之后被一些投资者接管。
骑士资本的系统是高性能的,但是不是resilent的。高性能但是没有resilience使得问题被放大,就像骑士资本发现的那样。他们甚至没有一个停止开关来使得自己的系统在出现严重问题时停止交易, 所以,当真的出了问题时,他们的自动交易系统在45分钟内耗尽了整个公司的资本。
这就是设计和开发只能用于"晴天“的程序的结果。如今,软件是我们的私人生活和生意的关键组成。如果我们没有为”阴天“的情况提前设计和准备,那么即使这种情况只存在了不到一个小时,也会给我们造成巨大的损失。
Scalable
Resililency和Scalability手拉手,一起共建始终responsive的程序。
A scalable system is easily upgraded on demand in order to ensure responsiveness under varioius load conditions.
一个可扩展的系统可以在面对不同的负载时轻松地进行升级,来保证respnsiveness
任何一个在网上卖过东西的人都明白一个事实:你的流量最大的时候就是你卖的东西最多的时候。除非流量的突增是由于故意的黑客袭击,不然期望流量突增是挺好的一件事。在一个突增的流量中,人们愿意花钱给你。
那么如何面对这种流量突增,或者是一个长久的但是大量的流量增长。
首先选择你的范式(paradigm),然后选择拥抱这种范式的语言和工具。很多情况下,开发者只是简单地选择语言和框架……。一旦选择了工具,就很难改回来. 所以,就像你在处理任何一个重大的投资一样进行这些决定。如果你的技术选择基于一些原则和分析,那么你就已经领先一步了。
Thread-based limitations to concurrency 基于线程的并发的不足
一个至关重要的技术选择就是框架的并发模型。在较高的层次,有两种并发模型:
- 传统的基于线程的并发模型,基于调用栈和共享内存
- 消息驱动的并发
一些流行的MVC框架,比如Rails,是蔡于线程的。这类框架的典型特点包括:
- 共享的可变状态
- 每个请求一个线程
- 对于可变状态的并发访问——变量以及对象实例——使用锁和其它复杂的同步结构来进行管理
这些特点结合动态类型、解释型语言比如Ruby,会迅速达到性能和扩展性的上限。你可以认为所有本质是脚本语言的语言都是如此。
Out or up?
让我们考虑扩展一个应用的不同方式。
向上扩展(scaling up)是关于最大化的利用一个CPU/服务器的资源,通常需要购买更强大的,专用的、更贵的机器。
向外扩展(scaling out)是关于把计算分布到由廉价的commdity机器组成的集群中。这样在成本上更划算,但是如果你的系统是基于时间和空间概念的(注:”基于时间的“可以认为程序的各个动作是同步的,需要阻塞,等待,”基于空间的“可以认为是基于一个进程内部的内存共享,比如不线程间共享的变量),那么向外扩展就非常困难。 就像我们之前提到的那样,一个消息驱动的架构提供了与时间和空间解耦合所需要的异步边界(asynchronous boundary),提供了按需向外扩展(scale out on demand)的能力,这种能力也被叫做elasticity. Scaling up是关于更高效地利用已有的资源,而elasticity是关于在系统需要改变时按需增加新资源。能够”scale out, on demand"的能力,就是Reactive programming在扩展性方面的终极目标。
Reactive应用程序很难构建于基于线程的框架之上,想想就知道把一个基于可变状态,线程和锁的的程序进行scale out有多困难。开发人员不仅需要利用单个机器上多核的优劣,在某些时候,开发者也需要利用计算机集群的力量。共享可变状态也使得scale up变得困难,即使不是不可能。任何曾尝试操作两个线程共享的可变状态的人都能够理解保证线程安全有多复杂,并且也能够理解为了确保线程安全所需要花费的多余努力所带来的性能上的惩罚。
Message-driven
一个消息驱动的架构是Reactive应用程序的基础。一个消息驱动的程序可以是事件驱动的(event-driven),基于actor的(actor-based),或者两者的结合。
一个事件驱动的系统基于事件,这些事件被一个或更多监听者监听。这和imperative programming不同,因此调用者不需要阻塞以等待被调用的例程的响应。事件并不是被导向特定的地址,而是被监听,我们将会更深入地讨论其隐含的意义。
基于actor的并发是消息传递架构的一种延伸,在基于actor的并发架构中,消息可以跨越线程的边界或者被传递到不同物理机器上的另一个actor的邮箱(mailbox)中。这使得elasticity——scale out on demand——变得可能,因为actor可以被分布到网络的各处,但是仍然可以像他们在同一个VM里一样通信。
消息和事件的不同在于消息是有方向的,而事件只是”发生“并没有方向。消息有一个清晰的目的的,而event可以被一个或者多的监听者监听。
让我们深入探讨下事件驱动和基于消息的并发。
Evnt-driven concurrency
典型的程序用命令的风格开发—一个命令的线性序列—并且是基于调用栈(call stack)的。调用栈的主要功能是追踪例程(routine,感觉应该是函数呀)的调用者,在阻塞调用者的同时执行被调用的例程,当执行完毕后把控制流交还给调用者,并且返回执行结果的值(或者不返回)
事件驱动的程序并不关注于调用栈,而是关注于触发事件。事件可以被编码成消息,放在队列里,被一个或者更多监听者监听。事件驱动和命令式的巨大区别在于调用者不被阻塞,不用在等待响应的时候占据一个线程(caller does not block and hold onto a thread while waiting for a response)。事件循环本身可以是单线程的,但是仍然可以实现并发,因为可以在(有时是单线程 的)事件循环处理新的请求的同时调用例程来完成工作(注:因为对例程的调用被搞成了非阻塞的,即不会阻塞event-loop)。不再是阻塞于一个请求,等待它被处理完,现在调用者的标识可以和请求消息本身一起被传递,使得(如果被调用的例程选择这么做)调用者可以被回调,在回调时传递响应(the caller can be called back with a response)。
选择一个事件驱动架构的主要结果是他们可有会遇到一个现象,叫做回调地狱(callback hell)http://callbackhell.com 。 回调地狱之所以发生,是因为消息的接受者是匿名函数而不是可寻址的接受者(addressable recipients).通常的解决方案仅仅关注于句法—aka, the Pyramid of Doom—而忽略了推导和调试代码中表达的event所产生的后续事件的困难。
Actor-based concurrency
基于actor的程序以在多个actor之间异步的消息传递为中心。
一个Actor由以下的属性组成:
- 用于接收消息的mailbox
- actor的逻辑,它依赖于模式匹配(pattern matching)来决定这个actor如何处理不同类型的消息
- 隔离的状态(isolated state),而不是共享的状态。使用隔离的状态来存储请求之间的上下文。
像事件驱动的并发一样,基于actor的并发也避开了调用栈,转向轻量级的消息传递。actor可以返回以及发送消息,甚至传递消息给自己。一个消息可以传递给自己消息,使得它先处理队列里的其它消息,然后再去结束一个长时间运行的请求。基于actor的并发的一个巨大好处是,它不仅可以有事件驱动的并发的优点,而且可以让计算跨越网络边界变得更容易,并且可以避免callback hell,因为消息被定向到actor。这是一个强大的概念,有助于构造易于设计、实施、维护的高可扩展性的程序。不用考虑“空间”和“时间”,也不用考虑深度嵌套的回调函数,你只需要考虑actor之间的消息流动。
另一个基于actor的架构的主要好处是组件之间解耦合。调用者不用阻塞自己的线程等待响应,因此调用者可以迅速地转去进行其它工作。被调用的例程,封装在一个actor中,只需在必须的回复调用者。这开启了很多可能性,比如分布例程(routines)到一个集群中,因为不再有调用栈把程序耦合在单一的内存中,actor模型可以使布署一个拓扑(topology)只是一个虚拟的配置而不再是编码在程序里。
Akka是一个基于actor的工具箱和运行时- Typesafe Reactive Platform的一部分,用来构造并发的、分布式的、容错的,基于actor的程序,运行于JVM。Akka还有其它的一些用于构造Reactive application的不可思忆的特性,像是用于rsilience的supervisor hierarchies,以及可以把分布工作来实现扩展性。对Akka的深入研究超出了本文的范围,但是我强烈推荐访问Let it Crash blog 获取更多关于Akka的内容。
我也强烈推荐阅读Benjamin Erb’s Diploma Thesis,Concurrent Programming for Scalable Web Architectures, 被用于本节的参考。
Conclusion
以上的内容描述了时下软件开发的的表面,引导我们认识到为什么Reactive programming不仅仅是一个潮流,而且是现在的软件开发者需要学习的范式。不管你选择什么语言和工具,优先考虑scalability和resilient来实现responsiveness是唯一能满足用户需求的方式。每过一年,这点就会变得更加重要。
想要手把手的学习吧?下载Typesafe Activator ,在今天开始构建你的Reactive applications. (广告呀……)
下面是另一篇文章
-
Reactive
is readily responsive to a stimulus
- react to events (event-driven)
- react to load (scalable)
- react to failures (resilient)
- react to users (responsive)
-
Event-Driven
Systems are composed from loosely coupled event handlers.
- Events can be handled asynchronously, without blocking.
-
Scalable
An application is scalable if it is able to be expanded according to its
usage.- scale up: make use of parallelism in multi-core systems
- scale out: make use of multiple server nodes
Important for scalability: Minimize shared mutable state.
Important for scale out: Location transparency, resilience. -
Resilient
An application is resilient if it can recover quickly from failures.
Failures can be:- software failures
- hardware failures
- connection failures
Typically, resilience cannot be added as an afterthought; it needs to be
part of the design from the beginning.
Needed:- loose coupling
- strong encapsulation of state
- pervasive supervisor hierarchies
-
Responsive
An application is responsive if it provides rich, real-time interaction with
its users even under load and in the presence of failures.
Responsive applications can be built on an event-driven, scalable, and
resilient architecture. Still need careful attention to algorithms, system design, back-pressure, and many other details.