2016年,我负责了一个线上Linux服务器的性能调优项目。

说起来,这个项目是公司的一个核心业务服务器,跑PHP+MySQL,每天有几十万的访问量。最近用户投诉很多,说网站打开很慢,有时候甚至打不开,影响很大。老板很生气,要求我尽快解决这个问题。

我登录服务器看了一下,发现服务器负载很高,CPU使用率经常在80%以上,内存也用得差不多了,磁盘IO也很频繁,响应时间平均2秒,有时候甚至5秒以上,确实很慢。

于是我花了一周时间进行性能分析和调优,最终把服务器的响应时间从平均2秒降到了200毫秒,降了90%,吞吐量提升了5倍,用户投诉也消失了,老板很满意。

今天就来聊聊这次Linux性能调优的实战经历,以及Linux性能调优的一些经验和方法。

一、问题现象

先说说问题的现象。

用户反馈网站打开很慢,有时候甚至打不开,特别是高峰期,上午10点左右和下午3点左右最严重。

我登录服务器,用top命令看了一下,发现:

  • CPU使用率:很高,经常在80%以上,高峰期甚至100%,load average经常在5以上(服务器是4核)。
  • 内存使用率:也很高,16G内存用了14G,只剩2G,而且swap也用了不少。
  • 磁盘IO:很频繁,iostat看一下,%util经常在80%以上,await也很高。
  • 网络:还好,带宽用得不多,不是瓶颈。
  • 响应时间:平均2秒,高峰期甚至5秒以上,错误率也有5%左右,主要是超时错误。

这些现象说明服务器确实有性能问题,而且是多方面的,CPU、内存、IO都有瓶颈,需要综合调优。

二、性能分析

做性能调优,第一步一定是性能分析,找到瓶颈在哪里,然后针对性地优化,不能盲目调优瞎猜。

我用了一些Linux性能分析工具,对服务器进行了全面的分析。

1. CPU分析

CPU分析主要用top、htop、mpstat、pidstat等工具。

用top看整体CPU使用情况,发现CPU主要被PHP-FPM进程占用了,每个PHP-FPM进程CPU使用率都很高,而且进程数量也很多,有50多个。

用mpstat看每个CPU核心的使用情况,发现4个核心都很忙,没有哪个核心特别闲,说明CPU确实是瓶颈。

用pidstat看每个进程的CPU使用情况,发现除了PHP-FPM,MySQL也占用了不少CPU,特别是一些复杂的SQL查询,很耗CPU。

2. 内存分析

内存分析主要用free、vmstat、sar等工具。

用free看内存使用情况,发现16G内存用了14G,但是仔细看,发现其中有8G是cache和buffer,真正被进程占用的只有6G,所以内存其实不算特别紧张,但是swap用了2G,这说明曾经出现过内存不足的情况,导致使用了swap。

用vmstat看内存的实时情况,发现si和so(swap换入换出)偶尔会有值,说明确实有swap的使用,这会影响性能,因为swap是磁盘,比内存慢很多。

3. 磁盘IO分析

磁盘IO分析主要用iostat、iotop、dstat等工具。

用iostat看磁盘IO情况,发现%util经常在80%以上,await也很高,平均20毫秒以上,说明磁盘IO确实是瓶颈。

用iotop看哪个进程在做IO,发现主要是MySQL在做大量的磁盘IO,因为查询很多,而且很多查询没有命中索引,导致全表扫描,大量读磁盘。

另外,PHP-FPM也有一些IO,主要是读PHP文件和日志写入。

4. 网络分析

网络分析主要用iftop、nethogs、sar等工具。

看了一下网络使用情况,发现带宽用得不多,入站和出站都只有几Mbps,服务器是100Mbps带宽,所以网络不是瓶颈。

5. 应用层分析

除了系统层的分析,还做了应用层的分析。

用Apache的ab工具做了压力测试,发现服务器的吞吐量只有50 QPS左右,响应时间平均2秒,确实很慢。

看了MySQL的慢查询日志,发现有很多慢查询,有的查询甚至要几秒,这些慢查询是性能的主要瓶颈之一。

