架构基础
架构到底是什么
要想解释清楚什么是 “架构“ ,关键在于梳理几个有关系而又相似的概念,包括系统、子系统、模块、组件、框架和架构。
系统与子系统
维基百科:系统泛指由一群高关联的个体组成,根据某种规则运作,能完成个别元件不能单独完成的工作的群体。宫的意思是“总体”“整体”或“联盟”。
提炼维基百科定义的关键内容。
-
关联:系统是由一群有关联的个体组成的,没有关联的个体堆在一起不能成为一个系统。例如,把一个发动机和一台PC 放在一起不能称之为一个系统,把发动机、底盘、轮胎、车架组合起来才能成为一台汽车。
-
规则: 系统内的个体需要按照指定的规则运作,而不是单个个体各自为政。规则规定了系统内个体分工和协作的方式。例如,汽车发动机负责产生动力,然后通过变速器和传动轴,将动力输出到车轮上,从而驱动汽车前进。
-
能力: 系统能力与个体能力有本质的差别, 系统能力不是个体能力之和, 而是产生了新的能力。例如,汽车能够载重前进,而发动机、变速器、传动轴、车轮本身都不具备这样的能力。
子系统的定义其实和系统的定义是一样的,只是观察的角度有差异, 一个系统可能是另外一个更大系统的子系统。
模块与组件
首先看看维基百科中两者的定义:
【模块】
软件模块( Module )是一套一致且相互有高紧密关联的软件组织, 它包含程序和数据结构两部分。现代软件开发往往利用模块作为合成的单位。
模块的接口表达了由该模块提供的功能和调用宫时所需的元素。
模块是可能分开被编写的单位,这使得它们可再用,并允许开发人员同时协作、编写及研究不同的模块。
【组件】
软件组件定义为自包含的、可编程的、可重用的、与语言无关的软件单元, 软件组件可以很容易地被用于组装应用程序。
模块和组件都是系统的组成部分,只是从不同的角度拆分系统而己。从逻辑的角度来拆分后得到的单元就是“模块”,从物理的角度来拆分系统得到的单元就是“组件”;划分模块的主要目的是职责分离,划分组件的主要目的是单元复用。
以一个最简单的网站系统为例,假设我们要做一个学生信息管理系统,这个系统从逻辑的角度来拆分,可以分为“登录注册模块”“个人信息模块”“个人成绩模块”:从物理的角度来拆分,可以拆分为Nginx 、Web 服务器、MySQL 。
框架与架构
参考维基百科,框架的定义如下:
软件框架( Software Framework )通常指的是为了实现某个业界标准或完成特定基本任务的软件组件规范,也指为了实现某个软件组件规范时, 提供规范所要求之基础功能的软件产晶。
提炼维基百科定义的关键部分:
( I )框架是组件规范:例如, MVC 就是一种最常见的开发规范,类似的还有MVP 、MVVM、J2EE 等框架。
( 2 )框架提供基础功能的产品:例如, SpringMVC 是MVC 的开发框架,除了满足MVC的规范, Spring 提供了很多基础功能来帮助我们实现功能,包括注解(@Controller 等)、Spring Security 、Spring JPA 等很多基础功能。
参考维基百科,架构的定义如下(请搜索英文关键字Software Architecture,中文的词条解释很粗浅) 。
Software architecture refers to the fundamental structures of a software system, the discipline ofcreating such structures, and the documentation of these structures .
简单翻译一下:软件架构是指软件系统的“基础结构”,创造这些基础结构的准则,以及对这些结构的描述。
基础结构可以从多个角度来分析。从不同的角度或维度,可以将系统划分为不同的结构,继续以学生管理系统为例。
从业务逻辑的角度分解,“ 学生管理系统”的架构如下图所示。
从物理部署的角度分解,“ 学生管理系统”的架构如下图所示。
从开发规范的角度分解,“ 学生管理系统”可以采用标准的MVC 框架来开发,因此架构又变成了MVC 架构, 如下图所示。
重新定义架构
我们参考维基百科的定义,将架构重新定义为:软件架构指软件系统的顶层结构!
这个定义很简单, 但包含的信息很丰富, 基本上把系统、子系统、模块、组件、架构等概念都串起来了,详细阐述如下。
首先,“系统由一群关联个体组成”,这些“个体”可以是“子系统”“模块”“组件”等,架构需要明确系统包含哪些“个体”。
其次,系统中的个体需要“根据某种规则”运作,架构需要明确个体运作和协作的规则。
第三, 维基百科的架构定义中用到了“基础结构”这个说法,我们改为“顶层结构”,可以更好地区分系统和子系统,避免将系统架构和子系统架构混淆导致架构层次混乱。
架构设计的目的
架构设计的误区
-
因为架构很重要, 所以要做架构设计
-
不是每个系统都要做架构设计吗
-
公司流程要求系统开发过程中必须有架构设计
-
为了高性能、高可用、可扩展, 所以要做架构设计
以史为鉴
探索一个事物的目的,最好的方式就是去追寻这个事物出现的历史背景和推动因素,我们简单梳理一下软件开发的进化历史,探索一下软件架构出现的历史背景。
- 机器语言( 1940 年之前)
在8086 机器上完成“ s=768+ 12288-1280 ”的数学运算,机器码如下:
101100000000000000000011
000001010000000000110000
001011010000000000000101
机器语言的主要问题是三难:太难写、太难读、太难改!
- 汇编语言( 20 世纪40 年代)
为了解决机器语言编写、阅读、修改复杂的问题,汇编语言应运而生。但是汇编语言还是面向机器的,写汇编语言需要我们精确了解计算机底层的知识。例如, CPU 指令、寄存器、段地址等底层的细节。这对于程序员来说同样很复杂,因为程序员需要将现实世界中的问题和需求按照机器的逻辑进行翻译。
- 高级语言( 20 世纪50 年代)
为了解决汇编语言的问题, 计算机前辈们从2 0 世纪50 年代开始又设计了多个高级语言。
- 第一次软件危机与结构化程序设计( 20 世纪60 年代~ 20 世纪70 年代)
高级语言的出现,解放了程序员,但好景不长,随着软件的规模和复杂度的大大增加, 20世纪60 年代中期开始爆发了第一次软件危机,典型的表现有软件质量低下、项目无法如期完成、项目严重超支等,因为软件而导致的重大事故时有发生。
差不多同一时间,“结构化程序设计”作为另外一种解决软件危机的方案被提出来了。
- 第二次软件危机与面向对象( 20 世纪80 年代)
结构化编程的风靡在一定程度上缓解了软件危机,然而随着硬件的快速发展,业务需求越来越复杂,以及编程应用领域越来越广泛,第二次软件危机很快就到来了。
第二次软件危机的根本原因还是在于软件生产力远远跟不上硬件和业务的发展。第一次软件危机的根源在于软件的“逻辑”变得非常复杂,而第二次软件危机主要体现在软件的“扩展”变得非常复杂。结构化程序设计虽然能够解决(也许用“缓解”更合适)软件逻辑的复杂性,但是对于业务变化带来的软件扩展却无能为力,软件领域迫切希望找到新的银弹来解决软件危机,在这种背景下,面向对象的思想开始流行起来。
- 软件架构
随着软件系统规模的增加,计算相关的算法和数据结构不再构成主要的设计问题;当系统由许多部分组成时, 整个系统的组织,也就是所说的“软件架构”,导致了一系列新的设计问题,例如:
-
系统规模庞大,内部耦合严重,开发效率低。
-
系统祸合严重,牵一发动全身,后续修改和扩展困难。
-
系统逻辑复杂,容易出问题,出问题后很难排查和修复。
可以说随着软件系统规模的增加,软件架构才逐渐流行起来。
架构设计的真正目的
从软件开发历史的介绍中可以看到,整个软件技术发展的历史,其实就是一部与“复杂度”斗争的历史,架构的出现也不例外。简而言之,架构也是为了应对软件系统复杂度而提出的一个解决方案, 因此我们可以基本得出结论:架构设计的主要目的是为了解决复杂度带来的问题。
复杂度来源
高性能
软件系统中高性能带来的复杂度主要体现在两方面, 一方面是单台计算机内部为了高性能带来的复杂度:另一方面是多台计算机集群为了高性能带来的复杂度。
单机复杂度:
计算机内部复杂度最关键的地方就是操作系统。计算机性能的发展本质上是由硬件发展驱动的,尤其是CPU 的性能发展。著名的“摩尔定律”表明了CPU 的处理能力每隔18 个月就翻一番;而将硬件性能充分发挥出来的关键就是操作系统,所以操作系统本身其实也是跟随硬件的发展而发展的,操作系统是软件系统的运行环境,操作系统的复杂度直接决定了软件系统的复杂度。
操作系统和性能最相关的就是进程和线程,如果需要完成一个高性能的软件系统,就需要考虑如下技术点:
-
多进程
-
多线程
-
进程间通信
-
多线程并发
集群复杂度:
虽然计算机硬件的性能快速发展,但和业务的发展速度相比,还是小巫见大巫了,尤其是进入互联网时代后,业务的发展速度远远超过了硬件的发展速度。
面对复杂的业务时,单机的性能无论如何是无法支撑的,必须采用机器集群的方式来达到高性能。通过大量机器来提升性能,并不仅仅是增加机器这么简单,让多台机器配合起来达到高性能的目的,是一个复杂的任务,我们针对常见的几种方式简单地分析一下。
任务分配
任务分配的意思是指每台机器都可以处理完整的业务任务,不同的任务分配到不同的机器上执行。下面是从一台机器变成两台机器的示意图:
从上图可以看到, 1 台服务器演变为2 台服务器后,架构上明显要复杂得多,主要体现在如下几个方面:
-
需要增加一个任务分配器;
-
任务分配器和真正的业务服务器之间有连接和交互;
-
任务分配器需要增加分配算法。
任务分解
通过任务分配的方式,我们能够突破单台机器处理性能的瓶颈,通过增加更多的机器来满足业务的性能需求,但如果业务本身也越来越复杂,单纯只通过任务分配的方式来扩展性能,收益会越来越低。此时我们就需要将复杂的业务拆分成较小的业务,以微信为例:
任务拆解的好处:
-
简单的系统更容易做到高性能。
-
可以针对单个任务进行扩展。
高可用
高可用指“系统无中断地执行其功能”的能力,代表系统的可用性程度,是进行系统设计时的准则之一。
系统的高可用方案五花八门,但万变不离其宗, 本质上都是通过“冗余”来实现高可用。通俗点来讲,就是一台机器不够就两台,两台不够就四台: 一个机房可能断电,那就部署两个机房;一条通道可能故障,那就用两条,两条不够那就用3 条(移动、电信、联通一起上) 。高可用的“冗余”解决方案, 单纯从形式上来看,和高性能是一样的,都是通过增加更多机器来达到目的,但其实本质上是有根本区别的:高性能增加机器的目的在于“扩展” 处理性能: 高可用增加机器的目的在于“冗余”处理单元。
计算高可用
计算高可用就是通过冗余机器来实现的,一机变多机,多机变更多机,而多机的复杂性同“高性能”的复杂性是一样的:
-
需要增加一个任务分配器,选择合适的任务分配器也是一件复杂的事情,需要综合考虑性能、成本、可维护性、可用性等各方面因素。
-
任务分配器和真正的业务服务器之间有连接和交互,需要选择合适的连接方式,并且对连接进行管理。例如,连接建立、连接检测、连接中断后如何处理等。
-
任务分配器需要增加分配算法。例如, 常见的双机算法有主备、主主。主备方案又可以细分为冷备、温备、热备。
存储高可用
对于需要存储数据的系统来说,整个系统的高可用设计关键点和难点就在于“存储高可用”。存储与计算相比,有一个本质上的区别:将数据从一台机器搬到另一台机器,需要经过线路进行传输。
以最经典的银行储蓄业务为例,假设用户的数据存在北京机房,用户存入了1万块钱,然后他查询的时候被路由到了上海机房,北京机房的数据没有同步到上海机房,用户会发现他的余额并没有增加l1万块。
高可用状态决策
无论计算高可用,还是存储高可用,其基础都是“状态决策”,即系统需要能够判断当前的状态是正常的还是出现了异常,如果出现了异常就要采取行动来保证高可用;
【独裁式】
独裁式决策指的是存在一个独立的决策主体一一我们姑且称它为“决策者” 负责收集信息然后进行决策:所有冗余的个体我们姑且称它为“上报者”一一都将状态信息发送给决策者。其基本架构如下图所示。
独裁式的决策方式不会出现决策混乱的问题,因为只有一个决策者,但问题也正是在于只有一个决策者:当决策者本身故障时,整个系统就无法实现准确的状态决策。如果决策者本身又做一套状态决策,那就陷入一个递归的死循环了。
【协商式】
协商式决策指的是两个独立的个体通过交流信息,然后根据规则进行决策,最常用的协商式决策就是主备决策,基本架构如下图所示。
【民主式】
民主式决策指的是多个独立的个体通过投票的方式来进行状态决策。例如, ZooKeeper 集群在选举leader 时就是采用这种方式, ZooKeeper 的基本架构如下图所示。
民主式决策和协商式决策比较类似,其基础都是独立的个体之间交换信息,每个个体做出自己的决策,然后按照“多数取胜”的规则来确定最终的状态
可扩展性
可扩展性指系统为了应对将来需求变化而提供的一种扩展能力,当有新的需求出现时,系统不需要或仅需要少量修改就可以支持,无须整个系统重构或重建。
设计具备良好可扩展性的系统,有两个基本条件:正确预测变化、完美封装变化。但要达成这两个条件,本身也是一件复杂的事情,我们接下来具体分析。
预测变化
预测变化的复杂性在于:
-
不能每个设计点都考虑可扩展性。
-
不能完全不考虑可扩展性。
-
所有的预测都存在出错的可能性。
应对变化
第一种应对变化的常见方案是将“变化”封装在一个“变化层”,将不变的部分封装在一个独立的“稳定层”,其基本架构如下图所示。
无论变化层依赖稳定层,还是稳定层依赖变化层都是可以的,需要根据具体业务情况来设计。
如果系统需要支持XML JSON 、ProtocolBuffer 三种接入方式,那么最终的架构就是上图中的“形式l ” 架构,如下图所示。
如果系统需要支持MySQL 、Oracle 、DB2 数据库存储,那么最终的架构就变成了“形式2 ”
的架构了,具体架构如下图所示。
无论采取哪种形式,通过剥离变化层和稳定层的方式应对变化,都会带来两个主要的复杂性相关的问题。
-
系统需要拆分出变化层和稳定层。
-
需要设计变化层和稳定层之间的接口。
第二种常见的应对变化的方案是提炼出一个“抽象层”和一个“ 实现层”,抽象层是稳定的,实现层可以根据具体业务需要定制开发,当加入新的功能时,只需要增加新的实现,无须修改抽象层。
底成本
当我们的架构方案只涉及几台或十几台服务器时, 一般情况下成本并不是我们重点关注的目标,但如果架构方案涉及几百上千甚至上万台服务器,成本就会变成一个非常重要的架构设计考虑点。
当我们设计“高性能”“高可用”的架构时,通用的手段都是通过增加更多服务器来满足“高性能”和“高可用”的要求;而低成本正好与此相反,我们需要减少服务器的数量才能达成低成本的目标。因此,低成本本质上是与高性能和高可用冲突的,所以低成本很多时候不会是架构设计的首要目标,而是架构设计的附加约束。
安全
从技术的角度来讲,安全可以分为两类: 一类是功能上的安全,另一类是架构上的安全。
功能安全
常见的XSS攻击、CSRF攻击、SQL 注入、Windows 漏洞、密码破解等,本质上是因为系统实现有漏洞,黑客有了可乘之机。黑客会利用各种漏洞潜入系统,这种行为就像小偷一样。小偷会翻墙、开锁、爬窗、钻狗洞进入我们的房子;黑客和小偷的手法都是利用系统不完善的地方潜入系统进行破坏或盗取。因此形象地说,功能安全其实就是“防小偷”。
架构安全
功能安全是“防小偷”,而架构安全就是“防强盗”。强盗会直接用锤子将大门砸开,或者用炸药将围墙炸倒;小偷是偷东西,而强盗很多时候就是故意搞破坏,对系统的影响也大得多。因此架构设计时需要特别关注架构安全,尤其是互联网时代,理论上来说系统部署在互联网上时,全球任何地方都可以发起攻击。
传统的架构安全主要依靠防火墙,防火墙最基本的功能就是隔离网络,通过将网络划分成不同的区域,制定出不同区域之间的访问控制策略来控制不同信任程度区域间传送的数据流。
例如,如下是一个典型的银行系统的安全架构。
防火墙的功能虽然强大,但性能一般,所以在传统的银行和企业应用领域应用较多。但在互联网领域,防火墙的应用场景并不多。因为互联网的业务具有海量用户访问和高并发的特点,防火墙的性能不足以支撑;尤其是互联网领域的DDOS 攻击,轻则几GB ,重则几十GB 。
一般也不会使用防火墙来防DDOS 攻击,因为DDOS 攻击最大的影响是大量消耗机房的出口总带宽。不管防火墙处理能力有多强,当出口带宽被耗尽时,整个业务在用户看来就是不可用的,因为用户的正常请求已经无法到达系统了。
互联网系统的架构安全目前并没有太好的设计手段来实现,更多是依靠运营商或云服务商强大的带宽和流量清洗的能力,较少自己来设计和实现。
规模
规模带来复杂度的主要原因就是“ 量变引起质变”,当数量超过一定的阔值后,复杂度会发生质的变化。
常见的规模带来的复杂度如下:
-
功能越来越多,导致系统复杂度指数级上升
-
数据越来越多,系统复杂度发生质变
架构设计原则
业务千变万化,技术层出不穷,设计理念也是百花齐放,看起来似乎很难有一套通用的规范来适用所有的架构设计场景。但是在研究了架构设计的发展历史、多个公司的架构发展过程(QQ 、淘宝、Facebook 等)、众多的互联网公司架构设计后,我们发现有几个共性的原则隐含其中,这就是:合适原则、简单原则、演化原则。架构设计时遵循这几个原则,有助于我们做出最好的选择。
合适原则
原则宣言:“合适优于业界领先”。
将军难打无兵之仗。
大公司的分工比较细, 一个小系统可能就是一个小组负责,比如说某个通信大厂,做一个OM 管理系统就有十几个人,阿里的中间件团队有几十个人,而大部分公司,整个研发团队可能就100 多人,某个业务团队可能就十几个人。十几个人的团队, 想做几十个人的团队的事情,而且还要做得更加好,不能说绝对不可能,但难度是可想而知的。
罗马不是一天建成的。
业界领先的很多方案,其实并不是一堆天才某个时期灵机一动,然后加班加点就搞出来的,而是经过几年时间的发展才逐步完善和初具规模的。阿里中间件团队2 00 8 年成立,发展到现在己经有十年了。我们只知道他们抗住了多少次“双11 ”,做了多少优秀的系统,但经历了什么样的挑战、踩了什么样的坑,只有他们自己知道! 这些挑战和踩坑,都是架构设计非常关键的促进因素,单纯靠拍脑袋或头脑风暴,是不可能和真正实战遇到挑战和问题同日而语的。
冰山下面才是关键。
很多人以为,业界领先的方案都是天才创造出来的,所以自己也要设计一个业界领先的方案,以此来证明自己也是天才。确实有这样的天才,但更多的时候,业界领先的方案其实都是“逼”出来的!简单来说,"业务”发展到一定阶段, 量变导致了质变,出现了新的问题,己有的方式已经不能应对这些问题,需要用一种新的方案来解决,通过创新和尝试,才有了业界领先的方案。GFS 为何在Google 诞生,而不是在Microsoft 诞生?我认为Google 有那么庞大的数据是一个主要的因素,而不是因为Google 的工程师比Microso仕的工程师更加聪明。
所以,真正优秀的架构都是在企业当前人力、条件、业务等各种约束下设计出来的,能够合理地将资源整合在一起井发挥出最大功效,井且能够快速落地。这也是很多B AT (百度,阿里巴巴、腾讯)出来的架构师到了小公司或创业团队反而做不出成绩的原因,因为没有了大公司的平台、资源、积累,只是生搬硬套大公司的做法,必然会失败。
简单原则
原则宣言: “简单优于复杂”。
“复杂”在制造领域代表先进,在建筑领域代表领先,但在软件领域,却恰恰相反,代表的是“问题”。
软件领域的复杂性体现在以下两个方面:
结构的复杂性
结构复杂的系统几乎毫无例外地具备两个特点: 组成复杂系统的组件数量更多,同时这些组件之间的关系也更加复杂。我们以图形的方式来形象地说明复杂性:
两个组件组成的系统如下图所示。
三个组件组成的系统如下图所示。
四个组件组成的系统如下图所示。
五个组件组成的系统如下图所示。
结构上的复杂性存在的第一个问题是: 组件越多,就越有可能其中某个组件出现故障,从而导致系统故障。这个概率可以算出来:假设组件的故障率是10% (有10% 的时间不可用),那么有3 个组件的系统可用性是( I ” 10%)°¡( 1-10%)°¡( 1-10%) = 72.9% ,有5 个组件的系统可用性是( 1-10%)°¡( 1-10%)°¡( 1-10%)°¡( 1-10%)°¡( 1-10%) =59% ,两者的可用性相差13% 。
结构上的复杂性存在的第二个问题是:某个组件改动,会影响关联的所有组件,这些被影响的组件同样会继续递归影响更多的组件。以上图5 个组件组成的系统为例: 组件A 修改或异常时,会影响组件B/C/E, D 又会影响E。这个问题会影响整个系统的开发效率,因为一旦变更涉及外部系统,需要协调各方统一进行方案评估、资源协调、上线配合。
结构上的复杂性存在的第三个问题是: 定位一个复杂系统中的问题总是比简单系统更加困难。首先是组件多,每个组件都有嫌疑,因此要逐一排查:其次组件间的关系复杂,有可能表现故障的组件并不是真正问题的根源。
逻辑复杂性
看到结构复杂性后,我们的第一反应可能就是“降低组件数量”,毕竟组件数量越少,系统结构越简单。最简单的结构当然就是整个系统只有一个组件,即系统本身,所有的功能和逻辑都在这一个组件中实现。
不幸的是这样做是行不通的,原因在于除了结构复杂性,还有逻辑复杂性,即如果某个组件的逻辑太复杂, 一样会带来各种问题。逻辑复杂的组件一个典型特征就是单个组件承担了太多的功能。以电商业务为例,常见的功能有:商品管理、商品搜索、商品展示、订单管理、用户管理、支付、发货、客服……把这
些功能全部在一个组件中实现,就是典型的逻辑复杂性。
逻辑复杂性典型的表现就是电路图。我们对比一下简单的电路图和复杂电路图,不用详细去研究这个电路图的含义,我们只是感觉一下复杂性的差异即可。
一个简单的电路图如下图所示。
一个复杂的电路图如下图所示。
逻辑复杂几乎会导致软件工程的每个环节都有问题,假设现在掏宝的这些功能全部在单一的组件中实现,可以想象一下这个恐怖的场景:
-
系统会很庞大,可能是上百万上千万的代码规模,“ clone ” 一次代码要30 分钟。
-
几十上百人维护这一套代码,某个“菜鸟”不小心改了一行代码,导致整站崩渍。
-
需求像雪片般飞来,为了应对,开几十个代码分支,然后各种分支合井、各种分支覆盖。
-
产品、研发、测试、项目管理不停地开会讨论版本计划,协调资源,解决冲突。
-
版本太多,每天都要上线几十个版本,系统每隔1 个小时重启一次。
-
线上运行出现故障,几十个人扑上去定位和处理,一间小黑屋都装不下所有人, 整个办公区闹翻天。
我们可以看到,无论结构的复杂性,还是逻辑的复杂性,都会存在各种问题,所以架构设计时如果简单的方案和复杂的方案都可以满足需求,一定要选择简单的方案,《UNIX 编程艺术》总结的KISS (Keep It Simple,Stupid !)原则一样适应于架构设计。
演化原则
原则宣言: “演化优于一步到位”。
对于建筑来说,永恒是主题;而对于软件来说,变化才是主题! 软件架构需要根据业务的发展而不断变化。设计Windows 和Android 的人都是顶尖的天才,即使如此,他们也不可能在1985年设计出Windows8 ,不可能在2009 年设计出Android 6.0 。
如果没有把握“软件架构需要根据业务发展不断变化”这个本质,在做架构设计的时候就很容易陷入一个误区:试图一步到位地设计一个软件架构,期望不营业务如何变化,架构都稳如磐石!
为了实现这样的目标,要么照搬业界大公司公开发表的方案;要么投入庞大的资源和时间来做各种各样的预测、分析、设计。无论哪种做法,后果都很明显:投入巨大,落地遥遥无期!更让人沮丧的是,就算跌跌撞撞拼死拼活终于落地,却发现很多预测和分析都是不靠谱的!
考虑到软件架构需要根据业务发展不断变化这个本质特点,软件架构设计其实更加类似于大自然“设计”一个生物:
-
首先,生物要适应当时的环境。
-
其次,生物需要不断地迭代繁殖,将有利的基因传递下去,将不利的基因剔除或修复。
-
最后,当环境变化时,生物要能够快速改变以适应环境变化;如果生物无法调整就被自然淘汰;新的生物会保留一部分原来被淘汰的生物的基因。
软件架构设计同样是类似的过程:
-
首先,设计出来的架构要满足当时的业务需要。
-
其次,架构要不断地在实际应用过程中迭代,保留优秀的设计,修复有缺陷的设计,改正错误的设计,去掉无用的设计,使得架构逐渐完善。
-
最后,当业务发生变化时,架构要扩展、重构、甚至重写:代码也许会重写,但有价值的经验、教训、逻辑、设计等(类似生物体内的基因〉却可以在新架构中延续。
架构设计流程
识到复杂度
架构设计的本质目的是为了解决软件系统的复杂性,所以在我们设计架构时,首先就要分析系统的复杂性。只有正确分析出了系统的复杂性,后续的架构设计方案才不会偏离方向;否则如果对系统的复杂性进行了错误的判断,即使后续的架构设计方案再完美再先进,都是南辕北辙,做得越好,错得越多、越离谱。
架构的复杂度主要来源于“高性能”“高可用”“可扩展”等几个方面,但架构师在具体判断复杂性的时候,不能生搬硬套,认为任何时候都从这三个方面进行复杂度分析就可以了。实际上绝大部分场景下,复杂度只是其中的某一个,少数情况下包含其中两个,如果真的出现同时需要解决三个或三个以上的复杂度,要么说明这个系统之前做得实在是太烂了,要么架构师的判断出现了严重失误。
例如,某公司2011 年的时候提出做用户中心,设计对标腾讯的QQ ,按照腾讯的QQ 用户量级和功能复杂度进行设计,高性能、高可用、可扩展、安全等技术一应俱全, 一开始就设计出了40 多个子系统,然后投入大量人力开发了将近1 年时间才跌跌撞撞地正式上线。
上线后发现之前的过度设计完全是多此一举,而且带来很多问题:
-
系统复杂无比,运维效率低下,每次业务版本升级都需要十几个子系统同步升级,操作步骤复杂,容易出错,出错后回滚还可能带来二次问题。
-
每次版本开发和升级都需要十几个子系统配合,开发效率低下。
-
子系统数量太多,关系复杂, 小问题不断,而且出问题后定位困难。
-
开始设计的号称每秒TPS 5 万的系统,实际TPS 连500 都不到。
由于业务没有发展,最初的设计人员陆续离开,整个系统成了一个烂摊子, 后来接手的团队,无奈又花了2 年时间将系统重构,合并很多子系统,将原来40 多个子系统合并成不到20个子系统,整个系统才逐步稳定下来。
如果运气真的不好,接手了一个每个复杂度都存在问题的系统,那应该怎么办呢?答案是一个个来解决问题,不要幻想一次架构重构解决所有问题。例如,上述的“用户中心”的案例,后来接手的团队其实面临几个主要的问题: 系统稳定性不高, 经常出各种莫名的小问题;系统子系统数量太多,系统关系复杂,开发效率低;不支持异地多活, 机房级别的故障会导致业务整体不可用。如果同时要解决这些问题,就可能会面临如下困境:
-
要做的事情太多,反而感觉无从下手。
-
设计方案本身太复杂, 落地时间遥遥无期。
-
同一个方案要解决不同的复杂性,有的设计点是互相矛盾的。例如,要提升系统可用性,就需要将数据及时存储到硬盘上,而硬盘刷盘反过来又会影响系统性能。
因此,正确的做法是将主要的复杂度问题列出来,然后根据业务、技术、团队等综合情况进行排序,优先解决当前面临的最主要的复杂度问题。例如,前面的“用户中心’的案例,团队就优先选择将子系统的数量降下来,后来发现子系统数量降下来后,不但开发效率提升了,原来经常发生的小问题也基本消失了,于是团队再在这个基础上做了异地多活方案,也取得了非常好的效果。
设计备选方案
确定了系统面临的主要复杂度问题后,方案设计就有了明确的目标,我们就可以开始真正进行架构方案设计了。
如何设计最终的方案,并不是一件容易的事情,这个阶段也是很多架构师容易犯错误的地方。
第一种常见的错误: 设计最优秀的方案!
很多架构师在设计架构方案时,心里会默认有一种技术情结:我要设计一个优秀的架构,才能体现我的技术能力!例如,高可用的方案中,集群方案明显比主备方案要优秀和强大;高性能的方案中,淘宝的xx 方案是业界领先的方案……
根据架构设计原则中“简单原则”的要求,挑选合适自己业务、团队、技术能力的方案才是好方案;否则要么浪费大量资源开发了无用的系统(例如,“用户中心”的案例,设计了50000TPS 的系统,实际TPS 只有500 ),要么根本无法实现(例如, 10 个人的团队要开发现在的整个掏宝系统)。
第二种常见的错误:只做一个方案!
很多架构师在做方案设计时,可能心里会简单地对几个方案进行初步的设想,再简单地判断哪个最好,然后就基于这个判断开始进行详细的架构设计了。
这样做有很多弊端:
-
心里评估过于简单,可能没有想得全面,只是因为某一个缺点就把某个方案给否决了,而实际上没有哪个方案是完美的,某个地方有缺点的方案可能是综合来看最好的方案。
-
架构师再怎么牛,经验知识和技能也有局限,有可能某个评估的标准或经验是不正确的,或者是老的经验不适合新的情况,甚至有的评估标准是架构师自己原来就理解错了。
-
单一方案设计会出现过度辩护的情况, 即架构评审时,针对方案存在的问题和疑问,架构师会竭尽全力去为自己的设计进行辩护,经验不足的设计人员可能会强词夺理。
因此,架构师需要设计多个备选方案,但方案的数量可以说是无穷无尽的,架构师也不可能穷举所有方案,那合理的做法应该是什么样的呢?
- 备选方案的数量以3 ~ 5 个为最佳。
少于3 个方案可能是因为思维狭隘,考虑不周全;多于5 个则需要耗费大量的精力和时间,并且方案之间的差别可能不明显。
- 备选方案的差异要比较明显。
例如,主备方案和集群方案差异就很明显,或者同样是主备方案,用ZooKeeper 做主备决策和用Keepalived 做主备决策的差异也很明显。但是都用ZooKeeper 做主备决策,一个检测周期是l 分钟, 一个检测周期是5 分钟,这就不是架构上的差异,而是细节上的差异了,不适合做成两个方案。
- 备选方案的技术不要只局限于己经熟悉的技术。
设计架构时,架构师需要将视野放宽,考虑更多可能性。很多架构师或设计师积累了一些成功的经验,出于快速完成任务和降低风险的目的,可能自觉或不自觉地倾向于使用自己己经熟悉的技术,对于新的技术有一种不放心的感觉。就像那句俗语说的:“如果你手里有一把锤子,那么所有的问题在你看来都是钉子”。例如,架构师对MySQL 很熟悉,因此不管什么存储都基于MySQL 去设计方案,系统性能不够了,首先考虑的就是MySQL 分库分表,而事实上也许引入一个Memcache 缓存就能够解决问题。
第三种常见的错误:备选方案过于详细。
有的架构师或设计师在写备选方案时,错误地将备边方案等同于最终的方案,每个备选方案都写得很细。这样做的弊端显而易见:
-
耗费了大量的时间和精力;
-
将注意力集中到细节中,忽略了整体的技术设计,导致备选方案数量不够或差异不大;
-
评审的时候其他人会被很多细节给绕进去,评审效果很差。例如,评审的时候针对某个定时器应该是1分钟还是30 秒, 争论得不可开交。
正确的做法是备选阶段关注的是技术选型,而不是技术细节,技术选型的差异要比较明显。
评估和选择备选方案
完成备选方案设计后,如何挑选出最终的方案也是一个很大的挑战,主要原因如下:
-
每个方案都是可行的,如果方案不可行就根本不应该作为备选方案。
-
没有哪个方案是完美的。例如, A 方案有性能的缺点, B 方案有成本的缺点, C 方案有新技术不成熟的风险。
-
评价标准主观性比较强,比如架构师说A 方案比B 方案复杂,但另外一个设计师可能会认为差不多, 架构师也比较难将“ 复杂” 一词进行量化。
正因为选择备选方案存在这些困难,所以实践中很多设计师或架构师就采取了如下指导思想。
-
最简派:设计师挑选一个看起来最简单的方案。
-
最牛派:最牛派的做法和最简派正好相反, 设计师会倾向于挑选技术上看起来最牛的方案。
-
最熟派:设计师基于自己的过往经验,挑选自己最熟悉的方案。
-
领导派:领导派就更加聪明了,列出备选方案,设计师自己拿捏不定,然后就让领导来定夺。
其实这些不同的做法本身并不存在绝对的正确或绝对的错误,关键是不同的场景应该采取不同的方式。也就是说,有时候我们要挑选最简单的方案,有时候要挑选最优秀的方案, 有时候要挑选最熟悉的方案,甚至有时候真的要领导拍板。因此关键问题是:这里的“ 有时候”到底应该怎么判断?答案就是“ 360 度环评” ! 具体的操作方式为: 列出我们需要关注的质量属性点, 然后分别从这些质量属性的维度去评估每个方案,再综合挑选适合当时情况的最优方案。
常见的方案质量属性点有:性能、可用性、硬件成本、项目投入、复杂度、安全性、可扩展性等。在评估这些质量属性时,需要遵循架构设计原则l “合适原则”和原则2 “简单原则”,避免贪大求全,基本上某个质量属性能够满足一定时期内业务发展就可以了。
下面我们以一个具体实例来展示一下方案“ 360 度环评”的具体做法。
业务背景
某个大约20 个人规模的创业团队做了一个垂直电商的网站,其中开发人员大约是6 个人。创业初期为了能够快速上线,系统架构设计得很简单,就是一个简单的Web 网站,其架构如下图所示。
由于业务飞速发展,目前的Web 服务器己经出现性能瓶颈,用户访问缓慢,用户投诉日益增多,影响业务的进一步发展。
备选方案设计
【方案1 :横向扩展】
横向扩展的实现比较简单,就是简单地增加Web 服务器,将单台Web 服务器扩展为Web服务器集群,其架构如下图所示。
【方案2 :系统拆分】
参考陶宝,将电商系统拆分为商品子系统、订单子系统、用户管理子系统,其架构示意图如下图所示。
备选方案360 度环评
我们分析本次方案设计需要关注的架构质量属性:
-
由于背景问题是系统性能不足,因此“性能”是首要考虑的架构质量属性。由于业务快速发展,我们希望本次方案做完后,性能上至少能支撑接下来1 年内的业务发展。
-
由于开发人员只有5 个人,因此复杂度和项目开发时间也是需要重点关注的,我们不希望一个方案需要做半年时间才能做完。
-
由于是创业公司, 目前还处于未盈利状态,因此方案的成本也需要考虑。
-
业务发展很快,各种新的功能不断提出,产品经理希望我们的业务迭代速度更快一些,因此系统需要能够快速扩展新的功能。
-
用户量增长很快,系统如果故障影响比较大,因此系统的可用性也需要关注。
最终的质量属性包括性能、复杂度、成本、可扩展、可用性。我们逐一对比两个方案,如下表所示。
完成方案的360 度环评后, 我们虽然能够从“360度环评”表格一目了然地看到各个方案的优劣点,但这样一个表格也只能帮助我们分析各个备选方案,还是没有告诉我们具体选哪个方案,原因就在于没有哪个方案是完美的。
正确的做法是“按优先级选择”,即设计师综合当前的业务发展情况、团队人员规模和技能、业务发展预测等因素,将质量属性按照优先级排序,首先挑选满足第一优先级的,如果方案都满足,那就再看第二优先级……以此类推。
回到我们的电商架构示例,由于整个开发团队只有5 个人,既要做业务版本开发,又要解决架构性能问题,同时业务发展还很快,因此方案能否快速实施是最优先考虑的(当然两个方案都必须能够解决性能问题),因此需要挑选一个简单一些的方案,那就是“集群方案”。
详细方案设计
简单来说,详细方案设计就是将方案涉及的关键技术细节给确定下来。
-
假如我们确定使用Elasticsearch 来做全文搜索,那么就需要确定E lasticsearch 的索引是按照业务划分,还是一个大索引就可以了:副本数量是2 个、3 个还是4 个,集群节点数量是3 个还是6 个等。
-
假如我们确定使用MySQL 分库分表,那么就需要确定哪些表要分库分表,按照什么维度来分库分表,分库分表后联合查询怎么处理等。
-
假如我们确定引入Nginx 来做负载均衡,那么Nginx 的主备怎么做, Nginx 的负载均衡策略用哪个(权重分配?轮询? ip hash? )等。
详细设计方案阶段可能遇到的一种极端情况就是在详细设计阶段发现备选方案不可行, 一般情况下主要的原因是备选方案设计时遗漏了某个关键技术点或关键的质量属性。例如,笔者曾经参与过一个项目,在备选方案阶段确定是可行的,但在详细方案设计阶段, 发现由于细节点太多, 方案非常庞大,整个项目可能要开发长达1 年时间,最后只得废弃原来的备选方案,重新调整项目目标、计划和方案。这个项目的主要失误就是在备选方案评估时忽略了开发周期这个质量属性。
这种情况可以通过如下方式能够有效地避免:
-
架构师不但要进行备选方案设计和选型,还需要对备选方案的关键细节有较深入的理解。
-
通过分步骤、分阶段、分系统等方式, 尽量降低方案复杂度,方案本身的复杂度越高,某个细节推翻整个方案的可能性就越高。
-
如果方案本身就很复杂,那么就采取设计团队的方式来进行设计,博采众长,汇集大家的智慧和经验,防止1、2 个设计师时可能出现的思维盲点或经验盲区。