Java真的要没落了?

本文涉及的产品
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
性能测试 PTS,5000VUM额度
容器服务 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搭建和管理企业级网站应用
相关文章
|
10月前
|
Kubernetes Java 程序员
Java会因容器技术盛行而没落吗?
Java会因容器技术盛行而没落吗?
74 0
|
SQL 前端开发 JavaScript
java是没落了还是更活跃了
你经常会听到“Java 开始没落了”的说法,所有人都应该尽快切换到 Go ,python等更先进的语言。他们说这对他们来说会拥有更多发展空间及就业机会,但对每个人都将要放弃的熟悉的编码语言来说付出的代价是非常大的。但这都是真的吗?
151 0
|
8天前
|
安全 Java 数据处理
Java并发编程:解锁多线程的潜力
在数字化时代的浪潮中,Java作为一门广泛使用的编程语言,其并发编程能力是提升应用性能和响应速度的关键。本文将带你深入理解Java并发编程的核心概念,探索如何通过多线程技术有效利用计算资源,并实现高效的数据处理。我们将从基础出发,逐步揭开高效并发编程的面纱,让你的程序运行得更快、更稳、更强。
|
7天前
|
Java 开发者
奇迹时刻!探索 Java 多线程的奇幻之旅:Thread 类和 Runnable 接口的惊人对决
【8月更文挑战第13天】Java的多线程特性能显著提升程序性能与响应性。本文通过示例代码详细解析了两种核心实现方式:Thread类与Runnable接口。Thread类适用于简单场景,直接定义线程行为;Runnable接口则更适合复杂的项目结构,尤其在需要继承其他类时,能保持代码的清晰与模块化。理解两者差异有助于开发者在实际应用中做出合理选择,构建高效稳定的多线程程序。
28 7
|
6天前
|
安全 Java 数据库
一天十道Java面试题----第四天(线程池复用的原理------>spring事务的实现方式原理以及隔离级别)
这篇文章是关于Java面试题的笔记,涵盖了线程池复用原理、Spring框架基础、AOP和IOC概念、Bean生命周期和作用域、单例Bean的线程安全性、Spring中使用的设计模式、以及Spring事务的实现方式和隔离级别等知识点。
|
6天前
|
存储 监控 安全
一天十道Java面试题----第三天(对线程安全的理解------>线程池中阻塞队列的作用)
这篇文章是Java面试第三天的笔记,讨论了线程安全、Thread与Runnable的区别、守护线程、ThreadLocal原理及内存泄漏问题、并发并行串行的概念、并发三大特性、线程池的使用原因和解释、线程池处理流程,以及线程池中阻塞队列的作用和设计考虑。
|
3天前
|
存储 缓存 安全
深度剖析Java HashMap:源码分析、线程安全与最佳实践
深度剖析Java HashMap:源码分析、线程安全与最佳实践
|
9天前
|
消息中间件 Java 大数据
"深入理解Kafka单线程Consumer:核心参数配置、Java实现与实战指南"
【8月更文挑战第10天】在大数据领域,Apache Kafka以高吞吐和可扩展性成为主流数据流处理平台。Kafka的单线程Consumer因其实现简单且易于管理而在多种场景中受到欢迎。本文解析单线程Consumer的工作机制,强调其在错误处理和状态管理方面的优势,并通过详细参数说明及示例代码展示如何有效地使用KafkaConsumer类。了解这些内容将帮助开发者优化实时数据处理系统的性能与可靠性。
37 7
|
6天前
|
安全 Java
Java模拟生产者-消费者问题。生产者不断的往仓库中存放产品,消费者从仓库中消费产品。其中生产者和消费者都可以有若干个。在这里,生产者是一个线程,消费者是一个线程。仓库容量有限,只有库满时生产者不能存
该博客文章通过Java代码示例演示了生产者-消费者问题,其中生产者在仓库未满时生产产品,消费者在仓库有产品时消费产品,通过同步机制确保多线程环境下的线程安全和有效通信。
|
5天前
|
缓存 前端开发 JavaScript
一篇文章助你搞懂java中的线程概念!纯干货,快收藏!
【8月更文挑战第11天】一篇文章助你搞懂java中的线程概念!纯干货,快收藏!
13 0
一篇文章助你搞懂java中的线程概念!纯干货,快收藏!