前几个月开发了一个服务器网关程序,测试过程当中发现了一些问题,问题的查找与修改花了一个月的时间。好在通过一个月的修改与完善终于解决了全部问题。如今网关服务器在局域网内吞吐量最高能够达到120MB/S,稳定运行7*24小时,性能和稳定性都让人满意。 然而其中遇到的问题也很多,如今回想起解决过程是痛并快乐着,以此文做一个经验教训的总结。c++
先说下项目背景,项目要求开发一个高性能的跨平台的网关服务器,主要性能指标是吞吐量达到70-80MB/s,并发链接数要到3000以上。现有的服务器性能达不到要求,最多不过30-40MB/s。这个项目也是我到新部门以后的首秀,也受到领导的高度关注。做为本身的首秀固然但愿能充分发挥本身的能力,高质量的完成这个项目。项目开始就受到领导的高度关注和期待,我也是信心满满。先是技术选型,由于要考虑跨平台和高性能,最终决定采用asio做为底层通讯库。而后业务分析、需求分析、系统设计。刚开始个人设计目标是作得通用灵活易扩展,不只仅是为了当前的项目,还要在从此能在其余项目中复用,成为一个平台,所以灵活性、通用性和可扩展性很重要,设计时就引入了AOP、IOC、插件、消息总线等基础框架。具体设计上是分层,将底层通讯与业务逻辑分开,业务逻辑采用插件方式,能够方便的实现业务逻辑的扩展和替换;业务类能够用IOC框架动态配置,以知足不一样的应用场景;经过AOP将非核心逻辑和核心逻辑分离开,提升可维护性;经过消息总线统一管理全部对象之间的通讯,避免对象间复杂的调用关系。设计中充斥了大量的设计模式,初步数了一下,大概用到了十二三种设计模式。这对于一个平台级规模的软件来讲也不算复杂,设计充分考虑到了可扩展性、可维护性和灵活性。编程
在完成设计后的概要设计评审会上,当我向参与评审的十几位领导同事侃侃而谈时,下面一片沉默,由于不少人都没据说过AOP、IOC等概念,也几乎没有提出设计上有什么不足。最后大领导问要多少人多长时间作完。我说四、5我的三四个月吧。结果领导说了一句让我大跌眼镜的话,只能给我两我的,一个半月能不能作个基本的东西出来,后面再继续完善,由于另一个项目等着用。这是典型的中国式项目开发,只想着尽快完成,至于设计是否巧妙、质量是否高不是很关心。我只得无奈的说若是要一个多月的话,那不少东西就要从设计中砍掉,也不能考虑那么多质量属性了,好比平台化、扩扩展性和灵活性等等。领导说不要紧,先作个基本的出来。没办法,这种事我也见怪不怪了,只是没想到时间那么紧。项目开工了仍是抓点紧干吧。先是学习asio,接着开发须要用到的各类基础框架和基础库,虽然项目给的时间很短,但我仍是想按照个人设计去开发,时间不够就加下班了,作就作好,不要打折,我不想作一个本身不满意的产品出来。等asio会用了、基础框架搭好了、基础库完成了都过了一个月了。剩下的半个月赶忙将具体业务逻辑引入进来。中间还要感谢个人直接领导给我争取了一些时间,最终是两个多月把项目作完了。网关服务器的各项性能都达到要求了,吞吐量都远高于设计目标,任务算是初步完成了。下图是吞吐量和链接数测试过程当中的截图。设计模式
接着一个任务又来了,算是网关服务器的一个具体应用:多个客户端往服务器发送解码数据,服务器将数据转发到其余解码的客户端,解码完以后再回发到服务器,服务器再将解码以后的数据转发到最终的客户端那里。这个应用稍微有点复杂,涉及的方面比较多,有客户端发送方,有解码方,还有接收方。果真在开发过程遇到各类问题,先是解码方常常挂掉,服务器有时候也挂掉,一时间问题集中出现,把人搞得焦头烂额。通过问题排查,服务器挂了有两个缘由,一个是服务器收到错误包时异常处理不足致使的,另一个是客户端断开链接后,服务器的asio异步操做还不知道,结果异步回调回来的时候对象自己已经析构,就报错了。这个问题花了很多时间解决,其实解决办法比较简单,就是保持对象的生命周期就好了,经过shared_from_this解决。解决服务器的问题以后,又要解决解码客户端挂了的问题,发现是解码器有问题,而后又是各类改,终于没有挂掉的现象出现了,松了一口气。不久发现又出现一个诡异的问题:系统运行一段时间后,全部的进程都阻塞住了,也不收数据也不发数据,也没有报错。真是诡异,而后又是各类查,怀疑是解码器的问题,干脆就不解码,将数据直接回发到服务器,但仍然出现这个问题。而后各类可能的地方都打印调试,仍是没解决问题,这时是最让人沮丧的时候,我开始怀疑以前全部的代码,由于以为设计得有点复杂,调试起来还挺麻烦,感受本身被本身坑了,干吗搞得这么复杂,甚至有了重写的念头,就这样毫无头绪的过了一周仍是没有进展。最后冷静的分析了一下,以为有多是socket死锁形成的,当一个链接双工通讯时,即这个链接又发数据又收数据,而且数据量很大时,形成对方的接收缓冲区满了,这时两边的发送都阻塞在发送了,死锁发生了!想清楚这个问题以后,就改为单工通讯了,双工通讯的话就建两个链接, 一个收一个发,这样就不会出现死锁问题了。又能够松口气了。安全
不料又出现一个诡异的问题,解码开启以后又出现全部进程阻塞的状况。刚开始觉得是解码器内部多线程死锁致使的,而后各类改,保证线程安全不死锁,状况仍然出现,按理说也不该该是socket死锁,由于已经改成两个链接分别去收发了,并且将回发的数据发到另一个服务器就不会出现阻塞。又是各类查,仍是没找到缘由,这时甚至开始怀疑asio是否是有bug。最后决定调试到asio内部去看看到底死在哪里了,结果让人很意外的发现两边仍是阻塞在发送那里了,仍是出现了socket死锁!天,两个链接都会产生socket死锁,简直要让人抓狂了。为何两个链接还会出现死锁?其实缘由仍是在解码。由于解码比较耗时致使服务器发数据会被阻塞,而客户端解码完以后回发时,io_service由于还在阻塞着,致使接收缓冲区慢慢的填满了,在某个时刻两边的接收缓冲区都满了,就都又阻塞在发送了,死锁再一次发生了!本质上是由于一个io_service时两个链接依然会产生一个环,造成死锁。解决办法是启用两个io_service,消除这个环就能避免死锁了。这时出现一个比较囧的问题,因为对象间通讯都是经过消息总线,而消息总线是一个单例,使用两个含io_service对象时会致使重复注册,这时消息总线的反作用出现了,虽然它能很好的解耦对象间复杂的关系,但也带来了调试不直观,相同对象出现重复注册的状况,这也是当初设计时始料未及的问题,好在问题不大,最后经过两个io_service来消除环,解决死锁问题,如今系统运行得很稳定。 再回过头来看,让人感慨写一个高性能又稳定的服务器程序真是不容易啊。中间解决问题的过程一波三折,充满挑战,从开始的烦恼、怀疑到后面的从容与自信,最后终于解决全部问题,以为仍是挺有成就感的,呵呵。服务器
因为大量的引入了一些框架和设计模式,致使调试的时候不方便,查问题的时候也要绕半天,最后发现其实现实没有那么多变化,不须要这么复杂的设计。灵活性和高可扩展性是有代价的,程序在知足当前需求且够用的前提下,应该尽量的简洁明了,不要玩那么多技巧,所谓“重剑无锋,大巧不工”,认真考虑一下一个系统是否有那么多变化,不要考虑“夸夸其谈的将来性”,够用就好,简单就好,这样好处不少,一来方便调试,二来方便维护,三来减小犯错误的可能。我在后面的代码重构过程当中摒弃了不少模式和框架,只要求简单够用,若是未来真的有变化的时候我再重构到模式、应用一些框架也不迟。再重温大师Kent Beck提出的编程中的价值观:沟通,简单,灵活。其中简单和灵活是排在前面的,这是颇有道理的。多线程
一个io_service时,双工通讯或者多个socket发送,经过io_service造成的环可能会形成死锁,须要注意。解决办法是消除环。并发
我对本身的代码仍是满意的,虽然时间那么紧,但我仍然坚持个人编码原则,而且在有空时用代码检查工具检查个人代码,发现有问题不合原则的代码立刻重构。在此也分享一下本身的编码原则:框架
一、过长的函数(不能超过12行)异步
二、重复代码(3行重复的代码)socket
三、类过大(不能超过500行);
四、过长的逻辑表达式(表达式不超过两个逻辑体;表达式文本长度不超过40个字符);
五、不能有魔法数;
六、圈复杂度不能超过5;
七、函数参数列表过多(参数列表不要超过5个);
八、switch语句,尽可能不用;
九、过多的注释;
最后代码检查,都符合上述原则,这也是我比较满意的地方,不要以时间紧为借口,编写高质量的代码要成为一种习惯、一种责任,要为本身的代码负责。
当嗅到代码的坏味道的时候,必定要重构,不要怕麻烦,代码是会慢慢腐化的,坏味道的代码就是技术债务,必须按期清理,不处理的话,腐化会愈来愈快。重构是为了让事情作得更好,经过重构能够改善设计、提升代码可读性、减小错误,提升软件质量。哪怕当前的代码能够工做,也要常常重构,保持对代码坏味道的警戒性。
c++11 boost技术交流群:296561497,欢迎你们来交流技术。