写点什么

性能之争:响应式编程真的有效吗?

  • 2019-04-24
  • 本文字数:3522 字

    阅读完需:约 12 分钟

性能之争:响应式编程真的有效吗?

响应式编程为 Java 的企业版应用提供了更高的性能,并降低了内存消耗,主要是通过减少进程的上下文切换来实现的。因为类似的上下文切换对 CPU 和内存的消耗是极大,所以要尽可能的减少这样的切换操作。不过,响应式编程带来的这种性能上的提高,代价是降低了软件的维护性,这样的代价交换是否值得呢?让我们在本文中深入地讨论一下这个问题吧。


在 Java 的早期时代,抽象线程是区别于其他编程语言的一大优势。直至今日,Java 依然提供了便捷的并行编程和同步的机制。在此基础上,我们可以非常轻松地实现 Web 框架,使 Web 请求可以直接与 servlet API 中的线程绑定在一起,以一种“虚拟”的方式处理请求,摒弃掉同步和并发的问题。调用链在到达 Tabbed Browsing 和 Ajax 之前,还可以设计确定同一 Session 的两个请求不需要并行执行在一个线程,对于普通开发者而言,就不需要担心 Session 级别的并行问题了。


但是目前,Java 的线程在实现上有一个严重的缺陷:Java 线程的实现被操作系统认定为系统进程,这使得线程的切换等价于操作系统的上下文切换,这个代价非常高。如果一个应用程序每分钟内只处理几千个请求,或许没什么问题。可是目前 Web 程序的需求与日俱增,不断增长的用户量和请求应用致使企业应用现在每分钟处理的数据量远远高于 15 年前。由操作系统线程来处理请求的这种模型渐渐地进入了瓶颈,尤其是当你在程序执行的过程中,阻塞住当前程序来访问数据库或者访问其他的微服务,这种情境下特别明显。


并行计算的时间应明显高于 Java 将线程实现为操作系统线程的时间,但是目前请求的执行时间很短,操作系统进程上下文的切换时间很长,完全不成比例。


而这正是响应式编程的用武之地,它与目前的 Java 线程模型完全相反。目前的线程模型是保证所有的事情都在当前线程执行,但是在响应式编程中,异步是一个准则。一个程序执行过程被认定为一系列异步执行的事件,每个事件都被 Publisher 创建,你不需要关心 Publisher 在哪个线程中创建。在响应式程序里面,程序代码包含了监听和执行异步事件的功能,而且会在必要时提供新的事件。


这个方式在某些场景下很有效果,比如说在程序里面访问外部数据库。传统的企业级 Java 程序里面,系统会发送一段 SQL 语句到数据库上面,阻塞住程序,直到数据库返回查询结果。但是在响应式编程里面,程序会跳过等待结果的过程,正常向下执行代码。当你发送一个 SQL 请求到数据库的时候,会用一个 Pushlisher 来替代阻塞的过程,调用者可以注册这个 Pushlisher,这样的话,在之后数据库返回结果的时候,就会通知 Publisher,然后 Publisher 会通知调用者。


这种方式对于解决回调地狱很有帮助,很多异步编程也确实是这样做的。


响应式编程的好处就是执行的代码和执行的线程是分开的。因此在操作系统的层面上,上下文切换的代价比较低。


这种方式在服务端的架构里面有很大的意义,只保留一个线程处理程序进程层面的代码,跟 Node.js 很相似。如果这个线程被阻塞的话,那整个服务器就不会再处理其他请求了。因此,在 JavaScript 中,每个调用在一开始的时候就是异步执行,在任何情况下都可以通过响应式的 API 或者其他有用的抽象方式,来解决回调地狱的问题。


但是正如文章开篇里面提到的,响应式编程也有一些比较严重的问题,写入的代码和执行的代码分离开来,导致阅读和编写代码的难度增加,对于这种异步的代码,编写单元测试和调试代码都很困难。


在与传统的企业应用集成的过程中,也出现了很多新的问题,像安全认证、事务还有链路追踪等仍然附加在当前线程里面。当你开始执行响应式编程的程序时,这种机制也无法奏效,所以需要找到一个更好的解决办法。


Project Reactor 是 Spring 的 Reactive Web Framework 的基础,目前支持测试,调试等等(详情)。不过,也是由于响应式编程的复杂度很高,所以开发者开始寻找是否还有其他的替代方案,来解决 Java 线程切换代价高的问题。

响应式编程的替代方案

