# 响应式编程笔记三:一个简单的HTTP服务器

本文我们将继续前面的学习,但将更多的注意力放在用例和编写实际能用的代码上面,而非基本的APIs学习。

我们会看到Reactive是一个有用的抽象 - 对于并发编程来说 - 但它还有一些非常低级别的特性,应该引起我们的注意。

如果我们开始使用这些功能,挖掘其全部潜能,那我们可以控制我们应用中的layers - 那些之前不可见的、被容器|平台|框架隐藏起来的layers!

## Bridging from Blocking to Reactive with Spring MVC | 在Spring MVC中,将阻塞式桥接到响应式

响应式会强迫你用不同的眼光去看待世界。

不再是请求和获取(也许没有获取到),所有的东西都作为一个`sequence(Publisher)`被投递过来,当然你必须先`subscribe`。

不再是一直等待一个响应,你必须注册一个`callback`。

当你习惯了的时候,就变得很轻松,但是,除非整个世界都变成了响应式的,不然你还是需要与旧式的阻塞式API打交道。

假定我们有一个阻塞式方法,会返回一个 `HttpStatus`:

private RestTemplate restTemplate = new RestTemplate();

private HttpStatus block(int value) {
return this.restTemplate.getForEntity("http://example.com/{value}", String.class, value)
.getStatusCode();
}

我们希望以不同的参数来调用它,并聚合所有的结果。这是一个典型的 **分散-聚集 (scatter-gather)** 用例,假定每次请求你都获得了一个分页结果,但最后需要得到所有结果的 top N。由于阻塞式操作与 scatter-gather模式无关,我们将其放到一个 `block()`方法中,并在稍后实现它。现在,先来看一个**坏例子**

Flux.range(1, 10) //(1)
.log()
.map(this::block) //(2)
.collect(Result::new, Result::add) //(3)
.doOnSuccess(Result::stop) //(4)

1. 发起请求的次数

2. 阻塞式代码

3. 收集结果,并聚合到一个独立的对象中

4. 最后结束(结果是 `Mono<Result>`)

不要学习这个坏例子,因为,虽然APIs用的没问题,但仍然会阻塞住线程!

这个例子和直接循环发起请求没什么不同。

更好的实现 则应该将对`block()`的调用放到一个后台线程中。例如:

private Mono<HttpStatus> fetch(int value) {
return Mono.fromCallable(()->block(value)) //(1)
.subscribeOn(this.scheduler); //(2)
}

1. 阻塞式代码现在位于Callable中,会延迟执行。

2. 在后台线程订阅。

`scheduler` 需要另外声明:

Scheduler scheduler = Schedulers.parallel();

然后,我们就可以使用`flatMap()`,而非`map()`:

Flux.range(1, 10)
.log()
.flatMap( //(1)
this::fetch, 4) //(2)
.collect(Result::new, Result::add)
.doOnSuccess(Result::stop);

1. 使用新的publisher来并行处理

2. flatMap 的并发量

## Embedding in a Non-Reactive Server 内置一个非响应式服务器

如果你想在一个非响应式服务器中运行上面的代码,可以使用Spring MVC:

@RequestMapping("/parallel")
public CompletableFuture<Result> parallel() {
return Flux.range(1, 10)
.log()
.flatMap(this::fetch, 4)
.doOnSuccess(Result::stop)
.toFuture();
}

如果你读过`@RequestMapping`的Javadocs,那你会发现其方法可以返回一个`CompletableFuture`,这样,应用会使用这个返回来生成一个真正的返回值 - 在另一个线程中。本例中的这个"另一个线程"是由"scheduler"提供的, scheduler是一个线程池,所以真正的处理是多线程的,上面的代码,会4线程并发!

## No Such Thing as a Free Lunch | 没有免费的午餐

虽然上面 用后台线程运行 scatter-gather 代码 是一个有用的模式,但它仍不够完美 - 虽然没有阻塞调用者,但阻塞了别的,就是说,它只是转移了问题。

我们有一个HTTP服务器,可能会带有NIO handlers,将工作传回到一个线程池,每个线程处理一个HTTP request - 所有的这些都发生在Servlet容器内部。

request是被异步处理的,因此Tomcat的worker线程没有被阻塞,scheduler中创建的线程池会用4线程来处理。

我们在处理10个后端请求( 对 `block()`的调用),因此,使用scheduler会有一个最大的、理论的受益,就是降低4倍延迟。

换句话说,如果在一个线程中处理需要 1000ms的话,那现在可能只需要 250ms了。

注意,这里只是可能:只有在没有竞争的情况下才会那么快。

