我们从工程项目中学习什么?

C++ 专栏收录该内容
4 篇文章 0 订阅


 

 

    这个题目有点大,我在此仅限于技术层面。

 

    这篇文章呢很早就想写,真正促使我付诸行动的是大约两个月前我开始给Mimas team讲街机模拟器的设计,大家普遍很漠然,大概完全搞不懂我为什么要讲这种东东,而且还是用离嵌入式开发工程师似乎很遥远的JavaScript / HTML。我的本意是想通过一个不是太复杂而且具备一定趣味性的案例,让大家体会怎么对已有的代码做重构,怎么提高软件的可重用性。至于为什么选择用这样一个案例,基于以下考虑:

    首先,我觉得这个东东比较好玩,而且那个实现有比较详细的教程,能让大家快速理解GameBoy CPU架构和模拟器设计思路,毕竟一个8位的CPU已经使模拟工作足够简单了。这个案例是我在PDUCC的时候注意到的,后来我曾经对它做过一些改动。我印象中应该给Atlas team发过一个链接同时还附带着一个JavaScript写的X86的模拟器。

    其次,JavaScriptLua其实很像(不是指语法啊),他们都是基于原型的语言,我们学习起来并不困难。而且,我们还需要提高的一项技能就是快速的学习能力。像这种语言类的学习周期一般就是两三天搞定基本内容和脉络,当然一些高级特性可能需要更多的时间去体会。我真的不希望再看见当初LuaSocket这种一两个小时就能搞懂的库,报价要好几天。

    第三,Web发展到今天已经有很强的表达能力了,桌面应用和Web应用的界限正在变得模糊,尤其是HTML5出来之后,大家去看看ChromeApplication就能体会到了,而且很多的手机前端应用已经开始用Web开发。这是一个值得关注和学习的领域。当Java刚出现的时候,我鄙视了半天,跑得又慢,GUI又丑,而且还想做个大而全的平台,结果我错过了学习Java最好的时机,导致了我今天依然只能做非常平庸的Java设计。曾经也很鄙视早期的Web开发,不就是写写HTMLCGI么,木有什么技术含量。结果今天Web技术如日中天,你会发现整个Web技术栈现在已经相当复杂了。我20年前就接触过LispAutoCAD的二次开发语言是AutoLisp),那个时候不可能明白Functional Programming,而且我看见Lisp的前缀表达式就头大,因为当你面对一个长长的表达式的时候,你一眼看不出来那是个啥,可绘制图纸那满篇的都是计算坐标和尺寸,所以当时我很反感Lisp(为了蹭机器用,只能忍了,那是个连鼠标都是奢侈品的年代,你能想象我很多时候画图都得手工输入命令,如果画一个三维的图我该多抓狂),谁会想到Functional Programming Language在多核时代又抬起头来了呢。所以今天我不敢轻易看低任何事情。再说,你怎么就知道未来的交换机软件里会不会跑JavaScript呢?其实并不是不可能啊,比如用Node.jsOAM后端。如果你玩过Beaglebone Black,你就会发现它提供了基于JavaScriptIO库和完整的Node.js环境。对于这么popularJavaScript,居然木有人肯多看一眼,我也是相当的纳闷啊。

    这个案例呢实质上折射出的是是我们要学习什么和怎么学习的问题,我就发表一下个人的看法,从某种程度上讲也可以算做是在我的年假变为20天之际回顾一下我在公司都干了些什么以及我在做一些决定的时候的思路和背景。 

    每年我们都会列一堆学习目标,最终结果如何我就不清楚了。公司不是学校,这就注定了其实没有多少专用的学习时间给我们。平心而论,参加一个几天的课程对设计功力的提升是微乎其微的,很大程度上我把它看作是公司的福利和提供给一个有相关经验工程师的一个总结和回顾的机会。我的确在很多年前参加过Rational为期一周的OOAD培训,但在此之前我已经有了很多的OO设计经验和理论知识。除了业余时间,工程项目应该是学习的一个非常重要的途径。对公司而言,实质上它不太关注项目的过程,结果决定一切。但对我们自己的成长而言,这个过程的重要程度绝不亚于结果。在项目中学习不是单纯的指理解这个项目的设计和实现过程,事实上它涉及到一系列的深层次的思考。我就以前几个月的Spitfire Chassis的设计尝试为例。这个项目如果纯看结果,很negative啊,很大程度上我们自己左右不了。有人问我有失落感没,我开玩笑说哪能啊,公司又没少发我钱。(再说项目乃至产品被Cancel这种事情经历的还少么?:-))其实在每个项目执行过程中,我都会思考很多东西,这足以弥补这个结果给我个人带来的负面影响,这里跟大家分享一下。

 

从坏的设计中学习,而不仅仅是抱怨

 

  对于我们这些在大外企里的开发人员来讲,不是每个人都有机会去做一个全新的设计,很多时候我们都在维护Legacy的代码,而且是运行了一二十年的古董,里面鱼龙混杂,精华和糟粕并存。我们经常会发出抱怨或听到抱怨:这是什么狗屎设计!你居然会抱怨了,太好了,说明你不麻木。其实呢,你会发现,你所经历过的每个项目都很烂,只不过烂的程度不一样而已。我相信若干年后一定会有人跳出来抱怨我们自己正在写的代码。如果你在职业生涯中能在Legacy系统中遇见过让你惊艳的设计,那么你很幸运,我很羡慕你,我迄今为止还没有过这样的机会。而且很多时候我们不得不忍受着和烂代码打交道,因为不管我们喜欢不喜欢,它毕竟事关我们的饭碗。如果仅仅停留在抱怨,除了释放一下负面情绪,我们得不到什么。大部分代码一开始的设计意图肯定很高大上,只不过之后的各种妥协,年久失修,导致了动不得也扔不得的两难境地。这其实是给我们的第一个警示,一定要频繁的有意识的做代码重构,否则代码会腐烂的很快。我刚到公司的时候,写的第一个程序就是扫描ETOS所有的代码,自动生成各个组件的依赖关系图,可以指定任意一个组件,画出它依-赖的组件和依赖于它的组件图。我清楚地记得那是一个周五的下午,我坐在公司大厦的过道里,一直干到晚上七八点钟,代价是被蚊子叮了一个大包。看了那些乱麻一样的依赖图之后,我就断念死心了,绝对不会动对ETOS做结构性重构的念头,会死人的。如果再深入一步,我们需要知道这个设计哪里做得不好。进入Spitfire项目之后,我扫了一下文档和code,发现很令人费解,尤其是告警处理,我根本就看不懂为什么那样做,直觉告诉我这个设计不是很合理。(我认为一个好的设计一定是一个易于理解的设计)。所以我紧接着对整个 PAd/CMS/CMSP 做逆向工程,生成了类图,然后又花了很大精力对照代码补全类图和重新布局,这个时候我就能从结构上感知它的缺陷在哪里。其实我当时生成这样一个图的一个主要目的是为了直观的告诉别人这个设计哪里不好,还真不是留给我自己用的。我知道了这个东西烂,也知道了它为什么烂,那么我会问自己,如果是我,我会怎样去做。当然很多情况下,我们根本没机会做这种重构或者重新设计,但在脑袋里演练一番其实就是一个自我提升的过程。这就是PAd重新设计的想法出炉的始末。当然这里的一部分想法其实也源于我对 ETOS 抱怨之余的思考。

    Legacy系统的维护中,如果我们不得不沿着旧的路线走,我们需要保持头脑清醒:我现在这样做是因为各种条件的限制,但我也知道其实还有更好的方式去做。我们要警惕温水煮青蛙的效应,这其实类似于一个洗脑的过程,如果我们长期按照某种方式行事,我们很可能会倾向于潜移默化的接受它,它会逐渐成为我们习惯的一部分,会让我们淡忘我们曾经认为它是不合理的,在以后的设计中可能会不自觉沿用它。

 

