软件开发中的保护自己不是指个人行为上的自我保护,而是指防犯别的模块的错误却最终表现在我所设计的模块上。当某一模块出现错误时,毫无疑问大多情形下模块的作者将会被“招见”。出现软件错误是正常的,但最为郁闷的是最后发现错误是别的模块误用而引起的。偶而这种情形的发生是可以理解的,但是如果这种事情是家常便饭,则需要考虑通过设计来“保护自己”。提出“保护自己”这一设计原则,其目的不在于让我们变得不敢承担责任,更不是鼓励“扯皮”,而在于高效地解决问题以提高工作效率。
      下面以作者所从事过的一个真实的项目为例来说明“保护自己”这一设计原则是如何起作用的。图1示例了某项目在一个电信级设备上的部署图,这一电信设备使用的是实时Linux操作系统(Monta Vista Linux和WindRiver Linux),图中的SNMP Agent进程正是作者所在的项目组开发的,而Box库是由其它的团队所开发的。图中的FDR(Function Definition Rule)是指一套函数定义规则,是Box库在实现函数时应当遵守的准则。SNMP Agent所实现的功能是使得设备之外的网络管理程序可以通过SNMP Agent上的SNMP接口来管理电信设备。SNMP Agent收到设备外部发送过来的SNMP消息之后,将消息最后转换为函数调用的形式来存取信息,而这些函数的实现是位于Box库中的,注意Box库是由一个印度团队开发的而不是中国团队。读者可以想象到的是,由于Box库是被SNMP Agent进程调用的,因此,一旦Box库中存在严重的缺陷,其最终将直接导致SNMP Agent进程崩溃。对于不少崩溃的情形,通过运用gdb工具查看崩溃时的调用栈能很快地找到问题的根源,这类问题或许也不算大,哪怕立即解决不了也还是有点方向的。但是,在使用gdb工具查看调用栈时,如果发现任务的堆栈被破坏了(stack corrupted),那问题可就麻烦了,因为这种错误可以说是行业内最难解决的问题。有读者会想到通过使用日志的方式将有助于接近问题的根源,这种做法的前提是问题容易重现,否则不可能运用打印调试日志的方式让软件在用户那运行,要知道很多问题在实验室是无法重现的,因为其所模拟的环境不如真实的那样复杂。另外,采用日志的方式并不经济,因为它需要占用大量的处理器时间,进而可能改变运行环境使得问题难以重现,因此这种方法有它极大的局限性。另外,还有一个更为重要的问题,这个完整的软件是由两个团队开发的,一个问题的出现,到底是出在SNMP Agent侧呢?还是Box库侧?在现实中,不可避免地由于是SNMP Agent出现了崩溃,Box库的开发人员自然希望SNMP Agent开发团队去找问题。不难想象,当问题是出在Box库侧时,无论SNMP Agent如何努力都发现不了问题的根源,而最终将导致一个问题被拖延很长时间也毫无结果。如果出现的问题非常的严重和紧急,那就意味着这一问题对于高层管理人员具有很高的可见性,也可能高层都将关注问题的解决过程,那意味着SNMP Agent —— 作者所在的团队,将面临很大的压力。读者可能看到这会想,是不是作者把问题给假设得太严重了?不是,这是真实发生过的事情!
图1
     除此之外,从业务逻辑来看SNMP Agent相对的稳定,因为它只是翻译收到的SNMP消息并最终调用Box库中的函数完成消息的处理,而Box库会因业务的演进而频繁地变动,因而Box库出错的可能性也更大。那如何通过设计来介定当堆栈被破坏这类严重的问题出现时,应当由哪个团队去负责追查问题的根源呢?或者说,SNMP Agent团队如何通过设计来保护自己以免“背黑锅”呢?这种防范是有意义的,它不只意味着问题出现时能快速的介定责任团队,也可以避免没有必要的团队扯皮,说到底就是能提高工作效率。
     作者所提出来的第一个解决方案的思路是,能否通过设计一种方法记录SNMP Agent程序当前正在运行哪一部分的代码?当然,这种信息只要足于区分是运行Box库中的代码或是SNMP Agent中的就行了。最为直观的做法是,每调用Box库中的函数时,在调用之前打印一行日志,在调用返回后又打印一行日志。这种方法在前面提及了,可能会存在一定的性能问题,但其思路还是对的,只是需要采用其它的更为高效的方式替代日志记录。作者想到了Linux中的共享内存!Linux中的共享内存有一个特点,一个程序如果分配了一块共享内存,如果程序不主动对其删除,则即使是程序出现了崩溃其中的内容也一直保存在那不会被更改,当然操作系统进行过了重启的话则除外。
     如此一来,通过设计可以将调用Box函数之前和之后的信息通过使用一个×××变量的方式记录程序是否是在调用Box库,当然,这个×××变量是位于SNMP Agent所创建的共享内存中的。比如设置整型值3表示将要调用Box库中的某一个函数,而在调用完了这一函数后将这一值设置成4。当SNMP Agent出现崩溃后,在其下一次启动的初始化阶段时(我们的系统有进程管理程序,发现一个进程出现崩溃以后,又会自动重启它),通过打开同一个共享内存块就可以知道上一次程序出错时,是否是出现在调用Box库其间,如果是则记录一个错误日志。比如,如果发现整型变量的值为3说明崩溃是发生在Box库内的,则记录一条错误日志。当然,设计考虑到了多线程的问题,且以线程为单位来设计的。有了这种方法,当出现问题时,通过查看日志就能很快地定位责任团队,且几乎完全不失程序的执行效率。
     这种设计方案在操作中存在一定的运作效率问题。比如,即使是Box库的问题,这一问题一旦在测试部门发现,则一定会将缺陷提交给SNMP Agent团队,SNMP Agent团队在查看日志后,发现是Box库造成的问题,于是将缺陷转交给了Box库团队。能不能又通过设计让Box库一出现问题就让测试部门将缺陷提交给Box库团队呢?图2展示了进一步的设计方案。
图2
     在这一方案中,增加了一个新的Proxy进程,这个进程的程序也是由SNMP Agent团队开发的。SNMP Agent进程和Proxy进程之间采用了IPC(Inter-Process Communication,即进程间通讯)进行通讯,就是将前一方案中SNMP Agent直接调用Box库函数的形式转化成了通过进程间通讯将这一调用发送给Proxy进程,Proxy进程再通过FDR接口调用Box案。当然上一方案中采用共享内存记录哪一部分代码正被调用的方法被运用到了Proxy进程上,以帮助介定问题是发生在Proxy进程本身还是在Box库内。在这种方案中,其中很重要的一个内容是,Proxy进程的逻辑非常的简单,即接受来自SNMP Agent的消息然后调用Box库函数并通过消息将Box库函数所返回结果传给SNMP Agent,其逻辑不会因为项目的不断变化而变化,除非FDR发生了变化。可能这种方案刚开始部署时,Proxy会有一定的缺陷,但经过一定的时期稳定后,一旦出现Proxy崩溃的问题,则极有可能就是Box库所引起的了,而这种情形下测试部门可以直接将缺陷提交给Box库团队。
     至此,相信读者已明白了“保护自己”这一设计原则是如何起作用的了,可能有的读者也会有一点灵感去解决自己项目中正面临的类似恼人问题。另外,不知读者注意到了没有,这里所谈的共享内存方案,其实可以作为一种通机制去辅助缩小出现问题时程序的出错范围。前面也提到是通过一个位于共享内存中整型变量来记录程序的运行点的,而这个变量可以定义232个值用于表示程序的不同执行点,当然应用程序需要在合适的地方对其设置不同的值来指示程序运行到了什么地方。