性能调优攻略

最近在读耗子叔《性能调优攻略》受益良多。(这篇篇幅较长,涉及到的东西也比较多,遇到具体的问题可以参看具体的部分。)虽然是12年写的,但是好的东西就是经久不衰的。只要linux还在,这些理论知识还是不变的,比如linux系统负载,网络调优,系统调优…总之写这个就是记录一哈,以便遇到性能的问题希望可以在这里找到解决方案。其中也加了一些文章中提到的一些关键词,查阅的一些参考内容链接。

系统性能调优,概括的一句话来说就是要进行优化就得先找到系统性能的瓶颈

系统性能定义

影响系统性能的两个因素:

  1. Throughtput,吞吐量。 每秒可以处理的请求数,任务数。
  2. Latency,系统延迟。系统在处理一个请求或一个任务时的延迟。

一般来说,一个系统的性能受到这两个条件的约束,缺一不可。比如,我的系统可以顶得住一百万的并发,但是系统的延迟是2分钟以上,那么,这个一百万的负载毫无意义。系统延迟很短,但是吞吐量很低,同样没有意义

  • Throughtput越大,Latency会越差。因为请求量过大,系统太繁忙,导致响应速度变慢。
  • Latency越好,能支持的Throughtput就会越高。因为Latency短说明处理速度快,自然可以处理更多请求。

系统性能测试

详细的可以参考16年耗子叔写的 这篇《性能测试应该怎么做?》

测试系统性能就是收集系统的Throughtput和Latency这两个值。

  • 首先,需要定义Latency的值,不同的业务有不同的值,比如网站系统响应时间必须在5s以内。
  • 其次,开放性能测试工具。可以参考。之前用过的Jmter(),也可以参考十个免费的WEB压力测试工具
  • 最后,开始性能测试。需要不断提升测试的Throughtput,然后观察系统的负载情况,如果顶得住,那就观察Latency。这样就可以找到系统最大负载,并且知道系统的响应延时是多少。

还要注意几点:

  • 关于Latency,Throughtput越来越大的时候,系统的Latency会出现非常剧烈的抖动。因此统计的Latency平均值是不可信的,因为仅有50%的达到了我们可接受的范围。那也没有意义。
  • 关于性能测试还要注意系统的峰值极限。如:在某个吞吐量上持续15分钟。当负载到达的时候,系统会变得不稳定,过几分钟才会稳定,也有可能,前几分钟还稳定,然后就不稳定了,甚至还垮了。这个时候的吞吐量就是峰值极限。
  • 性能测试还需要做Soak Test,也就是在某个吞吐量下,系统可以持续跑一周甚至更长。这个值,我们叫做系统的正常运行的负载极限。

性能测试有很多很复要的东西,比如:burst test 等。总之性能测试是个累活,细活。

定位性能瓶颈

上面的铺垫可以让我们测试到系统性能,如何才能找到性能的瓶颈呢?

查看操作系统负载

注意CPU利用率,内存使用率,操作系统IO,网络IO,网络链接数。

Linux下有很多相关的命令和工具,比如:SystemTapLatencyTOPvmstat, sar, iostat, top, tcpdump等等。

CPU:

CPU利用率不高,但是系统的Throughput和Latency上不去了,这说明我们的程序并没有忙于计算,而是忙于别的一些事,比如IO。(另外CPU的利用率还要看内核态、用户态的,内核态的一上去,整个系统性能就下来了。而对于多核CPU,CPU0就是关键了,如果CPU0负载高,那么会影响其他核的性能,因为CPU各核间是需要调度的,这靠CPU0完成)

IO:

一般CPU跟IO 是反着来的,CPU利用率高则IO不大,IO大则CPU小。

IO主要要注意:1.磁盘文件IO,2.驱动程序的IO(如:网卡),3.内存换页率。这三个事都会影响系统性能。

网络带宽:

Linux可以使用:iftop, iptraf, ntop, tcpdump这些命令来查看。

程序问题:

如果CPU不高,IO不高,内存使用不高,网络带宽不高。系统性能还是上不去。这说明你的程序出问题了。比如:程序被阻塞,可能是因为等哪个锁,可能是为等某个资源,或者是在切换上下文

使用Profiler测试程序

接下来,可以使用not个Profiler来查看一下程序的运行性能;如:
Java的JProfiler/TPTP/CodePro Profiler,PHP的X-debug(这个是mac的)(window(X-debug)),GUN的gprof,IBM的PurifyPlus,Intel的VTune,Linux的OProfile/pref,后面这两个可以让你对代码优化到CPU的微指令级别,如果你关心CPU的L1/L2的缓存调优,那么你需要考虑一下使用VTune。使用这些Profiler工具,可以让你看到程序中各个模块函数甚至指令的很多东西,如:运行的时间 ,调用的次数,CPU的利用率,等等。

