查看原文
其他

性能优化那些事儿(1)

张锦程 Thoughtworks洞见 2022-07-13

性能优化是个恒久的话题,它伴随着业务的一次次迭代,产品的一步步演进,它陪伴企业一步步走向壮大再走向衰败,是我们面临的不可回避的问题。就如同宇宙的熵增定律,一切都走向混乱走向无序,性能的劣化边随着企业的发展壮大,业务的膨胀,人员的流动,复杂度的提升,一定也最终走向不可收拾的一步。

我们没法像消除吸血鬼一样,性能的优化没有银弹可用,但不代表性能优化没有共性可言,本文针对笔者做过的一些性能优化案例,尝试总结下解决性能问题的常用手段,以及如何持续性地避免过快的熵增。

首先我们把性能优化分为两种情况,第一种是在企业发展阶段的平稳期产生的性能瓶颈,第二种是企业发展的临界点产生的性能瓶颈,知道第二曲线原理的同学们可以尝试对应到第二曲线上去,一种是在曲线内的性能优化,一种是跨越曲线的性能优化。

理论源自查尔斯·汉迪《第二曲线:跨越“S型曲线”的二次增长》

比如著名的C10K问题,和10年淘宝架构的演进基本都属于第二种情况,这种情况很难通过业务代码的优化或者简单的架构调整就能解决性能问题,这种情况的性能优化一般在算法&理论的突破或者是架构哲学&语言的调整层面了。我们没法在一条曲线上完成性能的突破,可以看到曲线后期的收益越来越小,我们必须跳跃到一个新的曲线上去,这就是为什么很多大企业会注重架构的演进,第一曲线和第二曲线重合的部分就是企业高层进行重要决策的时机,我们再看淘宝的架构演进很明显是符合第二曲线原理的。

针对这种如同换血般的性能优化,评估的时候需要结合现有流量和指标的分析给出强有理的数学模型,来证实在当前架构模型上是否能承载未来一段时间的业务高速发展,这种预判需要有前瞻性,和对市场有准确的估计。一旦发现数学模型证实架构模型无法承载更多的业务增长,那就需要果断的迁移到第二曲线上去,公司的前瞻性和战略性在这个阶段表现无遗。

我们很多的性能优化接触更多的其实是第一种情况,我们需要在不打破现有架构的情况下,进行性能调优。我们继续在这个场景下进行总结:

环境优化

所谓环境优化就是代码执行环境的优化,就如同你的工作环境影响你工作效率一样,程序的运行环境对性能影响也很大。举个栗子,网卡中断与CPU亲和性,在Linux的网络调优方面,如果你发现网络流量上不去,那么有一个方面需要去查一下:网卡处理网络请求的中断是否被绑定到单个CPU(或者说跟处理其它中断的是同一个CPU)。

这就是个典型的运行环境对性能的影响,你的服务会应为网卡中断的原因导致性能下降的很厉害。当然环境的优化比较吃经验,如果没有经验会比较难定位问题,但一些基础的Linux优化常识还是得必备的,需要学会看各项指标,有足够的敏锐力发现异常的指标,有足够的经验识别异常指标的诱因是什么。

轮子的优化和选择

很多库提供了非常便利的功能,但有些情况下这些便利的功能对性能不是很友好。准确来说很多轮子对开发而言是个黑盒,即使有源码也鲜有人去一行行研究,往往很多性能问题就暴露在简单的一句调用中。先说说简单的,大家都知道的,HashTable和ConcurrentHashMap,都是并发安全的组件,但是性能上差别就大了,明显用ConcurrentHashMap性能就会比HashTable好。

一样的道理,ArrayBlockingQueue是JDK提供的同步堵塞队列,很多场景下会用到这个组件,但是追求极致性能的情况下Disruptor是个更好的选择。对于大量定时任务的调用,Netty的时间轮算法就是更为优秀的选择。你知道JDK8的ConcurrentHashMap用的不好有严重的性能BUG么?可以看看这篇文章的例子:https://zhuanlan.zhihu.com/p/364340936

对于轮子的性能选择可以遵循下面的原则:无锁设计普遍优于有锁的设计,细粒度锁优于粗粒度锁,环形队列的设计普遍优于无边界队列的设计。

万恶的循环

一般烂代码都出现在循环里,比如几百次的REST请求,几千次的SQL请求,手动找起来海底捞针,特别是很深的调用堆栈很难发现,这里需要利用工具,我们单独去说,无论是基于语法树还是字节码的静态检测还是持续性能集成都能一定程度的预防这种情况的发生,经验告诉我这里是优化成果的大头,越复杂的项目这种问题越严重,解决方案是做批处理和小心的使用缓存。这里性能可视化和调用链分析可以帮助你快速定位问题。


锁可以说性能优化的难点,一类锁会牵扯到业务,优化的重心是如何合理的使用锁,有没有行成锁的使用规范,锁的粒度足够细么?有可能的集中管理锁,限制开发人员直接使用锁。那另一类锁一般在中间件那块,属于通用组件,和业务关系不大,但如果瓶颈在中间件那就得着手去优化了,最好的是实现无锁模型。

缓存

缓存是能够解决一些性能问题的,在某些场合是杀手锏的存在,但缓存需要注意的是时效性和生效范围,控制好这2点一般缓存会带来很大的收益。

线程池

线程池过大对性能也是有一定影响的,毕竟JAVA的线程是1:1的内核线程,解决方法是设置合适的线程池大小不要过于庞大,线程上下文切换的开销可是不小的,或者干脆使用阿里的JDK开启全局虚拟线程模式(黑科技)。

同步

同步一般会堵塞线程导致需要大量线程池,异步太难写了,协程JAVA不支持,有条件的用阿里的JDK吧。

慎用Hibernate

为啥单独说Hibernate,可能笔者有条件反射了,一般使用Hibernate的项目多多少少都对其用法有误解,或者完全沉迷于它带来的便利性而忽略了这些便利性带来的性能问题。简单的N+1问题经常在项目上遇到,复杂的级联更新问题导致的性能问题也屡见不鲜,总之大家小心使用Hibernate。

GC

由于频繁FullGC导致的性能问题也是很常见的,这块有点大,可以说个几天几夜了,这里不细说。

还有些奇葩的优化点,比如缓存行失效,一般来说业务涉及不到,都是中间件基础组件才有可能碰到的优化策略。

业务优化,业务优化往往会取得很喜人的成绩,但这是一个取舍的问题,而且涉及到业务,小心谨慎,一般来说在性能优化的专项工作中尽量不去修改业务。

上面这些仅仅是一些性能优化心得,有不少是经验很难总结全面,但有不少效果显著的优化项可以通过模式去解决,那么如何发现代码中的性能问题,快速识别出那些性能不友好的代码呢?请听下回分解。



- 相关阅读 -

性能优化那些事

浅谈机器学习模型推理性能优化


点击【阅读原文】可至洞见网站查看原文&绿色字体部分的相关链接。

本文版权属Thoughtworks公司所有,如需转载请在后台留言联系。

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存