目录

Java真的要没落了

Java真的要没落了?

https://i-blog.csdnimg.cn/blog_migrate/f0ad0be2d2f3a27b1a38faea6ca77cf6.gif

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

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


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

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

http server 概述

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

https://i-blog.csdnimg.cn/blog_migrate/7fb8cd27ef6755493d3f63df677e8aa9.jpeg

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性能高。

https://i-blog.csdnimg.cn/blog_migrate/420a859025b07bbba83f6acbb877e6be.jpeg

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

https://i-blog.csdnimg.cn/blog_migrate/cf02bb30e2c676f8ddd094ec2d44fb7c.jpeg

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

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

https://i-blog.csdnimg.cn/blog_migrate/47101469a3b37bb6a14919420e8e7f25.jpeg

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

https://i-blog.csdnimg.cn/blog_migrate/5a244e11446842cbc5f2a52bea5e166f.jpeg

可以使用如下提前 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 的内存分配,是由于这个压测代码中还存在下面的部分没有复用内存:

https://i-blog.csdnimg.cn/blog_migrate/bc61a2d507b9eb93f0a4618881e31929.jpeg https://i-blog.csdnimg.cn/blog_migrate/1c9617537e0421594874067349ba3c67.jpeg