我们重点观察运行时间最多,调用次数最多的那些函数和指令。这里注意一下,对于调用次数多但是时间很短的函数,你可能只需要轻微优化一下,你的性能就上去了(比如:某函数一秒钟被调用100万次,你想想如果你让这个函数提高0.01毫秒的时间 ,这会给你带来多大的性能)。

使用Profiler得注意一下,Profiler会让你的程序运行的性能变低,从而导致没法测出高吞吐量下的系统性能,对此一般有两个方法来定位系统瓶颈:

1)在你的代码中自己做统计,使用微秒级的计时器和函数调用计算器,每隔10秒把统计log到文件中。(microtime function)。

2)分段注释你的代码块,让一些函数空转,做Hard Code的Mock,然后再测试一下系统的Throughput和Latency是否有质的变化,如果有,那么被注释的函数就是性能瓶颈,再在这个函数体内注释代码,直到找到最耗性能的语句。

最后再说一点,对于性能测试,不同的Thoughtput下会出现不同的测试结果,不同的测试数据也会有不同的测试结果。所以,用于性能测试的数据非常重要。性能测试中我们要观测不同Thoughtput的结果

常见的系统瓶颈

关于架构方面的性能调优可以参看 《由12306.cn谈谈网站性能技术》,关于Web方面的性能调优可以参看《Web开发中需要了解的东西》一文中的性能一章。这里就不再讲设计和架构上的东西了。

一般来说,性能优化也就是下面几个策略:

  • 用空间换时间 各种cache如CPU L1/L2/RAM到硬盘,都是空间换时间的策略。这样的策略基本是把计算的过程一步步的保存或缓存下来,这样就不用了每次用的时候都要再计算一遍。比如数据缓冲,CDN等。这样的策略还表现为冗余数据,比如数据镜像,负载均衡什么的。

  • 用时间换空间 有时候,少量的空间性能可能会更好,比如网络传输,如果有一下压缩数据的算法(如:Huffman编码压缩算法 和 “rsync的核心算法”),这样的算法很耗时,但是因为瓶颈在网络传输,所以用时间换空间反而更能省时间。

  • 简化代码 最高效的程序就是不执行任何代码,所以,代码越少性能就越高。如:减少循环的层数,减少递归,在循环中少声明变量,少做分配和释放内存的操作,尽量把循环体内的表达式抽到循环外,条件表达式中的多个条件判断的次序,尽量在程序启动时把一些东西准备好,注意函数调用的开销(栈上开销),注意面向对象语言中临时对象的开销,小心使用异常(不要用异常来检查一些可以忽略并经常发生的错误),……等等,这些东西需要我们非常了解编程语言和常用的库。

  • 并行处理 如果CPU只有一个核,你要玩多进程,多线程,对于计算密集型的软件反而会更慢(因为操作系统调度和切换开销很大),CPU的核多了才能体现出多进程多线程的优势。并行处理需要我们的程序有Scalability[可拓展性],不能水平或垂直拓展的程序无法进行并行处理。从架构来说,是否可以做到不改代码只是加机器就可以完成性能提升?

总之,根据2:8原则,20%的代码耗了你80%的性能,找到那20%的代码,你就可以优化那80%的性能。下面列举一下耗子叔认为有价值的性能调优方法。

算法调优

算法非常重要,好的算法会有更好的性能。

过滤算法

系统需要对收到的请求做过滤,我们把可以被filter in/out的东西配置在了一个文件中,原有的过滤算法是遍历过滤配置,后来,我们找到了一种方法可以对这个过滤配置进行排序,这样就可以用二分折半的方法来过滤,系统性能增加了50%。

哈希算法

