原文地址:http://www.51ste.com/front/det-2-1.html
1系统性能定义
性能测试,主要是通过自动化的测试工具模拟多种正常、峰值以及异常负载条件来对系统的各项性能指标进行测试。系统性能主要包括两个值:
吞吐量(Throughtput),即每秒钟可以处理的请求数,事务数。
系统延迟(Latency),也就是系统在处理一个请求或一个事务时的延迟。
它们两者之间的关系:
Throughput越大,Latency会越差。请求量过大,系统繁忙,响应速度自然低。
Latency越好,能支持的Throughput就会越高。因为Latency短说明处理速度快,于是单位时间内就可以处理更多的请求。
2系统性能测试
要测试系统的性能,需要我们收集系统的Throughput和Latency这两个值。
首先,需要定义Latency这个值,如对于网站首页响应时间必需是4秒以内(根据不同的业务来定义)
其次,准备性能测试工具,一个工具用来制造高强度的Throughput(通常有Loadrunner、Jmeter等),另一个工具用来测量Latency。关于如何测量Latency,你可以在代码中测量,但是这样会影响程序的执行,而且只能测试到程序内部的Latency,真正的Latency是整个系统都算上,包括操作系统和网络的延时,一般性能测试工具都有配套组件。
最后,开始性能测试。不断地提升测试环境的Throughput,然后观察系统的负载情况,如果系统顶得住,那就观察Latency的值。这样,就可以找到系统的最大负载,并且你可以知道系统的响应延时是多少。
下面主要通过三个大的步骤来说明性能测试实施过程:
前期准备
1、确定用户、业务、系统需求(目标)
- 确定实际业务需求
主要确定用户的业务请求分布等:主要业务请求、平均日交易量、年交易量、峰值交易量等等。
- 确定系统需求
主要工作是分析系统的性能需求、确定合理的性能目标。
- 确定客户的需求和期望
在需求分析文档的支持下,对软件系统上的用户业务使用情况进行分析,提出我们所关注的性能测试需求,并告知业务人员。让业务人员来判断我们的性能需求是否满足客户的真实需求。
2、确定系统类别
分清系统类别是我们掌握什么样技术的前提,掌握相应技术做性能测试才可能成功。例如:系统类别是B/S结构,需要掌握HTTP协议,java,C#,html等技术。或者是C/S结构,可能要了解操作系统,winsock,com等。
3、确定系统构成
不同的系统构成性能测试会得到不同的结果。
4、确定实际网络带宽
便于测试时对带宽做模拟,尽可能真实的反馈带宽使用情况。
5、确定测试服务器与测试机配置清单
了解性能测试硬件资源(包括所测服务器,测试机等),根据实际情况添加设备。
6、系统功能流程图
便于测试人员分析系统哪些模块易出现瓶颈,从而针对性做性能测试。
7、测试时间评估
根据测试时间,制定相应的测试执行策略。
在我的实际性能测试工作中,会把上面作为问题列表的形式打印出来,然后通过不断沟通和分析去完善它,以便帮助我后期更好的制定性能测试策略。这样的问题列表如:
- 具体哪些业务需要做性能测试?
- 从测试类型来看(列出性能测试类型、对应的解释及指标,方便调查对象参考),要对性能关键业务做以上一项或多项测试?
- 采用什么系统部署环境?系统的版本和位数是多少?
- ……
测试实施
1、根据前期准备制定编写性能测试方案
应该包括性能测试通过的标准、所测的对象(业务或场景)、测试环境说明、测试所用时间及资源,测试策略等。
2、设计性能测试用例
指导测试人员进行性能测试。
3、编写性能测试脚本
测试脚本应该具有可重复执行性,可并发执行性,能尽可能真实模拟单用户业务使用场景。
4、模拟运行场景
用性能工具模拟用户负载使用系统的场景。
5、配置运行场景
被测环境应该在测试前做好备份;配置包括被测系统日志、中间件(MySQL、Apache等)使用情况、服务器资源使用情况的数据收据;运行应避免偶然性事件,所以一个测试场景应该至少运行两次及以上。
瓶颈分析和性能优化
瓶颈分析难点在于理解收集到的数据反馈的真是含义,所以要求测试人员具备相应的技术知识储备(包括数据库的知识、计算机原理知识、网络基础、各中间件性能计量含义等),这里先仅列举下性能测试结果分析原则:
- 把事实与推测分开,总是用实际的证据来证明你的推测;
- 在没有足够证据前,不对程序进行优化;
- 优先验证简单的假设(推测瓶颈);
- 日志文件中没有错误不代表真的没有错误;
- 从系统到应用、从外到内进行层层剥离,缩小范围;
- 范围缩小后,再分割成多个单元,对每个单元进行轮番压力测试,来证明或者否定是哪个单元引起的性能问题 。
瓶颈优化应该有难到易的进行优化,下面为推荐(应根据公司实际情况调整):
服务器硬件瓶颈->网络瓶颈->服务器操作系统瓶颈->中间件瓶颈->应用瓶颈
3定位性能瓶颈
3.1查看操作系统负载
系统有问题首要需要看的是操作系统报告。查看操作系统的CPU利用率,内存使用率,操作系统的IO,还有网络的IO,网络链接数等。
- 先看CPU利用率,如果CPU利用率不高,但是系统的Throughput和Latency上不去了,这说明程序并没有忙于计算,而是忙做其它事,比如IO。(另外,CPU的利用率还要看内核态的和用户态的,内核态的上去了,整个系统的性能就下来了。而对于多核CPU来说,CPU 0 是相当关键的,如果CPU 0的负载高,那么会影响其它核的性能,因为CPU各核间是需要有调度的,这靠CPU0完成)
- 然后,我们可以查看一下IO大小,IO和CPU一般是反着来的,CPU利用率高则IO不大,IO大则CPU就小。关于IO,我们要看三个事,一个是磁盘文件IO,一个是驱动程序的IO(如:网卡),一个是内存换页率。这三个事都会影响系统性能。
- 然后,查看一下网络带宽使用情况。
- 如果CPU不高,IO不高,内存使用不高,网络带宽使用不高。但是系统的性能上不去。这说明你的程序有问题,比如,你的程序被阻塞了。可能是因为等那个锁,可能是因为等某个资源,或者是在切换上下文。
通过了解操作系统的性能,我们才知道性能的问题,比如:带宽不够,内存不够,TCP缓冲区不够,等等,很多时候,不需要调整程序的,只需要调整一下硬件或操作系统的配置就可以了。
3.2使用profiler测试程序瓶颈
使用某个Profiler来查看一下我们程序的运行性能。如:Java的JProfiler。使用Profiler工具,可以让你查看程序中各个模块函数甚至指令的很多东西,如:运行的时间 ,调用的次数,CPU的利用率,等等。
我们重点观察运行时间最多,调用次数最多的那些函数和指令。这里注意一下,对于调用次数多但是时间很短的函数,你可能只需要轻微优化一下,你的性能就上去了。
因为Profiler会让你的程序运行的性能变低对此,一般有两个方法来定位系统瓶颈:
1)在你的代码中自己做统计,使用微秒级的计时器和函数调用计算器,每隔10秒把统计log到文件中。
2)分段注释你的代码块,让一些函数空转,做Hard Code的Mock,然后再测试一下系统的Throughput和Latency是否有质的变化,如果有,那么被注释的函数就是性能瓶颈,再在这个函数体内注释代码,直到找到最耗性能的语句。
查看汇编代码经常会给你一些意想不到的东西让你知道为什么程序的性能是那样。
对于性能测试,不同的Throughput会出现不同的测试结果,不同的测试数据也会有不同的测试结果。所以,用于性能测试的数据非常重要,性能测试中,我们需要测试观察不同Throughput的结果。
4性能调优(代码技术细节层面)
一般来说,性能优化也就是下面的几个策略:
- 用空间换时间。各种cache如CPU L1/L2/RAM到硬盘,都是用空间来换时间的策略。这样策略基本上是把计算的过程一步一步的保存或缓存下来,这样就不用每次用的时候都要再计算一遍,比如数据缓冲,CDN等。这样的策略还表现为冗余数据,比如数据镜象,负载均衡什么的。
- 用时间换空间。有时候,少量的空间可能性能会更好,比如网络传输,如果有一些压缩数据的算法(如 “Huffman 编码压缩算法” 和 “rsync 的核心算法”),这样的算法其实很耗时,但是因为瓶颈在网络传输,所以用时间来换空间反而能省时间。
- 简化代码。最高效的程序就是不执行任何代码的程序,所以,代码越少性能就越高。如:减少循环的层数,减少递归,在循环中少声明变量,少做分配和释放内存的操作,尽量把循环体内的表达式抽到循环外,条件表达的中的多个条件判断的次序,尽量在程序启动时把一些东西准备好,注意函数调用的开销(栈上的开销),注意面向对象语言中临时对象的开销,小心使用异常(不要用异常来检查一些可接受可忽略并经常发生的错误),…… 等等,这些东西需要我们非常了解编程语言和常用的库。
- 并行处理。如果CPU只有一个核,你要玩多进程,多线程,对于计算密集型的软件会反而更慢(因为操作系统调度和切换开销很大),CPU的核多了才能真正体现出多进程多线程的优势。并行处理需要我们的程序有Scalability,不能水平或垂直扩展的程序无法进行并行处理。从架构上来说,这表再为——是否可以做到不改代码只是加加机器就可以完成性能提升?
总之,根据2:8原则来说,20%的代码耗了你80%的性能,找到那20%的代码,你就可以优化那80%的性能。 下面为一些最有价值的性能调优的的方法,供参考。
4.1算法调优
算法非常重要,好的算法会有更好的性能。下面为几个算法调优例子:
- 一个是过滤算法,系统需要对收到的请求做过滤,我们把可以被filter in/out的东西配置在了一个文件中,原有的过滤算法是遍历过滤配置,后来我们找到了一种方法可以对这个过滤配置进行排序,这样就可以用二分折半的方法来过滤,系统性能增加了50%。
- 一个是哈希算法。计算哈希算法的函数并不高效,一方面是计算太费时,另一方面是碰撞太高,碰撞高了就跟单向链表一个性能。我们知道,算法都是和需要处理的数据很有关系的,就算是被大家所嘲笑的“冒泡排序”在某些情况下(大多数数据是排好序的)其效率会高于所有的排序算法。哈希算法也一样,广为人知的哈希算法都是用英文字典做测试,但是我们的业务在数据有其特殊性,所以,对于还需要根据自己的数据来挑选适合的哈希算法。
- 分而治之和预处理。以前有一个程序为了生成月报表,每次都需要计算很长的时间,有时候需要花将近一整天的时间。于是我们把我们找到了一种方法可以把这个算法发成增量式的,也就是说我每天都把当天的数据计算好了后和前一天的报表合并,这样可以大大的节省计算时间,每天的数据计算量只需要20分钟,但是如果我要算整个月的,系统则需要10个小时以上(SQL语句在大数据量面前性能成级数性下降)。这种分而治之的思路在大数据面前对性能有很帮助,就像merge排序一样。SQL语句和数据库的性能优化也是这一策略,如:使用嵌套式的Select而不是笛卡尔积的Select,使用视图,等等。
4.2代码调优
代码上的调优大致有下面这几点:
- 字符串操作。这是最费系统性能的事了,无论是strcpy, strcat还是strlen,最需要注意的是字符串匹配。所以,能用整型最好用整型。
- 多线程调优。有人说thread is evil,这个对于系统性能在某些时候是个问题。因为多线程瓶颈就在于互斥和同步的锁上,以及线程上下文切换的成本,怎么样的少用锁或不用锁是根本(比如:多版本并发控制(MVCC)在分布式系统中的应用 中说的乐观锁可以解决性能问题),此外,还有读写锁也可以解决大多数是读操作的并发的性能问题。另外,线程不是越多越好,线程间的调度和上下文切换也是很夸张的事,尽可能的在一个线程里干,尽可能的不要同步线程。这会让你有很多的性能。
- 内存分配。不要小看程序的内存分配。malloc/realloc/calloc这样的系统调用非常耗时,尤其是当内存出现碎片的时候。池化技术,如线程池,连接池等。池化技术对于一些短作业来说(如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(还整理内存碎片),他运行的时候,整个世界的时间都停止了。
4.3网络调优
下面只讲一些概念上的东西。
1、TCP调优
TCP链接是有很多开销的,一个是会占用文件描述符,另一个是会开缓存,一般来说一个系统可以支持的TCP链接数是有限的,我们需要清楚地认识到TCP链接对系统的开销是很大的。正是因为TCP是耗资源的,所以,很多攻击都是让你系统上出现大量的TCP链接,把你的系统资源耗尽。比如著名的SYNC Flood攻击。
所以,我们要注意配置KeepAlive参数,这个参数的意思是定义一个时间,如果链接上没有数据传输,系统会在这个时间发一个包,如果没有收到回应,那么TCP就认为链接断了,然后就会把链接关闭,这样可以回收系统资源开销。(注:HTTP层上也有KeepAlive参数)对于像HTTP这样的短链接,设置一个1-2分钟的keepalive非常重要。这可以在一定程度上防止DoS攻击。有下面几个参数(下面这些参数的值仅供参考):
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服务器上。对此,有两个参数需要注意,
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的带宽使用率会马上受到影响(会盲目减半),再丢包,再减半,然后如果不丢包了,就逐步恢复。相关参数如下:
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会有更好的网络传输性能(来来回回太多太影响性能了)。
2、UDP调优
说到UDP的调优, 就得说MTU——最大传输单元(其实这对TCP也一样,因为这是链路层上的东西)。所谓最大传输单元,你可以想像成是公路上的公交车,假设一个公交车可以最多坐70人,带宽就像是公路的车道数一样,如果一条路上最多可以容下100辆公交车,那意味着我最多可以运送7000人,但是如果公交车坐不满,比如平均每辆车只有20人,那么我只运送了2000人,于是我公路资源(带宽资源)就被浪费了。 所以,我们对于一个UDP的包,我们要尽量地让它大到MTU的最大尺寸再往网络上传,这样可以最大化带宽利用率。对于这个MTU,以太网是1500字节,光纤是4352字节,802.11无线网是7981。但是,当我们用TCP/UDP发包的时候,我们的有效负载Payload要低于这个值,因为IP协议会加上20个字节,UDP会加上8个字节(TCP加的更多),所以,一般来说,你的一个UDP包的最大应该是1500-8-20=1472,这是你的数据的大小。当然,如果你用光纤的话, 这个值就可以更大一些。(顺便说一下,对于某些千光以态网网卡来说,在网卡上,网卡硬件如果发现你的包的大小超过了MTU,其会帮你做fragment,到了目标端又会帮你做重组,这就不需要你在程序中处理了)
用Socket编程的时候,你可以使用setsockopt() 设置 SO_SNDBUF/SO_RCVBUF 的大小,TTL和KeepAlive这些关键的设置,当然,还有很多,具体可以查看一下Socket的手册。
最后UDP还有一个最大的好处是multi-cast多播,这个技术对于你需要在内网里通知多台结点时非常方便和高效。而且,多播这种技术对于机会的水平扩展(需要增加机器来侦听多播信息)也很有利。
3、网卡调优
对于网卡,也是可以调优的,这对于千兆以及网网卡非常必要,在Linux下,我们可以用ifconfig查看网上的统计信息,如果我们看到overrun上有数据,我们就可能需要调整一下txqueuelen的尺寸(一般默认为1000),我们可以调大一些,如:ifconfig eth0 txqueuelen 5000。Linux下还有一个命令叫:ethtool可以用于设置网卡的缓冲区大小。在Windows下,我们可以在网卡适配器中的高级选项卡中调整相关的参数(如:Receive Buffers, Transmit Buffer等,不同的网卡有不同的参数)。把Buffer调大对于需要大数据量的网络传输非常有效。
4、其它网络性能
关于多路复用技术,也就是用一个线程来管理所有的TCP链接,有三个系统调用要重点注意:一个是select,这个系统调用只支持上限1024个链接,第二个是poll,其可以突破1024的限制,但是select和poll本质上是使用的轮询机制,轮询机制在链接多的时候性能很差,因主是O(n)的算法,所以,epoll出现了,epoll是操作系统内核支持的,仅当在链接活跃时,操作系统才会callback,这是由操作系统通知触发的,但其只有Linux Kernel 2.6以后才支持(准确说是2.5.44中引入的),当然,如果所有的链接都是活跃的,过多的使用epoll_ctl可能会比轮询的方式还影响性能,不过影响的不大。
另外,关于一些和DNS Lookup的系统调用要小心,比如:gethostbyaddr/gethostbyname,这个函数可能会相当的费时,因为其要到网络上去找域名,因为DNS的递归查询,会导致严重超时,而又不能通过设置什么参数来设置time out,对此你可以通过配置hosts文件来加快速度,或是自己在内存中管理对应表,在程序启动时查好,而不要在运行时每次都查。另外,在多线程下面,gethostbyname会一个更严重的问题,就是如果有一个线程的gethostbyname发生阻塞,其它线程都会在gethostbyname处发生阻塞,这个比较变态,要小心。(可以试试GNU的gethostbyname_r(),这个的性能要好一些) 这种到网上找信息的东西很多,比如,如果你的Linux使用了NIS,或是NFS,某些用户或文件相关的系统调用就很慢,所以要小心。
4.4系统调优
1、I/O模型
前面说到过select/poll/epoll这三个系统调用,我们都知道,Unix/Linux下把所有的设备都当成文件来进行I/O,所以,那三个操作更应该算是I/O相关的系统调用。说到 I/O模型,这对于我们的I/O性能相当重要,我们知道,Unix/Linux经典的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的。
再说一下Windows下的I/O模型:
a)一个是WriteFile系统调用,这个系统调用可以是同步阻塞的,也可以是同步无阻塞的,关于看文件是不是以Overlapped打开的。关于同步无阻塞,需要设置其最后一个参数Overlapped,微软叫Overlapped I/O,你需要WaitForSingleObject才能知道有没有写完成。这个系统调用的性能可想而知。
b)另一个叫WriteFileEx的系统调用,其可以实现异步I/O,并可以让你传入一个callback函数,等I/O结束后回调之,但是这个回调的过程Windows是把callback函数放到了APC(Asynchronous Procedure Calls)的队列中,然后,只用当应用程序当前线程成为可被通知状态(Alterable)时,才会被回调。只有当你的线程使用了这几个函数时WaitForSingleObjectEx, WaitForMultipleObjectsEx,
MsgWaitForMultipleObjectsEx, SignalObjectAndWait 和 SleepEx,线程才会成为Alterable状态。可见,这个模型,还是有wait,所以性能也不高。
c)然后是IOCP – IO Completion Port,IOCP会把I/O的结果放在一个队列中,但是,侦听这个队列的不是主线程,而是专门来干这个事的一个或多个线程去干(老的平台要你自己创建线程,新的平台是你可以创建一个线程池)。IOCP是一个线程池模型。这个和Linux下的AIO模型比较相似,但是实现方式和使用方式完全不一样。
当然,真正提高I/O性能方式是把和外设的I/O的次数降到最低,最好没有,所以,对于读来说,内存cache通常可以从质上提升性能,因为内存比外设快太多了。对于写来说,cache住要写的数据,少写几次,但是cache带来的问题就是实时性的问题,也就是latency会变大,我们需要在写的次数上和响应上做权衡。
2、多核CPU调优
关于CPU的多核技术, CPU0是很关键的,如果0号CPU被用得过多时,别的CPU性能也会下降,因为CPU0作用是有调整功能的,所以,我们不能任由操作系统负载均衡,因为我们自己更了解自己的程序,所以,我们可以手动的为其分配CPU核,而不会过多地占用CPU0,或是让我们关键进程和一堆别的进程挤在一起。
对于Windows来说,我们可以通过“任务管理器”中的“进程”而中右键菜单中的“设置相关性……”(Set Affinity…)来设置并限制这个进程能被运行在哪些核上。
对于Linux来说,可以使用taskset命令来设置(你可以通过安装schedutils来安装这个命令:apt-get install schedutils)
多核CPU还有一个技术叫NUMA技术(Non-Uniform Memory Access)。传统的多核运算是使用SMP(Symmetric Multi-Processor )模式,多个处理器共享一个集中的存储器和I/O总线。于是就会出现一致存储器访问的问题,一致性通常意味着性能问题。NUMA模式下,处理器被划分成多个node, 每个node有自己的本地存储器空间。在Linux下,对NUMA调优的命令是:numactl 。如下面的命令:(指定命令“myprogram arg1 arg2”运行在node 0 上,其内存分配在node 0 和 1上)
numactl --cpubind=0 --membind=0,1 myprogram arg1 arg2
当然,上面这个命令并不好,因为内存跨越了两个node,这非常不好。最好的方式是只让程序访问和自己运行一样的node,如:
$ numactl --membind 1 --cpunodebind 1 --localalloc myapplication
3、文件系统调优
因为文件系统也是有cache的,所以,为了让文件系统有最大的性能:首要的事情就是分配足够大的内存,这个非常关键,在Linux下可以使用free命令来查看 free/used/buffers/cached,理想来说,buffers和cached应该有40%左右;然后是一个快速的硬盘控制器,SCSI会好很多;最快的是Intel SSD 固态硬盘,速度超快,但是写次数有限。
接下来,我们就可以调优文件系统配置了,对于Linux的Ext3/4来说,几乎在所有情况下都有所帮助的一个参数是关闭文件系统访问时间,在/etc/fstab下看看你的文件系统有没有noatime参数(一般来说应该有),还有一个是dealloc,它可以让系统在最后时刻决定写入文件发生时使用哪个块,可优化这个写入程序。还要注意一下三种日志模式:data=journal、data=ordered和data=writeback。默认设置data=ordered提供性能和防护之间的最佳平衡。
当然,对于这些来说,ext4的默认设置基本上是最佳优化了。
这里介绍一个Linux下的查看I/O的命令—— iotop,可以让你看到各进程的磁盘读写的负载情况。
4.5数据库调优
1、数据库引擎调优
数据库的锁的方式。这个非常地重要。并发情况下,锁是非常影响性能的。各种隔离级别,行锁,表锁,页锁,读写锁,事务锁,以及各种写优先还是读优先机制。性能最高的是不要锁,所以,分库分表,冗余数据,减少一致性事务处理,可以有效地提高性能。NoSQL就是牺牲了一致性和事务处理,并冗余数据,从而达到了分布式和高性能。
数据库的存储机制。不但要搞清楚各种类型字段是怎么存储的,更重要的是数据库的数据存储方式,是怎么分区的,是怎么管理的,比如Oracle的数据文件,表空间,段,等等。了解清楚这个机制可以减轻很多的I/O负载。比如:MySQL下使用show engines;可以看到各种存储引擎的支持。不同的存储引擎有不同的侧重点,针对不同的业务或数据库设计会让你有不同的性能。
数据库的分布式策略。最简单的就是复制或镜像,需要了解分布式的一致性算法,或是主主同步,主从同步。通过了解这种技术的机理可以做到数据库级别的水平扩展。
2、SQL语句优化
关于SQL语句的优化,首先也是要使用工具,比如:MySQL SQL Query Analyzer,Oracle SQL Performance Analyzer,或是微软SQL Query Analyzer,基本上来说,所有的RMDB都会有这样的工具,来让你查看你的应用中的SQL的性能问题。 还可以使用explain来看看SQL语句最终Execution Plan会是什么样的。
还有一点很重要,数据库的各种操作需要大量的内存,所以服务器的内存要够,优其应对那些多表查询的SQL语句,那是相当的耗内存。
下面简单说几个会有性能问题的SQL:
1)全表检索。比如:select * from user where lastname = “xxxx”,这样的SQL语句基本上是全表查询,线性复杂度O(n),记录数越多,性能也越差(如:100条记录的查找要50ms,一百万条记录需要5分钟)。对于这种情况,我们可以有两种方法提高性能:一种方法是分表,把记录数降下来,另一种方法是建索引(为lastname建索引)。索引就像是key-value的数据结构一样,key就是where后面的字段,value就是物理行号,对索引的搜索复杂度是基本上是O(log(n)) ——用B-Tree实现索引(如:100条记录的查找要50ms,一百万条记录需要100ms)。
2)索引。对于索引字段,最好不要在字段上做计算、类型转换、函数、空值判断、字段连接操作,这些操作都会破坏索引原本的性能。当然,索引一般都出现在Where或是Order by字句中,所以对Where和Order by子句中的字段最好不要进行计算操作,或是加上什么NOT之类的,或是使用什么函数。
3)多表查询。关系型数据库最多的操作就是多表查询,多表查询主要有三个关键字,EXISTS,IN和JOIN。基本来说,现代的数据引擎对SQL语句优化得都挺好的,JOIN和IN/EXISTS在结果上有些不同,但性能基本上都差不多。有人说,EXISTS的性能要好于IN,IN的性能要好于JOIN,个人觉得,这个还要看你的数据、schema和SQL语句的复杂度,对于一般的简单的情况来说,都差不多,所以千万不要使用过多的嵌套,千万不要让你的SQL太复杂,宁可使用几个简单的SQL也不要使用一个巨大无比的嵌套N级的SQL。还有人说,如果两个表的数据量差不多,Exists的性能可能会高于In,In可能会高于Join,如果这两个表一大一小,那么子查询中,Exists用大表,In则用小表。
4)JOIN操作。有人说,Join表的顺序会影响性能,只要Join的结果集是一样,性能和join的次序无关。因为后台的数据库引擎会帮我们优化的。Join有三种实现算法,嵌套循环,排序归并,和Hash式的Join。(MySQL只支持第一种)
嵌套循环,就好像是我们常见的多重嵌套循环。注意,前面的索引说过,数据库的索引查找算法用的是B-Tree,这是O(log(n))的算法,所以,整个算法复法度应该是O(log(n)) * O(log(m)) 这样的。
Hash式的Join,主要解决嵌套循环的O(log(n))的复杂,使用一个临时的hash表来标记。
排序归并,意思是两个表按照查询字段排好序,然后再合并。当然,索引字段一般是排好序的。
总之,具体要看什么样的数据,什么样的SQL语句,你才知道用哪种方法最好。
5)部分结果集。我们知道MySQL里的Limit关键字,Oracle里的rownum,SQL Server里的Top都是在限制前几条的返回结果。这给了我们数据库引擎很多可以调优的空间。一般来说,返回top n的记录数据需要我们使用order by,注意在这里我们需要为order by的字段建立索引。有了被建索引的order by后,会让我们的select语句的性能不会被记录数的所影响。使用这个技术,一般来说我们前台会以分页方式来显现数据,MySQL用的是OFFSET,SQL Server用的是FETCH NEXT,这种Fetch的方式其实并不好是线性复杂度,所以,如果我们能够知道order by字段的第二页的起始值,我们就可以在where语句里直接使用>=的表达式来select,这种技术叫seek,而不是fetch,seek的性能比fetch要高很多。
6)字符串。正如我前面所说的,字符串操作对性能上有非常大的噩梦,所以,能用数据的情况就用数字,比如:时间,工号,等。
7)全文检索。千万不要用Like之类的东西来做全文检索,如果要玩全文检索,可以尝试使用Sphinx。
8)其它。
不要select *,而是明确指出各个字段,如果有多个表,一定要在字段名前加上表名,不要让引擎去算。
不要用Having,因为其要遍历所有的记录。性能差得不能再差。
尽可能地使用UNION ALL 取代 UNION。
索引过多,insert和delete就会越慢。而update如果update多数索引,也会慢,但是如果只update一个,则只会影响一个索引表。
5性能调优(业务和设计层面)
无论你怎么设计,你的系统一定要能容易地水平扩展。也就是说,你的整个数据流中,所有的环节都要能够水平扩展。
5.1前端性能优化技术
5.1.1前端负载均衡
通过DNS的负载均衡器(一般在路由器上根据路由的负载重定向)可以把用户的访问均匀地分散在多个Web服务器上。这样可以减少Web服务器的请求负载。因为http的请求都是短作业,所以,可以通过很简单的负载均衡器来完成这一功能。最好是有CDN网络让用户连接与其最近的服务器(CDN通常伴随着分布式存储)。对前端页面也有些优化要求:
- 减少前端链接数
- 减少网页大小增加带宽
- 前端页面静态化
5.1.2优化查询
很多人查询都是在查一样的,完全可以用反向代理合并这些并发的相同的查询。这样的技术主要用查询结果缓存来实现,第一次查询走数据库获得数据,并把数据放到缓存,后面的查询统统直接访问高速缓存。为每个查询作Hash,使用NoSQL的技术可以完成这个优化。(这个技术也可以用做静态页面)
5.1.3缓存的问题
缓存可以用来缓存动态页面,也可以用来缓存查询的数据。缓存通常有那么几个问题:
1)缓存的更新。也叫缓存和数据库的同步。有这么几种方法,一是缓存time out,让缓存失效,重查,二是,由后端通知更新,一量后端发生变化,通知前端更新。前者实现起来比较简单,但实时性不高,后者实现起来比较复杂 ,但实时性高。
2)缓存的换页。内存可能不够,所以,需要把一些不活跃的数据换出内存,这个和操作系统的内存换页和交换内存很相似。FIFO、LRU、LFU都是比较经典的换页算法。
3)缓存的重建和持久化。缓存在内存,系统总要维护,所以,缓存就会丢失,如果缓存没了,就需要重建,如果数据量很大,缓存重建的过程会很慢,这会影响生产环境,所以,缓存的持久化也是需要考虑的。
诸多强大的NoSQL都很好支持了上述三大缓存的问题。
5.2后端性能优化技术
前面讨论了前端性能的优化技术,于是前端可能就不是瓶颈问题了。那么性能问题就会到后端数据上来了。下面说几个后端常见的性能优化技术。
5.2.1数据冗余
关于数据冗余,也就是说,把我们的数据库的数据冗余处理,也就是减少表连接这样的开销比较大的操作,但这样会牺牲数据的一致性。风险比较大。很多人把NoSQL用做数据,快是快了,因为数据冗余了,但这对数据一致性有大的风险。这需要根据不同的业务进行分析和处理。(注意:用关系型数据库很容易移植到NoSQL上,但是反过来从NoSQL到关系型就难了)
5.2.2数据镜像
几乎所有主流的数据库都支持镜像,也就是replication。数据库的镜像带来的好处就是可以做负载均衡。把一台数据库的负载均分到多台上,同时又保证了数据一致性(如Oracle的SCN)。最重要的是,这样还可以有高可用性,一台废了,还有另一台在服务。
数据镜像的数据一致性可能是个复杂的问题,所以我们要在单条数据上进行数据分区,也就是说,把一个畅销商品的库存均分到不同的服务器上,如,一个畅销商品有1万的库存,我们可以设置10台服务器,每台服务器上有1000个库存,这就好像B2C的仓库一样。
5.2.3数据分区
数据镜像不能解决的一个问题就是数据表里的记录太多,导致数据库操作太慢。所以,把数据分区。数据分区有很多种做法,一般来说有下面这几种:
1)把数据把某种逻辑来分类。比如火车订票系统可按各种车型分,可以按始发站分,可以按目的地分……,反正就是把一张表拆成多张有一样的字段但是不同种类的表,这样,这些表就可以存在不同的机器上以达到分担负载的目的。
2)把数据按字段分。比如把一些不经常改的数据放在一个表里,经常改的数据放在另外多个表里。把一张表变为1对1的关系,这样,你可以减少表的字段个数,同样可以提升一定的性能。另外,字段多会造成一条记录的存储会被放到不同的页表里,这对于读写性能都有问题。但这样一来会有很多复杂的控制。
3)平均分表。因为第一种方法是并不一定平均分均,可能某个种类的数据还是很多。所以,也有采用平均分配的方式,通过主键ID的范围来分表。
4)同一数据分区。也就是把同一商品的库存值分到不同的服务器上,比如有10000个库存,可以分到10台服务器上,一台上有1000个库存。然后负载均衡。
这三种分区都有好有坏。最常用的还是第一种。数据一旦分区,你就需要有一个或是多个调度来让你的前端程序知道去哪里找数据。
5.2.4后端系统负载均衡
前面说了数据分区,数据分区可以在一定程度上减轻负载,但是无法减轻热销商品的负载。这就需要使用数据镜像来减轻负载。使用数据镜像,必然要使用负载均衡,在后端,我们可能很难使用像路由器上的负载均衡器,因为那是均衡流量的,因为流量并不代表服务器的繁忙程度。因此,我们需要一个任务分配系统,其还能监控各个服务器的负载情况。
任务分配服务器有一些难点:
负载情况比较复杂。什么叫忙?是CPU高?还是磁盘I/O高?还是内存使用高?还是并发高?还是内存换页率高?你可能需要全部都要考虑。这些信息要发送给那个任务分配器上,由任务分配器挑选一台负载最轻的服务器来处理。
任务分配服务器上需要对任务队列,不能丢任务,所以还需要持久化。并且可以以批量的方式把任务分配给计算服务器。
任务分配服务器死了怎么办?这里需要一些如Live-Standby或是failover等高可用性的技术。我们还需要注意那些持久化了的任务的队列如何转移到别的服务器上的问题。
有很多系统都用静态的方式来分配,有的用hash,有的就简单地轮流分析。这些都不够好,一个是不能完美地负载均衡,另一个静态的方法的致命缺陷是,如果有一台计算服务器死机了,或是我们需要加入新的服务器,对于我们的分配器来说,都需要知道的。另外,还要重算哈希(一致性hash可以部分解决这个问题)。
还有一种方法是使用抢占式的方式进行负载均衡,由下游的计算服务器去任务服务器上拿任务。让这些计算服务器自己决定自己是否要任务。这样的好处是可以简化系统的复杂度,而且还可以任意实时地减少或增加计算服务器。但是唯一不好的就是,如果有一些任务只能在某种服务器上处理,这可能会引入一些复杂度。不过总体来说,这种方法可能是比较好的负载均衡。
5.2.5异步、 throttle 和 批量处理
异步、throttle(节流阀) 和批量处理都需要对并发请求数做队列处理的。
异步在业务上一般来说就是收集请求,然后延时处理。在技术上就是可以把各个处理程序做成并行的,也就可以水平扩展了。但是异步的技术问题大概有这些,a)被调用方的结果返回,会涉及进程线程间通信的问题。b)如果程序需要回滚,回滚会有点复杂。c)异步通常都会伴随多线程多进程,并发的控制也相对麻烦一些。d)很多异步系统都用消息机制,消息的丢失和乱序也会是比较复杂的问题。
throttle 技术其实并不提升性能,这个技术主要是防止系统被超过自己不能处理的流量给搞垮了,这其实是个保护机制。使用throttle技术一般来说是对于一些自己无法控制的系统,比如,和你网站对接的银行系统。
批量处理的技术,是把一堆基本相同的请求批量处理。比如,大家同时购买同一个商品,没有必要你买一个我就写一次数据库,完全可以收集到一定数量的请求,一次操作。批量处理的问题是流量低,所以,批量处理的系统一般都会设置上两个阀值,一个是作业量,另一个是timeout,只要有一个条件满足,就会开始提交处理。
所以,只要是异步,一般都会有throttle机制,一般都会有队列来排队,有队列,就会有持久化,而系统一般都会使用批量的方式来处理。
但是从业务和用户需求上来说可能还有一些值得我们去深入思考的地方:1)队列的DoS攻击。2)对列的一致性3)队列的等待时间。
说明:本文部分内容来自酷 壳 – CoolShell.cn 《性能调优攻略》、《由12306.cn谈谈网站性能技术》,结合自己有限的性能测试经验进行了部分补充、节选和整理。希望对这方面感兴趣的朋友更好的系统性了解性能测试。