你有没有想过,为什么跨行转账要告诉你2小时内到账,而不是立即到账?为什么抖音那么多用户同时在使用,却很少出现崩溃的情况?电商网站是如何支撑住双十一全国人民买买买的?
性能优化对一个产品的重要性不言而喻,它直接影响网站的用户留存率,APP在商店的评分和用户粘性。一个响应慢的应用,即便它功能再强大,也留不住用户。
性能优化对一个程序员同样非常重要——如果你是一个有追求的程序员的话。我们说,大多数人的职业生涯发展都应该是一个T字型,要在某一方面有深度,也要有广度。而性能优化,恰恰是一个既需要深度,又需要广度的话题。相信很多程序员都有一个成为架构师的梦,如果想要成为一个系统架构师,那性能优化是不得不学习了解的。
下面将从各个方面,把我知道的关于性能优化的各种“套路”介绍给大家,欢迎大家看完后留言交流探讨。
找到最慢的节点
我们谈性能时,一般来说有两个指标,一个是响应时间,一个是吞吐量。
说到响应时间,最直观的感受就是快与慢。打开一个网页需要多少毫秒、点击一个按钮需要等待多长时间、刷一个抖音视频需要加载多久?这些都是在说响应时间。
吞吐量指的是系统在单位时间能够承受的请求数量,反映是系统的承载能力。
下面列出一些常见的影响性能的地方。
网络传输
影响响应时间的因素有很多,我们可以通过一些监控去测试。但我可以很负责任的告诉你,在大多数场景,一次完整的网络请求中,最多的时间往往是消耗在网络传输上。
网络传输有很多种,比如服务端到客户端的请求和响应,还有服务端各个微服务之间的接口调用耗时,还有应用与数据库、应用与缓存、应用与消息中间件等等之间的网络传输。
当然,大多数时候,我们会把后端的一些东西尽量放在一个机房里面,这样可以直接走内网,网络传输时间不会很高。但相比于大多数应用代码的执行时间来说,这些网络传输也是一笔不小的消耗。
SQL查询
我们应用的数据可能会持久化在不同的地方,不管是关系型数据库、非关系型数据库、还是搜索引擎,一旦数据量上去了,如果没有做好性能优化,查询的时候很容易就耗费大量的时间。
最常见的就是SQL慢查询了,一旦产生了慢查询,轻则响应时间变慢,重则拖满线程池导致整个服务不可用。所以SQL查询也是我们经常会考虑到的性能优化方向。
线程等待
线程等待指的是线程同步造成的问题。现代服务器往往是多核的,我们通常会使用线程池来发挥多核服务器的优势。但使用多线程经常会遇到的问题就是线程的同步问题。
在高并发下,线程同步其实是一个很危险的操作。如果临界区的操作比较耗时,就会导致大量的线程等待、堆积,最终撑满线程池。比如上面提到的慢SQL就常常会导致这个问题。
所以我们在使用多线程的时候,一定要小心谨慎。尽量弄清楚它的原理,想办法避免或者更轻量级地上锁。比如Yasin前几天发的几篇关于ThreadLocal的文章,里面就介绍了在某些场景可以用ThreadLocal避免线程的同步。
多线程是一块比较大,也比较难的知识点,也是互联网大厂的入门门槛,面试必问,工作中也会经常用到。这里推荐我之前参与写作的一本关于多线程的开源电子书《深入浅出Java多线程》,在我的公众号“编了个程”回复“多线程”即可领取这本书的电子版。
Full GC
Full GC其实影响整个应用性能的概率比较小。但如果你的程序没写好,或者JVM参数没有设置好,造成了频繁Full GC或者Full GC时间过长,也是有可能会影响性能的。
对JVM有所了解的朋友都知道,Full GC会STW (Stop The World),这段时间程序会暂停,也就没法响应用户。
❝G1对Full GC做了优化,把单线程的Full GC变成了多线程并行Full GC。但如果Full GC频繁,仍然会影响应用的性能。
❞
这里列一下Full GC频繁的原因,有兴趣的朋友可以自己再深入了解一下:
- 老年代设置的空间太小
- 永久代空间不足
- 程序中写了很多大对象
- 晋升老年代的代数阈值设置太小
高频优化思路
针对上面提到的几个最容易影响程序性能的点,下面介绍一些高频的性能优化思路,大多数都是针对网络传输的,从各种角度去减少网络传输的消耗。从这些思路入手,大概率可以很明显地提升性能,小伙伴们可以作为参考。
CDN
前面提到,一般来说,一个用户请求大多数时间是消耗在网络传输上的,尤其是客户端与服务端之间的网络传输。
CDN全称是“Content Delivery Network”,翻译过来叫内容分发网络。原理其实很简单,就是把资源分发到全国各地甚至是世界各地,使得用户可以就近取得资源,「缩短网络传输的距离」,降低延迟,所以可以很明显地提升响应速度和成功率。
CDN原理
所以CDN多是用于「文件分发」,比如网站的css、js、图片、视频等资源文件。曾经听过一句话:如果你的网站没有使用CDN,那么使用CDN基本上可以让你的网站性能得到「大幅度提升」。
CDN一般是和OSS配合起来使用的。现在各大主流云厂商基本都提供了OSS和CDN的产品,我自己的个人网站是使用的七牛云,有「10G的免费容量」,比较适合于个人站长。
压缩
另一个优化网络传输消耗的思路就是压缩了。CDN的思路是让网络传输的距离更短,压缩的思路是「让网络传输的内容更小」。尤其适用于js/css等文本文件,压缩收益非常高,往往能节省很多的网络传输开销。
图片和视频当然也可以压缩,不过需要选择合适的压缩算法。比如我们用微信发送图片时,如果不点击“发送原图”,那图片就会被微信压缩,虽然可能没那么高清,但是文件小很多,使得用户可以更快收到图片,提升用户体验。
现在大多数网站都会使用nginx作为前端服务器或者负载均衡器,在nginx里面可以非常方便地启动gzip压缩功能:
server{ gzip on; gzip_buffers 32 4K; gzip_comp_level 6; gzip_min_length 100; gzip_types application/javascript text/css text/xml; gzip_vary on; }
在浏览器打开“开发者控制台”,查看资源的网络请求,可以查看该资源是否启用了压缩算法。
使用gzip
❝注意:gzip压缩算法比较适用于html、js、css等文本文件,不适用于图片等二进制文件。对图片使用gzip压缩收益不高,反而可能会增加体积。
❞
预加载
前面讲到了两个网络传输的优化思路。我们可以另辟蹊径:既然网络传输那么消耗时间,那我们「偷偷在不忙的时候提前下载好」不行吗?
大家可以做一个实验:刷抖音刷到一半,等一会儿,然后停掉自己的网络,再往下刷,可以发现还能刷好几个视频。
这就是因为抖音使用了预加载技术,当你在专心致志地看一个有趣的视频的时候,这个时候网络其实是空闲的,抖音就在悄悄下载后面几个视频,这样你就可以一直刷刷刷,用户体验就会很顺畅。
试想一下,如果不使用预加载,用户每次往下刷,都得等几秒钟把这个视频下载下来才能看,那自然用户体验极差。
当然了,预加载并不适用于所有场景。毕竟预加载是提前下载,并不是不下载。如果不是有特定的业务场景,其实也没必要使用预加载。不合理地使用预加载甚至有可能会影响正常的业务,还有可能造成数据不一致的问题。
慢SQL优化
很多应用后端会使用关系型数据库来持久化数据。如果数据量大了,索引设置不合理,就很有可能会产生慢SQL。
生产环境最好加上慢SQL监控和分析的工具。阿里出品的Druid就很不错。对很多中小型项目来说已经足够了。
慢SQL优化一般有几个思路。
- 先看是不是没有命中索引,如果没有命中,是否可以调整索引或者SQL语句?
- 看能不能从业务代码层面解决?
- 能不能在应用层面加缓存?
- 考虑是否需要分库分表?
索引这个东西大家应该或多或少都接触过或者听说过。分析索引需要有一定的数据库基础,这里推荐《高性能MySQL》这本书,对索引讲得比较清晰。我的个人网站yasinshaw.com上面也有我之前写的关于MySQL的系列文章:
搜索关键字MySQL
从业务层面解决也是可以思考的一个点。比如我之前优化过的一个慢SQL。优化前的做法是用count
查询数据库还有多少数据,如果大于0,就delete掉这批数据,每次delete 3k条。结果每天数据库都有几十万条符合这个查询条件的数据,导致每次count查询都会耗费许多时间,即使走了索引,也得扫描几十行。而delete也因为数据量太大,执行时间超过1秒,收到告警。
long count = dao.count(condition); while(count > 0) { dao.delete(condition); count = dao.count(condition); }
优化思路也很简单,因为delete的时候会返回删除的行数。所以我们直接用这个数据来决定是否退出循环就行了,根本不需要count查询。同时把批量删除的行数从3k条修改为1k条,这样两个慢SQL问题就都解决了。
long deletedNumber = 0; do { deletedNumber = dao.delete(condition); } while (deletedNumber > 0)
缓存和分库分表会在下文做详细介绍,这里不赘述。
JVM调优和升级
频繁的GC会占用大量的JVM资源,还会浪费CPU的资源。所以如果是生产环境,推荐给JVM也加上监控和告警,这样能够随时观察JVM的状况是否健康。
尤其是对于Full GC,要格外注意,因为Full GC会Stop The World。前段时间我们有一个应用就是因为永久代空间不足,触发Full GC,而每次回收效果又不好,导致一遍又一遍地Full GC,影响应用的正常工作。
Java也在对JVM不断进行优化和升级,比如最新的ZGC,在大堆下性能表现优异。G1也非常不错,如果项目条件允许,建议使用稍微新一点的垃圾收集器。