提示:tomcat默认分配了100个线程来处理HTTP请求。如果所有的请求都经过我们的scheduler线程池的话,那完全超出了线程池的容量。这是一个完全错误的搭配:scheduler线程池可能是一个瓶颈!这意味着性能调优会非常艰难,你可能调整了所有配置,然后达到一个很脆弱的平衡 - 随时可能被破坏。

Tomcat allocates 100 threads for processing HTTP requests by default. That is excessive if we know all the processing is going to be on our scheduler thread pool. There is an impedance mismatch: the scheduler thread pool can be a bottleneck because it has fewer threads than the upstream Tomcat thread pool. This highlights the fact that performance tuning can be very hard, and, while you might have control of all the configuration, it’s a delicate balance.

我们可以使用弹性的线程池,而非固定的。 这对Reactor来说非常简单,只要使用 `Schedulers.elastic()`即可 - 可以多次调用,但只会有一个实例!

## Reactive all the Way Down

从阻塞式到响应式的桥接是一个有用的模式,且很容易在Spring MVC中实现。

下一步就是完全地干掉应用线程中的阻塞式,这得使用新的APIs和新的工具。

极限就是完全响应式,从服务器到客户端。这是Spring Reactive的目标!

Spring Reactive是一个新的框架,与Spring MVC是完全不同的方向,但会实现同样的需求,并使用相似的编程模型。

>注意,Spring Reactive开始是一个单独的项目,但已经被打包进Spring Framework了,版本5 。

还是拿前面的 scatter-gather例子来说,如果想全响应式,那第一步就是使用`spring-boot-starter-web-reactive`来代替`spring-boot-starter-web`。

org.springframework.boot.experimentalspring-boot-starter-web-reactive
...org.springframework.boot.experimentalspring-boot-dependencies-web-reactive0.1.0.M1pomimport

>注意,上面的版本可能已过时,请自行查找新版本。

<dependencies>
<dependency>
<groupId>org.springframework.boot.experimental</groupId>
<artifactId>spring-boot-starter-web-reactive</artifactId>
</dependency>
...
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot.experimental</groupId>
<artifactId>spring-boot-dependencies-web-reactive</artifactId>
<version>0.1.0.M1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

然后,在controller中,只需要这样:

@RequestMapping("/parallel")
public Mono<Result> parallel(){
return Flux.range(1, 10)
.log()
.flatMap(this::fetch, 4)
.collect(Result::new, Result::add)
.doOnSuccess(Result::stop);
}

在Spring Boot中运行,会自动在Tomcat、Jetty或Netty中运行。默认是Tomcat,如果有不同的选择,需要先exclude tomcat!

我们仍然使用阻塞式后端调用 `block()`,因此,我们仍然需要`subscribe()`一个线程池来避免阻塞住调用者。

我们可以使用非阻塞式客户端WebClient,而非RestTemplate!

private WebClient client = new WebClient(new ReactorHttpClientRequestFactory());

private Mono<HttpStatus> fetch(int value){
return this.client.perform(HttpRequestBuilders.get("http://example.com"))
.extract(WebResponseExtractors.response(String.class))
.map(response -> response.getStatusCode());
}

注意 `WebClient.perform()` (或者更确切的说是 `WebResponseExtractor` ) 会有一个响应式返回类型,我们已经将其转换为 `Mono<HttpStatus>`,但我们没有`subscribe`它。我们希望框架负责所有的订阅,这样我们就是全响应式了。

>警告:Spring Reactive中 那些返回`Publisher`的方法,都是非阻塞的!而普通方法返回`Publisher`(或`Flux`|`Mono`|`Observable`) 则可能是非阻塞的!如果你在编写这样的方法,最好认真分析、测试一下它们是否阻塞。

>注意:上面我们使用了一个非阻塞的客户端来简化了HTTP栈,同样也可以在常规Spring MVC中工作。`fetch()`的结果可被转换成一个 `CompletableFuture`,从常规的`@RequestMapping`方法中传出。

## Inversion of Control | 控制反转

现在,我们可以移除并发数量了:

@RequestMapping("/netty")
public Mono<Result> netty() {
return Flux.range(1, 10) //(1)
.log()
.flatMap(this::fetch) //(2)
.collect(Result::new, Result::add)
.doOnSuccess(Result::stop);
}

1. 发起10个调用

2. 使用新的publisher来并发处理

现在我们不需要额外的 callable和 subscriber线程了,代码简洁了不要太多。

响应式`WebClient` 返回了一个 `Mono`,这会驱动我们在变形链中直接选择`flatMap()`,然后就可以得到我们需要的code了!

优雅,可读性更高,更易于维护!

还有,因为没有了线程池和并发量,也就没有了魔数 4

