最近接手了一个遗留系统,性能很差,首页加载要十几秒,接口响应要好几秒,用户体验很差,用户投诉很多,领导也很不满意,让我负责改造和优化。

这个系统,是几年前开发的,用的是老的技术栈,代码很乱,文档缺失,很多地方都是硬编码,没有注释,没有测试,维护起来很困难。而且,随着用户量的增长,数据量的增长,系统的性能越来越差,已经到了不能用的地步,必须改造和优化。

经过一个多月的改造和优化,性能提升了很多,首页加载降到了1秒以内,接口响应降到了几百毫秒,用户体验好了很多,用户投诉也少了很多,领导也很满意。

今天就来分享一下我的遗留系统改造性能优化实战经验,从问题分析、到数据库优化、到缓存优化、到代码优化、到架构优化、到前端优化、到测试和验证、再到踩坑和经验,详细分享遗留系统改造性能优化的全过程和心得体会,希望能给大家一些参考和启发。

一、问题分析

接手系统之后,我没有急着改代码,而是先做了全面的问题分析,找出系统的性能瓶颈在哪里,然后对症下药。

首先,我用监控工具,对系统进行了全面的监控,包括服务器的CPU、内存、磁盘IO、网络IO,数据库的慢查询、连接数、锁等待,接口的响应时间、吞吐量、错误率,前端的加载时间、渲染时间等。

通过监控,我发现系统的性能瓶颈,主要在以下几个方面:

1. 数据库慢查询很多

数据库有很多慢查询,有的查询要好几秒,甚至十几秒,主要是因为没有索引,或者索引不合理,或者SQL写得很差,比如全表扫描、嵌套子查询、不必要的JOIN等。而且,数据库的连接数经常满,导致请求排队,响应很慢。

2. 没有缓存

系统几乎没有缓存,所有的数据都从数据库查,很多热点数据,比如首页数据、商品数据、用户信息等,每次请求都查数据库,导致数据库压力很大,响应很慢。而且,很多计算结果,也没有缓存,每次都重新计算,浪费了很多资源。

3. 代码质量差,效率低

代码质量很差,很多地方都是循环里查数据库,N+1问题很严重;很多地方都是重复代码,没有封装,维护困难;很多地方都是硬编码,没有配置,修改困难;很多地方都是同步阻塞,没有异步,效率很低。而且,代码里有很多不必要的计算,不必要的查询,浪费了很多资源。

4. 架构不合理

架构不合理,所有的功能都耦合在一起,没有拆分,一个功能出问题,整个系统都受影响。而且,没有负载均衡,所有的请求都打到一台服务器上,服务器压力很大,很容易宕机。也没有读写分离,所有的读写都打到一个数据库上,数据库压力很大,响应很慢。

5. 前端性能差

前端性能也很差,首页加载要十几秒,主要是因为资源太多,没有压缩,没有合并,没有缓存;图片太大,没有压缩,没有懒加载;没有CDN,所有的资源都从源站加载,速度很慢。而且,前端代码也很乱,没有优化,渲染很慢,用户体验很差。

找出了性能瓶颈之后,我就开始制定优化方案,分步骤进行优化,先优化最严重的瓶颈,再优化其他的,逐步提升系统的性能。

二、数据库优化

数据库是系统最大的性能瓶颈,所以我首先优化数据库。

数据库优化,主要从以下几个方面入手:

1. 添加和优化索引

首先,我分析了所有的慢查询,找出了没有索引,或者索引不合理的查询,然后添加和优化索引。

对于经常用于WHERE、JOIN、ORDER BY、GROUP BY的字段,都添加了合适的索引,比如主键索引、唯一索引、普通索引、联合索引等。对于联合索引,遵循最左前缀原则,把区分度高的字段放在前面,把常用的字段放在前面。

对于已经存在的索引,进行了优化,删除了不必要的索引、重复的索引、很少使用的索引,减少索引的数量,提高写入的效率。