看了PHP-FPM的日志,发现有很多PHP进程因为执行时间太长被杀掉,说明PHP代码的执行效率也有问题。

经过全面的分析,我找到了主要的性能瓶颈:

  1. CPU瓶颈:PHP-FPM和MySQL占用大量CPU,CPU使用率过高。
  2. 内存瓶颈:内存使用偏高,有swap使用,影响性能。
  3. 磁盘IO瓶颈:MySQL大量磁盘IO,慢查询多,全表扫描多。
  4. 应用层瓶颈:PHP代码执行效率低,MySQL慢查询多,索引优化不足。

找到了瓶颈,就开始针对性地优化。

三、CPU优化

首先是CPU优化。

CPU主要被PHP-FPM和MySQL占用,所以主要从这两个方面优化。

1. PHP-FPM优化

PHP-FPM的优化主要是调整进程数量和进程管理方式。

原来PHP-FPM的配置是static模式,max_children=50,也就是固定50个进程。但是服务器只有4核,50个进程太多了,会导致频繁的进程切换,反而降低性能。

我把PHP-FPM的模式改成了dynamic动态模式,调整了参数:

  • pm = dynamic
  • pm.max_children = 20
  • pm.start_servers = 8
  • pm.minspareservers = 4
  • pm.maxspareservers = 12
  • pm.max_requests = 500

这样,PHP-FPM的进程数量会根据负载动态调整,平时8个进程,高峰期最多20个进程,不会因为进程太多导致频繁切换,也不会因为进程太少处理不过来。

另外,pm.max_requests=500,意思是每个进程处理500个请求之后自动重启,防止内存泄漏,这也能提升稳定性和性能。

2. PHP代码优化

除了PHP-FPM的配置优化,PHP代码的优化也很重要。

我看了一下代码,发现有几个问题:

  • 很多页面没有做缓存,每次访问都要查询数据库生成页面,很耗CPU。
  • 一些PHP代码写得比较低效,比如循环里查询数据库,导致大量数据库查询。
  • 一些函数调用比较频繁,而且效率不高。

我做了以下优化:

  • 增加了页面缓存,用Redis做缓存,一些不经常变化的页面缓存1小时,减少数据库查询和PHP执行。
  • 优化了PHP代码,把循环里的数据库查询改成批量查询,减少查询次数。
  • 优化了一些低效的函数调用,用更高效的方式替代。

这些优化效果很明显,PHP的CPU使用率降了很多。

3. MySQL CPU优化

MySQL也占用了不少CPU,主要是因为慢查询多,很多查询没有命中索引,导致全表扫描,很耗CPU。

我看了慢查询日志,找出了最慢的几个查询,进行了优化:

  • 给一些查询条件的字段加了索引,让查询能命中索引,不用全表扫描。
  • 优化了一些复杂的SQL语句,简化查询逻辑,减少计算量。
  • 一些查询结果用Redis缓存,不用每次都查询数据库。

这些优化效果很明显,MySQL的CPU使用率也降了很多。

四、内存优化

接下来是内存优化。

内存主要的问题是使用偏高,有swap使用,影响性能。

1. 调整swappiness

Linux有一个参数叫swappiness,控制系统使用swap的倾向,值越大越倾向于使用swap,值越小越倾向于使用物理内存。

默认swappiness=60,对于服务器来说,这个值偏高,会导致系统在还有不少物理内存的时候就开始使用swap,影响性能。

我把swappiness改成了10,让系统尽量使用物理内存,减少swap的使用。

修改方法:

# 临时修改
sysctl vm.swappiness=10

# 永久修改,写入/etc/sysctl.conf
echo "vm.swappiness=10" >> /etc/sysctl.conf
sysctl -p

2. 调整MySQL内存配置

MySQL占用了不少内存,原来的配置不太合理,innodbbufferpool_size只设了2G,对于16G内存的服务器来说太小了,导致很多数据不能缓存在内存,需要频繁读磁盘。

我把innodbbufferpool_size改成了8G,让MySQL能缓存更多的数据在内存,减少磁盘IO,也提升查询速度。

另外,也调整了其他一些MySQL内存参数,比如innodblogbuffersize、sortbuffersize、readbuffer_size等,让MySQL的内存使用更合理。

