Java真的要没落了?

本文涉及的产品
性能测试 PTS,5000VUM额度
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
简介: 最近也收到很多后端同学的提问,为什么Go的web框架速度还不如Java?为什么许多原本的 Java 项目都试图用 go 进行重写开源?Java会不会因为容器的兴起而没落?Java这个20多年的后端常青树难道真的要走下坡路了?橙子邀请了淘系技术部的同学对以上问题进行解答,也欢迎大家一起交流。

原创 风弈、空蒙、玄力 淘系技术  2月26日


最近也收到很多后端同学的提问,为什么Go的web框架速度还不如Java?为什么许多原本的 Java 项目都试图用 go 进行重写开源?Java会不会因为容器的兴起而没落?Java这个20多年的后端常青树难道真的要走下坡路了?橙子邀请了淘系技术部的同学对以上问题进行解答,也欢迎大家一起交流。


Q:为什么Go的web框架速度还不如Java?



风弈:华山论剑,让我们索性把各框架的性能分析跑一下再说话。


各种框架的应用场景不同导致其优化侧重点不同,下面我们展开详细分析。


 http server 概述

首先描述一下一个简单的 web server 的请求处理过程:


image.png


Net 层读取数据包后经过 HTTP Decoder 解析协议,再由 Route 找到对应的 Handler 回调,处理业务逻辑后设置相应 Response 的状态码等,然后由 HTTP Encoder 编码相应的 Response,最后由 Net 写出数据。


而 Net 之下的一层由内核控制,虽然也有很多优化策略,但这里主要比较 web 框架本身,那么暂时不考虑 Net 之下的优化。


看了下 techempower 提供的压测框架源码,各类框架基本上都是基于 epoll 的处理,那么各类框架的性能差距主要体现在上述这些模块的性能了。


▐  关于各类压测的简述


我们再看 techempower 的各项性能排名,有JSON serialization, Single query, Multiple queries, Cached queries, Fortunes, Data updates 和 Plaintext 这几大类的排名。


其中 JSON serialization 是对固定的 Json 结构编码并返回 (message: hello word), Single query 是单次 DB 查询,Multiple queries 是多次 DB 查询,Cached queries 是从内存数据库中获取多个对象值并以json返回,Fortunes 是页面渲染后返回,Data updates 是对 DB 的写入,Plaintext 是最简单的返回固定字符串。


这里的 json 编码,DB 操作,页面渲染和固定字符串返回就是相应的业务逻辑,当业务逻辑越重(耗时越大)时,则相应的业务逻辑逐渐就成为了瓶颈,例如 DB 操作其实主要是在测试相应 DB 库和 DB 本身处理逻辑的性能,而框架本身的基础功能消耗随着业务逻辑的繁重将越来越忽略不计(Round 19 中物理机下 Plaintext 下的 QPS 在七百万级,而 Data updates 在万级别,相差百倍以上),所以这边主要分析 Json serialization 和 Plaintext两种相对能比较体现出框架本身 http 性能的排名。


Round 19 Json serialization 中 Java 性能最高的框架是 firenio-http-lite (QPS: 1,587,639),而 Go 最高的是 fasthttp-easyjson-prefork(QPS: 1,336,333),按照这里面的数据是Java性能高。


image.png


从 fasthttp-easyjson-prefork 的 pprof 看除了 read 和 write 外, json (相当于 Business logic) 占了 4.5%,fasthttp 自身(HTTP Decoder, HTTP Encoder, Router)占了 15%,仅看 Json serialization 似乎会有一种 Java 比 Go 性能高的感觉。



image.png


那我们继续把业务逻辑简化,看一下 Plaintext 的排名,Plaintext 模式其实是在使用 HTTP pipeline 模式下压测的,在 Round 19 中 Java 和 Go 已经几乎一样的 QPS 了,在 Round 19 之后的一次测试中 gnet 已经排在所有语言的第二,但是前几个框架QPS其实差别很微小。


这时候其实主要瓶颈都在 net 层,而 go 官方的 net 库包含了处理 goroutine 相关的逻辑,像 gonet 之类的直接操作 epoll 的会少一些这方面的消耗,Java 的 nio 也是直接操作的 epoll 。