添加和优化索引之后,很多慢查询的速度,从几秒、十几秒,降到了几毫秒,提升非常明显。

2. 优化SQL语句

除了添加索引,我还优化了很多SQL语句,让SQL更高效。

比如,把嵌套子查询改成JOIN,因为JOIN通常比子查询更高效;把SELECT *改成只查询需要的字段,减少数据传输和内存使用;避免在WHERE子句中对字段进行函数操作,因为这样会导致索引失效;避免使用LIKE '%xxx%',因为这样会全表扫描,可以用全文索引或者搜索引擎代替;避免使用OR,可以用UNION代替,因为OR可能会导致索引失效。

还有,对于批量操作,用批量INSERT、批量UPDATE,而不是循环里一条一条操作,减少数据库交互次数,提高效率。

优化SQL语句之后,很多查询的速度,又有了进一步的提升。

3. 数据库结构优化

我还对数据库结构进行了优化,比如,把大字段拆分到单独的表,因为大字段会影响查询效率;把经常一起查询的字段,放在同一个表,减少JOIN;对表进行垂直拆分和水平拆分,减轻单表的压力。

还有,对字段类型进行了优化,比如,用TINYINT代替INT,用VARCHAR代替TEXT,用DECIMAL代替FLOAT,选择合适的字段类型,节省存储空间,提高查询效率。

4. 数据库配置优化

我还对数据库的配置进行了优化,比如,调整了innodbbufferpoolsize,让它尽可能大,缓存更多的数据和索引,减少磁盘IO;调整了innodblogfilesize,提高写入的效率;调整了maxconnections,增加连接数,避免连接数满导致请求排队;调整了querycachetype和querycache_size,开启查询缓存,缓存查询结果(注意:MySQL 8.0已经移除了查询缓存,这里是5.7版本)。

还有,开启了慢查询日志,记录慢查询,方便后续分析和优化;开启了二进制日志,用于主从复制和数据恢复。

5. 读写分离

为了减轻数据库的压力,我还做了读写分离,主库负责写,从库负责读,读请求分发到从库,写请求打到主库,主从同步,保证数据一致性。

读写分离之后,数据库的压力减轻了很多,读的速度也提升了很多,因为可以多个从库负载均衡,提高读的吞吐量。

数据库优化之后,数据库的性能提升了很多,慢查询少了很多,响应速度快了很多,数据库的压力也减轻了很多。

三、缓存优化

数据库优化之后,我开始做缓存优化,因为系统几乎没有缓存,所有的数据都从数据库查,数据库压力很大,响应很慢。

缓存优化,主要从以下几个方面入手:

1. 页面缓存

对于一些不经常变化的页面,比如首页、列表页、详情页等,做了页面缓存,把整个页面的HTML缓存起来,下次请求的时候,直接返回缓存的HTML,不需要查数据库,不需要渲染,速度非常快。

页面缓存的过期时间,根据页面的更新频率来定,比如首页,更新比较频繁,缓存时间短一些,比如5分钟;详情页,更新比较少,缓存时间长一些,比如1小时。当数据更新的时候,主动清除对应的缓存,保证数据的一致性。

2. 数据缓存

对于一些热点数据,比如用户信息、商品信息、分类信息、配置信息等,做了数据缓存,把数据缓存到Redis里,下次请求的时候,直接从Redis取,不需要查数据库,速度非常快。

数据缓存的过期时间,根据数据的更新频率来定,比如用户信息,更新比较少,缓存时间长一些,比如1天;商品信息,更新比较频繁,缓存时间短一些,比如10分钟。当数据更新的时候,主动更新或者清除对应的缓存,保证数据的一致性。

3. 查询结果缓存

对于一些复杂的查询,比如统计查询、聚合查询、多表JOIN查询等,做了查询结果缓存,把查询结果缓存起来,下次同样的查询,直接返回缓存的结果,不需要重新查询,速度非常快。