3. 减少PHP内存使用

PHP-FPM的每个进程也占用不少内存,原来50个进程,每个进程占用100M左右,总共占用5G内存,太多了。

调整了PHP-FPM进程数量之后,最多20个进程,内存占用降到了2G左右,省了不少内存。

另外,也优化了PHP代码,减少内存使用,比如及时释放不需要的变量,避免大数组占用过多内存等。

经过优化,内存使用降到了10G左右,其中cache和buffer占了6G,进程占用4G,swap也不再使用了,内存问题基本解决。

五、磁盘IO优化

接下来是磁盘IO优化。

磁盘IO主要的问题是MySQL大量磁盘IO,慢查询多,全表扫描多。

1. MySQL索引优化

索引优化是MySQL性能优化最有效的方法之一。

我看了慢查询日志,找出了最慢的几个查询,发现很多查询都没有命中索引,导致全表扫描,大量读磁盘。

我给这些查询条件的字段加了索引,比如:

  • 给文章表的category_id字段加了索引,因为分类查询很频繁。
  • 给文章表的created_at字段加了索引,因为按时间排序和查询很频繁。
  • 给评论表的post_id字段加了索引,因为按文章查询评论很频繁。
  • 给用户表的username字段加了唯一索引,因为登录查询很频繁。

加了索引之后,这些查询的速度提升了很多,从几秒降到了几毫秒,磁盘IO也降了很多。

2. MySQL查询缓存

MySQL有查询缓存功能,能缓存SELECT查询的结果,相同的查询直接从缓存取结果,不用再执行查询,能大幅提升查询速度,减少磁盘IO。

原来MySQL的查询缓存没有开启,我开启了查询缓存,调整了参数:

  • querycachetype = 1
  • querycachesize = 128M
  • querycachelimit = 2M

开启查询缓存之后,很多重复的查询直接从缓存取结果,速度提升了很多,磁盘IO也降了很多。

不过要注意,查询缓存也有一些缺点,比如表更新之后相关的缓存会失效,对于更新频繁的表,查询缓存效果不好,甚至会影响性能。所以要根据实际情况调整。

3. 增加innodbbufferpool_size

前面内存优化的时候已经提到了,把innodbbufferpool_size从2G改成了8G,这不仅是内存优化,也是磁盘IO优化,因为更多的数据缓存在内存,就不用频繁读磁盘了。

InnoDB的buffer pool是MySQL性能最重要的参数之一,一般设置为服务器内存的50%-70%比较合适。我们服务器16G内存,设置8G是比较合理的。

经过优化,磁盘IO降了很多,%util从80%降到了20%左右,磁盘IO问题基本解决。

六、网络优化

网络虽然不是主要瓶颈,但是也做了一些优化。

1. 开启TCP BBR

TCP BBR是Google开发的一个TCP拥塞控制算法,能提升网络吞吐量和降低延迟,特别是在高延迟的网络环境下效果很明显。

我开启了TCP BBR,调整了内核参数:

echo "net.core.default_qdisc=fq" >> /etc/sysctl.conf
echo "net.ipv4.tcp_congestion_control=bbr" >> /etc/sysctl.conf
sysctl -p

开启BBR之后,网络吞吐量有一定提升,延迟也有一定降低。

2. 调整TCP参数

也调整了一些TCP参数,比如:

  • net.ipv4.tcptwreuse = 1,允许TIME_WAIT状态的socket重新用于新的连接。
  • net.ipv4.tcpfintimeout = 15,减少FIN超时时间,加快连接回收。
  • net.core.somaxconn = 65535,增加连接队列长度,应对高并发。
  • net.ipv4.tcpmaxsyn_backlog = 65535,增加SYN队列长度,应对高并发。

这些参数调整能提升服务器的并发处理能力,减少连接超时和拒绝。

3. 开启gzip压缩

在Nginx里开启了gzip压缩,压缩网页内容,减少传输数据量,提升页面加载速度,也节省带宽。

gzip on;
gzip_min_length 1k;
gzip_comp_level 6;
gzip_types text/plain application/javascript application/json text/css application/xml;

开启gzip之后,页面大小降了60%左右,加载速度提升了很多。

