2008-02-13 15:40:06.0
作者:olivegames
由于工作的原因,需要对osip协议栈进行优化,前段时间在论坛上看到lw3223兄对于osip协议栈hash查找事务的讨论,鉴于此把自己这段时间对osip的优化和心得总结出来大家共享一下。写的比较匆忙,有点乱,大家见谅了。
众所周知,Osip协议栈是用C语言编写,实现了SIP协议基于事务层的处理,后来作者对协议栈进行了扩展,提供了call级的操作处理,也就是eXosip。Osip小巧,灵活,易扩展,比较方便用来实现UA,但是用作proxy的话性能上有点满足不了需求(听说有个改良版的partysip,没用过,我们这里只讨论osip的优化),有必要对其进行优化一番。
这里使用的是最新3.0.1的版本,有些固有的影响性能的因素,比如消息的解析,复制,转换到字符串以及释放等暂不予讨论,大家有什么好的方案也可以一起讨论一下。我们可以围绕协议栈线程主执行函数_eXosip_execute()来展开对它的优化工作。
1.1 SIP Message到transaction的映射
Osip性能低下的一个重要原因便是有太多的for循环,尤其是在事务层SIP消息到事务的匹配,好在osip预留了hash的处理方法,为我们的优化节省了不少的工作量。打开编译宏HAVE_DICT_DICT_H,协议栈便会使用libdict库对四种事务进行hash的插入,查找,删除等。如果您手头有这个库,到这里这一步就可以完成了。由于这个库不是linux自带的,你可能需要自己给协议栈实现一个hash算法,增加osip_hash.c和osip_hash.h两个文件基本上就OK了。
经过测试,这项操作对性能的提升约1/10左右。由于测试的压力较小,因此查询效率的提升不是很明显,但是当呼叫量和并发数较大时,对性能的改善应该会比较明显。
1.2 Osip状态机的处理效率
协议栈处理事务事件(这里指osip事件,非eXosip事件)的主函数是osip_xxx_execute(),在这里xxx分别代表osip的四种状态机ict, ist, nict, nist,以下如未经特别说明均使用该准则。
随便捡一个来看,比如osip_ist_execute()函数,你会发现这个函数也就是轮询事务列表,找到有事件需要处理的事务,然后调用每个状态下对不同事件的状态机处理函数fsm,仅此而已。重点就在轮询,至于它是怎么调用到具体的fsm,稍后再讨论。你可以想象一下,每秒100calls的呼叫率,每个call的接续时间为10秒,那么ist事务列表的长度便是1000。你可以认为循环1000次没什么,但是你要想想主执行函数_eXosip_execute()的调用频率(其实是一直阻塞于该函数中)是非常高的,只要有SIP消息的来往就会产生相应的SIP事件来唤醒协议栈。
经过实际测试,在nict事务列表平均长度为4xx时,平均不到一个事务需要处理事件,可见四个状态机的执行函数osip_xxx_execute()就不能执行太多的无用操作,所以这里有必要提高轮询事务的效率。
最简单的方法就是在osip_t结构体里增加四个活动的事务列表osip_xxx_active_transactions,分别对应四个状态机,只存放有事件需要处理的事务,为了不影响到原来的协议栈,也方便以后的调试,所有关于状态机的优化都加上编译宏OSIP_FSM_OPTIMIZE。这样四个状态机的执行函数osip_xxx_execute()就只需要轮询活动事务列表即可。
需要作出修改的函数如下:
__osip_add_xxx(),
__osip_remove_xxx_transaction(), /* 可能不需要修改 */
__osip_transaction_add_event(),
osip_xxx_execute()
修改时需要注意不要将事务重复添加到活动列表里,以尽量减少活动列表的长度。当osip_xxx_execute()函数从活动列表里取出一个事务后便删除该事务,这样在函数执行完毕后,活动列表就是空的了,保证该列表一直处于低长度下。因此实际上经过测试发现函数__osip_remove_xxx_transaction()里不需要再次试图从活动列表里删除事务了,但为了保险起见,这部分代码我还没有去除。另外,要注意函数__osip_transaction_add_event()的修改,这里可能会遇到信号量的问题。之后对定时器的优化时会提到。
经过以上的修改,测试协议栈性能有显著的提高,经过统计osip_xxx_execute()这四个函数执行时间减少到了之前的一半。它怎么就才一半呢?继续看这个函数osip_transaction_execute(),它是osip_xxx_execute()最终调用的事务处理函数,原来函数fsm_callmethod()(这个函数定位具体的fsm并调用)是用已知的状态和事件类型查找列表的方式来调用实际的fsm的,还不赶快改成数组的,别节约内存了。这里需要改动xxx_fsm.c中的的__xxx_load_fsm()和__xxx_unload_fsm()函数以及fsm_callmethod()函数。改动比较繁琐,注意不要改错了哦:)
最后再次测试,四个状态机执行函数osip_xxx_execute()的执行事件已经减少到原来的1/4了。以上的改动之后,对于事务的处理基本上克服了事务数增多导致的性能非线性下降的因素。
1.3 超时事件的产生
Osip的定时器超时事件是通过轮询所有事务的每个定时器而产生,然后将其放入事务列表(注意,这里可能需要将事务放入活动列表,因为之前事务处理完成后是从活动列表里删除了的),而每次SIP消息的事件唤醒协议栈后都会触发定时器遍历操作的执行,可见定时器的效率非常低下,并且大部分的定时器通常都不会超时。
改进的方法就是给osip增加一个定时器模块,这里大家可以参考相对定时器的实现。主要的思想就是将所有定时器进行排序,最先超时的放在前面,这样在触发定时器事件的时候通常只需要判断一下最前面的那个定时器。这对于osip定时器效率的提升大有帮助,虽然在启动定时器的时候要考虑排序的问题,至少在我的测试中这里的消耗还不至于影响到总体性能。当然,精益求精的你可能会考虑双向有序链表的插入问题了,如果有好的解决方法可别忘了通知我啊,呵呵。当然,为了尽量减少这类消耗,我的实现是按RFC3261规定的osip四种状态机下的定时器种类分类,所有同类的定时器都放到同一个定时器组(这个要取决于你实现的定时器了),启动定时器的时候是从链表的后面开始,通常在定时器长度相同的情况下,新加入的定时器总是最后超时,那么它就应该放到链表的最后位置。这样,一定程度的减少了启动定时器时的消耗,当然有几个重发请求的定时器的超时事件是变的,不过这已经没多大影响了,毕竟重发请求又不是经常要发生的。
对定时器的优化改动可能会比较多。你需要参考RFC3261,在相应的状态下启动或者停止对应的定时器。为了不对源代码作过多的修改,你需要添加两类回调函数,一是进入某个状态时,二是退出某个状态的时候,然后将定时器的起停操作放到这些回调函数里去,最后由函数__osip_transaction_set_state()来调用。
具体的改动涉及到xxx.c和xxx_fsm.c,我将状态切换回调函数(起停定时器)放到了xxx_fsm.c里,定时器超时回调函数(产生定时器事件)放到了xxx.c里。最后,用定时器组的触发函数(取决于你设计的定时器)替换掉四个定时器执行函数
osip_timers_xxx_ execute()。
经过测试,以上改动性能提升非常显著,之前未经优化,osip_timers_xxx_ execute()执行事件是优化过的osip_xxx_execute()的1.5倍,优化后变为了1/10。
需要大家注意的是,在之后的压力测试中发现了之前提到过的信号量的问题。之前在将事务加入到活动列表时(__osip_transaction_add_event())之前加了互斥,但是发现协议栈一运行就阻塞,然后就取消了互斥,但是呢,压力测试一段时间后协议栈就crash了,呵呵:)最后总算发现了调用该函数的某些时候加了互斥(比如定时器超时事件),某些时候又没有加互斥,头疼~~~没办法,只好做了该函数的两个版本!
经过上面的优化后,在相同的测试压力下,协议栈整体性能提升了一倍,并且消除了大部分的导致性能非线性下降的因素。
1.4 Call资源的释放
经过对协议栈的深入分析和测试,发现释放call资源(遍历所有的call)的函数(主要时eXosip_release_terminated_calls)调用过于频繁,也是每个事件都会触发一次。考虑到资源的释放不用那么及时,通常是在某个定时器超时后才需要释放,于是考虑让资源成批释放,启动一秒级的周期定时器来执行call资源的释放,具体多少秒执行一次,大家可以调整定时器的长度,我设的是5秒。
由于最终释放事务(eXosip.j_transactions)的资源时,该列表可能会比较长,因此将列表的遍历改为迭代器遍历,因为通过协议栈的osip_list_get()函数遍历链表的效率较低。
这项优化工作量算是很小的了,但是对性能的提升却非常实惠,经测试整体性能继续提高了一倍。至此,协议栈的性能比未经优化时已提升了4倍。
当然,这里也可以模仿定时器的优化一样只对超时了的事务进行资源释放,但我考虑这样修改好像比较麻烦,最终放弃了。有高手可以在这个函数上继续做文章,对协议栈的优化应该很有帮助。
5 Transaction的移除
事务的移除(只是从osip协议栈中移除,但仍被TU(eXosip)使用)对事务列表进行了遍历。由于osip的列表不能直接删除中间某个节点,原有的list iterator又不方便读取(主要不方便取得list的最后一个元素的迭代器,需要遍历list),因此实现一新的双向list替换协议栈原有list,并增加接口osip_list_get_last()直接获得链表尾的迭代器。
在事务的结构体里增加一迭代器成员,用于储存该事务在事务列表中的位置信息以便移除事务时可以直接通过迭代器来移除而不用遍历整个事务列表。
这里需要注意,为了保险起见,在hash里删除事务失败(找不到)后,我仍然轮询了事务列表,后来测试时发现函数__osip_remove_xxx_transaction()可能会被重复调用导致第二次调用时hash删除失败对事务列表进行了遍历!
其他的优化:
6 消息到字符串的转换
这部分代码存在比较多的内存拷贝和sprintf函数的调用,由于这个函数调用频繁,所以有必要对其进行优化。由于sprintf函数性能低下,因此将其通通更改为memcpy,但是这样做需要对sprintf函数输出的内容进行拆分重组。另外去除内存copy时,需要对大部分的消息内的结构体转字符串的函数重写一个版本。因此这项操作耗费了比较多的时间,也可能出错,需要经过比较详细的测试。
修改后,这部分代码的性能提升了将近一半,同sendto函数耗费时间差不多,加上消息日志的打印,发送消息的函数性能提升空间已经不大了。这项优化感觉性价比比较低,不感兴趣的同学可以不用去管它
1.7 字符串复制的优化
这个函数osip_strdup()同样简单,但是使用频率应该是系统里最高的了。去除一些不必要的操作,改strncpy为memcpy。
总结
经过以上改进,协议栈的整体性能提升至原来的5倍。经过测试,随着在线数的增加UA的性能会有所下降,主要原因是因为eXosip还没有优化,UA访问eXosip提供的 API时带来了一定程度的性能下降。现在性能瓶颈主要在集中于SIP消息的解析,到字符串转化,复制,释放等,相关函数如下:
osip_message_parse(); /* 还没做优化 */
osip_message_to_str(); /* 已优化 */
osip_message_clone();
osip_message_free();
如果对消息内的内存分配机制进行改进的话,最后两个函数有较大的提升空间,但是改动涉及面会很广。
经过测试,当在线数增加到256后,eXosip带来的性能下降所占的比重已经比较大了,导致最后UA占的CPU已经超过了协议栈的1/2,而在线数不多的时候不到1/3,加上测试程序的影响,非协议栈程序所耗CPU已经接近协议栈了。可以看出对eXosip进行优化后,性能应该还有一定的提升空间。
最后,希望有高手能做到更进一步的消息层面的优化,基本上也就是上面几个函数了,有机会大家可以互相认识探讨一下。
原文地址:http://tongxinbing.blog.china.com/200802/1911930.html