查询结果缓存的key,是根据查询的SQL和参数生成的,保证不同的查询有不同的key。过期时间,根据查询的复杂度和数据的更新频率来定,比如统计查询,数据更新不频繁,缓存时间长一些,比如1小时。

4. 本地缓存

除了Redis缓存,我还做了本地缓存,比如用APC或者OPcache,把一些数据缓存到PHP进程的内存里,下次请求的时候,直接从本地内存取,不需要连Redis,速度更快。

本地缓存,适合缓存一些不经常变化、且不需要跨进程共享的数据,比如配置信息、字典信息等。本地缓存的缺点是,每个进程都有一份,占用内存比较多,而且不能跨进程共享,所以要控制缓存的大小和数量。

5. 缓存的防穿透、防击穿、防雪崩

做缓存的时候,还要注意缓存的防穿透、防击穿、防雪崩。

缓存穿透: 查询一个不存在的数据,缓存里没有,数据库里也没有,每次请求都打到数据库,导致数据库压力大。解决方案:缓存空值,或者用布隆过滤器,过滤掉不存在的查询。

缓存击穿: 一个热点key,在缓存过期的瞬间,有大量的并发请求,都打到数据库,导致数据库压力大。解决方案:加互斥锁,只让一个请求去查数据库,其他请求等待;或者永不过期,后台异步更新缓存。

缓存雪崩: 大量的key,在同一时间过期,或者缓存服务宕机,导致大量的请求都打到数据库,导致数据库压力大,甚至宕机。解决方案:过期时间加随机值,避免大量key同时过期;做缓存集群,提高缓存的可用性;做降级,缓存不可用的时候,返回默认值或者降级页面,避免所有请求都打到数据库。

缓存优化之后,系统的性能提升了很多,大部分请求都直接从缓存取,不需要查数据库,响应速度非常快,数据库的压力也减轻了很多。

四、代码优化

数据库和缓存优化之后,我开始做代码优化,因为代码质量差,效率低,也是系统性能差的一个重要原因。

代码优化,主要从以下几个方面入手:

1. 解决N+1问题

代码里有很多N+1问题,就是循环里查数据库,比如,先查一个列表,然后循环列表,每条数据再查一次数据库,这样就会有N+1次数据库查询,效率很低。

解决方案:用批量查询,先把需要的ID收集起来,然后一次查询出所有的数据,再在内存里组装,这样只需要两次数据库查询,效率高很多。或者用JOIN,一次查询出所有需要的数据。

解决了N+1问题之后,很多接口的响应速度,提升了很多。

2. 减少不必要的查询和计算

代码里有很多不必要的查询和计算,比如,同一个数据,在一个请求里查了好几次;同一个计算,在一个请求里算了好几次;查询了很多不需要的字段,不需要的数据。

解决方案:把查询和计算的结果缓存起来,在同一个请求里复用;只查询需要的字段,需要的数据;避免重复的查询和计算。

减少了不必要的查询和计算之后,代码的执行效率,提升了很多。

3. 优化循环和递归

代码里有很多循环和递归,效率很低,比如,嵌套循环,时间复杂度O(n²);递归深度太深,导致栈溢出,效率低。

解决方案:优化循环,减少嵌套,用更高效的算法;把递归改成迭代,避免栈溢出,提高效率;用空间换时间,用哈希表、缓存等,提高查询效率。

优化了循环和递归之后,代码的执行效率,又有了进一步的提升。

4. 异步化

代码里有很多同步阻塞的操作,比如,发送邮件、发送短信、生成报表、调用第三方接口等,这些操作都很耗时,同步执行的话,会导致接口响应很慢。

解决方案:把这些耗时的操作异步化,用消息队列,比如RabbitMQ、Redis队列等,把任务放到队列里,后台异步执行,接口立即返回,不需要等待,响应速度快很多。

