第1章 软件架构
1.1 架构的意义
什么是架构?对于架构,每天都有人在耳边提起。但架构到底是什么,却很少有人说清楚。
网络上的解释包括:软件架构是一个系统的草图,软件架构是构建计算机软件实践的基础,以及软件架构是一系列相关的抽象模式,用于指导大型软件系统各个方面的设计等。这些说法都对,但是阐述得还是有些模糊,懂的人本来就懂,不用看,而不懂的人看了还是一头雾水。
“架构”这个词太抽象,难以准确定义,而现在的大部分图书和文章讨论的“架构”都是服务器端的部署图,所以大部分人一提到架构就觉得是几台服务器放这里或那里,用不同的软件连接合作,用各式的框架开发扩展等。
希望这一章可引导读者认识到,架构无处不在,它实质上是解决生活和工作中问题的一种方案。除了自己着手寻找解决问题的方案外,还可通过其他方法,比如直接购买现成的,或者以外包及部分外包的形式,或者以合作模式等来解决问题,这些都是切实可行的方案,都是值得考虑的方案。我们所求的不是最贵的,也不是最高级的,而是最好用的。
在软件系统中,架构的重要性不言而喻,项目从研发到上线运营,我们要在不同的方案中选择合适的架构,例如,前端渲染引擎是自己研发,还是使用商业引擎?商业引擎是使用Unity3D,还是Unreal或其他?具体到怎么用,则要考虑是使用UGUI,还是使用NGUI?UI里的事件系统如何进行统一处理?AI行为算法是选择行为树,还是状态机,抑或是选择事件型决策树?数据如何获取和存储?场景如何拆分?是否需要将资源分离出去?是使用长连接还是短连接?是选择TCP还是UDP?服务器端是用C++,还是用Java或Python?是全部使用关系型数据库,还是加入Cache机制?网络协议是用Protocol Buff,还是用JSON或XML,抑或是使用完全自定义格式等。
这些项目中的每个子系统都要有自己的方向,把子系统的决策方向合起来再加入它们之间的关联调用就构成了一个完整的架构,即每个系统、模块、组件都是软件系统架构中的一部分。
优秀的架构师不仅需要对每个子系统的决策方向进行深思熟虑,还要结合其他系统以及整体系统需求的方向进行设计。
在架构设计中,为了能够更好地整理、思考、描述、表达,我们最好使用架构图。架构师把架构中抽象的系统、模块、组件画在图上,用圆圈、方块和文字表示,让自己和大家能够更加系统地认识架构的意图、结构,以及子系统的简要设计。
一个完整的架构图通常会伴随些许子系统的细节,或者说子模块的架构图。UML对象关系图就是一种架构图,它描述了数据类之间的关系,可把系统中的对象用文字和连接图的方式描述清楚。部署图也是其中一种架构图,它把需要多少种服务器、它们分别起到什么作用,以及它们相互之间的关系描述清楚了,而时序图则把系统程序调用的次序与流程描述清楚了。这些不同角度的架构图合起来就构成了一个完整的项目架构图。如果把子系统架构细节略去,在不关心细节的情况下,描述各系统的合作方式,展现给人们的是整体的解决方案,从宏观的角度看整个项目的布局,会让人一目了然。
为了让更多人理解软件架构,我想把软件架构形容得更贴近实际生活一些。
可以把软件架构理解为软件程序的架子,与现实中的书架有异曲同工之妙,这个架子上有很多大大小小的格子,每个格子里都可以放置固定种类的程序。架子有大有小,大的需要花费点时间去定制,小的则轻便快捷。
架子的大小是由设计师决定的,设计师根据客户的需求设计,假如放置的空间大,且需要承载的东西多,那么就往容量大的方向设计,让它能容纳更多的东西,能放置各种不同类型的程序,反之则做得简约些,这样更容易理解,又轻又快。
架子完成后要拿出去用,如果一出现异常情况就倒了或散架了,就不算是一个好的架子,架子的好坏可从以下几个方面进行评估。
1.承载力
书架上能放多少东西,能放多重的东西,是使用者(这里使用者可以为客户、玩家或程序员)比较关注的问题。
从软件架构的程序意义来说,一个架构能承载多少个逻辑系统,当代码行数扩展到100万行时是否依然能够有序且规范地运行,以及程序员彼此工作的模块耦合度是否依然能保持原来的设计要求,能够承载多少个程序员共同开发,共同开发的效率又如何,这是对软件架构承载力的评定指标。
从架构的目标上来看,对于服务器来说,当前架构能承受多少人同时访问,能承载的日均访问量是多少,这就是它承载力的体现。而对于客户端来说,能显示多少UI元素,可渲染多少模型(包括同屏渲染和非同屏渲染),则是它的承载力的体现。
若访问量承载力太低,访问量一上来就都卡在加载上,大家就不会再有这个耐心来看你的产品了,运营和宣传部门在导入流量时,效果就会大打折扣。同样,如果客户端渲染承载不了多少元素,帧速率过低,画面卡顿现象严重,产品就不会得到认同。
承载力是重要因素,但也并非是唯一因素,综合因素才是评定好坏的关键,一个点的好坏并不能决定全盘的好坏,木桶效应里最短的那块木板才是产品好坏的关键。
2.可扩展性
如果书架上只能放书,这个书架的用途就太单一了,花瓶不能放、箱子不能放、鞋子不能放、袋子不能放、衣服不能放,客户八成不买单。
架子能适应不同类型的需求,可添加不同类型的系统、不同功能的子系统,是非常必要的。软件架构也是同样的,但要具备更多功能就必须有更高的可扩展性。
可扩展性的关键在于,是否能在添加新的子系统后不影响或者尽可能少影响其他子系统的运作。假设添加了子系统后,所有系统都需要根据新的子系统重写或者重构,那就是灾难,前面花费的时间、人力、物力和财力全都白费了,这是我们不想看到的,因此可扩展性也是衡量架构的非常重要的指标。
3.易用性
易用性是架构师比较容易忽视的一个点。如果架构师设计了完整的架构,但具体执行时被程序员认为不好用,这时架构师还是执着地推动它的使用,那么团队间就会加深矛盾,这样开发效率就会下降。
这就好比在书架上取东西,如果需要先输入密码,再打开门,剥去袋子,拿出来,把袋子放进箱子,关上门,当把东西放回书架上时,再来一遍以上所有步骤,这实在是太烦琐,即使功能再多,承载力再好,使用者也会感到备受煎熬,这样,开发效率下降是自然的事。甚至有些是机械重复的工作,精力和注意力都耗在了没有意义的地方,这种情况下,架构师应该适时地改进架构和流程。
易用性决定了架构的整体开发效率,程序员容易上手,子系统容易对接,开发效率自然就高,各模块、各部件的编写只需要花一点点精力来关注架构的融合即可,其他精力和注意力都可以集中在子系统的设计和编码上,这才能让各系统各尽其职,将效率发挥到极致。
4.可伸缩性
还是用书架比喻,假设现在没有这么多书和东西要放,房子也不够大,那么书架如果可以折叠缩小到我需要的大小则会更受用户欢迎,这就是可伸缩性的体现。
若软件架构能像我们制造的书架一样可随时放大或随时折叠缩小,那就太好了。当需要的承载量没有这么大时,可以不使用不需要的功能,化繁为简,只启用需要的部分功能,这样就可以随时简化开发流程。
例如,从服务器端的角度,当需要急速导入大量用户,做到能承载几百万人同时在线时,服务器可随时扩展到几百上千台服务器来提高承载量,而在访问量骤减,或者平时访问量比较少,甚至低到只有几十个人访问时,服务器可缩减到几台机子运作,这样就大大缩减了服务器费用的开销,可以根据需要随时变更架构的承载力来节省成本。
从客户端的角度,伸缩力体现在是否既能适应大型项目上,如上百人协同开发一个复杂系统,也能适应小项目上,如1~3人小团队的快速开发环境,即小成本小作品的快速迭代。
在实际项目中,有时可伸缩性看起来并不是关键的因素,很多人误认为伸缩性是程序员的负担,甚至有的项目在某些时期根本不需要可伸缩性,只需要适应当前特定时间的需求即可。这里不得不再次强调可伸缩性在架构中的作用,它是深入理解、设计架构的关键因素,是做出优秀的完整架构的重要因素。
5.容错性以及错误的感知力
书架也会有磕磕碰碰的时候,同样也会出现因某处做工不精导致使用时歪歪斜斜的情况,如果我们保证不了完全没有问题,至少需要保证它不会因为一点小小的毛病而彻底散架。
软件架构也是同样的,软件中的异常、Bug常有,我们无法预估设备何时损坏。容错性起到了防止产品在使用中出现错误而彻底不能使用的作用,它需要有备份方案自动启用功能,同时也能够让开发人员及时得知问题已经发生,以及问题的所在位置,最好能通过Email或者短信、电话等方式自动通知维护者,并记录错误信息。
从服务器端角度,容错性包括数据库容错性、应用服务器容错性、缓存服务器容错性,以及中心服务器容错性,每个环节出现问题都会通知相关中心服务器改变策略,或者监控服务器检测得知该服务器出现故障,自动更换成备用服务器或者更换链路。
从客户端角度,容错性包括当程序发生错误时,是否同样能够继续保持运行而不崩溃;当这个页面程序出错时,是否依然能够运行其他程序而不闪退或崩溃。同时所有出现的程序错误,都能及时地记录下来并发送到后台,存储为错误日志,便于开发人员及时得到详细的错误信息,能够根据错误信息快速找出问题所在。
在架构中,以上这五项能力缺一不可,某项能力特别突出并不能决定整个架构的好坏,要考虑综合因素。倘若哪一项比较弱,随着时间的推移,问题会不断地向该方向聚集,直到最终出现大的问题,甚至崩溃。我们需要一个牢固的、多样化的、好用的、可伸缩的、有韧性的书架,这也是我们在设计架构时所追求的目标。
万物相通,木桶原理也适用于架构设计,木桶上仅有一块或几块板比较长没有用,其他短板照样撑不住多少水。这契合架构理论,好的架构本来就是由其所有子系统的架构来支撑的,整体架构虽然比其他子系统的架构更具宏观性,但起不到决定性作用。综合因素决定成败,架构也同样如此。如何让所有因素都朝着好的方向发展,是架构师最终要思考和解决的问题。