做一个懒惰的程序员

 

    刚才提到了,抱怨也是一个好的开端,起码它让你发现系统什么地方可以做改进。做一个懒惰的设计人员有助于你发出更多的抱怨,然后你需要做得就是如何消除这些抱怨。懒惰是人的天性,设计人员倾向于做懒惰的人,用户更是懒惰的人,设计要适应人的本性,而不是有意识或无意识的创造各种门槛。我在team里常说要注重Usability,当你设计一个软件的时候,你要首先站在client(无论是外部客户还是内部developer)的角度,考虑如何最大限度的简化client和目标软件的交互(包括需要传递的信息量和流程)。软件是写给别人看得,表达一定要清晰简洁。你哪怕能省掉一个参数,它其实就降低了使用者的记忆负担,降低了可能产生的潜在依赖关系,最终提高的是软件的可维护性。我不知道Mimas的同学是否记得在做SP400 Chassis模拟器的时候,我们需要一个类RPC的服务,开始的时候想基于IPOS IPC,三天过去了,我们还没有一个能跑得哪怕是sample程序,我当时和team说如果你不能在一天之内跑起来一个client-server,那至少说明那个库的接口设计是失败的,因为它不是通俗易懂的,想一想我们能不能用Javasocket类接口或类似的商业类库很快的写一个TCPCllientServersample程序?所以我当时决定我们用UDP自己实现,那至少一天也能出来东西吧,而且额外的好处是跨平台了,我们可以在PC上测试我们写的大部分代码。 

    抱怨的要义就是能多简单就多简单,能少干就少干。当你发现别人的代码你看不懂,当你发现做某件事情相当繁琐,当你发现你需要知道太多的细节才能做一件事情,请毫不犹豫地大声抱怨。你可以抱怨:

  1. 什么?一个Chassis的适配层居然有250K+行代码?我认为50K差不多了吧?
  2. 天哪,加一个告警居然有将近20步个步骤?谁能告诉我这是为什么?我能做些什么使这个过程得到最大程度的简化?
  3. 整个IPOS居然没有一个通用的告警处理系统?
  4. 告警的处理为什么会有多次的数据结构的变换,我真的没搞懂耶!
  5. 一个告警不就是一条记录么,为什么还会对每个告警派生一个告警类?
  6. 怎么引入新的告警类型,告警数据库的class定义都需要跟着变啊?我不喜欢这样哦。
  7. 怎么会有AL Interface这么古怪的东东?一个PAd组件凭什么要暴露出1000+API,你想烦死我啊?我不写了,你来。
  8. AL Interface为啥要直接由PAd的类去提供啊?万一哪天AL Interface废弃了,不得改得鸡飞狗跳么?
  9. 这个系统中怎么会存在一些这么巨大的class来颠覆我的三观?
  10. 拜托,一些八竿子打不着的板卡都堆砌在一起,代码和目录结构就不能再划分的清晰一点么?
  11. 我没兴趣知道PAd的具体细节,哪位神仙姐姐能告诉我加一块板卡怎么做,能不能不在PAd里东刨一下西刨一下?我需要这些扩展集中在一个地方。
  12. 现有的模拟器不好玩,无法任意定义硬件状态,我需要一个更加聪明好用的模拟器。
  13. 我已经接触过数个产品的Chassis设计了,大家都在做相似的事情,可每个都有独特的气质,我厌烦透了,我能不能做一个能最大限度重用的设计?
  14. 我厌倦了C/C++了,开发成本这么高,还有无数的坑,我想做的更happy和傻瓜一点。

    可以说,我们前一阶段的设计基本上就是针对着这些抱怨来的。 

    因为我足够懒,所以在加入SP400 CLI Filter设计的第一天,还对IPOS一无所知的情况下就把原方案否定了,因为我认为不能让人去干那种体力劳动,第一,那是浪费公司的钱,第二,那是一个完全不可以重用的方案,第三,这种做事方式降低开发人员的成就感,直觉告诉我一定有更好的办法来解决这个问题,机器才是干体力活的最佳候选,我们完全可以让机器去自动修改code,重建Parse Chain 

    因为我足够懒,所以看到公司居然在嵌入式产品推广JCAT这样一个单元测试框架去驱动产品的集成测试的时候感到非常的不可思议。你能想象我们招聘测试工程师的时候并没有要求人家有多少编码的技能,然后突然要求大家都写Java代码,大家要学习Java语言,Java平台,JCAT框架还有访问测试仪的库,还有比这更抓狂和扰民的事情么?我们的测试工程师更熟悉的是CLIPerl/Tcl之类,而不是Java。政府行为我们无力抗拒,但至少我们想办法让自己过得更轻松一点,所以我做了一个JCAT测试例自动生成的Innova(当然没过多久,我们又被拉进了CBA的怀抱,JCAT就不了了之了)。当时的基本想法就是设计一个更high levelDSL代替Java语言,描述测试用例,然后自动生成符合JCAT要求的Java代码。我花了整整一周的时间用JRuby实现了一个DSL engine,用Java实现了一个简单的Telnet Server(支持最基本的Telnet选项协商),用JavaCC实现了一个简单CLI engine,然后把对测试仪的调用封装成了CLI操作,这样在脚本中对目标box和测试仪就都统一为CLI交互了。不可否认,那个Innova有骗钱的动机在。刚进公司的时候,没有关注过Innova,因为当时觉得Innova是件很神圣的事情。当有一天re-orgPDU MW,发现这里贴着满墙的Idea,立刻认识到了我们Innova庸俗的本质,惊觉失去了很多变现的机会。 

    因为我足够懒,不想在乱麻一样的代码中迷失,所以用Python编写工具分析log,自动画出ETOS软件升级的一个状态机子集。 

    因为我足够懒,所以会在某次ETOS意图降低软件耦合的运动中(当然那是个可望不可及的梦),用Python写了一系列工具去分析某类Passive Object API集合被哪些模块调用,某类Message在哪里被引用,并自动生成相应的excel表格。 

    因为我足够懒,所以在做Scrum Master的时候,不想忍受每次编辑完excel表格,还要手工抄写任务列表贴在白板,也无法忍受需要手工设定一个sprint理想的Burndown chart,而且还需要手工刨除各种假期,所以我会用Python写了那个excel模板的处理工具,可以自动生成要打印的任务列表小贴贴和更新burndown chart

    因为我足够懒,所以在做CBA的时候,力主要做OpenSAFC++ Wrapper。虽然上面没同意,但在team里我是要求了的。且不说OpenSAF本身如何,从接口设计角度来讲,绝不能算是一个优雅的软件,随便一个组件就有几百页的编程手册,连个Log的手册都一百页,我简直都要跪了。API里指针的指针'**'就开始让人晕了,它居然还有不少'***'。如果没有一个简单明了的C++ Facade,这得浪费多少人的生命啊。

 