image.png


拿了 gnet 的测试源码跑了下压测,看到 pprof 如下,其实这里 gnet 还有更进一步的性能优化空间:time.Time.AppendFormat 占用 30% CPU。



image.png


可以使用如下提前 Format ,允许减少获取当前时间精度的情况下大幅减少这部分的消耗。



var timetick atomic.Value
func NowTimeFormat() []byte {
  return timetick.Load().([]byte)
}
func tickloop() {
  timetick.Store(nowFormat())
  for range time.Tick(time.Second) {
    timetick.Store(nowFormat())
  }
}
func nowFormat() []byte {
  return []byte(time.Now().Format("Mon, 02 Jan 2006 15:04:05 GMT"))
}
func init() {
  timetick.Store(nowFormat())
  go tickloop()
}


这样优化后接下来的瓶颈在于 runtime 的内存分配,是由于这个压测代码中还存在下面的部分没有复用内存:



image.png


image.png


其实 gnet 本身的消耗已经做到非常小了,而 c++ 的 ulib 也是类似这样使用的非常简单的 HTTP 编解码操作来压测。


▐  分析


对于这里面测试的框架,影响因素主要如下:

1、直接基于epoll的简单http: 没有完整的 http decoder 和 route (如gnet, ulib 直接简单的字节拼接,固定的路由 handler回调)

2、zero copy 和内存复用: 内部处理字节的 0 拷贝(go 官方 http 库为了减少开发者的出错概率,没有使用 zero copy,否则开发者可能在无意中引用了已经放回 buff 池内的的数据造成没有意识到的并发问题等等),而内存复用,大部分框架或多或少都已经做了。

3、prefork:注意到 go 框架中有使用了 prefork 进程的方式(比如 fasthttp-prefork),这是 fork 出多个子进程,共享同一个 listen fd,且每个进程使用单核但并发(1 个 P)处理的逻辑可以避免 go runtime 内部的锁竞争和 goroutine 调度的消耗(但是 go runtime 中为了并发和 goroutine 调度而存在的相关“无用”代码的消耗还是会有一些)

4、语言本身的性能差异


对于第一点,其实简化了各种编解码和路由之后,虽然提高了性能,但是往往会降低框架的易用性,对于一般的业务而言,不会出现如此高的QPS,同时选择框架的时候往往还需要考虑易用性和可扩展性等,同时还需要考虑到公司内部原有中间件或者 SDK 所使用的框架集成复杂度。


对于第二点,如果是作为一个网络代理而言,没有业务方的开发,往往可以使用真正的完全 zero copy,但是作为业务开发框架提供出去的话是需要考虑一定的业务出错概率,往往牺牲一部分性能是划算的。


第三点 prefork , java netty 等是直接对于线程操作,可以更加定制化的优化性能,而 go 的 goroutine 需要的是一个通用协程,目的是降低编写并发程序的难度,在这个层次上难免性能比不上一个优化的非常出色的 Java 基于线程操作的框架;但是直接操作线程的话需要合理控制好线程数,这是个比较头疼的调优问题(特别是对于新手来说),而 goroutine 则可以不关心池子的大小,使得代码更加优雅和简洁,这对于工程质量保障其实是一个提升。另外这里存在 prefork 是由于 go 没法直接操作线程,而 fasthttp 提供了 prefork 的能力,使用多进程方式来对标 Java 的多线程来进一步提高性能。


第四点,语言本身来说 Java 还是更加的成熟,包括 JVM 的 Jit 能力也使得在热代码中和 Go 编译型语言的差异不大,何况 Go 本身的编译器还不是特别成熟,比如逃逸分析等方面的问题, Go 本身的内存模型和 GC 的成熟度也比不上 Java。还有很重要的一点,Go 的框架成熟度和 Java 也不在一个级别,但相信这些都会随着时间逐步成熟。


总之,对于这个框架压测数据意义在于了解性能天花板,判断继续优化的空间和ROI (投入产出比)。具体选择框架还是要根据使用场景,性能,易用性,可扩展性,稳定性以及公司内部的生态等作出选择,语言和性能分别只是其中一个因素。


