本文的讨论的要点是:在系统出现问题时,如何有理有据的保护好自己。
对于软件开发者来说,我们在公司里一般处于弱势群体,每当系统出现问题造成事故的时候,运营人员一般都会将矛头指向研发人员。他们这么做一般是将责任撇清,以防引火上身。当出现事故给公司造成了实际损失,那么公司老板肯定会介入进来,总有人要背这个黑锅的,这个责任会从上级一级一级的压下来,直到推到某个模块具体编码的程序员身上,这时候我们开发者唯一进行反击的武器就是日志,如果日志没有清楚的记录什么时间调用了什么代码,什么时间调用的代码出现异常,异常的原因是什么等详细信息,那么负责编码的程序员也将是一头雾水莫口难辩,屎盆子全都扣到你头上,最后的结果就是背了黑锅走人。
这种情况在我上一家公司已经发生了很多次,在入职的那段时间里Team leader经常提醒我们,编码一定要小心小心再小心,千万不要出错,出事故了我要挨老板的骂,你们有人就得被辞退。在部门svn服务器的文件夹里有专门存放事故的总结报告,一个事故,一个word文档。差不多有11个文档,直接造成经济损失的A级事故也有3个左右,这几个同仁被辞退的时候心情肯定是极其郁闷。前段时间得到上一家公司同事的消息,公司有个技术大牛写的程序和另外一家公司的系统进行业务交互,结果充值报文被人给修改了,金额被修改成了200万,结果这个大牛写的代码不严谨,没有考虑周全,收到响应报文时没有做金额校验就做了转账操作,听说这一下公司就损失了200万,老板娘在公司发飙,把笔记本重重的摔在他办公桌上。最后也是被辞退,无论技术多么牛的人还是小心些好,常言道:小心驶得万年船。
现在我用做过的一个充值系统的交互图来说明我的观点,如何调用黑洞代码(所谓黑洞代码就是说你将要调用的这个法,对你而言就是一个黑盒子,你不知道这个方法做了哪些操作,你不知道它会出现什么错误)
如图:
这是一个银联卡充值系统,这个系统的功能是:只要你将可以能够在网上进行支付的银行卡(哪个银行的都可以)和自己的手机号绑定,就可以随时用手机拨打自动语音充值电话给自己的手机号或着他人的手机号充值,这个系统的参与者有IVR语音服务商、收单服务商、电信充值中心。整个系统可划分为IVR语音模块、收单模块、电信指令交互模块、报文加解密模块、支付模块。
模块开发明细表:
模块名称 |
开发者 |
调用模块编号 |
序号 |
IVR语音交互模块 |
张三 |
2 |
1 |
收单模块 |
李四 |
5、3 |
2 |
电信指令交互模块 |
王五 |
4 |
3 |
报文加解密模块 |
那六 |
|
4 |
支付模块 |
小七 |
|
5 |
系统上线时非常的不稳定,充值失败率很高,平均每五笔就有一笔充值失败。问题定位在2和3这两个模块上,两位开发者各就执一词,争的面红耳赤。我们看看李四的代码,李四的代码去调用了王五的代码。
李四的代码截图:
用红色框圈起来的代码是王五的代码,王五的代码打成jar包供李四去调用,李四将王五的业务实现用Spring注入进来,然后直接去调用doCharge方法,并将结果返回,看起来没有问题。只不过只是表面看起来没有问题。
打问号的代码你敢大胆的这样用吗?
风险一:
doCharge方法对李四来说就是一个黑洞代码,不知道这个方法作了哪些操作,会不会有错误发生,因为doCharge方法未声明该方法可能要抛出的异常,李四以为这个doCharge方法是安全的,所以没有加try{}catch()代码捕获异常。什么事都怕万一,万一这个doCharge出现了异常怎么办?
风险二:
如果doCharge方法因为某种原因产生了死锁,那么你的调用结程死在里面了,永远不返回调用结果,这种情况怎么办?
风险一的应对措施:
对于要调用的关键方法,无论它有没有声明要抛出的异常,我们都要对它保持怀疑的态度,加try{}catch捕获,并将捕获到异常,记录日志后,包装下继续抛给上层调用者。让上层调用者知道出错了,异常抛出了你就尽到了通知的义务,系统出问题与你没有关系,不然出了问题问你:你为什么不捕获异常?为什么不抛异常?虽然有很多种理由可以向质问者解释,但还是多一事不如少一事,别偷懒加个try{}catch()捕获可能出现的异常。
风险二的应对措施:
打个比喻:你是猎人,你要在山洞里抓一只狼崽出来,但是你不确定这个山洞里有什么危险,聪明的猎人会放猎狗进去抓狼崽,如果猎狗进去一段时间没有出来,说明里面有危险,猎人再想其它办法。如果猎人自己进去是有风险的,谁知道这个洞里面是狼还是虎。这个比喻想说的是,如果你要调用一个你认为不太安全的方法,不要用主线程调用(猎人),创建一个调用线程(一只猎狗)去调用,这样做的好处是能够监控调用是否成功,还可以设置调用的超时时间。
用这个比喻我们创建一个猎狗工具类,调用黑洞方法时,自动生成调用线程,如果调用时间超时,抛出TimeoutException
采用猎狗模式修改后的代码:
事后终于找到了bug了,问题出在王五的电信指令交互模块上,发送给电信的报文长度必须符合协议,否则电信那端收到非法包后会将Socket连接断开,协议规定充值金额必须是4位数字,不足4位的,左补0,比如说客户要充值10,补全的就是0010
充值100,就是0100.如果客户充值少于10元,要在左侧补3个0,这个bug出现在王五在处理个位充值时,少补了一个0,结果是客户充值2位数金额的话费就成功,一充值个位数的话费Socket连接就断开,李四的调用线程一直堵塞在这里。
如果一开始李四采用猎狗模式的话,出现问题一看日志便知道问题出在哪里,有理有据的指出问题所在,也不用背这个黑锅了。
上面的代码适合Jdk1.5以上使用,如果想在jdk1.4使用,请自己改造下。
PS:应网友要求,贴出猎狗代码,优势和缺点请大家比对
普通调用:
猎狗调用:
http://www.iteye.com/topic/1116449
这个仁兄想法很全面,如果你做Team leader我相信大部分研发人员都愿意跟着你干,因为有系统出事故你不会把问题直接推到下属那。这种想法和建议有人提过,我记得有个新员工刚入职的时候,他认为架构组设计的代码很搓就群发邮件和架构组交流,但是不知道为什么这种交流变了味,最后变成了口水仗。他的建议没有被采纳反而得罪了架构组的人。
我们无力改变这种研发管理混乱的局面,只能去适应。
我们能做的是:
1、小心谨慎的写好代码,将能想到的问题做好应对措施。
2、异常处理好,该抛的抛,该捕获的捕获,详细的记录日志。出事故责任不会推到我头上,我们只关注每个月的薪水能否打到工资卡上。
3、有合适的机会,离开这个尔虞我诈的公司。
学到很多,原来项目小,一两个人做,几乎没记录过日志,现在项目大了,人多了,各个模块集成,很有必要记录日志,不是说为了找责任人,而是为了更高效的工作。而且对于黑洞方法要持有一定的怀疑态度,我也认为非常有必要
个人感觉,从纯技术角度上看,楼主的做法其实属于治标不治本,实在是丑陋的架构和管理手法下不得已采用的丑陋做法。
从风险一的应对措施来看,这段代码一次性不管三七二十一把所有Exception都catch起来,加上了一段没有任何业务语义的描述 (An error occured during XXX之类,如果方法名称起得好的话,从异常的调用栈里就能得到同样的信息。如果你觉得这种异常信息有用,有三种可能:1. 你打算让这条异常一直抛到用户界面上去,显示异常信息。或者 2.你所在的团队没有仔细看异常调用栈的习惯。或者 3.你们的代码类名和方法名起得不知所谓。) ,然后包装成另一个Unchecked Exception抛出来。正常来说,只要在架构的高层位置有一个大的try catch统一来干这个事情,这里就根本不用管。
通用的异常处理应该放在高层,通常底层代码是不用管更底层的代码抛出的RuntimeException的,除非:
1. 你有处理这个异常的具体方法(也就是在你的层面看,这个根本不是异常,而是业务的一部分)。
2. 你认为这段代码的调用者有能力处理这个异常,你就应该把这个异常封装成一个Checked Exception再抛出去。
3. 你为这个异常加入更具业务语义的message再抛出去。
从发现错误的角度看,一前一后两个log已经解决所有问题了,这里的异常处理纯属多余。除非,你的上层代码把异常吃掉了(既没日志又没继续往上抛),这纯属是做架构的人胡闹,害人不浅(如果你能拿到他们的代码,把吃异常的地方找出来,追究责任时可以拿来说事)。
不过就算是上层代码不可控,楼主也应该在可控的范围内把这种通用的异常处理往高层放,不用在底层搞那么多层层叠叠的try catch。
第二个风险的应对也不是正路,你开了新线程去调别人的东西,超时了你的线程就异常终止了,另一条线程还在跑呀(就算你强行kill了这条新线程,你也不知道黑箱里有没有再开新线程,或者通过异步方式干其他东西,例如读写数据库之类),它跑的结果怎么样,有没有什么副作用你都不管了。你这个异常终止之后,整个系统就处于一种未知状态了,后面的操作还怎么做,干脆直接Runtime.exit()再重新启动还安全一点。
既然别人的包本来预期是同步使用的,而且你不知道它的运行细节,把它强行改为异步没什么好处。如果你怕别人的包死锁,一般来说也是一前一后两个log就能发现问题了。感觉你在这里有点贪心,除了想发现错误,还想搞点错误恢复,但异步调用不能这么简单处理的。但如果要处理好,成本太高了。
从管理角度看,也很有问题:
1. 自己人的代码还搞什么黑箱。我一般都要求程序员如果调用不是自己写的接口,至少要往下看三层实现代码,包括别人的代码和开源代码。如果要修改代码,需要先往上找出所有调用关系并阅读代码,保证不会修改了一条调用路径上的bug却把另外的调用路径搞死了。如果是黑箱代码(我所在的项目基本没有),除非有完善的技术支持或者口碑非常好,否则一律不用,而且调用黑箱代码的前后必须做好日志。违反者一经发现,口头鄙视。
2. 找人负责也不是不行,但也不能老板越过技术主管直接找程序员的麻烦吧。不懂技术(甚至懂技术,但不了解具体上下文)的人直接批评程序员,往往只找到了表面现象就开骂(例如楼主说的李四),等到找到真正原因,又挂不住面子,反而更不爽你。楼主举的例子,我想象的场景是这样的:老板问张三,你这个语音模块怎么回事,这么卡。张三就说,具体不清楚,不过我调用了李四的包,可能跟这个有关系(典型的推卸责任)。然后老板就问李四,你怎么回事,李四说具体不清楚,能不能告诉我怎样重现错误,我调试一下(典型的认真负责)?老板一个笔记本拍李四脸上:去死吧你,就是你这个害群之马,人头猪脑。。。。(省略一百字)。这种搞法怎么能管好技术团队?
好在楼主说是上一家公司,估计已经脱离苦海了吧,恭喜恭喜。
=======================================
补充一点。。。
我本来想这种方法调用的日志其实最好用debug或trace级别,放在info级别,岂不是逼人在生产环境下关闭info级别的日志(我循环调用一个黑箱方法100次就会看到200条重复的日志,再来50个用户并发访问。。。)。不过后来想想也有道理,这里的日志不是为了查错,而是为了在第一时间撇清责任,等到发现错误再打开日志就太晚了。所以说丑陋的管理导致丑陋的代码。。。
很喜欢这样的帖子,谈谈我的观点。
从技术角度上:
1. 内部调用可以通过约定,大家都是白盒,避免使用无用的try/catch。外部调用的业务异常尽量提前约定好,不要自己猜测;系统级别异常根据情况决定是否捕获,是否有必要继续向上层抛出。外部调用日志信息要记录完整,便于线上问题定位。
2. 猎狗模式是一种好的远程调用方式,对于经常出问题的服务提供方,可以这样做防御性编码。但是如果每处调用都这么写,那太蛋疼了。
从管理上:
1. 出问题后,要对事不对人。大呼大叫,赶人,这是下下策。这样大家都战战兢兢,人心迟早要散的。出问题后,通过分析找到深层次原因,可以让当事人做案例分享,让其他同学从中吸取经验教训,防止后人犯同样错误。长期坚持下去,可以积累一份“经验教训宝典”,让大家定期学习,减少犯同类错误概率。
2. 工作中鼓励大家大胆写代码,不要怕犯错误,对于不合理的代码能够及时重构。
在位一天,就要尽最大努力去改变。如果你改变不了,还是乘早闪人。
应该配合拦截器机制使用,否则因为对方的异常、死锁或者更多的隐患而为对方补漏。代价实在太大了。
1. 真正安全的应该是try catch(Throwable ex) 如果只是exception 照样也有一场泄露。
2. 就算要开人,也应该开掉测试而不是开发。
3. 遇见这样的公司,可见是人数不多的公司,极有可能开发和测不分。
4. 这样的软件根本不是什么银行软件,应该各种移动电信广电运营商的小应用拿来收钱的。对应的叫法有BSS,BOSS,支付平台,呼叫中心,银行托收,银行代缴,银行代扣等等。这样的软件不能算是金融软件。
5. 能够随便划掉200万不大可能,银行这种小额支付通常都有限额的。自动取款机上一天才只能取几万块钱,超过十万就需要到银行预约。网上银行和电话银行的转账也没有200万这么高的额度。高额度必须要到柜台办理。
6. 我不懂报文被修改了是什么意思,如果文档上规定是已对方传回的金额为准,那就直接充值呗。从流程上来说,这个充值200万也是充值到某个客户的账本。这笔钱也只是划款到了电信移动运营商,而运营商只是修改了一下客户的账本金额(话费余额)而已。换句话说:银行扣200万给运营商,运营商只是给客户的话费的额度调高了一点。处理办法运营商退200万给银行,并且把客户的余额调整回来就可以了。200万不是损失,损失是一包烟一瓶茅台吧。
8. 我觉得真正的情况应该是客户明明只从银行扣款了200块钱,确被别人把账本金额改成200万,变成客户好像有了200万话费一样。 那这样一条sql把数据改回来就得了
最后一句话,第三方集成商真可怜。