当然,还是有一个限制,但不会再影响我们在应用层(application tier)的选择,也不会局限于服务器容器了。

这不是魔法,因为仍然是物理规律综合的结果,所以,后端调用仍然会执行 100ms,但竞争很少 - 我们可能看到10个请求完全同时执行。

当服务器的负载增加了延迟时,吞吐率会自然地降级,而降级方式则由缓冲竞争和内核网络治理,而非应用线程管理。

这是一种控制反转,由底层控制,而非应用代码控制。

请记住,同样的应用代码运行在Tomcat、Jetty或Netty中。

目前,Tomcat和Jetty的支持,是基于Servlet 3.1 异步处理之上的,所以受限于 一个请求一个线程。

当同样的代码运行在Netty服务器平台上时,就没有这个限制了,服务器可以分派请求到web客户端。

只要客户端没有阻塞,所有人都会高兴。

netty服务器和客户端的性能指标是类似的,但Netty服务器不受限于 一个线程处理一个请求,所以,它不使用大的线程池,我们可能期望看到一些不同的资源利用。

我们会在本系列的其他文章中讨论它。

>提示:在[样例代码](https://github.com/dsyer/reactive-notes)中,reactive样例支持的maven profiles有:tomcat、tomcatNext(for Tomcat 8.5)、jetty、netty。

>注意:很多应用中的阻塞式代码不是HTTP后端调用,而是数据库交互。目前很少有数据库支持非阻塞式客户端(MongoDB和Couchbase是礼物,但也不如HTTP客户端成熟)。线程池和 blocking-to-reactive pattern会存在很长时间,一直到所有数据库能够跟上。

## Still No Free Lunch | 还是没有免费的午餐

虽然到目前为止我们做的看起来都很好,但很快就会有一些错误发生,例如 表现恶劣的网络连接、后端服务忍受严重的延迟。

首先,最明显的就是我们写的代码都是声明式的,所以很难调试。当错误发生时,诊断可能很模糊。使用原生的、低级别的APIs,例如不带Spring的Reactor,或者没有Reactor的Netty级别,可能会让情况变得更糟,因为我们必须构建大量错误处理,每次与网络交互,都要重复一些呆板的代码。起码,混合使用Spring和Reactor,我们可以看到栈追踪记录、未捕获的异常。它们可能不是那么好理解,因为它们是发生在我们不能控制的线程中;有些时候,它们还给出一些非常低级的信息。

另一个痛苦之源则是,如果我们犯了错误,并阻塞在我们的响应式callbacks里,我们会停住(hold up)该线程里的**所有requests**。

在基于Servlet的容器中,每个request都被隔离到一个线程中,阻塞不会停住其他的requests,因为它们是在不同的线程上处理。

阻塞所有requests是麻烦的来源,但它仅会在延迟增加(见下面原文) 时出现。在响应式世界里,阻塞一个request会导致所有requests加大延迟,而阻塞所有requests则会让服务器跪下来唱征服,因为没有额外的缓冲层和线程去处理。

> Blocking all requests is still a recipe for trouble, but it only shows up as increased latency with roughly a constant factor per request.

## Conclusion | 结论

能够控制异步处理中的每个移动部分,是一件很爽事:每一层都有一个线程池尺寸和一个队列。

我们可以让某层使用弹性的线程池,根据它们的工作去调整。

但同时,这也是一种负担,我们开始寻找更简单的或者更简洁的。

大量分析的结论是,移除额外的线程,配合物理硬件的限制来使用,通常是一个更好的选择。

This is an example of "mechanical sympathy", as is famously exploited by LMAX to great effect in the [Disruptor Pattern](https://lmax-exchange.github.io/disruptor/).

我们已经开始看到响应式的强大,但是请记住,强大伴随着责任。

它是激进的,它也是基础的。

它是"放下一切,从头开始"的领域。

你可能希望看到响应式不是所有问题的解决方案。事实上它的确不是,它只是特定一类问题的解决方案。

你的收获可能远超学习、修改、维护的代价。

## 原文

https://spring.io/blog/2016/07/20/notes-on-reactive-programming-part-iii-a-simple-http-server-application

响应式编程笔记三:一个简单的HTTP服务器的更多相关文章

  1. how tomcat works 读书笔记(一)----------一个简单的web服务器

    http协议 若是两个人能正常的说话交流,那么他们间必定有一套统一的语言规则<在网络上服务器与客户端能交流也依赖与一套规则,它就是我们说的http规则(超文本传输协议Hypertext tran ...

  2. 响应式编程(Reactive Programming)(Rx)介绍

    很明显你是有兴趣学习这种被称作响应式编程的新技术才来看这篇文章的. 学习响应式编程是很困难的一个过程,特别是在缺乏优秀资料的前提下.刚开始学习时,我试过去找一些教程,并找到了为数不多的实用教程,但是它 ...

  3. 【响应式编程的思维艺术】 (1)Rxjs专题学习计划

    目录 一. 响应式编程 二. 学习路径规划 一. 响应式编程 响应式编程,也称为流式编程,对于非前端工程师来说,可能并不是一个陌生的名词,它是函数式编程在软件开发中应用的延伸,如果你对函数式编程还没有 ...

  4. Spring Boot (十四): 响应式编程以及 Spring Boot Webflux 快速入门

    1. 什么是响应式编程 在计算机中,响应式编程或反应式编程(英语:Reactive programming)是一种面向数据流和变化传播的编程范式.这意味着可以在编程语言中很方便地表达静态或动态的数据流 ...

  5. Reactive(1) 从响应式编程到"好莱坞"

    目录 概念 面向流设计 异步化 响应式宣言 参考文档 概念 Reactive Programming(响应式编程)已经不是一个新东西了. 关于 Reactive 其实是一个泛化的概念,由于很抽象,一些 ...

  6. RxJava(一):响应式编程与Rx

    一,响应式编程 响应式编程是一种关注于数据流(data streams)和变化传递(propagation of change)的异步编程方式. 1.1 异步编程 传统的编程方式是顺序执行的,必须在完 ...

  7. how tomcat works 读书笔记(二)----------一个简单的servlet容器

    app1 (建议读者在看本章之前,先看how tomcat works 读书笔记(一)----------一个简单的web服务器 http://blog.csdn.net/dlf123321/arti ...

  8. 一个菜鸟所喜欢用的响应式布局,操作方便简单、时尚简约,适合新手!(一个Dreamweaver cs6生成响应式布局)

    前端开发并不是一个容易的工作,不仅需要掌握HTML.CSS和JavaScript,针对不同的浏览器版本和平台,还需要了解如何设计出跨平台的网站.如今随着响应式设计的流行,前端开发变得越来越困难,且花费 ...

  9. 函数响应式编程及ReactiveObjC学习笔记 (-)

    最近无意间看到一个视频讲的ReactiveObjC, 觉得挺好用的 但听完后只是了解个大概. 在网上找了些文章, 有的写的比较易懂但看完还是没觉得自己能比较好的使用RAC, 有的甚至让我看不下去 这两 ...

随机推荐

  1. bzoj 1076 状态压缩最优期望

    题意: 你正在玩你最喜欢的电子游戏,并且刚刚进入一个奖励关.在这个奖励关里,系统将依次随 机抛出k次宝物,每次你都可以选择吃或者不吃(必须在抛出下一个宝物之前做出选择,且现在决定不吃的宝物以后也不能再 ...

  2. java中线程安全的map是ConcurrentHashMap

    原理:http://www.cnblogs.com/ITtangtang/p/3948786.html 与hashtable的区别:  http://blog.csdn.net/songfeihu08 ...

  3. EasyUI学习总结(二)——easyloader分析与使用(转载)

    本文转载自:http://www.cnblogs.com/haogj/archive/2013/04/22/3036685.html 使用脚本库总要加载一大堆的样式表和脚本文件,在easyui 中,除 ...

  4. maven学习一(HelloWorld工程)

    maven是一个出色的java工程依赖管理的工具,刚刚开始学习用maven建立一个HelloWorld工程. maven安装 $ wget http://mirrors.cnnic.cn/apache ...

  5. OUI启动时的小错误PRVF-0002

    [oracle@bys3 database]$ Starting Oracle Universal Installer... Checking Temp space: must be greater ...

  6. Asp.net 子域共享cookie

    最近项目遇到要共享cookie的问题,本来后台保存session用的是Redis来保存数据的.所以只需要2个站点发的ASP.NET_SessionId是相同的就可以,并且它的Domain 是父级域名. ...

  7. chrome 浏览器的插件权限有多大?

    转自:https://segmentfault.com/q/1010000003777353 1)Chrome插件本身有机制控制,不会无限制的开放很多权限给你2)页面的DOM元素时可以操作的,Chro ...

  8. 【T03】理解私有地址和NAT

    1.私有地址包括三块: 10.0.0.0 到 10.255.255.255 172.16.0.0 到 172.31.0.0 192.168.0.0 到 192.168.255.255 2.私有地址接入 ...

  9. springcloud学习笔记(五)Spring Cloud Actuator

    <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring- ...

  10. 内联汇编中的asm和__asm__

    基本的内联汇编代码: asm格式: asm("assembly code"):   使用替换的关键字: 如果必须的话,可以改变用于标识内联汇编代码段的关键字asm.ANSI C规范 ...