各种框架的应用场景不同导致其优化侧重点不同,如 spring web 为了易用性,可扩展性和稳定性而牺牲了性能,但它同样拥有庞大的社区和用户。再比如 Service Mesh Sidecar 场景下 Go 的天然并发编程上的优势,以及小内存占用,快速启动,编译型语言等特点使得比 Java 更加适合。


(附:其实我使用上述代码和 dockerfile 构建,并且使用同样的压测脚本,在阿里云4核独享机器测试下 go fasthttp-easyjson-prefork 框架 Json serialization 的性能要高于 Java wizzardo-http 和 firenio-http-lite 30% 以上且延迟更低的,这可能和内核有关)。



Q:为什么许多原本的 Java 项目都试图用 go 进行重写开源?



空蒙:Java还是go核心是生态问题。


生态发展会经历起步、发展、繁荣、停滞、消亡几个阶段,Java目前至少还在繁荣阶段,go还是发展阶段,不同阶段在开发人员的数量与质量、开源能力丰富性、工程配套上是有巨大差异的,go是在狂补这三块。另外不同公司还有个公司内部小生态的所处阶段问题,也会影响技术的选型判断。


现阶段go的火热,很大因素是云原生裹挟着大家往前,k8s operator go语言实现的自带光环,各种中间件能力在下沉与k8s融合,带动着一波基础中间件能力的go实现潮头,但基础的中间件能力相对是有限集合,如RPC、config、messagequeue等,这些中间件能力,以及云原生k8s对上层业务而言应该做的是开发语言的中立性,让业务基于公司的小生态和整个语言技术的大生态去抉择,如果硬逼着业务也用go语言开发那就是耍流氓了。


总结来说,基础中间件能力需要与k8s的融合需要会有go语言的动力,但整个开源生态其他能力并不见得是必须;业务开发依据公司生态和技术大生态选择最合适的开发语言,不要盲目的追从而导致在人、开源能力、工程配套上的尴尬。go语言能否在业务研发上发力,还有待其生态的进一步发展。



Q:Java会不会因为容器的兴起而没落?



玄力:近年来以容器为核心的云原生技术,让服务端部署的伸缩性、可协作性,得到巨大的提升。使得原本开发语言本身选取的重要性,有一定程度的减弱。但不妨碍Java语言本身继续保持活力。


毕竟,作为研发而言,研发输出效率也是蛮关键的一个考量点,得益于Java完善而有庞大的开发者生态,提供了比大多数语言都要丰富的类库/框架,也得益于Java强大的IDE工具,开发起来往往事半功倍。


而且,Java自身也有一些变种语言(如Scala),也是在朝更灵活更好用的方向发展;


另一方面,在大数据领域,Java仍在大放异彩,我们所熟知的 ES、Kafka、Spark、Hadoop。


我们评估和预测一个技术的生命力的时候,往往不会孤立地只看技术本身,同时也会结合它背后的整个生态。一个具有顽强生命力的技术的背后往往都有一个成熟的生态体系支撑,上面也提到Java在多个领域都有完善而庞大的生态,因此,我们认为Java的生命力仍然是顽强的。


但由于众所周知的原因,客观来讲,Java本身在使用上,也会有一定的限制性。并且,在容器场景中,Java进程的内存配置,是需要小心谨慎的。


总的来说,Java的地位仍难撼动,而且在云原生场景中,也仍绽放着生命力。