计算哈希算法的函数并不高效,一方面是太耗时,另一方面就是碰撞太高,碰撞高了就跟单向链表一个性能(可参看Hash Collision DoS 问题)。 算法其实跟要处理的数据很有关系的,就算是被大家所嘲笑的“冒泡排序”在某些情况下(大多数是排好序的)其效率会高于所有的排序算法。 还有哈希算法(一定要看看StackExchange上的这篇关于各种hash算法的文章

分而治之和预处理

以前有一个程序为了生成月报表,每次都需要计算很长的时间,有时候需要花将近一整天的时间。于是我们把我们找到了一种方法可以把这个算法发成增量式的,也就是说我每天都把当天的数据计算好了后和前一天的报表合并,这样可以大大的节省计算时间,每天的数据计算量只需要20分钟,但是如果我要算整个月的,系统则需要10个小时以上(SQL语句在大数据量面前性能成级数性下降)。这种分而治之的思路在大数据面前对性能有很帮助,就像merge排序一样。SQL语句和数据库的性能优化也是这一策略,如:使用嵌套式的Select而不是笛卡尔积的Select,使用视图,等等。

代码调优

字符串操作

这是最费系统性能的事了,无论是strcpy, strcat还是strlen,最需要注意的是字符串子串匹配。所以,能用整型最好用整型。举几个例子,第一个例子是N年前做银行的时候,我的同事喜欢把日期存成字符串(如:2012-05-29 08:30:02),我勒个去,一个select where between语句相当耗时。另一个例子是,我以前有个同事把一些状态码用字符串来处理,他的理由是,这样可以在界面上直接显示,后来性能调优的时候,我把这些状态码全改成整型,然后用位操作查状态,因为有一个每秒钟被调用了150K次的函数里面有三处需要检查状态,经过改善以后,整个系统的性能上升了30%左右。还有一个例子是,我以前从事的某个产品编程规范中有一条是要在每个函数中把函数名定义出来,如:const char fname[]=”functionName()”, 这是为了好打日志,但是为什么不声明成 static类型的呢?

多线程调优

有人说,thread is evil,这个对于系统性能在某些时候是个问题。因为多线程瓶颈就在于互斥和同步的锁上,以及线程上下文切换的成本,怎么样的少用锁或不用锁是根本(比如:多版本并发控制(MVCC)在分布式系统中的应用 中说的乐观锁可以解决性能问题),此外,还有读写锁也可以解决大多数是读操作的并发的性能问题。另外,线程不是越多越好,线程间的调度和上下文切换也是很夸张的事,尽可能的在一个线程里干,尽可能的不要同步线程。这会让你有很多的性能。

内存分配

不要小看程序的内存分配。malloc/realloc/calloc这样的系统调非常耗时,尤其是当内存出现碎片的时候。我们的程序有一天不响应了,用GDB跟进去一看,系统hang在了malloc操作上,20秒都没有返回,重启一些系统就好了。这就是内存碎片的问题。这就是为什么很多人抱怨STL有严重的内存碎片的问题,因为太多的小内存的分配释放了。有很多人会以为用内存池可以解决这个问题,但是实际上他们只是重新发明了Runtime-C或操作系统的内存管理机制,完全于事无补。当然解决内存碎片的问题还是通过内存池,具体来说是一系列不同尺寸的内存池(这个留给大家自己去思考)。当然,少进行动态内存分配是最好的。说到内存池就需要说一下池化技术。比如线程池,连接池等。池化技术对于一些短作业来说(如http服务) 相当相当的有效。这项技术可以减少链接建立,线程创建的开销,从而提高性能。

异步操作

我们知道Unix下的文件操作是有block和non-block的方式的,像有些系统调用也是block式的,如:Socket下的select,Windows下的WaitforObject之类的,如果我们的程序是同步操作,那么会非常影响性能,我们可以改成异步的,但是改成异步的方式会让你的程序变复杂。异步方式一般要通过队列,要注意队列的性能问题,另外,异步下的状态通知通常是个问题,比如消息事件通知方式,有callback方式,等,这些方式同样可能会影响你的性能。但是通常来说,异步操作会让性能的吞吐率有很大提升(Throughput),但是会牺牲系统的响应时间(latency)。这需要业务上支持。

语言和代码库

熟悉语言以及所使用的函数库或类库的性能。比如:STL中的很多容器分配了内存后,那怕你删除元素,内存也不会回收,其会造成内存泄露的假像,并可能造成内存碎片问题。再如,STL某些容器的size()==0 和 empty()是不一样的,因为,size()是O(n)复杂度,empty()是O(1)的复杂度,这个要小心。Java中的JVM调优需要使用的这些参数:-Xms -Xmx -Xmn -XX:SurvivorRatio -XX:MaxTenuringThreshold,还需要注意JVM的GC,GC的霸气大家都知道,尤其是full GC(还整理内存碎片),他就像“恐龙特级克赛号”一样,他运行的时候,整个世界的时间都停止了。

网络调优

关于网络调优,尤其是TCP Tuning(你可以以这两个关键词在网上找到很多文章),这里面有很多很多东西可以说。看看Linux下TCP/IP的那么多参数就知道了(顺便说一下,你也许不喜欢Linux,但是你不能否认Linux给我们了很多可以进行内核调优的权力)。强烈建议大家看看《TCP/IP 详解 卷1:协议》这本书。我在这里只讲一些概念上的东西。

TCP调优

TCP链接是有很多开销的,一个是会占用文件描述符,另一个是会开缓存。一个系统可以支持的TCP链接数是有限的,我们需要清楚地认识到TCP链接对系统的开销是很大的。正是因为TCP是耗资源的,所以,很多攻击都是让你系统上出现大量的TCP链接,把你的系统资源耗尽。比如著名的SYNC Flood攻击。

所以,我们要注意配置KeepAlive参数定义一个时间,如果链接上没有数据传输,系统会在这个时间发一个包,如果没有收到回应,那么TCP就认为链接断了,然后就会把链接关闭,这样可以回收系统资源开销。(注:HTTP层上也有KeepAlive参数)对于像HTTP这样的短链接,设置一个1-2分钟的keepalive非常重要。这可以在一定程度上防止DoS攻击。有下面几个参数(下面这些参数的值仅供参考):

1
2
3
net.ipv4.tcp_keepalive_probes = 5
net.ipv4.tcp_keepalive_intvl = 20
net.ipv4.tcp_fin_timeout = 30

对于TCP的TIME_WAIT这个状态,主动关闭的一方进入TIME_WAIT状态,TIME_WAIT状态将持续2个MSL(Max Segment Lifetime),默认为4分钟,TIME_WAIT状态下的资源不能回收。有大量的TIME_WAIT链接的情况一般是在HTTP服务器上。对此,有两个参数需要注意,

1
2
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1

前者表示重用TIME_WAIT,后者表示回收TIME_WAIT的资源。

TCP还有一个重要的概念叫RWIN(TCP Receive Window Size),这个东西的意思是,我一个TCP链接在没有向Sender发出ack时可以接收到的最大的数据包。为什么这个很重要?因为如果Sender没有收到Receiver发过来ack,Sender就会停止发送数据并会等一段时间,如果超时,那么就会重传。这就是为什么TCP链接是可靠链接的原因。重传还不是最严重的,如果有丢包发生的话,TCP的带宽使用率会马上受到影响(会盲目减半),再丢包,再减半,然后如果不丢包了,就逐步恢复。相关参数如下:

1
2
3
4
net.core.wmem_default = 8388608
net.core.rmem_default = 8388608
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216

一般来说,理论上的RWIN应该设置成:吞吐量 * 回路时间。Sender端的buffer应该和RWIN有一样的大小,因为Sender端发送完数据后要等Receiver端确认,如果网络延时很大,buffer过小了,确认的次数就会多,于是性能就不高,对网络的利用率也就不高了。也就是说,对于延迟大的网络,我们需要大的buffer,这样可以少一点ack,多一些数据,对于响应快一点的网络,可以少一些buffer。因为,如果有丢包(没有收到ack),buffer过大可能会有问题,因为这会让TCP重传所有的数据,反而影响网络性能。(当然,网络差的情况下,就别玩什么高性能了) 所以,高性能的网络重要的是要让网络丢包率非常非常地小(基本上是用在LAN里),如果网络基本是可信的,这样用大一点的buffer会有更好的网络传输性能(来来回回太多太影响性能了)。

另外,我们想一想,如果网络质量非常好,基本不丢包,而业务上我们不怕偶尔丢几个包,如果是这样的话,那么,我们为什么不用速度更快的UDP呢?你想过这个问题了吗?

UDP调优

网卡调优

在Linux下,我们可以用ifconfig查看网上的统计信息,如果我们看到overrun上有数据,我们就可能需要调整一下txqueuelen的尺寸(一般默认为1000),我们可以调大一些,如:ifconfig eth0 txqueuelen 5000。Linux下还有一个命令叫:ethtool可以用于设置网卡的缓冲区大小。在Windows下,我们可以在网卡适配器中的高级选项卡中调整相关的参数(如:Receive Buffers, Transmit Buffer等,不同的网卡有不同的参数)。把Buffer调大对于需要大数据量的网络传输非常有效。

其他网络性能

关于多路复用技术,也就是用一个线程来管理所有的TCP链接,有三个系统调用要重点注意:一个是select,这个系统调用只支持上限1024个链接,第二个是poll,其可以突破1024的限制,但是select和poll本质上是使用的轮询机制,轮询机制在链接多的时候性能很差,因主是O(n)的算法,所以,epoll出现了,epoll是操作系统内核支持的,仅当在链接活跃时,操作系统才会callback,这是由操作系统通知触发的,只有Linux Kernel 2.6以后才支持(准确说是2.5.44中引入的)当然,如果所有的链接都是活跃的,过多的使用epoll_ctl可能会比轮询的方式还影响性能,不过影响的不大。

系统调优

I/O模型

前面说到过select/poll/epoll这三个系统调用,我们都知道,Unix/Linux下把所有的设备都当成文件来进行I/O,所以,那三个操作更应该算是I/O相关的系统调用。说到 I/O模型,这对于我们的I/O性能相当重要,我们知道,Unix/Linux经典的I/O方式是(关于Linux下的I/O模型,大家可以读一下这篇文章《使用异步I/O大大提高性能》):

第一种,同步阻塞式I/O,这个不说了。

第二种,同步无阻塞方式。其通过fctnl设置 O_NONBLOCK 来完成。

第三种,对于select/poll/epoll这三个是I/O不阻塞,但是在事件上阻塞,算是:I/O异步,事件同步的调用。

第四种,AIO方式。这种I/O 模型是一种处理与 I/O 并行的模型。I/O请求会立即返回,说明请求已经成功发起了。在后台完成I/O操作时,向应用程序发起通知,通知有两种方式:一种是产生一个信号,另一种是执行一个基于线程的回调函数来完成这次 I/O 处理过程。

第四种因为没有任何的阻塞,无论是I/O上,还是事件通知上,所以,其可以让你充分地利用CPU,比起第二种同步无阻塞好处就是,第二种要你一遍一遍地去轮询。Nginx之所所以高效,是其使用了epoll和AIO的方式来进行I/O的。

多核CPU调优

关于CPU的多核技术,我们知道,CPU0是很关键的,如果0号CPU被用得过狠的话,别的CPU性能也会下降,因为CPU0是有调整功能的,所以,我们不能任由操作系统负载均衡,因为我们自己更了解自己的程序,所以,我们可以手动地为其分配CPU核,而不会过多地占用CPU0,或是让我们关键进程和一堆别的进程挤在一起。

多核CPU还有一个技术叫NUMA技术(Non-Uniform Memory Access)。传统的多核运算是使用SMP(Symmetric Multi-Processor )模式,多个处理器共享一个集中的存储器和I/O总线。于是就会出现一致存储器访问的问题,一致性通常意味着性能问题。NUMA模式下,处理器被划分成多个node, 每个node有自己的本地存储器空间。关于NUMA的一些技术细节,你可以查看一下这篇文章《Linux 的 NUMA 技术》,在Linux下,对NUMA调优的命令是:numactl 。如下面的命令:(指定命令“myprogram arg1 arg2”运行在node 0 上,其内存分配在node 0 和 1上)

1
$ numactl --cpubind=0 --membind=0,1 myprogram arg1 arg2

当然,上面这个命令并不好,因为内存跨越了两个node,这非常不好。最好的方式是只让程序访问和自己运行一样的node,如:

1
$ numactl --membind 1 --cpunodebind 1 --localalloc myapplication

文件系统调优

关于文件系统,因为文件系统也是有cache的,所以,为了让文件系统有最大的性能。首要的事情就是分配足够大的内存,这个非常关键,

在Linux下可以使用free命令来查看 free/used/buffers/cached,理想来说,buffers和cached应该有40%左右

调优文件系统配置了,对于Linux的Ext3/4来说,几乎在所有情况下都有所帮助的一个参数是关闭文件系统访问时间,在/etc/fstab下看看你的文件系统 有没有noatime参数(一般来说应该有),还有一个是dealloc,它可以让系统在最后时刻决定写入文件发生时使用哪个块,可优化这个写入程序。还要注间一下三种日志模式:data=journal、data=ordered和data=writeback。默认设置data=ordered提供性能和防护之间的最佳平衡。

当然,对于这些来说,ext4的默认设置基本上是最佳优化了。

这里介绍一个Linux下的查看I/O的命令—— iotop,可以让你看到各进程的磁盘读写的负载情况

其它还有一些关于NFS、XFS的调优,大家可以上google搜索一些相关优化的文章看看。关于各文件系统,大家可以看一下这篇文章——《Linux日志文件系统及性能分析

数据库调优

MYSQL性能优化的最佳20+条经验

极客时间上的MySQL实战45讲

数据库引擎调优

SQL语句优化

Mysql 使用 optimizer_trace 查看执行流程,分析、验证优化思路

GDB 调试 Mysql 实战

group by + order by 性能优化分析