异步化之后,很多接口的响应速度,提升了很多,用户体验也好了很多。

5. 代码重构

除了性能优化,我还对代码进行了重构,提高代码的可读性、可维护性、可扩展性。比如,把重复的代码封装成函数、类;把硬编码改成配置;添加注释和文档;添加单元测试;分层架构,把业务逻辑、数据访问、控制层分开,解耦。

代码重构之后,代码的质量提升了很多,维护起来也容易了很多,后续的开发和优化,也方便了很多。

五、架构优化

数据库、缓存、代码优化之后,我开始做架构优化,因为架构不合理,也是系统性能差、稳定性差的一个重要原因。

架构优化,主要从以下几个方面入手:

1. 服务拆分

原来的系统,所有的功能都耦合在一起,一个大的单体应用,维护困难,扩展困难,一个功能出问题,整个系统都受影响。

解决方案:按照业务领域,把系统拆分成多个服务,比如用户服务、商品服务、订单服务、支付服务、搜索服务等,每个服务独立部署,独立扩展,服务之间通过接口或者消息队列通信,解耦。

服务拆分之后,系统的可维护性、可扩展性、稳定性,都提升了很多,一个服务出问题,不会影响其他服务,而且可以针对某个服务,单独扩容,提高性能。

2. 负载均衡

原来的系统,所有的请求都打到一台服务器上,服务器压力很大,很容易宕机,而且没有冗余,一台服务器宕机,整个系统就不可用了。

解决方案:用Nginx或者LVS做负载均衡,把请求分发到多台应用服务器上,多台服务器同时提供服务,提高系统的吞吐量和可用性。而且,多台服务器有冗余,一台服务器宕机,其他服务器还能继续提供服务,系统不会不可用。

负载均衡之后,系统的吞吐量和可用性,都提升了很多。

3. 动静分离

原来的系统,静态资源和动态请求,都由应用服务器处理,应用服务器的压力很大,而且静态资源的处理效率不高。

解决方案:用Nginx处理静态资源,比如图片、CSS、JS、HTML等,动态请求转发给应用服务器处理,动静分离,减轻应用服务器的压力,提高静态资源的处理效率。而且,静态资源可以放到CDN上,让用户从最近的节点获取,速度更快。

动静分离之后,应用服务器的压力减轻了很多,静态资源的加载速度也提升了很多。

4. CDN加速

原来的系统,所有的资源都从源站加载,速度很慢,特别是异地用户,访问速度更慢。

解决方案:用CDN加速,把静态资源,比如图片、CSS、JS、视频等,缓存到CDN的节点上,用户访问的时候,从最近的节点获取,速度非常快。而且,CDN可以减轻源站的压力,提高系统的可用性。

CDN加速之后,静态资源的加载速度,提升了很多,用户体验也好了很多。

5. 容器化和自动化部署

为了提高部署的效率和一致性,我还做了容器化和自动化部署,用Docker打包应用,用Docker Compose或者Kubernetes编排,用CI/CD自动构建、自动测试、自动部署,提高部署的效率和一致性,减少人为错误。

容器化和自动化部署之后,部署的效率提升了很多,部署的一致性也提高了很多,扩容也方便了很多。

六、前端优化

后端优化之后,我还做了前端优化,因为前端性能差,也是用户体验差的一个重要原因。

前端优化,主要从以下几个方面入手:

1. 资源压缩和合并

原来的前端,CSS、JS文件很多,没有压缩,没有合并,加载的时候,要发很多请求,而且文件很大,加载很慢。

解决方案:把CSS、JS文件压缩,去掉空格、注释、换行,减小文件大小;把多个CSS、JS文件合并成一个,减少请求数量。而且,开启Gzip压缩,进一步减小文件大小,提高传输速度。

资源压缩和合并之后,前端的加载速度,提升了很多。

2. 图片优化

原来的前端,图片很大,没有压缩,没有懒加载,加载很慢,而且浪费流量。

