写点什么

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

  • 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:2849392

评论 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
回复
没有更多了
发现更多内容

高性能网关基石——OpenResty

ooooooh灰灰

lua nginx openresty

AI 训练加速原理解析与工程实践分享

百度Geek说

人工智能 机器学习 12 月 PK 榜

模块四课后作业 - 设计千万级学生管理系统的考试试卷存储方案

闲人Eric

架构实战营

华为云桌面,助力企业数字化转型

爱尚科技

持续发力折叠屏生态,OPPO 携手开发者探索大屏应用新体验

科技热闻

研讨会回顾 | 自动化测试“领导者”SmartBear解析软件质量与测试现状调研

龙智—DevSecOps解决方案

软件测试 自动化测试 测试自动化

【观察】融云百幄:为政企数智办公按下“快进键”

融云 RongCloud

融云 数智化 政企 百幄

内网穿透你真的了解吗?

C++后台开发

网络安全 NAT Linux服务器开发 内网 网络穿透

32篇年度最佳AI论文;Python编译器Codon开源;ChatGPT的前世今生

OneFlow

人工智能 深度学习 大模型

使用无代码构建客户门户

间隔

ins快拍下载保存到手机相册最新图文教学!

frank

ins图片下载 ins

基于Seata探寻分布式事务的实现方案

京东科技开发者

大数据 分布式事务 微服务架构 关系型数据库 seata

软件测试面试真题 | 什么是 Redis ? Redis缓存应用场景有哪些?

测试人

redis 软件测试 自动化测试 测试开发

Docker daemon configuration overview(Docker 守护进程配置概览)

独钓寒江

【经验】硬件工程师与PCB槽孔斗智斗勇的故事

华秋PCB

工具 PCB PCB设计 槽孔

有序存储对于高性能的意义

华为云开发者联盟

后端 开发 华为云 12 月 PK 榜

管理有方!华为云数据库为医药行业管理加速

华为云开发者联盟

数据库 后端 华为云 12 月 PK 榜 医药

启科量子或将开源环境部署工具Runtime

启科量子开发者官方号

#开源 #量子计算

小程序游戏的3大分发平台

FinFish

小程序 小游戏 小游戏运营

还原火山引擎A/B测试产品——DataTester 私有化部署实践经验

字节跳动数据平台

大数据 AB testing实战 12 月 PK 榜

代码质量与安全 | 如何将清洁代码标准扩展到整个企业,促进业务上的成功?

龙智—DevSecOps解决方案

代码质量 代码安全

案例丨多元业态管理服务厂商如何走通数字化转型之路

优秀

数字化转型 数字化管理

小游戏开发投放平台及分成政策盘点

Onegun

小游戏 小游戏开发

当云原生网关遇上图数据库,NebulaGraph 的 APISIX 最佳实践

NebulaGraph

图数据库 API网关

多年缓慢成长,近3年野蛮狂飙,谁是这个赛道的王者?

ToB行业头条

融云任杰:激活组织生命力 让听见炮火的人做决策 | TGO专访

融云 RongCloud

专访 程序员‘

版本控制 | SVN,Git与Helix Core,谁的数据管理基础设施更安全?

龙智—DevSecOps解决方案

git svn 版本控制 版本控制系统

语音生成领域模型又填一名猛将

felix

深度学习 语音合成 TTS

华为云桌面,让企业随时随地开启云上办公

爱尚科技

华为云桌面解决方案,打造安全高效办公新体验

科技怪授

云桌面

PHP转Go之后,我又开始研究机器学习和自动驾驶了。

王中阳Go

自动驾驶 Apollo

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