注重提升通用计算技术的功力

 

    我曾经在各种场合下提过我们需要注意提高自己的通用计算技术,这是我接触过的大部分嵌入式开发人员的短板。做一个产品我们需要掌握领域知识和计算技术,交换机恰恰是对领域知识要求比较高的,所以我们大部分人的精力其实是放在学习和掌握领域知识上了,计算技术的不足反而被领域知识给掩盖了,我感觉很多人是在凭着直觉写程序。作为一个有潜力的开发人员,我认为他应该不仅仅是熟悉领域知识,而且还应该是一个计算专家,也就是说在我们的知识结构中,领域知识应该和计算技能得到均衡的发展。领域知识决定了要表达的内容,领域知识的不足将导致我们没法写出满足用户需求的软件;计算技能决定了系统的表达方式和骨架,计算计能的缺乏将使我们不清楚软件能不能以合理的代价完成。对一个开发人员来讲,领域知识相对来说很大程度上是能速成的,而计算技术是需要长期的理论和实践积淀的。从我个人的观点来看,我们OAM部门开发人员的知识结构中应该是计算技能远重于领域知识,因为相对于L2/L3,我们的领域知识还是比较容易掌握的。我在这里无意贬低大家掌握的特定产品的特定OAM相关的知识,那是你在公司安身立命的前提条件和资本,我想表达的是大家在学习过程中要注意均衡和取舍,从长远来看有些知识在你的知识体系中未必会处在最重要的位置,特别是特定硬件特定平台相关的知识,很可能就是过眼云烟。在 DOS的时代(那是个自力更生的年代,至今仍怀念那个让我打下扎实的软件基础的时代),我曾经从最基本的画点、画线、区域涂色开始,利用BIOSOS、视频控制器开发过一个比较完整的GUI环境,我曾关注过如何通过控制软盘控制器做出各种千奇百怪的磁道(早期的软磁盘加密是削尖脑袋在软盘上写上独一无二的无法复制的签名,比如在用高密度的访问方式在低密度盘两个磁道中间写几个扇区,或者格式化出一个非标准磁道,还有人变态的造出螺旋型磁道),我曾关注过DOS下病毒如何去感染磁盘文件,如何加载自己和取得控制权,我写过在DOS单任务环境中用多个栈模拟多任务切换,我学习过XMS/EMS只是为了在DOS实模式下访问扩展内存(1MB以上的地址空间),但随着计算机软硬件技术的发展,眨眼间这些知识统统变成了昨日黄花,很多秘笈就被无情归零了,从此之后,我学乖了很多,在很specific的知识上的投资比重是比较小的,除非绝对必要。通用计算技术背后的支撑就是坚实的理论基础,我们今天的一些绚酷的应用技术在十年之后很可能就烟消云散了,但其背后的计算机科学、数学、逻辑的知识依然是有效的。

    由于各种原因,一个软件开发人员很少会长期固守在某个领域,如果不注重计算技能的提升,当切到一个全新的领域时候,他很有可能会感到茫然。我个人参加工作以来做过移动网络NMS的应用层,做过光网络NMS的适配层,做过EMSSNMP适配,做过交换机的L2/L3,做过P2PDPI,做过P2P网络的搜索,做过手机应用,做过交换机的OAM,当从一个领域到另一个领域的时候,从来没有过不适应的感觉,因为我知道那只是运用我的计算技能去解决一个新的问题而已,和我以前做的事情没有什么本质区别。当然你可以认为那些大部分还是跟电信有关联的,但是如果现在把我扔到互联网领域或者游戏领域,我相信我还是能很快找到感觉的。 

    有一点需要澄清的是,我说某些知识相对没有那么重要,并不意味着在运用这些知识时获得的技能不重要,换句话说,我们要善于总结和提炼在这个过程中获得的通用计算技能。事实上,那个DOS下的GUI组件库的设计奠定了我的OO设计基础,我在看Ardunio的时候又想起了我曾经做过的DOS多任务,然后我就上网查,发现已经有人提供类似的库了。在Chassis开发过程中,Kane同学曾经表达过学习Lua的良好愿望,我的建议是单纯的Lua令测试工程师获益不多,Lua设计之初也没想作为Full Stack的开发语言,如果觉得Perl老旧了,还不如学习PythonRuby。但我认为更重要的一点是我们对目前基于PerlARTS平台的深层次的理解,想比之下Perl语言本身并没有那么重要。当前的设计有什么优点?又有哪些缺陷?有没有改进的余地?如果想从测试工程师向开发工程师的方向迈进一步,我觉得更应该关注表象之下的设计理念,否则结果就停留在又学习了一门时髦的语言而已。

    特别对我们OAM部门转做嵌入式系统的前Java工程师(尤其是前端工程师)建议,大家以前在应用层的设计经验非常重要,应用层的很多设计理念在这里依然适用。很多人认为前端设计是机械的活,就是摆摆控件,写一些简单逻辑。错!在我心目中把前端做好是一个技术活,首先这关系到人机界面设计,关系到Usability的意识,工程师得时刻站在用户的角度,思考如何降低操作成本,其次,前端软件很容易做烂,尤其是业务流程复杂的时候,界面逻辑很容易和业务逻辑搅在一起,这就是为什么大家一直在不厌其烦得提MVC的原因。前端软件和后端软件一样需要良好的架构设计,需要考量一系列的质量属性。第三,个人感觉一个好的前端工程师对OO的理解是比较深刻的,一个GUI类库(比如Java Swing)能体现出很多的OO设计准则,而且前端设计中我们常常不满足于标准控件提供的功能,而去设计定制的控件(看看我们AndroidiOS手机应用就知道了,很多花里胡哨的界面都是标准类库里没有的),一个可重用可扩展的控件设计往往需要扎实的OO基础。最后一点,有前端的基础,更有助于写出方便友好的工具,提高生产效率。

    我们做软件的过程,实际上就是一个表达的过程,我们需要寻求一种清晰的描述来表达一个业务问题是如何转换为一个可计算的问题的,也就是如何从Problem Domain映射到Solution Domain的。这首先涉及到抽象建模的技能,回想我们的Chassis软件设计,一个挑战是如何去应付千差万别的硬件以及软件本身的可扩展性。如果我们纠结于具体的领域问题,比如板卡有哪些传感器,有哪些LED,如何通过不同的控制总线读写硬件资源,那么我们很可能会得到一个非常specific的设计。如果我们换一个角度看,无论被管理对象是什么,从纯计算的观点来看,它无非就是资源空间的一个结点,并和一个状态值关联,这个状态值可以通过某种方式设置或读取,基于这种想法,我们设计了一个通用的资源管理器,Chassis数据库维护了通用的逻辑资源树,逻辑资源到物理资源的访问是通过资源访问层RAL的映射,然后路由到相关资源访问插件完成的,这个映射完全是模型驱动的。Chassis的核心软件其实对被管理对象并没有一个全面的理解,它不关注其中存储的product specific的内容,这些内容由上层的应用程序和相关板卡插件去理解。我们的告警处理的设计也基本上体现了高度的抽象,我们的软件仅仅把告警看作是一条数据记录,软件并不对内容进行解释,我们把整个告警处理的流程看作是一条数据记录流经预先配置的各pipeline的一个过程,这些pipeline是可以灵活组合的。告警的过滤,抑制和防抖动功能都会映射到相关pipeline。每个pipeline有自己独立的规则处理引擎,它对告警的处理完全是规则驱动的。建模的另一个重要的方面是模型的表达,也就对软件架构和高层设计的表达。我们要关注问题域什么样的结构,我们要对软件质量属性做什么样的均衡和取舍,这是需要认真体会和修炼的,这里面有太多需要考虑的地方。当我们用用UML去描述软件的时候,我们需要从OO的角度考虑问题,我们需要对优秀的设计有基本概念,我们需要有结构的把握能力,我们需要清晰的理解每个Notation的具体含义,我们应该能说清楚每一条线的来由,因为每一条线都意味着潜在的依赖关系。这个真的不是一日之功。具体实施层面的表达首先就是是算法。我们的大部分开发人员对算法可能已经比较生疏的,其实算法很重要,是计算的基本功,现在互联网巨头们不都在拼算法么?交换机软件里链表,树,图,哈希都是频繁用到的数据结构,而且是最基础的数据结构。当初的SP400 CLI Filter方案我想也不是没有人抱怨过,问题是抱怨之后没人提出明确的思路,我觉得算法恐怕是障碍之一,那里面涉及到C源文件的parsing和图算法(整个SSR EXECCLI是手写的总数超过4万个结点的大约三四百张图,具体数目我不记得了,做可视化的时候程序在HUB里跑了一晚上才生成所有的图片)。如果你觉得这些算法很麻烦,你根本就不会朝那个方向想。在搞懂IPOS CLI Parse Chain之前,我都准备好了用ANTLRCparser,但随即我发现这个问题可以大大简化,完全可通过正则表达式处理GCC的编译中间文件搞定。我们这次做的ChassisRAL设计的核心就是资源树的一个处理算法。在告警处理引擎里面有个Lua写的定时器管理算法,第一次review的时候,让我给打回去了,算法写的太直白了,插入/删除/计时的时间复杂度全是O(n)。做这个问题的时候我们不是没有参考设计,我们可以回想一下Linux Kerneltimer管理,从以前的Timer Wheel到现在hrtimerR-B Tree。当然我们没必要弄那么复杂,考虑到可能产生的scalability issue,我们总不能奔着效率最低的算法去吧。 对实施层面需要重视的另一个方面是表达的清晰程度,这也是我在team里反反复复强调的。大家为啥经常骂软件烂啊,除了结构的因素,还有一个原因就是软件写得晦涩啊,那浪费的是无数后来者的时间和精力,浪费的是公司白花花的银子。我们不能保证以前的软件写的如何,但至少我们要有意识的努力从我做起,写代码的时候要经常切换视角,多想想如果是别人来看、来用我的代码会怎样想,当代码review的时候看到别人写的不清晰的时候要提出来。否则这个代码就陷入我骂前人,后人骂我的轮回了。当你给我看你的代码的时候,借用一本著名的讲Web可用性的书的名字:Don't Make Me Think,代码要足够descriptive,不要让我去费尽脑筋去琢磨,我很懒。    

 

