4.1算法调优
  算法非常重要,好的算法会有更好的性能。举几个我经历过的项目的例子,大家可以感觉一下。
  一个是过滤算法。系统需要对收到的请求做过滤,我们把可以被filter in/out的东西配置在了一个文件中,原有的过滤算法是遍历过滤配置,后来,我们找到了一种方法可以对这个过滤配置进行排序,这样可以用二分折半的方法来过滤,系统性能增加了50%。
  一个是哈希算法。计算哈希算法的函数并不高效,一方面是计算太费时,另一方面是碰撞太高,碰撞高了跟单向链表一个性能(可参看Hash Collision DoS 问题)。我们知道,算法都是和需要处理的数据很有关系的,算是被大家所嘲笑的“冒泡排序”在某些情况下(大多数数据是排好序的)其效率会高于所有的排序算法。哈希算法也一样,广为人知的哈希算法都是用英文字典做测试,但是我们的业务在数据有其特殊性,所以,对于还需要根据自己的数据来挑选适合的哈希算法。对于我以前的一个项目,公司内某牛人给我发来了一个哈希算法,结果让我们的系统性能上升了150%。(关于各种哈希算法,你一定要看看StackExchange上的这篇关于各种hash算法的文章)
  分而治之和预处理。以前有一个程序为了生成月报表,每次都需要计算很长的时间,有时候需要花将近一整天的时间。于是我们把我们找到了一种方法可以把这个算法发成增量式的,也是说我每天都把当天的数据计算好了后和前的报表合并,这样可以大大的节省计算时间,每天的数据计算量只需要20分钟,但是如果我要算整个月的,系统则需要10个小时以上(SQL语句在大数据量面前性能成级数性下降)。这种分而治之的思路在大数据面前对性能有很帮助,像merge排序一样。SQL语句和数据库的性能优化也是这一策略,如:使用嵌套式的Select而不是笛卡尔积的Select,使用视图,等等。
  4.2代码调优
  字符串操作。这是费系统性能的事了,无论是strcpy,strcat还是strlen,需要注意的是字符串子串匹配。所以,能用整型好用整型。举几个例子,第一个例子是N年前做银行的时候,我的同事喜欢把日期存成字符串(如:2012-05-29 08:30:02),我勒个去,一个select where between语句相当耗时。另一个例子是,我以前有个同事把一些状态码用字符串来处理,他的理由是,这样可以在界面上直接显示,后来性能调优的时候,我把这些状态码全改成整型,然后用位操作查状态,因为有一个每秒钟被调用了150K次的函数里面有三处需要检查状态,经过改善以后,整个系统的性能上升了30%左右。还有一个例子是,我以前从事的某个产品编程规范中有一条是要在每个函数中把函数名定义出来,如:const char fname[]=”functionName()”,这是为了好打日志,但是为什么不声明成static类型的呢?
  多线程调优。有人说,thread is evil,这个对于系统性能在某些时候是个问题。因为多线程瓶颈在于互斥和同步的锁上,以及线程上下文切换的成本,怎么样的少用锁或不用锁是根本(比如:多版本并发控制(MVCC)在分布式系统中的应用中说的乐观锁可以解决性能问题),此外,还有读写锁也可以解决大多数是读操作的并发的性能问题。这里多说一点在C++中,我们可能会使用线程安全的智能指针AutoPtr或是别的一些容器,只要是线程安全的,其不管三七二十一都要上锁,上锁是个成本很高的操作,使用AutoPtr会让我们的系统性能下降得很快,如果你可以保证不会有线程并发问题,那么你应该不要用AutoPtr。我记得我上次我们同事去掉智能指针的引用计数,让系统性能提升了50%以上。对于Java对象的引用计数,如果我猜的没错的话,到处都是锁,所以,Java的性能问题一直是个问题。另外,线程不是越多越好,线程间的调度和上下文切换也是很夸张的事,尽可能的在一个线程里干,尽可能的不要同步线程。这会让你有很多的性能。
  内存分配。不要小看程序的内存分配。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(还整理内存碎片),他像“恐龙特级克赛号”一样,他运行的时候,整个世界的时间都停止了。
  4.3网络调优
  关于网络调优,尤其是TCP Tuning(你可以以这两个关键词在网上找到很多文章),这里面有很多很多东西可以说。看看Linux下TCP/IP的那么多参数知道了(顺便说一下,你也许不喜欢Linux,但是你不能否认Linux给我们了很多可以进行内核调优的权力)。强烈建议大家看看《TCP/IP详解卷1:协议》这本书。我在这里只讲一些概念上的东西。
  A)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