七、系统参数调优

除了上面的优化,也调整了一些系统参数。

1. 文件描述符限制

Linux默认的文件描述符限制是1024,对于高并发服务器来说太小了,会导致"Too many open files"错误。

我把文件描述符限制改成了65535:

# 修改/etc/security/limits.conf
* soft nofile 65535
* hard nofile 65535

2. 调整内核参数

也调整了一些内核参数,比如:

  • fs.file-max = 65535,系统最大文件描述符数量。
  • vm.overcommit_memory = 1,允许内存过量分配,提升内存利用率。
  • kernel.sem = 250 32000 100 128,调整信号量参数,提升并发处理能力。

这些参数调整能提升服务器的并发处理能力和稳定性。

八、踩坑经验

在调优的过程中,也踩了一些坑,分享一下。

1. 不要盲目调优

刚开始我也犯了一个错误,就是盲目调优,看到网上说某个参数要怎么调,就跟着调,也不管自己的服务器情况适不适合,结果调了之后性能反而更差了。

后来我明白了,性能调优一定要先测量,找到自己的服务器的瓶颈在哪里,然后针对性地优化,不能盲目照搬别人的配置,因为每个服务器的情况都不一样,适合别人的配置不一定适合你。

2. 调优要一步一步来

性能调优不要一下子改很多参数,因为如果改了之后性能变了,你不知道是哪个参数起了作用,也不知道哪个参数导致了问题。

要一步一步来,每次只改一个参数或者一个方面,然后测试效果,看看性能是提升了还是下降了,如果提升了就保留,如果下降了就改回来,然后再试下一个。

这样虽然慢一些,但是能确保每个优化都是有效的,也能避免改了之后出问题不知道是哪里的问题。

3. 注意备份

调优之前一定要备份原来的配置文件,因为如果调优之后出问题,能快速恢复原来的配置,不会导致服务长时间不可用。

我每次改配置之前都会先备份原来的配置文件,比如cp nginx.conf nginx.conf.bak,这样如果改了之后出问题,直接cp nginx.conf.bak nginx.conf就能恢复,很方便。

4. 注意监控

调优的过程中一定要注意监控服务器的性能指标,比如CPU、内存、IO、网络、响应时间、吞吐量、错误率等,看看调优之后这些指标是变好了还是变差了。

可以用一些监控工具,比如Zabbix、Prometheus、Grafana等,监控服务器的性能指标,也可以用ab、wrk等压力测试工具测试服务器的性能,看看调优的效果。

九、最终效果

经过一周的调优,最终效果如下:

指标调优前调优后提升
平均响应时间2000ms200ms90%
吞吐量50 QPS250 QPS400%
CPU使用率80%+30%左右62.5%
内存使用率87.5%62.5%28.6%
磁盘IO %util80%+20%左右75%
错误率5%0.1%98%

效果很明显,响应时间降了90%,吞吐量提升了4倍,CPU、内存、IO使用率都大幅下降,错误率也几乎消失了。

用户投诉也消失了,网站打开速度很快,用户体验大幅提升,老板也很满意。

而且服务器现在还有很大的性能余量,能应对更大的访问量,不用马上升级服务器,节省了成本。

十、写在最后

Linux性能调优实战:从2秒到200毫秒。

这次性能调优虽然只有一周时间,但是收获很大,不仅把服务器的性能提升了很多,也学到了很多Linux性能调优的经验和方法。

性能调优是一个系统工程,需要先测量找到瓶颈,然后针对性地优化,从CPU、内存、IO、网络、应用层等多个方面综合优化,才能达到最好的效果。

不要盲目调优,不要照搬别人的配置,要根据自己的服务器情况针对性地优化,一步一步来,用数据说话,才能确保每个优化都是有效的。

也要注意备份和监控,调优之前备份配置,调优的过程中监控性能指标,确保调优的效果,也避免出问题。

希望我的这些经历和经验能对大家有所帮助,特别是做运维和后端开发的朋友。

最后,用一句话结尾:

"性能调优没有最好,只有更好,持续测量,持续优化,才能让系统一直保持最佳状态。"

愿大家的服务器都能跑得又快又稳,用户体验越来越好。