他山之石,可以攻玉

 

    我们OAM teamkernel,会写driver,会调板子,这是我们的强项。但如果我们只懂这些,对大部分人来说就是劣势,很简单,适应面太窄了。你如果看看我们我们现在做的OAM软件和采用的开发技术,和二十年前基本没有什么区别。你再想想这二十年整个IT领域发生了多大的变化。如果我们的视野仅仅限于ETOS或者IPOS平台,是会营养不良的的。做设计是一个厚积薄发的过程,要做一个好的设计,离不开平时的积累,我们需要去阅读和关注不同领域的设计,再回过头来想想如何应用在嵌入式领域。这其实也是前面我为什么会给大家讲JavaScript写的街机模拟器的原因之一。

    回想一下我们在Chassis设计的时候做过的一些设计决策,而这些设计决策或多或少都有其他领域的影子在里面。

    第一点,我选了动态语言Lua来做整个Chassis资源访问层和告警处理引擎的建模和实现,这个引起了相当大的争议,但我至今不认为我做了一个离谱的决定。很早就知道Lua在游戏产业用的很多,但真正关注Lua大约是在五年前,也差不多是加入公司的同期。我当时问过曾经带过的一个小盆友,他在做游戏,我说你们为什么不用Python啊,那个功能不比Lua强多了啊,他说用来做游戏good enough,而且他们的游戏策划人员是拿Lua写脚本。在PDU CC的时候我就在想如何简化嵌入式开发,因为用C/C++开发成本太高了,尤其是C++,那里面陷阱太多,对工程师的要求很高。不是打击大家,现在整个NDO SP估计也找不出几个合格的C++程序员。这也是为什么工业界在尝试推出新的系统级设计语言,比如googleGoMozillaRust,当然在此之前有个D语言呼声较高,但一直没有发展起来。虽然C/C++是我最熟悉的语言,但我自己写东西的时候不到万不得已是不会用的,因为那需要关注太多的底层细节,比如内存、竞争、平台差异,反而干扰了对真正要解决的问题的思考,再说我也很懒么,用别的语言写一两句话就能解决的事情,犯不上非用C写俩小时吧。去年其实也也考虑给自己抽空用动态语言实现整个Chassis管理,然后给大家演示一下动态语言的简洁和威力。倒退10年,C/C++是绝对的王者,现在我们再看看,很多C/C++的传统领地已经让位于表现力更强和开发成本更低的语言,我感觉用现在C/C++就像20年前用汇编语言写程序,所以我勉励自己:珍惜生命,尽量远离C/C++ :-)。开玩笑啦。当然对于我们来讲,这不可能,C/C++目前还是在基础系统软件领域占统治地位的语言,我们还得每天和它打交道。所以我的基本想法就是在适当的层面引入动态语言,降低开发成本,因为现在嵌入式的CPU也越来越很强大了,在很多不是性能要求很苛刻的地方,动态语言完全能够胜任。这里没有贬低C/C++的意思,我的意思是用right language to do rightthing,当然C++也在演进,让我们拭目以待C++ 17能给我们带来什么。当然在PDU CC的时候根本没有机会做这件事情,仅仅是想想而已。当时面试过一个华为的candidate,他说华为交换机平台的资源管理用的是Lua语言。 我在LuaPython之间犹豫过,仔细权衡之后还是认定Luagood enough,因为Python那堆庞大的库在我们交换机上根本用不到多少,而且Lua虚拟机本身的执行效率和与C语言的天然的集成能力都胜于Python。我很喜欢Ruby,它的meta programming能力实在太强大了,所以还小小关注了一下mruby,那是在移动平台的一个轻量Ruby实现,但担心它的成熟度和Ruby的执行速度,也不在考虑之列。现代的JavaScript引擎已经做了很多优化,而且它的效率据说也优于Lua,所以JavaScript也自然是一个可选项,尤其是Node.js还是挺让人动心的,最后还是抑制了自己的冲动,一个最简的、高效的和方便于和C语言的互操作的动态语言才是最重要的。 其实在2013年下半年做SP1.8的时候我就让Atlas teamLua集成到板子上了,希望大家能用它写一些tool,我也给大家发放过一本打印的Lua的书,就是我最初学习Lua用的书,但最后吧其实也没人理会我的苦心,或许大家就是不明白我为什么要推这么一个小众的语言。没有错,这确实是一个小众语言,你如果问我要Java的书,我能给你至少上千本,但Lua,我手头上最多五六本,估计全世界也没多少本,你去Amazon搜一下就知道了,而且Lua20155月份的Tiobe排行榜也才名列33位,你说学这玩意儿有前途么?这个问题我稍后面再解释。Chassis资源访问层的前身来源于我在SP1.8后期的时候做的一个Innova,当时的想法就是在嵌入式box上打造基于Lua的工具集,目标有点大,所以选了个有可操作性的切入点是一个通用的硬件寄存器访问工具,支持DSL建模和插件,方便硬件工程师和软件工程师调试板卡。花了大约一周的时间(包括复习LuawxPython,那是我写的第一个Lua程序,wxPython这些年我也就写过两个程序,而且我学习的时候经常是不动手的,所以必须复习一下 ),其中三天用Lua实现了核心算法,两天半用wxPython实现了一个前端来浏览硬件资源树,在Ruijun的帮助下提供了SP400sample插件,然后在去年的Fedex Day展示过,然后就空着手回来了。当时很纳闷Titan那个富有浓郁乡土气息的SP400 Chassis模拟器居然能够获奖,后面Mimas基于RAL做的模拟器比它帅多了,看来包装是相当重要啊。

    第二点,资源访问层提供类RESTfulAPI。软硬件资源用我们自己的URI格式表征,提供统一的GET/SET接口访问资源,上层应用软件不会再面对针对特定硬件的访问接口,硬件差异实际上体现在了URI编码里。这个设计决策是进行硬件抽象的基础,保证了资源访问层软件的可复用性和可扩展性。 

    第三点,所有的消息通信基于JSON。基于二进制的结构好处是效率高,但它带来了编译期的耦合,比如消息类型的枚举值空间的变化会传播到所有组件。这里实际上是用一定的效率损失换取文本协议带来的可扩展性的提高,而且这个开销随着嵌入式硬件能力的提高,也是在可接受范围之内的,而且文本协议调试也会比较方便。    

    以上的一些考虑其实都可以看作是汲取了互联网和游戏领域的营养,设计都是相通的,这也是通用计算技能的训练。再看我们告警的pipeline设计,和一些流行的Web框架的(比如Node.js)的middleware设计也是很类似的,我做的时候也没有想起来什么Node,但这是一个common的模式。其实很多东西我们可以借鉴到到交换机上来。我们做的多板卡的chassis其实就是一个小的分布式计算环境啊,那么在互联网的server集群里的技术是否有可能在我们这里应用呢?在PDU CC的时候,我设想过应用类似Redis这种Key-Value内存数据库做通用的交换机配置和状态数据库,而不是各种业务specific的表格(CBAIMM其实不也是类似的东东么,我逆向过IMM,实际上就是一个用STL写的查找表);我设想过用开源消息中间件(如ZeroMQ)取代私有的板间通信;我设想过如何用P2P进行软件升级;我设想过MongoDB如何在测试和CI中的应用;我设想过用Hadoop对大量网元的log进行数据挖掘;我设想过用XMPPM2M;我设想过如何方便的对交换机的状态数据做可视化。总之,多关注一下IT其他领域的发展,比如Big DataNoSQLMachine LearningVirtualizationCloud ComputingSDNIoTIndustrialInternetMobile Internet等等,你的眼界和思路会变的很开阔。我们很多开发人员并不是计算机系专业,你也可以思考如何把IT技术和你熟悉的专业背景结合,从而产生一些有意思的想法。现在有很多创客在搞一些很有创意的项目,我们OAM组的工程师其实也有这方面的优势啊。

 