上文提到了,当前的 Web 应用程序,代码的执行时间和 Java 线程之间切换的时间不成比例。Java 的线程是以 1:1 的形式分配给操作系统的。尽管 Java 有线程池的方式来帮你在不进行上下文切换的情况下执行代码,但是终究只是一个笨拙的解决方式。


类似于 C#、JavaScript、Kotlin 等这一类的编程语言在这个领域已经走在了前面,他们在编程语言中集成了这部分功能,可以帮助你执行一小段异步代码,并在返回的时候阻塞,直到执行结束。在 C#和 JavaScript 中,可以使用 Async 和 Await 关键字,在 Go 和 Kotlin 中,同样包括了协程的概念来提供类似的功能。


以上这些概念的想法都是类似的,如果我发现需要执行一个比较长时间的请求(例如:访问数据库),代码不应该阻塞住,而是在响应结束并返回结果的时候,指定执行需要的代码,并且实现代码要简单易读,方便调试和测试。

Java 1 中的绿色线程

所有的努力都是为了保证开发人员能够轻松地开发、测试、维护的调试响应式程序代码,但是关键问题在于,响应式编程在运行时机制下,是否能够提供更好的性能。上面提到的一些替代方案里面,响应式编程又是否是解决问题的正确方法?虽然以上问题,目前无法得到一个确切的答案,但是,其他语言的替代方案(Asyn,Await 和协程)是添加在编程语言内部的,并不是第三方类库。那么 Java 中额外的第三方类库是不是有点多余呢?


我们先来回顾一下 Java 的历史,了解一些有趣的背景故事:


Java 1.1 的版本中,整个线程模型的实现被称为“绿色线程”,Java 虚拟机只在单核系统中执行,Java 线程在虚拟机中使用自己的调度算法。在 Java 的虚拟内存管理下,线程切换和上下文切换非常快,几乎没有内存开销。


这个方式的优点是,Java 程序中同步访问数据的操作并不复杂,根本不会出现对变量真正的并行访问,所有的内容都在一个系统进程里面执行,因此可以说这只是一个“虚拟”的并行。


当然,问题也随之而来,这类程序在多核系统里面,只能使用一个系统内核,根本无法发挥计算机的整体性能。在实践中,这个缺点被不断地放大,甚至足以掩盖它的优点。


因此,绿色线程的想法很快就被终止了,在 Java1.2 中,提供了一个切换绿色线程和本地线程(详情链接)的开关。在 Java1.3 中,就只支持本地线程了。直到今天,所有的计算机内核都可以得以利用。

Project loom 结合了线程模型

Project Loom是在去年推出,JVM 背后的想法就是重新提供类似绿色线程的支持。但与过去不同的是,这些内容不是为了取代当前操作系统的线程模式,而是更好的支持它。因此两个线程模型会在 JVM 虚拟机中并存,而且可以在程序中同时使用。


操作系统线程继续由 Java 的 Thread 类实现,而新的绿色线程,则交由 Fiber 类管理。如果有必要的话,他们可以拥有一个共同的基类,这样现有的代码就不需要为了 Fiber 而修改代码,也不需要知道线程背后的情况。在 Java 虚拟内存的管理下,通过 Fiber 切换上下文几乎没有开销,同步也应该会变得更高效。有一个实现思路是,调度程序确保一个线程里面执行两个彼此相互依赖(比如说访问相同的变量)的 Fibers,这样的话它们就永远不会并行执行,也没有了同步的必要。


为了实现 Fibers,Java 的执行线程将会被分为两部分:执行过程和调度。执行过程标记了执行状态,例如上下文要执行代码的状态,包括调用参数、堆栈等。而调度则负责匀速的执行所有要执行的部分。


将调度程序与执行过程分离的好处是,目前调度和执行的上下文都由操作系统管理,我们无法干预。而分离之后,JVM 可以控制只执行其中的一个或者多个。因此,绿色线程(Fibers)可以只由 Java 自己实现,且 Java 既有的调度器也可以被重用(例如 Fork-Join 池)。


还有一个有趣的优点是,分离之后,执行过程包含的内容可以作为一个 Java API 开放给每个开发者,并成为 Java 语言的一个特性。

结论

程序通过操作系统线程,每个请求一个线程执行的模式,带来了很大的性能问题,这个问题被响应式编程的方式解决了。但与此同时,也带来了很多开发和维护上的问题,因为响应式编程的代码更难测试和维护。