解决方案:压缩图片,减小图片大小,比如用WebP格式,比JPG、PNG小很多;对图片进行懒加载,只加载可视区域的图片,滚动到的时候再加载,减少初始加载的资源;用响应式图片,根据屏幕大小,加载不同尺寸的图片,避免加载过大的图片。

图片优化之后,前端的加载速度,又有了进一步的提升。

3. 浏览器缓存

原来的前端,没有设置缓存策略,每次访问,都要重新加载所有的资源,很慢。

解决方案:设置合理的缓存策略,比如,静态资源(CSS、JS、图片等),设置较长的缓存时间,比如1年,用文件名hash做版本控制,更新的时候,文件名变了,缓存就失效了;HTML页面,设置较短的缓存时间,或者不缓存,保证用户能看到最新的内容。

浏览器缓存之后,第二次访问的速度,提升了很多,因为大部分资源都从浏览器缓存取,不需要重新加载。

4. 渲染优化

原来的前端,渲染很慢,主要是因为DOM操作太多,JS执行时间太长,没有优化。

解决方案:减少DOM操作,批量操作DOM,用文档碎片;把JS放在页面底部,或者用async/defer,避免阻塞页面渲染;用虚拟列表,只渲染可视区域的DOM,减少DOM数量,提高渲染速度;用防抖和节流,减少频繁的事件触发和计算。

渲染优化之后,页面的渲染速度,提升了很多,用户交互也流畅了很多。

5. 首屏优化

首屏加载速度,对用户体验影响很大,所以我重点做了首屏优化。

解决方案:首屏的CSS内联,避免额外的请求;首屏的JS精简,只加载首屏需要的JS,其他JS异步加载;首屏的图片,用较小的缩略图,或者用占位图,懒加载;用骨架屏,先显示骨架屏,内容加载完成后再替换,提高用户体验;服务端渲染,首屏由服务端渲染,加快首屏显示速度,也有利于SEO。

首屏优化之后,首屏的加载速度,提升了很多,用户体验也好了很多。

七、测试和验证

优化的过程中,还要做充分的测试和验证,确保优化没有引入新的问题,确保功能正常,数据正确,性能达标。

测试和验证,主要包括以下几个方面:

1. 功能测试

优化之后,要做全面的功能测试,确保所有的功能都正常,没有因为优化而导致功能异常。比如,数据库优化之后,要确保查询结果正确;缓存优化之后,要确保数据一致性;代码重构之后,要确保业务逻辑正确;架构优化之后,要确保服务之间的调用正常。

2. 性能测试

优化之后,要做性能测试,验证性能是否达标,比如,接口的响应时间、吞吐量、并发数,页面的加载时间、渲染时间,服务器的CPU、内存、磁盘IO、网络IO等。可以用压测工具,比如JMeter、ab、wrk等,模拟大量的并发请求,测试系统的性能和稳定性。

3. 数据一致性验证

缓存优化、读写分离、服务拆分之后,要做数据一致性验证,确保数据的一致性,没有因为优化而导致数据不一致。比如,缓存和数据库的数据是否一致;主库和从库的数据是否一致;服务之间的数据是否一致。

4. 灰度发布

优化之后,不要直接全量发布,可以先灰度发布,比如,先让10%的流量走新系统,观察一段时间,没有问题,再逐步扩大流量,直到全量发布。灰度发布,可以降低风险,如果新系统有问题,只影响少量用户,而且可以快速回滚。

5. 监控和告警

优化之后,要完善监控和告警,实时监控系统的性能和状态,比如,服务器的CPU、内存、磁盘IO、网络IO,数据库的慢查询、连接数、锁等待,缓存的命中率、内存使用,接口的响应时间、吞吐量、错误率,前端的加载时间、错误率等。当出现异常的时候,及时告警,通知开发人员,快速响应和处理。

八、踩坑和经验