保持你的好奇心

 

    面试的时候我经常会问candidate平时会读什么书,对什么方向感兴趣,会在工作之余做些什么有意思的东东,目的就是考察candidate是否对这一行有足够的激情和好奇心。如果一个开发人员平时不读书、不看报、不思考,也不动手,我很难想像他能成长为一个出色的设计人员。相反,如果一个开发人员对计算技术保持足够的好奇心,我相信他总有一天会脱颖而出。 我个人有个习惯,当见到一个软件觉得比较有意思的时候,我会琢磨它是如何实现的,这个习惯从我学习软件的第一天一直到现在。我会关注IT领域都在做什么,并会对某些方向做一些跟踪。我会在学习中产生一些有意思的想法,然后付诸实施自娱自乐一下。我们做工程项目的时候也是一样啊,如果你有好奇心,总能比别人多想一步,就能让本来很平淡的例行开发活动创造更大的价值。今年年初line meeting上有几个Cirus team的同学表达了要学习Lua的愿望,但随着项目取消恐怕没人会再捡起来看了。对于参与过Spitfire Chassis这个项目的同学来说,如果把这段应用Lua经历仅仅当作是学习了一们新的而且以后可能再也不会用到了的语言,我觉得其实是错过了很多学习的机会。仔细想来,就是这样的一个看似普通的过程也有太多值得思考的地方了:

    在项目之初,你是否好奇为什么要引入Lua呢?至少不是所有的人都问过我这个问题,我相信MimasCirus team的同学现在已经深有体会了,大家可以想像一下用C/C++要付出多少代价才能做出同样的东西。如果用C/C++,我们不可能采用简单灵活的internal DSL,而用external DSL必然引入一系列的parsing,这里不仅仅是DSL本身的parser(这个用XML就可以,不过我本人十分不喜欢用XML手写配置文件,XML适合做机器和机器的接口,而不是人机接口,这也是为什么Ruby青睐YAML的原因之一),而且包含DSL表达的内容的parser,想想我们的告警关联处理规则里实质上要做变量赋值、表达式计算和函数调用,这已经是一个小型语言解释器了。如果用C++,很多地方的表达远没有动态语言简洁,我们不能在运行时刻动态增删对象属性,我们不能在运行时刻动态加载并解释执行字符串(当然必须要考虑安全性),我们不能不从一个接口派生得到一个具体的实现类(而对于动态语言来讲,duck typing是常态,所以我们不需要通过继承去实现接口),我们无法应用reflection

    你是否好奇和关注Lua本身一些特有的feature以及它可能会产生怎样的应用呢?当你看到表是Table是唯一的数据结构,协程,闭包,弱引用和基于原型的语言设计的时候,你有没有和其他的语言做一些比较?有没有思考怎么用这些特性呢?Lua支持Procedural, Object-Oriented以及Functional Programming的编程范式, 在我们的设计中已经用到了最基本的Lua OO编程。由于基于原型的语言没有类的概念,它的OO设计和我们传统上基于类的OO设计相比会带来什么样的差别呢?GoF当初主要用静态语言描述design pattern的,如果用Lua这种动态语言描述,可能会有些什么样的变化呢?亦或会产生什么样的新的pattern除此次之外,你是否好奇我们能不能用它的FP做些什么呢?现代的语言都在不同程度上引入FP的支持,例如C++11Java 8中都引入了lambda。如果对FP不熟悉,你会不会潜心钻研一下FP呢?仔细想想我们的告警处理引擎,实质上就相当于这样的一个函数调用:pipeline_n(...pipleline_2(pipeline_1(alarm))),我们有没有可能做一个基于FP的设计呢?

    你是否想过Lua在交换机的开发中还能做些什么呢?我最初引入Lua主要基于以下考虑:(1)代替C/C++做部分feature开发,这个刚才已经提过了;(2)统一和简化box上的工具开发,我们现在的工具开发基本上处于一个无序的状态,各team都在独立开发自己的各种小工具,有一些可能就是重复工作,很简单的一些事情也要用C编程序再交叉编译上板子,我期待建立一个工具包分发的机制,类似于于一个内部的Tool Market,有专门的服务器可以存储提交的工具,在box上可以得到工具列表,可以安装和卸载工具,可以对工具评分;(3)让我们的box更加Programmable,我们可以做一些wrapper,暴露出一些RESTFul API方便测试,而不是一定通过CLI才能控制box;(4)建立硬件的参考测试平台,对硬件Team来讲,他其实不关心板子上是否跑了IPOS,他只需要有OS和相关driver,然后在此之上跑各种测试程序,所以这里可以用Lua做一套可以扩展的硬件测试平台。

    你是否好奇Lua在其他领域都是怎么应用的呢?做Chassis刚开始的时候我其实是留了一些时间让大家思考这类的事情的。我让海鹏去调研Lua C/C++ Binding 的库其实就是让他看看大家都是怎么用Lua的。LuaBind库我们最终没采用,不是因为它不好,而是因为我想保持最简,我们软件里并没有用到一些复杂的case,自己写的简单wrapper已经good enough了,我也不想在大家都对Lua不熟悉的情况下玩一些很cooltrick。我倒是希望大家能研究研究LuaBind,想想如果是你自己来做这件事情会怎样考虑。游戏领域就不多说了,手游的兴起一度使LuaTiobe排名进入前十,Lua 网易十多年前就开始用Lua了,大家有空可以关注一下云风的博客,那是国内游戏界的大拿,Lua也是他在网易做大话西游的时候建议采用的。如果你觉得好玩,你也可以尝试用Cocos2d Lua bindingLove 2D自己做个2D小游戏陶冶一下情操。淘宝前工程师章亦春维护了一个OpenNesty开源项目,把Ningix打造成了用Lua编写业务的高性能应用服务器,这也是一个很有意思的软件,据说已经在不少公司用了,他本人也因此受雇于北美的一家公司专职维护OpenNestyNetBSD已经把Lua解释器集成进内核,并准备在7.0支持用Lua来写内核驱动程序,NetBSD这个步子迈得还是挺大的,着实让我有点吃惊。如果你有时希望Lua的程序有个GUI,那么wxLua可以帮你做这件事情。它是跨平台的wxWidgets库的Lua Binding,它的设计仿照VC++MFC,如果你熟悉MFC,上手会很快。我这里不是鼓吹让你用wxLua写前端,只是提供一个Option而已,就像我写Python程序需要界面的时候也会用wxPython

    如果你注意到LuaJavaScript如此相似,你是否会从JavaScript的世界里借鉴些什么呢?如果你觉得Lua的书籍很少,但JavaScript的书可是满天飞,你可以阅读JavaScript的东东,很多其实都是适用于Lua的。比如见到Node.js的第一眼我马上意识到这个设计可以完全复制到Lua,然后就有把它做出来的冲动,因为有了它就可以用Luafull stack的开发,然后就到网上搜索,结果发现已经有人做了luvit

    Lua的实现非常精炼和小巧,总共也就两万多行代码,你是否好基于寄存器的虚拟机是如何设计的?你是否好奇Lexical Scoping怎么实现的?你是否想知道它的垃圾收集算法?你有木有读一下的冲动?顺着这个再展看,你是否想看看基于堆栈的虚拟机的实现呢,比如JVM?你是否还想关注一下其他VM怎么做GC呢?如果对JIT技术感兴趣,你也可以去阅读LuaJIT的源码啊。

   如果你找不到顺手的Lua开发调试工具,你是否想过自己写一个呢?在项目开始的时候我提过要用Lua profiler看一下我们的代码性能,说如果没有合适的我们就自己写,毕竟Luadebug库给我们提供了足够的hook。由于LuaJIT不支持ARM64,我们就面临源码泄露的问题,我也提过自己做obfuscator的建议,甚至另一个选项是从long term来看,我们可不可以让LuaJIT 支持ARM64。我也想过在我们的box上做一个轻量的基于WebLua IDE,当然只是个人兴趣。

   我们设计了自己的DSL,你是否关注过DSL设计相关的知识呢?这几年也出版过几本有关DSL的书籍,你是否有兴趣研读一下呢?ETOSmodel,和我们现在支持的ECIM,其实都属于external DSL的范畴。这个领域关注一下,其实对以后的设计很有帮助。

    回到刚才提到的Lua是否值得学习的问题。近几年前有一个《Seven X in Seven Weeks》系列,其中一本《Seven Languages in Seven Weeks》获得了2011年的Jolt Award图书大奖,去年年底,它的续篇Seven More Languages in Seven Weeks》面世了,又选了7种语言来演示现代的编程范式,Lua就是其中的一个。松本行弘在《代码的未来》“编程语言的新潮流”一章中专门用一节来讨论Lua,并把它和Ruby进行了比较,他也在主持mruby的开发,他说:“伴随着嵌入式计算机性能的提高,软件的复杂化也逐步推进,像Lua这样以小规模的引擎面向应用程序嵌入的语言,今后的舞台应该会越来越广阔。”我深以为然。我觉得真的不要太小瞧Lua,当然我也不是在这里鼓吹Lua是多牛的silver bullet,我个人感觉这是个很有用的东东,我一直觉得让自己的软件支持一种script是一件挺酷的事情,你想想Emacs可以通过Emacs Lisp扩展,AutoCAD可以通过AutoLisp做二次开发,MongoDBshell就是个JavaScript shellRedisCLI里支持嵌入LuaMS的软件支持VBA Script。此外,具体语言学习是一个方面,而我更看重的是类似上面列出来的在这个过程中产生的思考,也就是语言背后的东西,也就是前面提过的通用计算技术,这是从见山是山到见山不是山的一个飞跃。即使Lua几年之后就没有什么粉丝了,那又如何呢?这个过程中的所得已经远远大于付出了。

   顺便扯一下语言学习的问题。现代程序设计语言和开发框架令人眼花缭乱啊,有时候搞得人无所适从,我自己的candidate list里面已经排了好几个了。Apple发布Swift的第一天,我就把书发给Mimas team了,不过至今我自己还没功夫看呢。语言方面,有的同学重在精通,有的同学重在博学,这些都未尝不可,没有什么可以进行优劣比较的。我觉得这完全取决于你的目的吧,只要不是为了学语言而学语言。语言不仅仅是语法,它反映的是解决问题的思维方式。语言重要么?重要。它事关软件的表达方式,事关开发成本。但有时它也没那么重要。有一天我跟Kane宣布我要学习一下Perl了,尽管Perl是我很不喜欢的语言(我不喜欢它是因为它用不同前缀来区分数据类型,像我这么懒惰的人压根记不住,所以看到Perl的第一眼我就把它放弃掉了),原因是我发现《High Order Perl》是一本相当棒的书,我是关注它背后的思想,而不是语言本身。除了工作需要,我感觉我选择语言主要基于下面的考虑:我对某种编程范式或语言特性感兴趣,比如我学习Clojure是为了掌握FP;我对某项技术感兴趣,但相关软件是用我不熟悉的语言写的。在研究P2P的时候,觉得有个多协议的P2P客户端mldonkey可以移植到家用网关上,但它是用OCaml写的,所以还读了几天OCaml(别问我OCaml长啥样,忘光了)。Docker现在火爆了,所以我觉得是时候读一读它的源代码了,但我得先搞明白Go;还有一个就庸俗一点了,跟潮流,这个我觉得恐怕也是大多数人选择语言的标准之一。

   在我们做Chassis过程中,除了Lua相关的知识,如果你以前不熟悉C++,你是否关注过POCO是怎样实现的呢?从这种类库其实能学到很多东西,Haiyan不是带着Mimas team走过一个遍Design Pattern么,那么大家有没有从POCO里看到各种Pattern是怎样应用的呢?RAL的设计借鉴了ROA,你是否有兴趣关注一下ROA呢?我期待Chassis软件能脱离IPOS环境在Windows上用VC++调试的时候,一开始提过做一个基于ZeroMQ的消息组件用于在模拟环境下替换原来的消息通信,虽然我们最后没用,但有多少人留意并研究过ZeroMQ本身呢?我感觉除了Altlas相关的开发人员之外,恐怕大多数人连ZeroMQ是啥东东都没上网查过。

   源码阅读能力也应该是一个软件工程师着重培养的一个技能。在这个互联网的时代,只要你有心,你可以很轻易的学习到各种软件开发技术,你可以就你感兴趣的题材找到大量源代码来阅读,而且你还会发现有很多人在和你干同样的事情,因此你可以阅读到各种源码分析报告。我们的维护的代码动辄几十万几百万行,一个软件工程师如果没有快速理解代码的能力,没有掌握有效阅读代码代码的方法的话,他做起来会很吃力的。我到公司后,领导交待我的第一件事情就是确认SPO 1400code base到底支持不支持License,因为大家都说不支持。我对ETOS代码还没什么概念呢,大约花了一天时间翻了下文档然后又扫了一下源码,觉得这个可以有,是真的已经有了,同事还善意的提醒我不要乱说话啊。阅读代码实际上就是利用各种工具正向/反向,静态/动态对代码分析和猜测/验证的一个过程。当我拿到一个陌生的软件的时候,我基本上是首先试图理解它的结构而不是代码细节,如果是用OO的语言设计的话,我个人的习惯是用CASE工具先对代码逆向工程,当我看到静态关系图的时候,我基本上能对它的设计思路有一个整体的把握,在此基础上再做代码分析就会有的放矢。阅读代码的更重要的方面在于这是一个学习别人优秀设计的机会,黎万强在《参与感:小米口碑营销内部手册》提到:“做设计某个角度讲就是再设计,把各类设计元素重新组合。见过足够多的设计,对做设计的效率是有帮助的,同时身边的好设计等于建立了一个参考坐标,有时候也决定了设计师做设计的眼界,这些好设计就是他的天花板。”虽然他是在谈设计师招聘,但这同样适用于软件工程师。软件工程师要提升设计功力,学习同行的优秀设计非常重要。互联网的普及使我们学习好的设计的成本非常之低,这个世界不缺少美,只可能缺少发现美的眼睛。当你用心学习足够多的优秀设计的时候,这些设计元素会潜移默化根植到你的设计习惯中。不止一次有人问我如何学习和应用Design Pattern,我的回答就是:“无招胜有招”,因为我从来没去刻意去想过我要在软件设计中去应用什么样的Pattern,我的直觉能告诉我哪样的是比较好的设计,从某种程度上你也可以认为这种意识是长期训练导致的条件反射。各种层面的Pattern只是业界在归纳总结好的设计经验并给它们起了个名字,去记那些列表没有太多意义,不管它叫或是不叫pattern,好的设计就在那里。我曾经给带过的实习生们讲一个纯C写的OLSR Ad-hoc路由协议软件设计里体现哪些designpattern,并告诉他们这些在软件设计中无处不在。我觉得GoFDesign Pattern是适合每过一段时间有了新的设计感悟之后再回头细细品味的书。

