问题本质:为什么软件开发需要结构
1:基本定义
参考维基百科的定义,可以将软件架构重新定义为:软件系统的顶层结构。
首先定义:“系统是一群关联个体组成”,这些“个体”可以是“子系统”“模块”“组件”等;
其次,系统中的个体需要“根据某种规则”运作,而软件架构需要明确个体运作和协作的规则。
这就是系统的基本特征。
2:架构的发展史
想深入理解一个事物的本质,最好的方式就是去追寻这个事物出现的历史背景和推动因素。
(1)机器语言(1940 年之前)
使用二进制的形式,其主要问题是:太难写、太难读、太难改!
(2)汇编语言(20 世纪 40 年代)
汇编语言又叫“符号语言”,用助记符代替机器指令的操作码,用地址符号(Symbol)或标号(Label)代替指令或操作数的地址。
解决了机器语言读写复杂的问题,但是本质上还是面向机器的,因此,需要去了解计算机很多底层知识(CPU、寄存器等);同时,不同 CPU 的汇编指令和结构还不一样。
(3)高级语言(20 世纪 50 年代)
称为高级语言是因为不需要程序员去关注计算机底层的知识,只需要关注自己的业务即可;同时,通过编译器的处理,能将其编译成适合不同 CPU 指令的机器语言。
(4)第一次软件危机与结构化程序设计(20 世纪 60 年代~20 世纪 70 年代)
1963 年美国水手一号火箭发射失败事故”,IBM 的 System/360 的操作系统开发事故等事故,创造了“软件危机”一词,为了解决软件危机问题,1968、1969 提出“软件工程”方案”,
”1968 年“结构化程序设计”方案“,奠定了软件工程和软件结构的基础,
结构化程序设计的主要特点是抛弃 goto 语句,采取“自顶向下、逐步细化、模块化”的指导思想。结构化程序设计本质上还是一种面向过程的设计思想,但通过“自顶向下、逐步细化、模块化”的方法,将软件的复杂度控制在一定范围内,从而从整体上降低了软件开发的复杂度。结构化程序方法成为了 20 世纪 70 年代软件开发的潮流。
(任何一件事只要规模足够大,就一定需要方法论来提高效率,这个方法论就是一些学科存在的基础)
(5)第二次软件危机与面向对象(20 世纪 80 年代)
第二次软件危机的根本原因在于软件生产力远远跟不上硬件和业务的发展。第一次软件危机根源在于软件的“逻辑”变得非常复杂,既有程序本身的设计问题,也有规模问题。 而第二次软件危机主要体现在软件的“扩展”变得非常复杂。结构化程序设计虽然能够解决(也许用“缓解”更合适)软件逻辑的复杂性,但是对于业务变化带来的软件扩展却无能为力,软件领域迫切希望找到新的银弹来解决软件危机,在这种背景下,面向对象的思想开始流行起来。当然,实际上这也是一种缓解问题的方法,不是解决问题的答案。
(6)软件架构
软件架构的出现,是某些大型企业发展到一定程度后,为了解决面临的大型软件问题,而被提出。如: Rational 或者 Microsoft 这样的大公司。
(也说明了一点,需要进入大公司,那就要学习这些软件架构,成为大公司需要的人才)
软件架构的出现有其历史必然性。20 世纪 60 年代第一次软件危机引出了“结构化编程”,创造了“模块”概念;20 世纪 80 年代第二次软件危机引出了“面向对象编程”,创造了“对象”概念;到了 20 世纪 90 年代“软件架构”开始流行,创造了“组件”概念。我们可以看到,“模块”“对象”“组件”本质上都是对达到一定规模的软件进行拆分,差别只是在于随着软件的复杂度不断增加,拆分的粒度越来越粗,拆分的层次越来越高。
3:为什么需要架构
架构本质是为了应对软件系统复杂度(逻辑、扩展等)而提出的一个解决方案,其目主要是为了:解决软件系统复杂度带来的问题。
那么我们在做系统设计的时候,首先就是要分析系统复杂度的来源,分析清楚复杂度后,接下来就是设计,来解决系统中的复杂度,
(1)解决的问题
既然架构设计的主要问题就是分析系统的复杂度,那么复杂度主要包括哪些?
1.高性能
性能的提升,一定会带来复杂度的提升吗?并不一定,
如:硬件存储从纸带→磁带→磁盘→SSD,并没有显著带来系统复杂度的增加。
因为新技术会逐步淘汰旧技术,这种情况下我们直接用新技术即可,不用担心系统复杂度会随之提升。只有那些不是用来取代旧技术,而是直接开辟了一个全新领域的技术,才会给软件系统带来复杂度,因为软件系统在设计的时候就需要在这些技术之间进行判断选择或者组合。要考虑新技术是否成熟,能不能替代其他原有技术并带来提升,
比如单台机器和多台机器
(1)单台计算机内部为了高性能带来的复杂度
单机性能的提升:手工操作 》批处理操作系统》进程》线程
带来的复杂度:进程/线程间的通信方式(管道、消息队列、信号量、共享存储)、多核处理器架构
示例:Redis 采用的是单进程、Memcache 采用的是多线程、Nginx 可以用多进程也可以用多线程
(2)多台计算机集群为了高性能带来的复杂度
集群性能的提升:任务分配(使用多台机器来分担运行)、任务分解(对任务的拆分,如:多机器协调或者服务拆分)
任务分配的复杂度:需要任务分配器(Nginx、F5)、连接管理、分配算法(轮询)
任务分解的复杂度:不同任务间的通信、任务的拆分
2.高可用
高可用的定义为:系统无中断地执行其功能的能力,代表系统的可用性程度,是进行系统设计时的准则之一。
但无论是单个硬件还是单个软件,都不可能做到无中断,硬件会出故障,软件会有 bug;硬件会逐渐老化,软件会越来越复杂和庞大……, 所以硬件和软件本质上无法做到“无中断”。
各种高可用的方案,其实本质上都是一种冗余(单个机器保证不了),也就是通过添加机器的方式来实现,这和高性能的方式一样,但是,两者的目的有所不同:高性能增加机器目的在于“扩展”处理性能;机器越多,性能越强,高可用增加机器目的在于“冗余”处理单元。多台机器,一台故障另外一台顶上继续运行。
在高可用体系中,根据其特性,可以简单的将其分为计算高可用和存储高可用。
(1)计算高可用:
特点:无论在哪台机器上进行计算,同样的算法和输入数据,产出的结果都是一样的。(多机器运行)
复杂度:需要任务分配器(Nginx、F5)、连接管理、分配算法(双机算法:主备<冷备、温备、热备>、主主)
举例:ZooKeeper 采用的就是 1 主多备、Memcached 采用的就是全主 0 备
(2)存储高可用:(保证运行保存数据的持久性和一致性)
特点:将数据从一台机器搬到到另一台机器,需要经过线路进行传输。
复杂度:如何进行数据的备份、如何减少或者规避数据不一致
举例:数据在备份的过程中,有物理上的传输速度限制(光纤、带宽)、也有传输线路本身的问题(中断、拥塞、异常<错包、丢包>)。但是无论是正常情况下的传输延迟,还是异常情况下的传输中断,都会导致系统的数据在某个时间点或者时间段是不一致的,而数据的不一致又会导致业务问题;但如果完全不做冗余,系统的整体高可用又无法保证,所以存储高可用的难点不在于如何备份数据,而在于如何减少或者规避数据不一致对业务造成的影响。
共同点:状态的决策
无论是计算高可用还是存储高可用,其基础都是“状态决策”,即系统需要能够判断当前的状态是正常还是异常,如果出现了异常就要采取行动来保证高可用。但是由于高可用的本质是“冗余”,而数据的冗余又依赖于数据的备份(数据的传输),所以,高可用体系中的状态决策不可能做到完全正确。创建的状态决策方式有以下几种:
(1)独裁式
指的是存在一个独立的决策主体,来收集信息,然后进行决策。其问题在于“决策者”本身怎么去解决单点问题。
(2)协商式
指的是两个独立的个体通过交流信息,然后根据规则进行决策,如:主备决策。其难点在于信息交换的时候出现了问题(比如主备连接中断),此时状态决策应该怎么做?
(3)民主式
指的是多个独立的个体通过投票的方式来进行状态决策。和协商式类似,其基础都依赖于独立的个体之间交换信息。但是,民主式决策更加复杂,可以看成是对协商式的一种升级版本。这种决策方式在集群中可能会出现“脑裂”现象,其解决办法是采用【节点奇数个数 + 过半原则来解决】。但是,这种处理办法,会降低了系统整体的可用性(不是脑裂,而是节点故障)。
示例:ZooKeeper 集群在选举 leader 时就是采用这种方式。
综合上述的分析,无论采取什么样的方案,状态决策都不可能做到任何场景下都没有问题。高可用的解决方法不是解决,而是减少或者规避,而规避某个问题的时候,一般都会引发另一个问题,只是这个问题比之前的小,高可用的设计过程本质上就是一个取舍的过程。这也就是为什么系统可用性永远只是说几个九,永远缺少那个一。
3.可扩展
可扩展性指系统为了应对将来需求变化而提供的一种扩展能力,当有新的需求出现时,系统不需要或者仅需要少量修改就可以支持,无须整个系统重构或者重建。
设计具备良好可扩展性的系统,有两个基本条件:正确预测变化、完美封装变化。
(1)正确预测变化:
根据经验,去进行一些需求的预测,但是如何把握预测的程度和提升预测结果的准确性,是一件很复杂的事情,而且没有通用的标准可以简单套上去,更多是靠自己的实践经验、直觉。
(2)完美封装变化:常用的方式主要有两种:
第一种:将“变化”封装在一个“变化层”,将不变的部分封装在一个独立的“稳定层”:
需要去分析,哪些是不变层,哪些是稳定层?对于变化层来说,还要在有差异的多个实现方式中找出共同点,并且保证在有新功能添加的时候,接口设计不会做太大的修改。
第二种:提炼出一个“抽象层”和一个“实现层”
抽象层是稳定的,实现层可以根据具体业务需要定制开发。
4.低成本
当我们设计“高性能”“高可用”的架构时,通用的手段都是增加更多服务器来满足“高性能”和“高可用”的要求;而低成本恰恰与此相反,我们需要减少服务器的数量才能达成低成本的目标。因此,低成本本质上是与高性能和高可用冲突的,所以低成本很多时候不会是架构设计的首要目标,而是架构设计的附加约束。
也就是说,我们首先需要设定一个成本目标,当我们根据高性能、高可用的要求设计出方案时,评估一下方案是否能满足成本目标,如果不行,就需要重新设计架构;如果无论如何都无法设计出满足成本要求的方案,那就只能找老板调整成本目标了。
低成本给架构设计带来的主要复杂度体现在,往往只有“创新”才能达到低成本目标。这里的“创新”既包括开创一个全新的技术领域(开创新技术),也包括引入新技术,如果没有找到能够解决自己问题的新技术,那么就真的需要自己创造新技术了。
5.安全
从技术的角度来讲,安全可以分为两类:一类是功能安全,一类是架构安全。
(1)功能安全:
如常见的 XSS 攻击、CSRF 攻击、SQL 注入、Windows 漏洞、密码破解等,其本质上是因为系统实现有漏洞,黑客有了可乘之机。
从实现上来看,功能安全更多地是和具体的编码相关,与架构关系不大。并且,这类安全问题,是无法完全进行预测的,更多的是在问题出现后,针对性的处理,然后不断完善系统安全的一个过程。
功能安全其实也是一个“攻”与“防”的矛盾,只能在这种攻防大战中逐步完善,不可能在系统架构设计的时候一劳永逸地解决。
(2)架构安全:
在互联网时代,理论上来说只要系统部署在互联网上,全球任何地方都可以发起攻击。
防火墙:
主要运用在传统的架构安全中,一般都有成熟方案,如:银行,其成本较高,性能一般,并且在面对一些攻击时(DDos 攻击),其效果并不是很好。架构也自带一些防护方案,也可依靠运营商或者云服务商强大的带宽和流量清洗的能力来抵抗攻击。
6.规模
规模带来复杂度的主要原因就是“量变引起质变”,当数量超过一定的阈值后,复杂度会发生质的变化。常见的规模带来的复杂度有:
(1)功能越来越多,导致系统复杂度指数级上升
一个系统,包含了太多的功能需求,各个功能点之间相互依赖耦合。
(2)数据越来越多,系统复杂度发生质变
系统数据越来越多时,也会由量变带来质变。如:MySQL 单表数据超过 5000 万、10 亿等。
四.架构设计原则
架构设计和平时写代码不一样,两者的差异主要体现在“不确定性”上。对于编程来说,本质上是确定的,对于同样一段代码,不管是谁写的,不管什么时候执行,执行的结果应该都是确定的,就像写命题作文,
而对于架构设计来说,本质上是不确定,并没有像编程语言那样的语法来进行约束,更多的时候是面对多种可能性时进行选择。
比如:
是要选择业界最先进的技术,还是选择团队目前最熟悉的技术?
是要选 MySQL 还是 MongoDB? 团队对 MySQL 很熟悉,但是 MongoDB 更加适合业务场景?
淘宝的电商网站架构很完善,我们新做一个电商网站,是否简单地照搬淘宝就可以了?
1.合适原则
合适优于业界领先。
在进行架构设计的同时,需要考虑自身业务,而不是一味的去参照业界顶尖的规模,如:QQ、微信、淘宝架构。真正优秀的架构都是在企业当前人力、条件、业务等各种约束下设计出来的,能够合理地将资源整合在一起并发挥出最大功效,并且能够快速落地。
2.简单原则
简单优于复杂。
软件架构设计是一门技术活,当我们进行架构设计时,会自然而然地想把架构做精美、做复杂,这样才能体现我们的技术实力,也才能够将架构做成一件艺术品。然而,“复杂”在制造领域代表先进,在建筑领域代表领先,但在软件领域,却恰恰相反,代表的是“问题”。
软件复杂度的体现,主要有以下两个方面:
– 组成复杂系统的组件数量更多;
– 组件之间的关系也更加复杂。
其问题主要有:
(1)组件越多,就越有可能其中某个组件出现故障,从而导致系统故障。
(2)某个组件改动,会影响关联的所有组件。
(3)定位一个复杂系统中的问题总是比简单系统更加困难。
逻辑的复杂性
逻辑的复杂性来源于一个组件集中了太多的功能,修改协作困难;并且,其中某些业务还可能使用了一些复杂的算法,导致难以理解、修改困难。
一个组件集中了太多功能,就会表现出一些逻辑复杂性的特征,为了解决这个问题,一般的手段是进行组件的拆分,但随着组件的细化,又会引入结构复杂性的一些特征,所以,在做结构设计的时候,需要权衡这两者。
3.演化原则
演化优于一步到位。
维基百科对“软件架构”的定义如下:
从和目的、主题、材料和结构的联系上来说,软件架构可以和建筑物的架构相比拟。
这个定义中,将建筑和软件架构做了一个比较,但是,两者之间是有一个本质区别的:对于建筑来说,永恒是主题;而对于软件来说,变化才是主题。
也就是说,软件架构的本质是:软件架构需要根据业务发展不断变化,所以,我们在做软件架构设计的时候,不要试图一步到位设计一个软件架构,期望不管业务如何变化,架构都稳如磐石。
架构设计的过程基本上可以总结为下面三个历程:
首先,设计出来的架构要满足当时的业务需要。
其次,架构要不断地在实际应用过程中迭代,保留优秀的设计,修复有缺陷的设计,改正错误的设计,去掉无用的设计,使得架构逐渐完善。-- 小重构
最后,当业务发生变化时,架构要扩展、重构,甚至重写;代码也许会重写,但有价值的经验、教训、逻辑、设计等(类似生物体内的基因)却可以在新架构中延续。-- 大重构
我们在做架构设计的时候,切勿贪大求全,或者盲目的照搬大公司的做法,而是要牢记软件架构的本质(软件架构需要根据业务发展不断变化)。认真分析当前业务的特点,明确业务面临的主要问题,设计合理的架构,快速落地以满足业务需要,然后在运行过程中不断完善架构,不断随着业务演化架构。
参考链接:
架构基础(一):什么是架构?架构的发展历程-CSDN博客