相关实践学习
通过Ingress进行灰度发布
本场景您将运行一个简单的应用,部署一个新的应用用于新的发布,并通过Ingress能力实现灰度发布。
容器应用与集群管理
欢迎来到《容器应用与集群管理》课程,本课程是“云原生容器Clouder认证“系列中的第二阶段。课程将向您介绍与容器集群相关的概念和技术,这些概念和技术可以帮助您了解阿里云容器服务ACK/ACK Serverless的使用。同时,本课程也会向您介绍可以采取的工具、方法和可操作步骤,以帮助您了解如何基于容器服务ACK Serverless构建和管理企业级应用。 学习完本课程后,您将能够: 掌握容器集群、容器编排的基本概念 掌握Kubernetes的基础概念及核心思想 掌握阿里云容器服务ACK/ACK Serverless概念及使用方法 基于容器服务ACK Serverless搭建和管理企业级网站应用
相关文章
|
11月前
|
Kubernetes Java 程序员
Java会因容器技术盛行而没落吗?
Java会因容器技术盛行而没落吗?
75 0
|
SQL 前端开发 JavaScript
java是没落了还是更活跃了
你经常会听到“Java 开始没落了”的说法,所有人都应该尽快切换到 Go ,python等更先进的语言。他们说这对他们来说会拥有更多发展空间及就业机会,但对每个人都将要放弃的熟悉的编码语言来说付出的代价是非常大的。但这都是真的吗?
161 0
|
8天前
|
监控 Java 调度
【Java学习】多线程&JUC万字超详解
本文详细介绍了多线程的概念和三种实现方式,还有一些常见的成员方法,CPU的调动方式,多线程的生命周期,还有线程安全问题,锁和死锁的概念,以及等待唤醒机制,阻塞队列,多线程的六种状态,线程池等
63 6
【Java学习】多线程&JUC万字超详解
|
1天前
|
Java 调度 开发者
Java并发编程:深入理解线程池
在Java的世界中,线程池是提升应用性能、实现高效并发处理的关键工具。本文将深入浅出地介绍线程池的核心概念、工作原理以及如何在实际应用中有效利用线程池来优化资源管理和任务调度。通过本文的学习,读者能够掌握线程池的基本使用技巧,并理解其背后的设计哲学。
|
1天前
|
缓存 监控 Java
Java中的并发编程:理解并应用线程池
在Java的并发编程中,线程池是提高应用程序性能的关键工具。本文将深入探讨如何有效利用线程池来管理资源、提升效率和简化代码结构。我们将从基础概念出发,逐步介绍线程池的配置、使用场景以及最佳实践,帮助开发者更好地掌握并发编程的核心技巧。
|
3天前
|
缓存 监控 Java
java中线程池的使用
java中线程池的使用
|
2天前
|
算法 Java 数据处理
Java并发编程:解锁多线程的力量
在Java的世界里,掌握并发编程是提升应用性能和响应能力的关键。本文将深入浅出地探讨如何利用Java的多线程特性来优化程序执行效率,从基础的线程创建到高级的并发工具类使用,带领读者一步步解锁Java并发编程的奥秘。你将学习到如何避免常见的并发陷阱,并实际应用这些知识来解决现实世界的问题。让我们一起开启高效编码的旅程吧!
|
7天前
|
存储 Java 程序员
优化Java多线程应用:是创建Thread对象直接调用start()方法?还是用个变量调用?
这篇文章探讨了Java中两种创建和启动线程的方法,并分析了它们的区别。作者建议直接调用 `Thread` 对象的 `start()` 方法,而非保持强引用,以避免内存泄漏、简化线程生命周期管理,并减少不必要的线程控制。文章详细解释了这种方法在使用 `ThreadLocal` 时的优势,并提供了代码示例。作者洛小豆,文章来源于稀土掘金。
|
4天前
|
Java 开发者
Java中的多线程编程基础与实战
【9月更文挑战第6天】本文将通过深入浅出的方式,带领读者了解并掌握Java中的多线程编程。我们将从基础概念出发,逐步深入到代码实践,最后探讨多线程在实际应用中的优势和注意事项。无论你是初学者还是有一定经验的开发者,这篇文章都能让你对Java多线程有更全面的认识。
14 1
|
12天前
|
安全 Java 程序员
Java编程中实现线程安全的策略
【8月更文挑战第31天】在多线程环境下,保证数据一致性和程序的正确运行是每个程序员的挑战。本文将通过浅显易懂的语言和实际代码示例,带你了解并掌握在Java编程中确保线程安全的几种策略。让我们一起探索如何用同步机制、锁和原子变量等工具来保护我们的数据,就像保护自己的眼睛一样重要。