做可以复用的软件

 

    不知道大家是否有这样的感觉,在一个行业做久了,总觉得年复一年日复一日都在做重复的事情,而且是换着花样去做,这种感觉很让人抓狂,我相信软件工程师长期在这种状态下会逐渐变得颓废。身边的大部分同事都经历过两个平台系列的产品:ETOSIPOS(干了大半年夭折掉的CBA就不算了,毕竟没有真正的产品出来)。想想看,这两个平台做的OAM的工作有任何本质上的不同么?但当我们从ETOS切换到IPOS的时候,什么感觉?我挥一挥衣袖,不带走一片云彩。我们几乎没有什么可以从ETOS带到IPOS的,一切又从头开始了。做软件到这份上,很悲哀的境地。

    软件复用(这里只限定于最低级别的代码复用)是一个经常被喊滥了的名词,每一个项目都在强调复用,但很少见到真正能复用的成果。做可复用的软件难么?难。做一个可复用的设计付出的努力很可能数倍于一个不可复用的设计,对工程师的要求很高,这个要求有两个方面:能力和意识。只有当一个工程师同时具备设计可复用软件的能力和意识的时候,他才有可能完成出一个可复用的设计。但这只是一个必要条件,而非充分条件。人是懒惰的,当AB两条路的成本相差比较大的时候,选择一个低成本的路径也不足为奇。另外,组织很有可能没有一个良好的机制设计来促使工程师在这种情况下去选择一个高成本的路径,因为组织往往对可复用设计的成本缺少合理的估计以及对可复用设计缺乏有效激励,从而使工程师觉得得不到预期的回报。

    DRYDon't Repeat Yourself)是在软件设计中很基本的一个原则,在日常的code review中,我可以见到大量的对该项原则的违反,这反映出我们大部分的工程师的可复用性设计的功底还有待提高。当我们从已有项目继承遗产的时候,最常用的就是Copy过来,然后再改,这不是复用,这就叫Copy/Paste,是Repeat Yourself。真正可复用的设计只需要根据目标环境定制部署参数或者扩展(不是修改)功能来满足新的需求,这实际上又涉及到另一个基本原则OCP Open/Close Principle)。

    我对自己有个要求,那就是我做过的每一个项目,或多或少都要产生可以复用的软件组件,这基本上始于我的软件职业生涯的早期。可以说最近两年(从E-Mode 3开始)我基本上就在做一件事情:DRY

    PDU CC真正开始带团队进行软件设计和开发大约是从SPOAttach Unit项目开始,这个项目在挪威site被关掉之前主要由原ETOSArchitects在主导,而且一开始的目标也比较ambitious,同时满足PDU MWOM两个产品线的需求,在很长一段时间里都是各方在argue、解决分歧的过程,最后MW退出了,就变成针对OM一家的解决方案了。也正是因为这段经历,re-orgPDU MW之后我又主导了MWAttach UnitE-Mode 2/3SPPT的集成)管理平面和控制平面的软件解决方案的设计。在OMAU这个项目中我对自己的评价是中规中矩,甚至有些缩手缩脚,我在早期犯过一个错误,挪威的Architects把网元间的通信协议定为BEEP,我对它的应用广泛程度提出过疑议,但没有坚持,这个决策到Einar来到系统组的时候得到了纠正,我们就采用了TIPC作为AU cluster的传输层协议。此后呢,我常以此为戒,在和海外site的合作中(无论是在SPx10E-ModeSP400CLI filter,还是去年的Spitfire PAd redisign对自己认为正确的主张还是比较坚持的。挪威的site的关闭提上日程之后,设计和实现的主导权就主要在北京了,然后就是说服意大利的team采用TIPC了,这也是一个花时间的事情。现在回头看当时的方案设计,基本上是一个常规设计,受限于已有架构,软件和平台和业务耦合还是比较紧的,如果现在让我重新设计的话,我会在网元通信的应用层会采用文本协议,我会把multi-shelfDB设计得更加generic一些。但整个设计中让我有成就感的实际上是用Python写的AU的模拟器,这个其实可以看作是一个能复用的设计,相关细节会在下面讲测试时谈到。

   PDU CC re-orgPDU MW,整个部门就莫名其妙的加入到CBA的大潮了,我们处在明知道最后没有希望但又不得不做的境地。那个平台太复杂了,还完全没有成熟到能随意剪裁到嵌入式系统的地步就匆匆推出了。我觉得我在这场CBA运动中最有价值的活动可能就是倡导并带领team实施了OpenSAFwrapper 组件(虽然没有得到官方认可)。我希望能赶在所有team大面积铺开应用OpenSAF的时候给大家提供一个简单的API,我能想象出大家怨声载道的场景,因为已经有team在挠头那个简单log服务了。不过最后CBA又莫名其妙的收场了,所以我们的wrapper。平心而论,CBA也不是一无是处,CBA的理论意义在于尝试以一些通用组件和模型驱动的方式来搭建交换机平台,这是非常积极的一步。常用的OAM服务都以通用组件的方式部署,这其实是我们设计开发的一个借鉴和参考。如果我们有机会做这件事情,就应该以这种方式来构建系统,而不是像ETOSIPOS这种内部组件纠缠不清的专有设计。

    紧接着IPOS取代了SEAETOS前身)作为下一代SP400系列产品的平台。我所在的Atlas team依旧工作在基于SEASPx10MW启动了一个当时声称比较重要的featureE-Mode 2,也就是在管理平面和控制平面把SPx10PT集成,它们逻辑上视为一个网元,但并不做IM的集成。两者通过PTCLI通信,SPx10更多的是作为PTCLI代理。CLI格式是比较松散的,它是人机界面,并不是一个合适的编程界面,但这是PT对外承诺的唯一接口,它的XML-RPC接口只用于PTRadio,而且PT Team也不打算为Radio子系统之外的功能提供XML-RPC。这是我们当时无法左右的,据说今天的PT team又开始抱怨这个决定了,因为他们不能随便改CLI格式了,因为那已经属于接口规范的一部分了。Feature的交付周期比较短,基本上是为了满足TTM,而且未来E-Mode 3的软件该怎么走也还在讨论中,这个阶段的主要任务就是把功能快速实现,我把可复用性设计限定在提供一个类Expect的组件,它可以通过Pseudo Terminal和基于终端的进程(如SSH Client)交互,并对输出流用POSIX的正则表达式进行字符串模式匹配和子串抽取。这个库可以简化管理任何基于CLIbox的通信开发工作。E-Mode的应用程序就是利用它管理和控制PT,并分析CLI输出。这和我十五年前在Nortel研发中心做的第一个项目很类似,那也大约是我在工作中(自己业余写的东东不算)第一个正式的可复用性设计。当时我做Nortel NMS的网管适配层软件,交待给我的任务是用C++操纵CLI抓取CDMA MTX的性能数据,至于要抓什么数据,没人说的清,用什么命令,不知道,1000多页的手册我都不知道从哪里开始看啊,在这种情况下我决定提供一个可以定制的数据采集软件,网元登录过程、性能数据获取命令以及对命令的分析规则全部是可以配置的。那时候我并不懂Pseduo Terminal,否则我会省很多事,只觉得我需要一个Telnet的模块,Telnet终端程序满大街都是,但是并没有满足我的要求的能通过程序控制,并提供类似于Expect那样模式匹配功能的Telnet库,但Perl有这样的包,所以我又兴致勃勃的仿照那个Perl接口,从Telnet协议开始实现了一个支持正则表达式匹配的客户端,当然只是一个协议子集的实现,支持一些基本的协商选项,乱七八糟的终端支持我也不需要。我把它定位于一个跨平台的可复用的Telnet客户端组件,支持Win32Unix,并对socket,regular expression的平台差异, 以及不同的字符串类库实现(Nortel在用RougeWaveSourcePro,那是一个很棒的C++商业类库,那时候还没有boost以及后来的POCOC++ STL也还在早期)进行了适配。当时我对自己的设计比较满意,两年后其他组的同事想用这个的时候看了我的代码后夸我做的相当professional

    E-Mode 2之后是E-Mode 3,这是SPPT的深度集成,管理平面、控制平面、数据面及IM都要集成,用户从各个层面都不应该感觉到这是两个网元,而且E-Mode 1/2/3也要求可以同时配在一个SPx10的不同端口上。此外基于IPOSSP400系列和MWTN也在考虑之中了。跨平台可复用自然而然成为了管理平面/控制平面软件架构设计的目标之一。SP400SPx10的编程模型是有很大差异的,虽然从架构层面来讲,它们都是event driven,但SPx10对事件处理有严格的时间budget(10ms)限制,一但某个事件处理阻塞,整个系统内的实时进程全部阻塞,所以同步编程模型在SPx10基本上是不允许的。而SP400是没有这个问题的,使用同步模型会使编码更加简单。至于TN平台,我们根本就是一无所知。为了最大限度降低软件移植的成本,我设计了一个统一的面向PT管理的API,该库对上层应用程序屏蔽了PT的控制细节,真正对PT的管理和控制交给后端的一个单独的Service进程,该Service管理和调度交换机连接的所有E-Mode 1/2/3PT。为了满足不同平台的需求,这个PT API库同时支持异步和同步编程模型。API库和PT Service之间的交互是通过私有的RPC来完成的,之所以开发了一个私有的RPC栈,主要是基于以下考虑,首先我们希望同时提供异步和同步调用,其次,支持请求和响应的一对多。这个麻烦是由SW Upgrade引入的,通过CLIPT进行软件升级涉及到一系列命令,软件升级命令发出之后,要不停的再通过CLI去轮询状态,然后分析CLI输出,得到PT内部状态机的状态和升级进度,从应用程序的角度来说,就是做一个异步调用,传入一个回调函数,回调函数会得到多次调用以更新升级进度,负责PT管理的Service察觉到升级完成后,通知客户端结束这个调用。第三呢,我们希望提供单向的通知来支持PT告警。这些无法通过已有的RPC栈(例如Thrift)干净利落的完成。一开始的时候,因为只涉及到SP主控卡上进程间通信,所以用了Unix Domain Socket和基于key-value的文本协议(考虑过JSONXML,但因为我们自己不需要复杂的嵌套数据类型,简单的key-value足够,即时要支持嵌套,通过定义key的层次也能达到同样效果,正如Redis并没有提供复杂数据结构定义一样,而且key-value的解析非常简单,避免了引入三方软件)。因为E-Mode 3 需要原来运行在PT上的Radio子系统跑在SP上了,这样Radio也分布到两个box上了,等瑞典的同学们亲自做了之后就立马感觉出用CLI的各种不爽来了,他们开始怀念RPC了,要做一个基于JSONRPC,这个时候我说我们已经做了一个类似的东东用于内部通信了,只要refactoring一下就可以了。就这样,我们把那个RPC栈抽象出了一个Transport Layer,提供TCPSSL(用于Radio子系统和PT的通信)的插件,数据封装就统一用JSON,没有抽象出Encoding Layer是因为还没有看到这样的必要。最后,对PT的控制协议就很奇葩的分成了两个通道,OAM软件用CLI通道,Radio软件用RPC通道。再后来的故事就是新一轮的re-org开始了,我们和MW分开了,MW转而优先考虑在TN上做E-Mode,直至最后的市场和战略的变化导致这个feature可能现在已经不需要了。总结一下,这个方案的可复用性设计分为两个层面吧,基础设施部分主要包含Pseudo Terminal Regular ExpressionExpect-like类库和RPC Framework等,这些都是单独可以复用的组件,应用框架部分是一个面向基于CLIbox的通信控制Framework,该Framework主要负责SessionPending的异步Handler管理,这个并不是专门针对PT的,PT的管理逻辑体现在具体的Handler插件类里。

  之后呢,我转入了Mimasteam,接手SP400CLI Filter。这是一个Inner SourcingFeature,旨在提供对目标产品剪裁IPOS CLI的能力。在我进入之前,team已经确定了一个方向,我认为那个方案的实现无法复用到其他的基于IPOS的产品,而且需要开发人员做很多机械的工作,所以给否掉了。解决方案无非就是运行时刻剪裁还是静态剪裁,然后又有各种变种,我的最终方案是编译时刻静态剪裁,根据Product Pecific的规则自动修改编译中间文件,这在前面都提到了,这里就不多说了。当然这是在IPOS当前EXEC CLI实现框架内的一个解决方案,应该说是一种Hacking,比较无奈的一个选择。

  再之后呢,就是SpitfirePAd redesign。我对SP400Chassis设计非常不满意。在PDU CC的时候,我就觉得Chassis的设计不能让维护人员感觉那么痛苦啊,1000家做交换机的厂家,会有1000Chassis的解决方案。那时候我就想如果有朝一日让我来设计chassis,我一定会把它做成一个通用的软件,可以轻松适配到任意交换机平台,就像卖协议栈的公司一样。一些基本的设计原则就是那时候形成的,比如模型驱动、Plug-and-Play的架构、每类板卡做成一个插件、Generic Key-Value StoreRESTful API等。到了IPOS平台,Chassis居然变得更加痛苦。正好呢,领导也很支持我改天换地的想法,然后我们就面临究竟是对PAd做重构呢还是重新设计的选择,我觉得重构的代价一点也不会比重新设计低,因为这是理念完全不同的两个设计,所以我倾向于重新设计,于是我们就开做了,斗争斗争斗争,大半年之后,就叫停了,原来的PAd穿了个马甲变成了Common PAd,号称会变得更Common,但其实换汤不换药,我们基本上还是在走老路。这里面有商业的逻辑,政治的逻辑和技术的逻辑,总之,技术的逻辑落败了,也算比较正常的现象吧。而且我们也有天然的劣势,手里只有概念没有能跑的东西啊,如果SP400Chassis设计不墨守成规,从那时我们就开始做些扎实的工作,我们的腰板或许还会硬些,当然最大的可能性依然是于事无补。