操作系统中进程的切换带来了很大的损失,绿色线程是一个比较可行的解决方案。但是在 Java1.3 之后就不再支持了,主要原因是它不能在多核系统上执行。Project Loom 是绿色线程的一个变种,这个提议会伴随着 Java 过程执行的理念,作为一个附属品提供给开发者。这部分内容也是参考了其他编程语言的特性,例如 Go 和 Kotlin 的协程。


针对 Project Loom,大家可能比较关心它何时集成到 JDK 中?会对响应式编程产生多大的影响?因为单从性能角度上来看,相比响应式编程,它对于 Java 生态的意义不大。而这些内容,我们还得再观察一段时间,才能有定论。


原文链接:The fight for performance – Is reactive programming the right approach?


2019-04-24 06:2849547

评论 4 条评论

发布
用户头像
真的,用了reactor之后我觉得有些难受,响应式在后端一定会成为一个过渡技术,终极还是协程啊
2019-05-15 21:16
回复
用户头像
callback/promise这种把业务逻辑打散到不同的回调中,而且加上java这个半残的lamda,很难说这就是未来啊。Reactor也是这种。

至于async/await,确实能够写的很爽,debug的话,有debugger配合,问题也不大。但是现有代码要做很多修改才能跑起来,一处用await,处处用await,成本太高。唉~
2019-05-02 23:41
回复
用户头像
如果类似js加入await不就搞定了,方便编写也方便维护
2019-04-24 09:14
回复
作为C#程序员,我也同意,哈哈。
2019-04-24 10:09
回复
没有更多了
发现更多内容

CorelDRAW2023最新绿色免费版矢量图形处理软件

茶色酒

CorelDraw2023 CorelDraw

CleanMyMacX2023永久版Mac系统清理软件

茶色酒

CleanMyMac CleanMyMac X CleanMyMac X2023

架构实战营模块四作业

张Dave

外包学生管理系统架构设计

陈天境

Portraiture2023汉化中文版磨皮滤镜软件下载

茶色酒

Portraiture2023 Portraiture

勿以善小而不为,让AI成为温柔的力量

wood

AI 烟火气 温柔

假如面试官问你Babel的原理该怎么回答

loveX001

JavaScript

《我有一个朋友》首集上线,曹操出行CEO讲述热爱经历

极客天地

常见的Web安全攻击

穿过生命散发芬芳

HTTP 1月月更

朋友圈的架构设计

lory(侯保国)

LinearLayout(线性布局)

芯动大师

Android Studio android布局 LinearLayout weight属性

前端高频面试题集锦

loveX001

JavaScript

网络堵塞?华为云CDN为你带来一站式解决方案

i生活i科技

CDN

2023-01-02:某天,小美在玩一款游戏,游戏开始时,有n台机器, 每台机器都有一个能量水平,分别为a1、a2、…、an, 小美每次操作可以选其中的一台机器,假设选的是第i台, 那小美可以将其变成

福大大架构师每日一题

算法 rust Solidity 福大大

Java高手速成│实战:应用数据库和GUI开发产品销售管理软件(1)

TiAmo

JDBC GUI 数据库·

那些高级前端是如何回答面试题的

loveX001

JavaScript

面试官:说说React-SSR的原理

beifeng1996

React

华为云双十一、双十二系列直播圆满收官,助力企业获数智化发展商机

i生活i科技

CDN

令人头秃的js隐式转换面试题,你能做对吗

loveX001

JavaScript

vivo 服务端监控体系建设实践

vivo互联网技术

云原生 监控 可用性 可观测

架构训练营 模块六

张建闯

架构实战营

osx安装mpd和ncmpcpp

Geek_pwdeic

macos

Thanos 升级顺序分析

耳东@Erdong

Prometheus 版本 Thanos 升级迭代

红海竞争下,华为云CDN凭借什么冲出重围?

i生活i科技

CDN

国产 ETL 工具 etl-engine

weigeonlyyou

postgresql Prometheus Clickhouse MySQL 数据库 InfluxDB Cluster

华为云大数据BI赋能企业数字化发展

i生活i科技

企业数字化转型?华为云CDN为你提供智能加速!

i生活i科技

CDN

5个接口性能提升的通用技巧

JAVA旭阳

Java

模块四-考试试卷存储方案

悟空

存储 考试

能够释放大量Mac内存空间的方法教程

茶色酒

CleanMyMac X CleanMyMac X2023

架构训练营 模块五

张建闯

架构实战营

性能之争:响应式编程真的有效吗?_编程语言_Arne Limburg_InfoQ精选文章