本章主要介绍综合运用之前提出的性能工具来缩小性能问题产生原因的范围。阅读本章后,你将能够:
本章假设可以通过改变软件来解决性能问题。通过对应用程序或系统调优来达到性能目标并非总是可行的。如果调优失败,就可能要求进行硬件升级或更换。如果系统容量达到极限,那么性能调优就只能起到一定的作用。
举例来说,升级系统内存容量可能是必要的(或者是更便宜的),而不是追踪哪个应用程序在使用系统内存,然后对它们进行调整以降低其使用量。只升级系统硬件而非追踪并调整特定的性能问题,这个决定依赖于问题本身,同时,它也是进行调查的个人的价值判断。它实际上取决于哪种选择更加便宜,是(问题调查的)时间方面,还是(购买新硬件的)经费方面。最后,在某些情况下,调优将是首选或唯一的选择,这就是本章要描述的内容。
当你决定在Linux上优化某些东西之后,你首先要确定的是要优化什么。本章使用的方法覆盖了一些比较常见的性能问题,通过举例来说明如何利用前面介绍的工具共同解决这些问题。接下来的几个小节将帮助引导你找到性能问题的成因。在很多小节中会先要求运行各种性能工具,然后根据结果跳转到本章的其他小节。这有助于找到问题的根源。
就像在前面章节里陈述的一样,保存每次执行的测试结果是一个好办法。这使你能在之后的时间查看结果,假如调查结果尚不能确定,还可以将结果发送给其他人。
现在开始。调查问题的时候,初始系统运行的无关程序越少越好,因此,关闭或终止任何不需要的应用程序或进程。一个干净的系统有助于消除由任何无关应用程序可能导致的混淆干扰。如果某个特定的应用或程序没有按预期执行,直接跳到9.3节。如果不是某个应用程序性能不好,而是整个Linux系统执行效果不如预期,则跳到9.4节。
优化应用程序时,其执行的多个方面有可能出现问题。本节将根据你发现的问题,将你引导到正确的小节。图9-1展示了优化应用程序的步骤。诊断从9.3.1节开始。
使用top或ps确定应用程序使用了多少内存。如果该程序消耗的内存量超过预期,转到9.6.6节。否则,继续见9.3.2节。
如果应用程序启动所花费的时间有问题,见9.3.3节。否则,转到9.3.4节。
要测试问题是否出在加载器上,就按前面章节所述来设置ld环境变量。如果ld统计数据显示在映射所有的符号时有明显的延迟,那么就尽量减少应用程序使用的库的数量和大小,或者尽量预链接库。如果加载器确实表现出有问题,转到9.9节。如果没有问题,继续见9.3.4节。
用top或ps来确定应用程序的CPU使用量。如果应用程序是CPU消耗大户,或其完成时间特别长,那么该程序就存在CPU使用问题。
通常,一个应用程序的不同部分会具有不同的性能表现。那么就可能需要隔离那些性能不佳的部分,这样使用性能工具时就不用测量那些对性能没有负面影响的部分,而只需要测量被隔离部分的性能统计数据。为此,可能要改变应用程序的行为,使之易于分析。如果应用程序某个特定的部分对性能非常重要,那么在测量整个应用程序的性能统计信息时,要么试着只测量关键部分执行时的性能统计数据,要么就让该部分运行相当长的时间,直到应用程序无关部分的数据在与之不相干的整个性能统计数据中只占一小部分。尽量减少应用程序的工作,以便它只执行对性能至关重要的功能。比方说,在收集一个应用程序整体运行的性能统计信息时,我们不希望启动和退出过程占据大部分的应用程序运行时的总时间量。在这种情况下,比较有效的方法是启动应用程序,多次运行时间消耗大的部分,然后立即退出程序。这样分析工具(如oprofile或gprof)就可以捕获运行缓慢代码的更多信息,而不是那些执行了但却与问题无关部分(如启动和退出)的信息。比这个方法更好的是修改应用程序的源代码,当应用程序启动时,自动运行耗时部分,然后退出程序。这有助于最大程度减少与特定性能问题无关的分析数据。
如果应用程序的CPU使用有问题,转到9.5节。如果没有问题,见9.3.5节。
如果已经知道应用程序会导致大量的磁盘I/O,转到9.7.3节以确定它访问了哪些文件。否则,见9.3.6节。
如果已经知道应用程序会导致大量的网络I/O,转到9.8.6节。
如果上述问题均不存在,你遇到的应用程序性能问题可能就不在本书范围之内,请转到9.9节。
有时候,处理一个行为异常的系统,找出究竟是什么拉低了每件事情的速度是很重要的。由于调查的是系统级的问题,其原因可能存在于任何地方,从用户应用程序到系统库,再到Linux内核。幸运的是,与很多别的操作系统不同,对Linux来说,即使不能得到系统上所有应用程序的源代码,也可以获取其中大多数的源代码。如果有必要的话,你可以修复这个问题并将其提交给该部分的维护者。最糟糕的情况也不过是在本地运行已修复的版本。这就是开源软件的力量。
图9-2展示的是诊断系统级性能问题的流程。
调查从9.4.1节开始。
使用top、procinfo或mpstat来确定系统在哪些地方消耗了时间。如果整个系统空闲和等待时间的比例不足全部时间的5%,那么该系统就是受CPU限制的。转到9.4.3节。否则,前进到9.4.2节。
虽然系统作为一个整体可能不是受CPU限制的,但在一个对称多处理(SMP)或超线程系统中,单个处理器可能是受CPU限制的。
使用top或mpstat来确定单个CPU的空闲和等待时间是否少于5%。如果是,那么一个或多个CPU就是受CPU限制的,对这种情况,转到9.4.4节。否则,各处理器都不是受CPU限制的,则转到9.4.7节。
下一步是要找出是否有特定应用程序或应用程序组使用了CPU。最简单的方法是运行top。默认情况下,top按CPU使用量的降序来排列进程。top按照该进程消耗的用户时间和系统时间总和来报告进程的CPU使用量。举个例子,如果一个应用程序在用户空间代码上消耗了20%CPU时间,在系统代码上消耗了30%CPU时间,那么,top将会报告该进程消耗了50%CPU时间。将所有进程的CPU时间加起来,如果这个时间明显少于整个系统的系统时间加用户时间,那么内核所做的重要工作就与应用程序无关,转到9.4.5节。
否则,每个进程都转9.5.1节一次,以便确定时间是在哪里消耗的。
下一步是要找出是否有特定应用程序或应用程序组使用了单个CPU。实现该目标最简单的方法是运行top。默认情况下,top按CPU使用量的降序来排列进程。在报告进程的CPU使用量时,top显示的是应用程序使用的总CPU和系统时间。举个例子,如果应用程序在用户空间代码上消耗了20%的CPU,在系统代码上消耗了30%的CPU时间,那么,top将会报告该应用程序消耗了50%的CPU时间。
首先,运行top,然后将最后一个CPU添加到top显示的字段中。打开Irix模式,以便top显示每个处理器使用的CPU时间总量而不是整个系统的CPU时间。对于每个利用率高的处理器,将其上运行的特定应用程序或多个应用程序的CPU时间加起来。如果在一个CPU上,应用程序时间总和低于内核加用户时间之和的75%,这表明内核似乎花了大量的时间在其他的工作上而不是在应用程序上。对这种情况,见9.4.5节。否则,应用程序很有可能就是CPU消耗量的原因,对每个应用程序,转到9.5.1节。
这看起来好像是内核花费了大量时间完成那些不代表应用程序的工作。对这种情况的一种解释是I/O卡提交了很多中断,比如,一个忙碌的网卡。运行procinfo或cat/proc/interrupts来确定有多少中断被提出,其提出频率是怎样的,以及哪些设备导致了这些中断。这可能为系统的行为提供线索。将这些信息记录下来,并进入9.4.6节。
最后要搞清楚的是内核究竟做了些什么。在系统上运行oprofile,记录下哪些内核函数消耗了大量的时间(超过总时间的10%)。尝试阅读这些函数的内核源代码,或是在Web上搜索这些函数的引用。可能不会立即弄清楚这些函数的功能,但是可以试着找出它们在哪个内核子系统中。仅仅确定使用的是哪个子系统(如内存、网络、调度或磁盘)可能就足以判断是哪里出了问题。
知道这些函数的功能还有可能了解到它们被调用的原因。如果函数是设备特定的,就要试着找出为什么要使用特定的设备(尤其是如果它还有大量的中断)。向其他发现同样问题的人发电子邮件,有可能的话,与内核开发人员联系。
转到9.9节。
下一步是检查交换空间的使用量是否在增加。不少系统级性能工具,如top、vmstat、procinfo和gnome-system-info等都会提供这个信息。如果交换空间在增加,就需要找出是系统的哪个部分消耗了更多的内存。要实现这个目的,请转到9.6.1节。
如果被使用的交换空间没有增加,则见9.4.8节。
运行top时,查看系统是否在等待状态上消耗了大量的时间。如果这个时间比例超过了50%,那么系统就在等待I/O上消耗了相当多的时间,我们就要确定这个I/O是哪种类型,见9.4.9节。
如果系统没有花大量的时间等待I/O,那么你遇到的问题就不在本书范围之内,则转到9.9节。
接下来,运行vmstat(或iostat)并查看磁盘读写的块数。如果磁盘读写的块数很大,那么这可能就是磁盘瓶颈,转到9.7.1节。否则,继续见9.4.10节。
接下来,我们要查看系统是否使用了大量的网络I/O。最简单的方法是运行iptraf、ifconfig或sar来找出每个网络设备上传输了多少数据。如果网络流量接近网络设备的容量,那么就可能是网络瓶颈,则转到9.8.1节。如果看上去没有网络设备进行了网络通信,那么内核等待的是没有包含在本书范围内的其他一些I/O设备。查看内核调用了哪些函数以及哪些设备向内核发起了中断可能会有所帮助,转到9.4.5节。
当确定了某特定进程或应用程序是CPU瓶颈后,就必须查明其消耗时间的位置(和原因)。图9-3展示了调查进程CPU使用情况的方法。调查从9.5.1节开始。
你可以用time命令来确定一个应用程序是否在内核或用户模式下消耗了时间。oprofile 也可以用来确定时间花在了哪里。通过分析每一个进程,能够看到一个进程是否将其时间花在了内核或用户空间。
如果应用程序在内核空间消耗了大量的时间(超过25%),见9.5.2节。否则,转到9.5.3节。
下一步,运行strace来查看有哪些系统调用以及它们完成的时长是多少。你还可以运行oprofile找出哪些内核函数被调用了。
减少系统调用的次数或者改变代表程序进行的系统调用都有可能提升性能。有些系统调用可能是意想不到的,是应用程序调用各种库的结果。你可以运行ltrace和strace来帮助确定它们被调用的原因。
现在问题已经明确了,就由你来解决它,转到9.9节。
下一步,使用周期事件在应用程序上运行oprofile,确定哪些函数使用了全部的CPU 周期(即,哪些函数消耗了所有应用程序时间)。
记住,尽管oprofile可以向你显示在一个进程上花费了多少时间,但是在进行函数级分析时,一个特定函数成为热点的原因是由于其频繁被调用,还是仅仅由于其完成时间很长,是无法弄清楚的。
一种能弄明白上述两种情况中哪种是正确的方法是:从oprofile获得源代码级注释,并查找应该几乎没有开销的指令/源代码行(如赋值)。相对于其他高成本的源代码行,它们的样本数量将接近于函数被调用的次数。再次声明,这仅仅是近似的,因为oprofile只采样了CPU,乱序处理器会误判一些周期。
做出函数的调用图对明确热点函数是如何被调用的也是有帮助的。实现这个目的,见9.5.4节。
接下来,你可以找出耗时函数是怎么被调用的及其被调用的原因。把应用程序与gprof 一起运行能够显示每个函数的调用树。如果耗时函数在一个库中,你可以使用1trace来查看是哪些函数。最后,你可以使用oprofile较新的版本来支持调用树的跟踪。还有一种方法,你可以在gdb中运行应用程序,在热点函数上设置断点。然后运行该应用程序,在每次调用热点函数时,它都会暂停。此时,可以生成一个回溯,看看究竟是哪些函数和源代码行产生了这个调用。
知道是哪些函数调用了热点函数能够让你消除或减少对这些函数的调用,相应地加快应用程序的速度。如果减少对耗时函数的调用不能加快应用程序,或者无法消除这些函数,见9.5.5节。否则,转到9.9节。
下一步,针对你的应用程序运行oprofile、cachegrind和kcache,看看耗时函数或源代码行是否具有大量的cache缺失。如果是,则尝试重新安排或压缩你的数据结构和访问,让它们变得更加cache友好。如果热点代码行没有高cache缺失率,那么就尝试重新安排你的算法来减少特定行或函数执行的次数。
在任何情况下,工具都会尽其所能地向你提供信息,转到9.9节。
一般,要使用大量内存的应用程序通常会导致其他一些性能问题的产生,比如cache缺失、转换后援缓冲器(TLB)缺失以及交换。
图9-4展示了我们在试图弄清楚系统内存使用情况时的决策流程。调查从9.6.1节开始。
要追踪谁使用了系统内存,首先要确定内核自身是否分配内存。运行slabtop查看内核的内存总大小是否增加。如果增加了,则跳到9.6.2节。
如果内核的内存使用量没有增加,那么可能是特定进程导致了用量增加。要追踪是哪个进程该为内存使用量的增加负责,转到9.6.3节。
如果内核的内存使用量在增加,就再次运行slabtop来确定内核分配的内存类型。分片的名字多少会暗示一下内存被分配的原因。通过Web搜索,你可以找到内核源代码中每个分片名字的更多详细信息。只需在内核源代码中搜索该分片的名字,并确定它被用于哪些文件,就有可能弄清楚它被分配的原因。在明确了哪些子系统分配了所有的内存后,可以尝试调整特定子系统可以消耗的最大内存量,或者减少该子系统的使用量。
转到9.9节。
接下来,你可以使用top或ps来查看特定进程的驻留集大小是否在增加。最简单的方法是在top的输出中添加rss字段,并按照内存使用量来排序。如果一个特定进程不断增加内存的使用量,我们就需要弄清楚它用的内存类型是什么。要弄清楚应用程序使用的内存是什么类型,转到9.6.6节。如果没有特定进程使用了更多内存,则见9.6.4节。
使用ipcs来确定被使用的共享内存的数量是否在增加。如果是,见9.6.5节以确定哪些进程在使用内存。否则,你遇到是不在本书讨论范围内的系统内存泄露问题,转到9.9节。
用ipcs来确定哪些进程使用并分配了共享内存。确定了使用了共享内存的进程之后,就调查各个进程来找出它们为什么使用内存。比如,在应用程序的源代码中寻找对shmget (分配共享内存)或shmat(附加到它上面)的调用。阅读应用程序的文档,查找解释并减少其共享内存使用的选项。
尝试减少共享内存使用量并转到9.9节。
找出进程使用的内存类型最简单的方法是在/proc文件系统中查看其状态。文件cat /proc//status给出了进程内存使用情况的详细信息。
如果进程具有大的VmExe值,这就意味着可执行文件很大。要指明可执行文件中哪些函数导致了这个大小,请转到9.6.8节。如果进程具有大的VmLib值,这就意味着该进程使用了大量的共享库,或是几个体积较大的共享库。要指明哪些库导致了这个大小,请转到9.6.9节。如果进程的VmData值较大并且在增加,这就意味着该进程的数据区或堆在增加。要分析其原因,请转到9.6.10节。
要找出哪些函数分配了大量的栈,我们必须使用gdb和一点点技巧。第一步,使用gdb 附加到正在运行的进程。第二步,用bt要求gdb产生回溯。第三步,(在i386上)用info registers esp输出栈指针。这个输出就是栈指针的当前值。现在键入up并输出栈指针。前面栈指针和当前栈指针的差值(十六进制)就是前一个函数使用的栈容量。继续这样up回溯,你将可以发现哪个函数使用了大部分的栈。
当你确定了哪个函数或函数组消耗了大部分的栈之后,你可以修改应用程序,减少该函数(或这些函数)的调用次数和大小。转到9.9节。
如果可执行文件使用了相当可观的内存容量,那么确定哪些函数占用了最多的空间,并删除不必要的函数可能会有所帮助。对一个可执行文件或符号编译的库来说,可以请求nm显示所有符号的大小,并用如下命令对它们进行排序:
nm -S -size-sort
了解每个函数的大小后,就可能减少它们的大小或者从应用程序中移除不必要的代码。转到9.9节。
要了解进程使用了哪些库以及这些库各自的大小,最简单的方法是查看/proc文件系统中的进程映射。文件cat /proc//map显示的是每个库及其代码与数据的大小。当你知道进程使用了哪些库之后,就有可能淘汰对大型库的使用,或者是用小一点的库来代替它们。但是,这样做的时候必须要小心,因为移除大型库未必会减少整个系统的内存使用量。
如果某库正在被其他任何应用程序使用(可以运行lsof来确定该库),库就已经被加载到了内存。任何新应用程序在使用这个库的时候都不需要再加载一个该库的副本到内存。让程序转而使用不同的库(即使是个小库)实际上就会增加总的内存使用量。这个新的库没有被其他进程使用,因此需要为其分配新的内存。最好的解决方法是缩小库自身的大小,或是修改它们以便使用更少的内存来保存库的特定数据。如果可行,则所有的应用程序都将受益。
要了解特定库中函数的大小,转到9.6.8节。否则,转到9.9节。
如果你的应用程序是用C或C++编写的,就可以使用内存剖析器memprof来找出哪些函数分配了堆内存。memprof能够动态展示应用程序使用的内存量是如何增长的。
如果你的应用程序是用Java编写的,就在java命令行上添加-Xrunhprof命令行参数,它将会给出应用程序分配内存的详细信息。如果你的应用程序是用C#(Mono)编写的,就在mono命令行上添加-profile命令行参数,它也会给出应用程序分配内存的详细信息。
当你知道了哪些函数分配了最多的内存之后,就有可能减少被分配的内存大小。由于内存便宜,且越界错误很难被侦测到,因此,为了安全考虑,程序员常常超量分配内存。然而,如果一个特定的分配导致了内存问题,那么仔细分析最小分配就可能在保证安全的前提下,显著减少内存使用量。转到9.9节。
当你确定是磁盘I/O有问题后,明确是哪个应用程序引起了I/O就会有所帮助。图9-5给出了确定磁盘I/O使用原因的步骤。调查从9.7.1节开始。
在扩展统计模式下运行iostat,寻找平均等待(await)大于零的分区。await是等待请求被响应所平均花费的毫秒数。这个数值越高,则磁盘超负荷越多。可以通过查看磁盘的读写流量并确定其是否接近该驱动器可以处理的最大量来确认超负荷。
如果单个驱动器上的很多文件都被访问了,那么,将这些文件分散到多个磁盘就可能提高性能。不过,首先要确定的是哪些文件被访问了。
转到9.7.2节。
在前面关于磁盘I/O的章节中已经介绍过,确定哪个进程导致了大量的I/O是有难度的,因此,我们必须在缺少直接实现该功能工具的情况下来试着解决这个问题。通过运行top,首先寻找非空闲进程。对于每个这样的进程,转到9.7.3节。
首先,通过strace,用strace -e trace=file来追踪应用程序中所有与文件I/O相关的系统调用。然后strace用摘要信息来查看每个调用花费的时长。如果某些读写调用完成时间很长,那么这个进程可能造成了I/O的缓慢。在正常模式下运行strace就可以发现是从哪个文件描述符进行读写的。要把这些文件描述符映射回文件系统中的文件,我们可以查看proc 文件系统。/proc//fd/中的文件是从文件描述符到实际文件的符号链接。该目录下的ls -la 会显示进程使用了哪些文件。通过了解进程访问的文件,就有可能减少该进程执行的I/O 量,将其更均匀地分散于多个磁盘,或者将其迁移到更快的磁盘。
确定进程访问哪些文件后,转到9.9节。
当知道网络发生了问题时,Linux提供了一组工具来确定哪些应用程序涉及其中。但是,在与外部机器连接时,对网络问题的修复就不完全由你控制了。图9-6展示了调查网络性能问题的步骤。调查从9.8.1节开始。
要做的第一件事就是用ethtool来确定每个Ethernet设备设置的硬件速度是多少。如果有这些信息的记录,就可以调查是否有网络设备处于饱和状态。Ethernet设备和/或交换机容易被误配置,ethtool显示每个设备认为其应运行的速度。在确定了每个Ethernet设备的理论极限后,使用iptraf(甚至是ifconfig)来明确流经每个接口的流量。如果有任何网络设备表现出饱和,转到9.8.3节。否则,转到9.8.2节。
网络流量减缓的原因也可能是大量的网络错误。用ifconfig来确定是否有接口产生了大量的错误。大量错误可能是不匹配的Ethernet卡/Ethernet交换机设置的结果。联系你的网络管理员,在Web上搜索遇到类似问题的人,或者把问题e-mail给一个Linux网络新闻组。
转到9.9节。
如果特定设备正在服务大量的数据,使用iptraf可以跟踪该设备发送和接收的流量类型。当知道了设备处理的流量类型后,转到9.8.4节。
接下来,我们想要确定是否有特定进程要为这个流量负责。使用netstat的-p选项来查看是否有进程在处理流经网络端口的类型流量。如果有应用程序要对此负责,转到9.8.6节。如果没有这样的程序,则转到9.8.5节。
如果没有应用程序应对这个流量负责,那么就可能是网络上的某些系统用无用的流量攻击了你的系统。要确定是哪些系统发送了这些流量,要使用iptraf或etherape。
如果可能的话,请与系统所有者联系,并尝试找出发生这种情况的原因。如果所有者无法联系上,可以在Linux内核中设置ipfilters,永久丢弃这个特定的流量,或者是在远程机与本地机之间建立防火墙来拦截该流量。
转到9.9节。
确定使用了哪个套接字要分两步。第一步,用strace-e trace=file跟踪应用程序所有的I/O系统调用。这能显示进程是从哪些文件描述符进行读写的。第二步,通过查看proc文件系统,将这些文件描述符映射回套接字。/proc//fd/中的文件是从文件描述符到实际文件或套接字的符号链接。该目录下的1s-la会显示特定进程全部的文件描述符。名字中带有socket的是网络套接字。之后就可以利用这些信息来确定程序中的哪个套接字产生了这些通信。
转到9.9节。
当你看到这里的时候,你的问题可能得到也可能没有得到解决,但是,你会获取大量描述它的信息。在Web和新闻组上搜索遇到相同问题的人,向他们和开发者发电子邮件,看看他们是如何解决问题的。尝试一个解决方案,并观察系统或应用程序的行为是否发生了变化。每次尝试新方案时,请转到9.2节重新开始系统诊断,因为,每一个修复都可能会让应用程序的行为发生变化。
本章提供了综合运用Linux性能工具跟踪不同类型性能问题的方法。虽然这个方法不可能捕捉到每一种可能出错的性能问题,但是它有助于发现一些比较常见的问题。此外,即便你面对的问题在这里没有涉及,你所收集的数据仍然是有用的,因为,这些数据可能会开启调查的不同方面。
接下来的几章将演示如何在Linux系统中使用该方法找出性能问题。