从第一天就关注测试

 

    可测试性是重要的软件质量属性。如何在增量开发过程中保持软件的可测试性是一个在设计之初就应该统筹考虑的问题,反过来,对软件可测试性的注重又有助于提升软件质量,一个可观可控易测的软件一定意味着良好的结构划分,可测试性差的软件往往是强耦合的。无论是我们做ETOS平台还是IPOS平台的项目,都有UT和覆盖率的指标,我们做得很抓狂,至于怎么让那一大坨剪不断理还乱的代码能达标的那就是工程师们公开的秘密了 :-)。上周同学们还在抱怨没法对CMSP那些代码做UT和覆盖率,编译的时候它还必须把PAd扯进来一起编,其实这也是对coderefactoring的时机啊,我们的CMSP是从SP400 copy来的,SP400又是从SSR copy来的,这种copy把代码的缺点原封不动的继承了过来,我不喜欢这种copy,我们需要把CMSP的代码写的更加Clean,更加Expressive,更加Testable对于交换机这种软硬件同时开发的大型系统来讲,在软件开发的前期甚至是中期,没有目标平台很正常的事情,在这种情况下,一个低成本的模拟器往往会对保证项目交付和降低项目风险起着重要作用。不知道大家注意到没有,ETOS在可测试性方面有一点考虑得相当周全,当IM确定之后,LCTbox的开发是解耦的。LCT的同学们肯定都清楚snap_sever.py,那是ETOS box的模拟器,原理就是从boxdump出来一个状态的快照,然后再加载到这个server,对外提供XRPC接口供LCT访问,应该说这个模拟器的设计理念是不错的,总代码量也就600行。相比snapshot server,另一个工具知道的人就很少了,在SPO14xx开发过程中,经常有同学说某某功能还没法测试,因为LCT还不ready,这个想法是错误的,LCTready很正常,但那绝不是我们不从北向进行测试的理由。事实上ETOS提供了另外一个工具xrpclib,这是一个Python的库,可以用来构造ETOS box的测试驱动代码。

    在公司的五年里,我几乎在每个项目的设计中都把写一个多快好省的模拟器放在比较高的优先级。    

    前面提到了SPOAttach Unit Simulator,用于模拟APS设备,接收SPO14xx的消息,然后给出响应消息。从常规意义上来讲,它可以设计成一个专用的APS消息模拟器,因为它处理的是ETOS specific的消息,当模拟器收到一个消息的时候,它需要感知消息的语义,然后触发后续的特定行为,这些逻辑一般来讲都是product specific的,但当时我的想法是能不能以比较低的代价做一个更加通用一点的消息模拟器,以后再碰到类似情况的时候完全可以复用,而不是重新从头写一个。具体想法源于我以前写Wireshark插件时的思考。我们的消息解析类似于Wireshark的包解析过程。整个Wireshark的包解析可以认为就是一个巨大的if-else嵌套,软件首先根据包的某个字段的内容找到对应协议的dissectordissector里会再手写一堆if-else去构建包的解析树,这个过程完全是protocol specific的。看到这个设计之后,我的直觉是为什么不能对协议包格式做形式描述,由软件自动构建解析树呢?至少对相当大的一部分协议可以这么搞,当然一些非结构化的文本协议可能这么玩有困难。在ETOS平台下,消息描述格式自然而然就是XML,模拟器就根据这个描述构建消息的语法树,消息的解析过程转换为在前序遍历语法树的过程中构造对应消息的实例树,这样我们就可以对消息做自动解析,然后我又用Python(我很想用Ruby,因为用Ruby设计DSL很方便,但考虑到teamcompetency,还是决定用Python)设计了DSL去表达消息所触发的动作,可以是发送响应消息,可以是延时,可以是调用外部脚本,也可以是各种动作的组合,这样测试和开发工程师就可以很方便的构造各种测试脚本。这个模拟器没有最终完成,是因为意大利的同学们搞不清楚TIPC怎么玩,Cliff同学建议我们用C语言写个的简单的APS模拟程序,这样意大利同学可以依葫芦画瓢,否则他们就卡在那里了。为了能让他们go ahead,我们就把那个模拟器停了,但主要的代码都写完了,虽然最终没有用到,我对那个设计还是比较满意的,因为我初步验证了另一种实现Wireshark这样的工具的思路。

   EMode 2/3的开发中,由于在相当长的一段时间内,北京这边根本就没有PT用,所以构建一个PT模拟器是情理之中的事情,SPx10PT之间的OAM通道是CLI,所以我用PythonplyPython Lex-Yacc)快速实现了一个简单的shell程序,模拟了软件用到的PT CLI命令,并加上了一些特定的状态逻辑(如根据配置或随机模拟失败;产生软件升级的进度逻辑)。它可以部署到Linux主机,作为特定账号的登录shell,这样就对外就呈现出了一台PT,这是给测试工程师的用法。此外,由于SP软件是通过Pseduo Terminal启动SSHPT进行CLI通信的,它根本不关心输入是产生自远程还是本地,所以我在代码里加入了一个使用模拟器的模式,即直接通过Pseduo Terminal启动那个模拟器shell,这是开发工程师的用法。可是让我郁闷的是除了我和Wang Kai,这个模拟器基本上没人用过。测试工程师不熟悉Linux SSH配置(因为要创建账号,配置登录shell),觉得不好用,我觉得很不应该啊,我们没对测试工程师的代码技能做太多要求,但应用环境的技能应该有啊。后来有了PT了,大家可能更喜欢连真实设备,可我自己的感觉是很多时候模拟器还是很方便的,比如在后期,Mimas team帮过一段时间的忙,做PT License的集成,他们用真实设备调试了几天没有搞定,有一天晚上我跟Chen Lixiang说干嘛不用模拟器啊,然后我就把PT LicenseCLI模拟给加进去了,第二天再问,他比较happy的说搞定了,这个东东很好使。

   在去年开始做Spitfire Chassis的时候,我对软件的可测试性是很重视的。大家可以回顾一下我提的一些要求:除了IPOS glue layer,大部分Chassis组件必须是跨平台的(LinuxWindows),而且是可以脱离IPOS环境单独运行和测试的;在新的PAd能替换老的PAd之前,新PAd是作为旁路和老PAd并存的,所有给老PAd的消息和事件都会复制给它去驱动状态变迁,当然最后触发的响应是在北向接口扔掉的,新老PAd的响应应该做自动比对来判断是否一致,即两个PAd是否功能等价;Chassis模拟器必须是一个全功能的模拟器,能够在运行时刻任意改变硬件状态,以触发各种事件;Chassis模拟器应该能测试绝大部分Product Specific的代码;Chassis模拟器的基础组件应该是可以复用到任何交换机的。在大型软件系统中,组件不能单独测试是件很头疼的事情,它其实反映了软件的耦合程度,再者我把Chassis定位于一个通用软件框架,所以和IPOS做最低限度的耦合是一个基本的要求,我们利用了POCO来屏蔽OS的差异,我们对IPOS IPC做了封装,并提供了Windows环境下的基于UDP的简单message queue服务实现,这样大部分Chassis软件就可以跑在PC上,而不是时时刻刻都需要在目标板上运行,从理论上我们还可以用Visual Studio进行调试。我们的Chassis模拟器设计基于RAL,并提供了CLI程序在运行时刻获取或设置虚拟硬件状态,基本上能满足绝大多数的硬件模拟需求。虽然我们现在走了CPAd的技术路线,但是Chassis模拟器的设计是完全可以复用的。这个模拟器的设计和我03年在Nortel做的一个SDH网元模拟器在设计思路上是类似的,也可以说RAL的设计更早的是源于我那个时候的想法。当时我在做一个光网络网管的适配层项目,Nortel OEM了三方的2.5G SDH设备来竞争低端市场(那个项目是失败的,据说Nortel最后把客户现场的所有box都替换成自己的高端设备了,亏大了),早期自然是没有设备用了,所以依我的习惯我就想设计一个模拟器,设备管理接口是TL-1,当然我可以做一个专用的模拟器,最简单的做法就是根据输入读配置文件去产生预定义好的输出,但那不是我的风格啊,那样很不灵活,也无法复用。我从一开始就奔着一个通用的网元模拟器去设计的,花了大约六周时间实现了一个模拟器,那个设计我现在依然记得比较清楚,我用Flex / Bison实现了TL-1 parser,用VC++ / MFC / Xtreme Toolkit做了个花里胡哨的GUI,模拟器的核心是资源树的管理,它是模型驱动的,在这里我是用XML来描述一个通用设备模型,实际上就是走的external DSL的路线(因为当时还不懂什么internal DSL,而且04年我才开始学习第一门动态语言Python),资源树的层级和节点属性集合可以通过GUI可以任意定义,节点的标识就是从根开始的带层级的名字,实际上就相当于URI。我把网元树的管理和对应的GUI看作是对任意网元都适用的可复用组件,网元的北向管理协议及相关GUIProduct Specific的,所以计划是以插件形式提供的(当然因为时间关系,我并没有做插件的Framework,而是直接引用了)。这里面有个小插曲,这个东东本来我做了是给自己用的,Xtreme Toolkit用的是以前他们在CodeGuru上的开源版本,可后来又给闭源了,北美的team见到我的模拟器觉得不错,想拿过去用,我跟领导说这里面有IPR问题啊,领导只好被迫花钱买了Xtreme Toolkit,当然也没多少钱哈。

    顺带提一下我在刚进入交换机领域的时候构思的一个虚拟测试网络,我其实很后悔没有自己把它做出来,因为那就是类似现在火爆的虚拟网络的概念啊。大约在05年的时候,Nortel中国研发中心和富士康合作做一个IPDSLAM产品原型,那算我做的第一个交换产品,我带的team的任务是移植RSTPPIMv2OSvxWorks,大家在热火朝天干的时候我就在想,如果真正启动了产品开发这个东东该怎么测试呢?Lab里也没那么多设备啊,我如何搭建一个大型的网络扑呢?所以我就想啊,能不能做一个虚拟的网络,每个PC机上可以跑多个实例(包括IP栈),每个实例可以定义虚拟Ports,一个虚拟Port可以attach到一个虚拟链路,这个链路可以是广播链路也可以是点到点链路,这可以是由软件任意定义,虚拟链路完全可以通过三层tunnelPC连结,虚拟链路也可以连结物理port用来和真实设备或测试仪通信,这样就可以构建任意拓扑。然后我又构思了一整套前端GUI来定义和管理虚拟网络,例如拓扑图管理,链路事件模拟,链路质量模拟等等。不过由于当时的普通PC配置也就1G,同时跑多个虚拟机几乎不太可能,所以我并没有把这里的实例例定义为虚拟机实例,我没打算把整个交换机软件做到一个实例中,我当时只是想着我那一亩三分地里的协议的测试,所以那个实例实际上是一组运行着某些协议的进程。由于没有操作系统层面的二层Bridge参与,所以还需要做一些二层的模拟。我的构想初步完成之后还是挺兴奋的,我知道给我机会我肯定能做出来,如果那个项目没被cancel的话我就大刀阔斧的做了,不幸的是Nortel在我们完成原型后并没有批准那个产品线,我们就半途而废了,然后呢,我就没有机会把这个想法付诸实施,虽然后来我多次想利用业余时间做些工作,但最终还是没有动手。想在回想起来,如果我当时能顺着这个思路做下去的话,应该能搞出很多有意思的东西。这件事情的教训就是想法很多人都会有,但动手做出来才是最重要的。

 

