当前位置: 首页 > 后端技术 > Java

这波性能优化太炸了!

时间:2023-04-01 17:13:01 Java

你好,我是why。不,不是我。我比他年轻,也比他帅。这就是今天文章的主角。他的名字叫BrettWooldridge,你可能不认识他。不过我给你看他的github截图,你肯定知道他写的开源项目:看到了吗?他是著名的HikariCP的父亲。如果你看一下他的github个人资料,就会发现其中的文字非常情绪化:一位天使的父亲坠落到地球并以某种方式进入了我的生活。天使坠落人间的父亲,她不知不觉走进了我的生活。照片上应该是他的孩子,他笑起来和蔼可亲,就像在他身边的老父亲一样。文章开头的图片是我从这篇报道中找到的:https://blog.jooq.org/2017/02...第一个问的问题是这样的:你创建了最流行的连接池之一,HikariCP.那么是什么让您的图书馆如此受欢迎?下一节,我会以第一人称的视角,告诉大家这位老人是如何回答这个问题的。为什么要写HikariCP?什么,你问我为什么写HikariCP?天呐,不就是还没遇到好人吗。几年前写代码的时候,需要用到数据库连接池,于是和大多数开发者一样,为浏览器编程,在网上找了一个开源的连接池,就用上了。别告诉我,它看起来还不错。但是后来在项目上做性能测试的时候,慢慢发现这个pool不太好用,老是遇到死锁,连接状态不正确等问题。我想知道这东西是不是在作弊?但是当时使用的连接池是开源的。本着开源的精神,我只是想把代码拉下来看看。你能帮我修一下吗。结果一打开代码,好家伙,代码量好大啊,至少比我预想的多了几千行。即使代码太多,也可以耐心看完。神奇的是代码逻辑。我去排查死锁问题,结果发现锁是一个一个设置的。有时在一个方法中获取了锁,我就是找不到释放它的地方。终于,在相隔数十万里的地方,看到了解除锁定的地方。我当时大概是这样的:因为我知道,我没办法在代码的角落里找到死锁潜伏在什么地方。即使我解决了当前的问题,按照项目的写法,迟早会遇到其他问题。所以我欣然接受并决定……在网上再找一个。这次我学会了如何做人。找到新的连接池后,首先阅读了它的代码。因为怕死锁,所以特别注意了关于锁的部分。新发现的连接池锁语义确实更清晰了,但是代码量还是比我预想的多一倍多。除此之外,我看过的所有连接池都以各种方式违反了JDBC契约。例如,我发现的最常见的问题之一就是这个。当链接用完并放回池中时。有些pool并没有清理这个链接中的消息,比如自动提交、事务隔离级别等,导致下一个消费者再次获取这个链接时出现“脏”链接。我在想:真的吗?这就是Java生态中连接池的现状?不,我要自己做。因此,出于需要和沮丧,我创建了HikariCP。回到最初的问题。上面说了,在我写HikariCP之前,其实已经有很多成熟的连接池了,那么HikariCP是怎么火起来的呢?在我看来,如果我专注于正确性和可靠性,那不是一个好的卖点,因为我认为它是必备的东西。所以我专注于概括性能。在我的社交媒体上宣传它。2015年的某个时候,Wix工程团队写了一篇关于使用HikariCP的博客。这一波我直接弹射起飞。HikariCP也算是进入了大家的视野。最后,我希望随着时间的推移,会有更多的用户将正确性和可靠性并重,否则性能将毫无意义。就我而言,我打算写更多关于HikariCP这些方面的文章。为什么性能这么棒?如前所述,HikariCP的卖点在于其强大的性能。那么它的性能为何如此牛逼呢?其实答案写在HikariCP的github主页上:https://github.com/brettwold...在进入我们如何做之前,先简单说一下这个项目的名字。可以看到一个大大的汉字:光。关于名字的由来,其实在前述报道中提到过:HikariCP,译为“光”,英文,在HikariCP语境中,是一个双关语。在这个项目中,“轻”不仅意味着速度快,还意味着代码量小。Hikari发音为Hi-ka-lee。每个人都记住这一点。记得有一次面试,面试官提到了这个连接池,但是他不知道怎么读。他说:是H开的CP末尾的连接池,忘记怎么读了。但是我马上就反应过来了。我说:嗯,我知道你说的是哪个连接池,你继续说。其实我当时也不知道怎么读,所以很尴尬。好吧,接下来我们就来看看为什么性能如此牛逼。作者把答案写在github中:https://github.com/brettwold...首先,这篇文章的标题很有意思:wathmeanisDowntheRabbitHole?直译是“在兔子洞里”。我觉得没那么简单,于是查了一下:哦,downtherabbithole原来是比喻冒险进入未知世界。出自名著《爱丽丝梦游仙境》。一般我们用downtherabbithole来形容陷入越来越奇怪、迷惑或意想不到的境地,一件事促使另一件事接二连三地发生,于是越陷越深,无路可逃的场景。一点英语俚语,适合所有人。知道标题的意思后,看完作者写的文章,再看看这个“兔子洞”的标题,你会发现:真是贴切。看完全文理解后,发现作者之所以要这么快的表达出来有四个原因:字节码级优化——尝试使用JIT的内联方式级优化——使用改造后的FastList而不是ArrayList代码级优化——使用无锁的ConcurrentBag让我们一一来看。字节码级优化文章开头,作者说:我这次是在字节码操作,就问你牛不牛逼。关键点简单翻译:为了让HikariCP更快,我进行了字节码级别的优化。我拿出了我知道的所有技巧来利用JIT优化来帮助您。我研究了编译器的字节码输出,甚至JIT的汇编输出,以将关键程序限制在JIT的内联阈值以下。这个地方作者提到了JIT的inlineoptimization。什么是内联?内联实际上是一个动作。选择一个被调用的方法并将其内容复制到被调用的地方。举个简单的例子,假设代码是这样的:intresult=add(a,b);privateintadd(intx,inty){returnx+y;}那么经过JIT内联优化后,代码就是了会变成这样:intresult=a+b;这样就节省了调用add方法的开销。内联,也就是优化,为其他优化方法打下了很好的基础,所以除了上面写的例子,还有很多更高级的表达方式,比如逃逸分析,循环展开,锁消除:.png)那么调用的开销到底是多少?我觉得无外乎这几个步骤:首先,设置方法调用需要传递的参数,对吧?有了参数,我们是不是还要查询调用哪个方法,对吧?然后,如果有局部变量或评估等方法,您必须创建一个新的调用堆栈框架,创建一个新的运行时数据结构,对吗?最后,可能需要返回一个结果给调用者,对吧?有朋友会说,至于吗?成本看起来并不多,是吗?是的,它确实不大,但是当一个小的优化点乘以一个巨大的调用量时,最终的结果是非常可观的。我想每个人都明白这个道理。作者在文中也表示:HikariCP包含很多微优化,单独衡量几乎无法衡量,但结合起来可以提升整体性能。即使在数百万次调用中,优化级别也以毫秒为单位进行衡量。也许这就是老板。在我看来,对性能的极致追求无外乎如此。接下来说说另一个字节码级别的优化:invokevirtualvsinvokestatic。我觉得这波优化简直就是大气。作者举了一个例子。之前,Connection、Statement、ResultSet的代理对象都是通过单例工厂方法获取的。类似于:ROXY_FACTORY是一个静态字段。上面代码的字节码大致是这样的:通过字节码可以看到先有一个getstatic调用,获取静态字段PROXY_FACTORY的值。还有一个invokevirtual指令调用,对应ProxyFactory实例的getProxyPreparedStatement()方法:15:invokevirtual#69//Methodcom/zaxxer/hikari/proxy/ProxyFactory.getProxyPreparedStatement:(Lcom/zaxxer/hikari/proxy/ConnectionProxy;Ljava/sql/PreparedStatement;)Ljava/sql/PreparedStatement;这个地方还有优化的空间吗?作者将代码修改为:ProxyFactory是通过Javassist生成的。所以你看ProxyFactory的源码,全是空实现。其真正的实现逻辑是源码对应的类,这里不再详细展示。有兴趣的可以下去看看:com.zaxxer.hikari.util.JavassistProxyFactory然后,把getProxyPreparedStatement方法做成静态的。那么字节码就变成了这样:神奇的事情发生了:getstatic指令消失了,invokevirtual被invokestatic调用代替,更容易被JVM优化。最后,也许乍一看没有注意到,堆栈大小从5减少到4。这是因为在invokevirtual的情况下,ProxyFactory实例是隐式传递到栈上的(也就是this对象),调用getProxyPreparedStatement()时多了一次出栈操作。第1点和第3点应该没问题。每个人都能明白是怎么回事。但是这第二点:invokevirtual被invokestatic调用代替,更容易被JVM优化。说实话,我第一眼看到的时候大概是这样的:为什么?我还记得invokevirtual和invokestatic的作用。但是invokestatic的性能好一点吗?所以我就带着这个问题翻了一下《深入理解JVM虚拟机》,但是并没有直接找到答案。但是还是有意想不到的收获。我刚刚写了这篇文章:《报告!书里有个BUG》不然你觉得我为什么突然翻到这本书的这一部分?虽然书上没有直接写答案,但是相关部分有这么一段话:我理解是invokevirtual指令,需要查询虚方法表来确定方法的直接引用。而invokestatic可以在类加载时从符号引用转换为直接引用。这样看来,invokestatic确实比invokevirtual好。那么问题又来了。类加载的过程是怎样的?加载、验证、准备、解析、初始化。invokestatic在哪个进程做事情?绝对是解析阶段,我的朋友们。解析阶段是JVM将常量池中的符号引用替换为直接引用的过程。扯开,再说一遍。以上只是我的猜测。相信不是只有我看了作者的《兔子洞》一文后对invokevirtualvsinvokestatic存疑。所以,我去看看。果然。(不好意思,我确实找了很久。)找到这个链接,前半部分和我的问题一模一样:https://github.com/brettwold...作者的回复如下:后一段加起来很容易理解。前面提到,静态调用的栈少了一个,运行时的push/pull操作少了一个,进一步提升了性能。主要是前一段,有点难懂。他说:简而言之,当JVM进行内联调用时,即使是单态内联,也必须安装一个陷阱(trap),以防止出现另一个实现,将调用变成多态。这个陷阱的设置和清除给调用增加了一点开销。你好吗,困惑与否?其实他说的,我个人的理解,是在讲Java的动态调度,讲的是JVM的CHA(ClassHierarchyAnalysis,类型继承关系分析)技术。答案写在《深入理解Java虚拟机(第三版)》的417页,翻过来:你非要问我证据是什么,所以这两个字相呼应,这是什么巧合?invokevirtual调用虚方法。按照书上的说法,上面说的trap其实就是这里的“逃生门”:andThistrapsettingandclearingaddsslightlymoreoverheadtotheinvocation(设置和清除这个trapaddsslightlymoreoverheadtotheinvocation)Alittleoverhead)这句话其实对应的是:现在你知道为什么invokestatic对于JVM来说比invokevirtual更容易优化了吧?优化是指内联。invokestatic调用静态方法。对于非虚方法,JVM可以直接进行内联。这种内联是100%安全的。但是,invokevirtual调用虚拟方法。对于虚方法的内联,必须使用CHA机制来设置逃生门。虽然都是内联的,但这并没有多消耗一点性能。内联已经是一种性能优化,让代码更好的内联,性能优化的优化。该波在大气中运行。好了,以上就是字节码层面的优化,接下来我们来看代码层面的优化。代码层面的优化代码层面最著名的就是FastList替代ArrayList。首先,我去看了项目的提交记录。2014年1月15日,作者投稿:后半部分的言论大家应该很熟悉了,我们已经讲过了。上一篇是用FastList替换ArrayList的提交。每次调用get(intindex)时,JavaArrayList都会进行一次范围检查。在HikariCP项目中,可以保证索引在正确的范围内,所以这个检查没有意义,所以去掉:再比如ArrayList的remove(Objecto)方法是从头扫描到尾.假设要删除最后一个元素,需要遍历整个数组。巧合的是,比如HikariCP的Statement,按照我们的编码习惯,删除(close)应该先删除最后一个。因此,FastList对remove(Objectelement)方法进行了优化,将查找顺序改为逆向查找:总体来说,FastList的优化点就是上面提到的get和remove方法。接下来看另外一个代码层面的优化:作者列举了几点:一个无锁设计线程本地缓存窃取队列直接切换优化作者介绍的很简单,但是里面的东西还是很多的。一个重要的技巧是ConcurrentBag通过ThreadLocal进行连接预分配。通过ThreadLocal在一定程度上避免了共享资源的竞争。自己看代码的话,主要是看add(空闲连接加入队列)、borrow(获取连接)、requite(释放连接)方法。网上也有很多相应的文章来介绍。如果您有兴趣,可以了解更多相关信息。我不会在这里写。哦,你不想看其他文章,只想等我告诉你?好吧,先欠着,欠着。偷懒,文章太长了,没人会看。在写文章的过程中,也看到了这样一个问题,觉得有点意思,写一下。https://github.com/brettwold...有小弟说:你好,我觉得你对Java数据库池的分析很有价值。偶然发现了阿里巴巴的这个druid线程池(号称是Java最快的数据库池!)。从我的快速浏览来看,它似乎有一些很酷的功能。对此有任何想法。谢谢。HikariCP的作者很快做出回应:至少在他的基准测试中,Druid在获取和返回连接方面是最慢的,在创建和关闭语句方面排名第三。他们wiki中的基准页面没有显示他们正在运行的配置,但我怀疑他们禁用了借用的测试。虽然我不会说这是“作弊”,但这并不是我在生产中使用它的方式。据我所知,他们也不提供测试的源代码。这有点意思。虽然我不会说这是“坑爹”,但我说的就是:有一句话不知道该不该说。然后它接着说了出来。接着,另一位吃瓜网友表示:Druid的设计理念是专注于数据访问行为的监控和增强(比如数据库自动切片)。它提供了一个SQL解析器来分析用户的SQL查询,并收集大量数据用于监控。所以,如果你需要JDBC监控方案,可以试试Druid。HikariCP的作者也表示这句话没有错,但是他强调他的HikariCP也给监控留了个坑:这是一个有效的观点。我想指出的是,HikariCP也提供监控数据,但提供的指标是“池级别”指标,而不是特定于查询执行时间等。以上对话均发生在2015年1月。但是一年半之后,也就是2016年7月26日,这个问题被另外一个人激活了:文少,谁来了?此人是德鲁伊的父亲之一,世人都称他为邵文。或许你不知道文少,或许你不知道文少写的druid,但你一定知道文少的另一部杰作:问题有点多,但不妨碍别人当高??手。可以直接端茶:首先文少说:如果配置了maxWait属性,druid会使用公平锁,降低性能。至于为什么会这样,是因为在生产环境中遇到了一些问题,也就是设计。然后他接着提到淘宝:点开链接,标题是这样的:关于2015年天猫双11。标题翻译过来就是:阿里巴巴集团光棍节头90分钟销售额50亿美元。还在链接里看到了好久不见的马爸爸:我了解到文少方的链接是阿里内部使用druid的意思,天猫双十一是一个非常牛逼的场景,druid经受住了这个场景的测试。HikariCP作者没有回复邵文。直到另一位吃瓜群众推波助澜:HikariCP的作者认为,这是一场数据量的较量。那么我非常欢迎。HikariCP是世界上使用最广泛的连接池之一,被一些最大的公司使用,每天为数十亿用户提供服务。至于德鲁伊,不好意思,我说的有点直白:在国外很少见。但对于他的回答,很快有人提出了疑问:一些最大的公司是否在使用它,每天为数十亿用户提供服务?例如?想要数据,对吧?请耐心等待:wix.com拥有超过1.09亿个网站,每天处理超过10亿个请求。Atlassian的产品拥有数百万客户。HikariCP是springboot默认的连接池。HikariCP每月从中央Maven存储库解析超过300,000次。这些公司都在用:这个回答之后,双方都没有说话。双方的战斗结束了。不过还是有人继续发帖,我觉得这哥们是清醒的吃瓜群众:又一个老家伙的回答有意思:别吵了,别吵了。我是来学习技术的,不是来看你讨论“资本主义”工具和“共产主义”工具的区别的。而且我觉得这场战斗真的意义不大。在技??术选择上,没有最好,只有合适。Druid和HikariCP各有各的优势。最后一句话(求关注)好的,看到这里请关注,周庚很累,需要一点正反馈。感谢阅读,本人坚持原创,欢迎并感谢您的关注。