在开发对外提供服务的模块的时候,系统的性能经常会是令我们头疼的问题,具体系统性能的定义与瓶颈的定位方法,可以参考陈皓的这篇文章:
性能调优攻略http://coolshell.cn/articles/7490.html 大牛的这篇文章还是很全面的。
下面我会以我们的一些工程经验和曾经遇到过的问题,来实例化一些系统性能调优的经验。
Ø 代码级别
1. 减少数据copy
1)使用指针,代替静态数据copy
以下面的数据结构为例:
struct goods_info_for_rank_t
{
good_info_ptrp_goods_info;//静态数据,只可读不可写!!!
….//省略若干
//策略相关的字段
float doc_weight;
float goods_weight;
};
以前这个goods_info_for_rank_t 进行内容赋值的时候要从p_goods_info 拷贝全部的数据到goods_info_for_rank结构,其实很多静态数据只是可读的并且数据量很大,当时优化的一个思路是去掉这个结构体,只使用good_info_ptr这个结构,但是忽略了这个结构体是静态只读,并且多线程并发使用的,从而使得多线程情况下出现core dump(修改后需要写,导致多线程同时写一份good_info_ptr数据),后来才用这个方案优化了这段程序性能;
2)使用引用代替栈上数据;
这个是常识性的知识,即当我们传入一个大结构体或者类的时候,尽量使用引用或者通过指针访问,否则栈上变量进行大内存申请将是很大的性能损耗。
3)返回指针,减少数据的COPY,对比下面的两个函数:
void read(uint32 id, void *brief)
{
if(id >= _num)
return;
memcpy(brief, _array_mem + id * _size, _size);
}
void read_no_copy(uint32 id)
{
if(id>= _num)
return NULL;
return _array_mem + id * _size;
}
如果数据只读,后面的函数减少一次内存copy效率更高;
2. 内存申请
1)减少不必要的malloc
堆是多线程共享的,频繁的malloc会导致系统申请内存速度的下降,所以确定的大块内存,我们的建议都是在线程空间预先申请好,而不是每次都申请内存。
对多线程频繁malloc 内存消耗的解释:
“
堆是进程内共享的, malloc和free的原理,大概是维护一个空闲内存块的hash表:
1. malloc时直接从这里去,如果没合适块,还要做一些分裂;
2. free时,视时机做一些内存块合并,并挂接到hash表中;
在多线程情况下,如果malloc和free太频繁,线程间肯定需要同步,而且分裂和合并等操作也会更多,也是开销。
“
使用tcmalloc库,可以优化malloc内存效率,一些测试的结果是在多线程下可以优化内存申请效率30%,tcmalloc源码
http://gperftools.googlecode.com/svn/trunk/
也可以使用自己实现的内存池技术来优化内存申请。
2)小心的memset
还是继续说前面的问题,当我们在线程里面预先申请大块内存后,当一个请求处理完成再进入另外一次请求时,我们自然的想到要用memset清理内存,但是这个内存空间如果很大,memset这个内存就会非常的慢,曾经的一个项目里面由于memset 一个10M的内存,使得系统性能下降一半。
避免memset的一个方案是通过一个长度变量控制内存的读取长度。
3)谨慎使用局部变量
局部变量真”好”,用的时候随便申请一个,等作用域失效后自动释放。但方便的同时也很可能引入问题,这个问题和1.2比较类似,linux系统的线程栈空间有限可以通过ulimit –s查询,查看我们系统的限制为10K(我写了一个程序,发现超过2M的时候会core dump),所以当声明一个较大的局部变量的时候,就会出现系统运行速度减慢,甚至会出现core dump。
3. 减少系统调用
对于程序的时间开销我们应该有这样的认识:
所以当进行频繁的系统调用的时候对系统性能会有影响,在跟进检索请求超时的问题中,发现每召回一个obj都要进行次过滤并调用time()函数看是否过期,导致请求时间超长。后面修改为一次请求只获取time一次,则修复超时问题。在我们调试性能问题的时候也经常在系统加入很多获取时间的系统调用,本身这些系统调用也会影响到性能,从而使性能测试结果不准。
Ø 网络级别
1)使用NODELAY参数
对于我们的网络服务器,我们都是希望发出的包立刻能够发送给对方,所以一般设置NODEAL参数,以防止nagle算法对数据报进行缓存。
有关nagle算法:
http://en.wikipedia.org/wiki/Nagle's_algorithm
2)长连接与短连接
对于内部服务器间的通信,如果可以我们都建议使用长连接,以提高响应的速度;
3)异常数据包的处理
一般当下游服务器接受到一个异常的数据包的时候,我们可能会简单的丢弃它,这样可能引发一些问题。比如:一个同学在升级下游服务的时候将异常的报丢弃掉,并且没有关闭连接,导致上游服务器一直等下游回应,而hang住整个系统,因为你无法知道上游服务器的处理逻辑,如果它设计的很挫,可能对等待这个响应报很久才超时;另外当异常报比较多的情况下,我们如果reset连接,也会影响到整个系统的响应时间,因为上游服务器要重新建立连接。(上游最好不要把错误的包传到下游,因为那可能引发下游的报警或者异常)
Ø 架构级别
1)并发访问
当我们有些请求要发送给多个下游服务器,而这些请求又是独立的时候,可以采用并发访问。比如一个检索系统里面我们同时可能要获取几个检索分类的结果,这个时候我们就可能用到并发访问(网络模型、select、epoll奔腾而过)。
2)Cache使用
Cache是提升系统性能的常用方法,如果检索系统中有些query就是比较慢,一个直观的方法就是使用Cache,我们可以使用结果缓存,甚至可以采用预充的手段,将长响应query预充到缓存区;好吧,我们又必须面临缓存区脏了的问题。
3)空间换时间&线下计算
如果有些数据可以在线下计算,那尽量在线上完成,但一般会增加些系统的复杂度和空间的占用。我们的一个项目以前要实时计算每个站点的结果数(召回站点结果然后计数),后面把每个站点的结果数建到索引里面,性能提升30倍。
Ø 编译优化
-O2是推荐的优化等级,也是我们程序使用的优化等级。