磨刀不误砍柴工

 

    在项目执行过程中,我们总会遇见各种各样细碎繁琐的事情,比如log分析、状态采集、dump packet,在这个时候不要忘记我们是懒惰的工程师,请毫不犹豫的花点时间写个tool,造福自己也造福全人类。

  ETOS有些tool的设计还是不错的,比如它在内核代码任务切换的时候加了一个hook,用以记录了任务切换的时刻,进而用Time Doctor以图形方式呈现出各进程的CPU占用情况。

  我在PDU CC的时候用Python写过不少tool,除了前面提到过的各种log、源代码分析工具,我还写过SPO14xx板卡间ICCC通信协议的Wireshark插件,用来观察板间通信,我转到MW之后的某天,OM的同学们让我帮忙debug一个ICCC的问题,我又把这个工具兜售给了他们。在做SPO14xx AU项目时,我给同学们讲了Wireshark插件怎么写,然后team实现了一个AU通信协议的插件。

  在做基于CBASP160 Bring Up时,因为搞不清楚CBA到底用了哪些三方库,我写了一个Python程序扫描所有二进制代码的依赖关系。

  在做E-Mode时,我帮Sun Hao做过从现场抓来的巨大log文件的分析tool,把结果存进SQLite数据库,然后定位频繁告警的网元。

  在做SP400 CLIfilter时,为了能清晰的看到ParseChain剪裁的效果,我利用GraphViz做了CLI Parse Chain的可视化。

  这些tool的实现代价其实都很低,最多的也就一天的时间,但往往能取得事半功倍的效果。

 

拿来主义

 

    我一向认为,我们所写的99%的代码,在这个世界上都有无数人做过,至少是做过类似的东东。很多情况下,我们没有必要reinvent wheels,干吗不借鉴或者干脆直接拿来呢?当然这是建立在对软件QualityIPR的正确评估基础之上的。我在这里没有鼓励大家只是一味拿来,不动脑筋啊,拿来是有吸收的拿来,有创造的拿来。在做一件事情之前,我常常要查一下别人都做过一些什么,有什么可以拿来的。这个时候,掌握多语言和平台的技能就有些优势了,因为这能给你提供更多一些选择可以借鉴。大家做工程产品有没有注意到,相当一部分工作其实就是一个组装的工作,把一堆软件攒起来,改造一下,然后变成了自己的东西。想起了N年前的一件事情,一同事去中关村,攒机的小伙问:你做什么工作啊,答曰:软件,小伙立马热烈响应:我们是同行啊,都是搞IT的。现在看来,人家想必说的是极好的。

 

  最后,学习总是需要付出时间代价的,大家其实都差不多,都会有各种各样的事情,如果你觉得没有时间学习,那很可能你认为这是优先级比较低的事情。这种事情就在于坚持,哪怕每天啃一点,也是集腋成裘。

  

 

 

 

 

  • 2
    点赞
  • 0
    评论
  • 0
    收藏
  • 扫一扫,分享海报

参与评论 您还未登录,请先 登录 后发表或查看评论
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值