[转]性能调优攻略(3)
4.4系统调优
A)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的。
再说一下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会变大,我们需要在写的次数上和相应上做权衡。
B)多核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有自己的本地存储器空间。关于NUMA的一些技术细节,你可以查看一下这篇文章《Linux的NUMA技术》,在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
C)文件系统调优
关于文件系统,因为文件系统也是有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,可以让你看到各进程的磁盘读写的负载情况。
其它还有一些关于NFS、XFS的调优,大家可以上google搜索一些相关优化的文章看看。关于各文件系统,大家可以看一下这篇文章——《Linux日志文件系统及性能分析》。
4.5数据库调优
数据库调优并不是我的强项,我就仅用我非常有限的知识说上一些吧。注意,下面的这些东西并不一定正确,因为在不同的业务场景,不同的数据库设计下可能会得到完全相反的结论,所以,我仅在这里做一些一般性的说明,具体问题还要具体分析。
A)数据库引擎调优
我对数据库引擎不是熟,但是有几个事情我觉得是一定要去了解的。
- 数据库的锁的方式。这个非常非常地重要。并发情况下,锁是非常非常影响性能的。各种隔离级别,行锁,表锁,页锁,读写锁,事务锁,以及各种写优先还是读优先机制。性能最高的是不要锁,所以,分库分表,冗余数据,减少一致性事务处理,可以有效地提高性能。NoSQL就是牺牲了一致性和事务处理,并冗余数据,从而达到了分布式和高性能。
- 数据库的存储机制。不但要搞清楚各种类型字段是怎么存储的,更重要的是数据库的数据存储方式,是怎么分区的,是怎么管理的,比如Oracle的数据文件,表空间,段,等等。了解清楚这个机制可以减轻很多的I/O负载。比如:MySQL下使用show engines;可以看到各种存储引擎的支持。不同的存储引擎有不同的侧重点,针对不同的业务或数据库设计会让你有不同的性能。
- 数据库的分布式策略。最简单的就是复制或镜像,需要了解分布式的一致性算法,或是主主同步,主从同步。通过了解这种技术的机理可以做到数据库级别的水平扩展。
B)SQL语句优化
关于SQL语句的优化,首先也是要使用工具,比如:MySQL SQL Query Analyzer,Oracle SQL Performance Analyzer,或是微软SQL Query Analyzer,基本上来说,所有的RMDB都会有这样的工具,来让你查看你的应用中的SQL的性能问题。 还可以使用explain来看看SQL语句最终Execution Plan会是什么样的。
还有一点很重要,数据库的各种操作需要大量的内存,所以服务器的内存要够,优其应对那些多表查询的SQL语句,那是相当的耗内存。
下面我根据我有限的数据库SQL的知识说几个会有性能问题的SQL:
- 全表检索。比如: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)。
- 索引。对于索引字段,最好不要在字段上做计算、类型转换、函数、空值判断、字段连接操作,这些操作都会破坏索引原本的性能。当然,索引一般都出现在Where或是Order by字句中,所以对Where和Order by子句中的子段最好不要进行计算操作,或是加上什么NOT之类的,或是使用什么函数。
- 多表查询。关系型数据库最多的操作就是多表查询,多表查询主要有三个关键字,EXISTS,IN和JOIN(关于各种join,可以参看图解SQL的Join一文)。基本来说,现代的数据引擎对SQL语句优化得都挺好的,JOIN和IN/EXISTS在结果上有些不同,但性能基本上都差不多。有人说,EXISTS的性能要好于IN,IN的性能要好于JOIN,我各人觉得,这个还要看你的数据、schema和SQL语句的复杂度,对于一般的简单的情况来说,都差不多,所以千万不要使用过多的嵌套,千万不要让你的SQL太复杂,宁可使用几个简单的SQL也不要使用一个巨大无比的嵌套N级的SQL。还有人说,如果两个表的数据量差不多,Exists的性能可能会高于In,In可能会高于Join,如果这两个表一大一小,那么子查询中,Exists用大表,In则用小表。这个,我没有验证过,放在这里让大家讨论吧。另,有一篇关于SQL Server的文章大家可以看看《IN vs JOIN vs EXISTS》
- JOIN操作。有人说,Join表的顺序会影响性能,只要Join的结果集是一样,性能和join的次序无关。因为后台的数据库引擎会帮我们优化的。Join有三种实现算法,嵌套循环,排序归并,和Hash式的Join。(MySQL只支持第一种)
(1)嵌套循环,就好像是我们常见的多重嵌套循环。注意,前面的索引说过,数据库的索引查找算法用的是B-Tree,这是O(log(n))的算法,所以,整个算法复法度应该是O(log(n)) * O(log(m))这样的。
(2)Hash式的Join,主要解决嵌套循环的O(log(n))的复杂,使用一个临时的hash表来标记。
(3)排序归并,意思是两个表按照查询字段排好序,然后再合并。当然,索引字段一般是排好序的。
还是那句话,具体要看什么样的数据,什么样的SQL语句,你才知道用哪种方法是最好的。
-
部分结果集。我们知道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要高很多。
- 字符串。正如我前面所说的,字符串操作对性能上有非常大的恶梦,所以,能用数据的情况就用数字,比如:时间,工号,等。
- 全文检索。千万不要用Like之类的东西来做全文检索,如果要玩全文检索,可以尝试使用Sphinx。
- 其它。
(1)不要select *,而是明确指出各个字段,如果有多个表,一定要在字段名前加上表名,不要让引擎去算。
(2)不要用Having,因为其要遍历所有的记录。性能差得不能再差。
(3)尽可能地使用UNION ALL 取代UNION。
(4)索引过多,insert和delete就会越慢。而update如果update多数索引,也会慢,但是如果只update一个,则只会影响一个索引表。
先写这么多,欢迎大家指正补充。