最近也收到很多后端同学的发问,为什么Go的web框架速度还不如Java?为什么许多本来的 Java 我的项目都试图用 go 进行重写开源?Java会不会因为容器的衰亡而败落?Java这个20多年的后端常青树难道真的要走下坡路了?橙子邀请了淘系技术部的同学对以上问题进行解答,也欢送大家一起交换。
Q:为什么Go的web框架速度还不如Java?
风弈:华山论剑,让咱们索性把各框架的性能剖析跑一下再谈话。
各种框架的利用场景不同导致其优化侧重点不同,上面咱们开展详细分析。
http server 概述
首先形容一下一个简略的 web server 的申请处理过程:
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性能高。
从 fasthttp-easyjson-prefork 的 pprof 看除了 read 和 write 外, json (相当于 Business logic) 占了 4.5%,fasthttp 本身(HTTP Decoder, HTTP Encoder, Router)占了 15%,仅看 Json serialization 仿佛会有一种 Java 比 Go 性能高的感觉。
那咱们持续把业务逻辑简化,看一下 Plaintext 的排名,Plaintext 模式其实是在应用 HTTP pipeline 模式下压测的,在 Round 19 中 Java 和 Go 曾经简直一样的 QPS 了,在 Round 19 之后的一次测试中 gnet 曾经排在所有语言的第二,然而前几个框架QPS其实差异很渺小。
这时候其实次要瓶颈都在 net 层,而 go 官网的 net 库蕴含了解决 goroutine 相干的逻辑,像 gonet 之类的间接操作 epoll 的会少一些这方面的耗费,Java 的 nio 也是间接操作的 epoll 。
拿了 gnet 的测试源码跑了下压测,看到 pprof 如下,其实这里 gnet 还有更进一步的性能优化空间:time.Time.AppendFormat 占用 30% CPU。
能够应用如下提前 Format ,容许缩小获取以后工夫精度的状况下大幅缩小这部分的耗费。
var timetick atomic.Valuefunc 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 的内存调配,是因为这个压测代码中还存在上面的局部没有复用内存:
其实 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的位置仍难撼动,而且在云原生场景中,也仍绽开着生命力。
参考: 《2020最新Java根底精讲视频教程和学习路线!》
链接:https://juejin.cn/post/693560...