这次遗留系统改造性能优化,踩了很多坑,也积累了很多经验,在这里分享一下,希望大家能少踩坑。

1. 不要过早优化,也不要过度优化

优化之前,一定要先做性能分析,找出真正的瓶颈,然后对症下药,不要凭感觉优化,不要过早优化,也不要过度优化。过早优化,会浪费时间和精力,而且可能会引入新的问题;过度优化,会增加代码的复杂度,降低可维护性,而且收益不大。

2. 优化要循序渐进,不要一步到位

遗留系统改造,不要想着一步到位,要循序渐进,分步骤进行,先优化最严重的瓶颈,再优化其他的,逐步提升系统的性能。而且,每一步优化,都要做测试和验证,确保没有问题,再进行下一步。一步到位,风险很大,很容易出问题,而且出了问题,很难定位和回滚。

3. 缓存是把双刃剑,要注意数据一致性

缓存能大大提升系统的性能,但是缓存也是把双刃剑,要注意数据一致性,避免缓存和数据库的数据不一致。而且,要注意缓存的防穿透、防击穿、防雪崩,避免缓存出问题,导致数据库压力大,甚至宕机。

4. 数据库优化,索引不是越多越好

索引能大大提升查询的效率,但是索引不是越多越好,索引会占用存储空间,会降低写入的效率,因为写入的时候,要同时更新索引。所以,要合理添加索引,只在需要的字段上添加索引,删除不必要的索引、重复的索引、很少使用的索引。

5. 代码重构,要小步快跑,不要大重构

遗留系统的代码,质量很差,需要重构,但是不要大重构,要小步快跑,每次只重构一小部分,测试验证没有问题,再进行下一部分。大重构,风险很大,很容易出问题,而且出了问题,很难定位和回滚。

6. 要有回滚方案

优化和重构的时候,一定要有回滚方案,一旦出问题,能快速回滚,减少影响。比如,数据库变更,要写回滚脚本;代码发布,要能快速回滚到上一个版本;架构变更,要保留旧的系统,能快速切回。

7. 文档和注释很重要

遗留系统,文档缺失,注释很少,维护起来很困难。所以,改造和优化的时候,一定要补充文档和注释,记录系统的架构、功能、接口、数据库结构、优化方案、踩坑经验等,方便后续的维护和开发。

8. 性能优化是一个持续的过程

性能优化,不是一劳永逸的,是一个持续的过程。随着用户量的增长,数据量的增长,业务的变化,系统的性能瓶颈也会变化,需要持续地监控、分析、优化,才能保证系统的性能和稳定性。

写在最后

遗留系统改造性能优化实战:从慢到快。

经过一个多月的改造和优化,系统的性能提升了很多,首页加载从十几秒降到了1秒以内,接口响应从好几秒降到了几百毫秒,用户体验好了很多,用户投诉也少了很多,领导也很满意。

这次遗留系统改造性能优化,我学到了很多,也积累了很多经验,从问题分析、到数据库优化、到缓存优化、到代码优化、到架构优化、到前端优化、到测试和验证、再到踩坑和经验,都有了更深刻的理解和认识。

遗留系统改造,是很多程序员都会遇到的问题,也是一个很有挑战性的工作。通过这次改造,我深刻地认识到,性能优化,没有银弹,需要根据系统的实际情况,分析瓶颈,对症下药,才能取得好的效果。而且,优化要循序渐进,小步快跑,不要一步到位,不要大重构,要做好测试和验证,要有回滚方案,才能降低风险,保证系统的稳定。

希望我的经验,能给大家一些参考和启发,让大家在做遗留系统改造性能优化的时候,少踩坑,少走弯路,取得好的效果。

最后,用一句话结尾:

"性能优化,没有银弹,需要根据系统的实际情况,分析瓶颈,对症下药,循序渐进,持续优化,才能让系统又快又稳。"

祝大家都能做好性能优化,让系统又快又稳,用户体验越来越好!