架构设计相关的特性:
系统泛指由一群有关联的个体组成,根据某种规则运作,能完成个别元件不能单独完成的工作的群体。
子系统是由一群有关联的个体所组成的系统,多半会是更大系统中的一部分。
软件模块(Module)是一套一致而互相有紧密关连的软件组织。它分别包含了程序和数据结构两部分。现代软件开发往往利用模块作为合成的单位。
模块和组件都是系统的组成部分,只是从不同的角度拆分系统而已。
软件框架(Software framework)通常指的是为了实现某个业界标准或完成特定基本任务的软件组件规范,也指为了实现某个软件组件规范时,提供规范所要求之基础功能的软件产品。
软件架构指软件系统的“基础结构”,创造这些基础结构的准则,以及对这些结构的描述。
框架(Framework)关注的是“规范”,架构(Architecture)关注的是“结构”。
软件架构指软件系统的顶层结构。
整个软件技术发展的历史,其实就是一部与“复杂度”斗争的历史。架构也是为了应对软件系统复杂度而提出的一个解决方案,架构设计的主要目的是为了解决软件系统复杂度带来的问题。
架构过程:
复杂度的六个来源:
软件系统中高性能带来的复杂度主要体现在两方面:
计算机内部复杂度最关键的地方就是操作系统,计算机性能的发展本质上是由硬件发展驱动的,尤其是 CPU 的性能发展。而将硬件性能充分发挥出来的关键就是操作系统,所以操作系统本身也是随硬件的发展而发展的,操作系统是软件系统的运行环境,操作系统的复杂度直接决定了软件系统的复杂度。
操作系统和性能最相关的就是进程和线程。
进程:用进程来对应一个操作系统执行的任务,每个任务都有自己独立的内存空间,进程间互不相关,由操作系统来进行调度。
多进程:为了达到多进程并行运行的目的,采取了分时的方式,即把 CPU 的时间分成很多片段,每个片段只能执行某个进程中的指令。
进程间通信:为了解决进程在运行时相互通信的问题,进程间通信的各种方式被设计出来了,包括管道、消息队列、信号量、共享存储等。
多线程:多进程让多任务能够并行处理任务,但本身还有缺点,单个进程内部只能串行处理,而实际上很多进程内部的子任务并不要求是严格按照时间顺序来执行的,也需要并行处理。为了解决这个问题发明了线程,线程是进程内部的子任务,但这些子任务都共享同一份进程数据。为了保证数据的正确性,又发明了互斥锁机制。有了多线程后,操作系统调度的最小单位就变成了线程,而进程变成了操作系统分配资源的最小单位。
多核处理器:多进程多线程虽然让多任务并行处理的性能大大提升,但本质上还是分时系统,并不能做到时间上真正的并行。解决这个问题的方式显而易见,就是让多个 CPU 能够同时执行计算任务,从而实现真正意义上的多任务并行。
目前这样的解决方案有 3 种:
- SMP(Symmetric Multi-Processor,对称多处理器结构),最常见,主流的多核处理器方案
- NUMA(Non-Uniform Memory Access,非一致存储访问结构)
- MPP(Massive Parallel Processing,海量并行处理结构)
操作系统发展到现在,如果要完成一个高性能的软件系统,需要考虑如多进程、多线程、进程间通信、多线程并发等技术点,而且这些技术并不是最新的就是最好的,也不是非此即彼的选择。
在做架构设计的时候,需要花费很大的精力来结合业务进行分析、判断、选择、组合,这个过程同样很复杂。例如,下面的系统都实现了高性能,但是内部实现差异很大:
Nginx
可以用多进程也可以用多线程JBoss
采用的是多线程Redis
采用的是单进程Memcache
采用的是多线程让多台机器配合起来达到高性能的目的,是一个复杂的任务,常见的方式有:
任务可以指完整的业务处理,也可以指某个具体的任务。
业务量继续提升,需要增加任务分配器的数量。
微服务架构。
通过任务分配的方式,能够突破单台机器处理性能的瓶颈,通过增加更多的机器来满足业务的性能需求,但如果业务本身也越来越复杂,单纯只通过任务分配的方式来扩展性能,收益会越来越低。
通过这种任务分解的方式,能够把原来大一统但复杂的业务系统,拆分成小而简单但需要多个系统配合的业务系统。从业务的角度来看,任务分解既不会减少功能,也不会减少代码量(事实上代码量可能还会增加,因为从代码内部调用改为通过服务器之间的接口调用),任务分解能够提升性能的主要原因是:
最终决定业务处理性能的还是业务逻辑本身,业务逻辑本身没有发生大的变化下,理论上的性能是有一个上限的,系统拆分能够让性能逼近这个极限,但无法突破这个极限。因此,任务分解带来的性能收益是有一个度的,并不是任务分解越细越好,而对于架构设计来说,如何把握这个粒度就非常关键了。
系统无中断地执行其功能的能力,代表系统的可用性程度,是进行系统设计时的准则之一。
本质上都是通过“冗余”来实现高可用。高可用的“冗余”解决方案,单纯从形式上来看,和高性能是一样的,都是通过增加更多机器来达到目的,但其实本质上是有根本区别的:高性能增加机器目的在于“扩展”处理性能;高可用增加机器目的在于“冗余”处理单元。
通过冗余增强了可用性,但同时也带来了复杂性。
计算的特点是无论从哪台机器上进行计算,同样的算法和输入数据,产出的结果都是一样的,所以将计算从一台机器迁移到另一台对业务没有影响。
对于需要存储数据的系统来说,整个系统的高可用设计关键点和难点就在于“存储高可用”。存储与计算相比,有一个本质上的区别:将数据从一台机器搬到到另一台机器,需要经过线路进行传输。
线路的传输是有延迟的,速度在毫秒级别,距离越长延迟越高,各种异常情况(传输中断、丢包、拥塞)会导致延迟更高,对于高可用系统来说,意味着整个系统在某个时间点撒谎嗯,数据肯定是不一致的。。按照“数据 + 逻辑 = 业务”这个公式来套的话,数据不一致,即使逻辑一致,最后的业务表现就不一样了。
如果完全不做冗余,系统的整体高可用又无法保证,所以存储高可用的难点不在于如何备份数据,而在于如何减少或者规避数据不一致对业务造成的影响。
分布式领域中著名的 CAP 定理,从理论上论证了存储高可用的复杂度。存储高可用不可能同时满足“一致性、可用性、分区容错性”,最多满足其中两个,这就要求我们在做架构设计时结合业务进行取舍。
无论是计算高可用还是存储高可用,其基础都是“状态决策”,即系统需要能够判断当前的状态是正常还是异常,如果出现了异常就要采取行动来保证高可用。
如果状态决策本身都有错误或有偏差,那么后续的任何行动和处理都没有意义,但是通过冗余来实现的高可用系统,状态决策本质上不可能做到完全正确。
常见的几种决策方式包括:
存在一个独立的决策主体,称为“决策者”,负责收集信息然后进行决策;所有冗余的个体,称为“上报者”,都将状态信息发送给决策者。
独裁式的决策方式不会出现决策混乱的问题,因为只有一个决策者,但是当决策者本身故障时,整个系统就无法实现准确的状态决策。如果决策者本身又做一套状态决策,那就陷入一个递归的死循环了。
两个独立的个体通过交流信息,然后根据规则进行决策,最常用的协商式决策就是主备决策。这个架构的基本协商规则可以设计成:
协商式决策的架构不复杂,规则也不复杂,其难点在于,如果两者的信息交换出现问题(比如主备连接中断),此时状态决策应该怎么做。
协商式状态决策在某些场景总是存在一些问题的。
多个独立的个体通过投票的方式来进行状态决策。
民主式决策和协商式决策比较类似,其基础都是独立的个体之间交换信息,每个个体做出自己的决策,然后按照“多数取胜”的规则来确定最终的状态。不同点在于民主式决策比协商式决策要复杂得多,ZooKeeper
的选举算法 Paxos
,绝大部分人都看得云里雾里,更不用说用代码来实现这套算法了。除了算法复杂,民主式决策还有一个固有的缺陷:脑裂。
脑裂的根本原因是,原来统一的集群因为连接中断,造成了两个独立分隔的子集群,每个子集群单独进行选举,于是选出了 2 个主机。
为了解决脑裂问题,民主式决策的系统一般都采用“投票节点数必须超过系统总节点数一半”规则来处理。这种方式虽然解决了脑裂问题,但同时降低了系统整体的可用性,系统因为节点故障导致存活节点数少于一半,此时系统不会选出主节点,整个系统就相当于宕机了,尽管此时仍然有正常运行的节点。
可扩展性指系统为了应对将来需求变化而提供的一种扩展能力,当有新的需求出现时,系统不需要或者仅需要少量修改就可以支持,无须整个系统重构或者重建。
在软件开发领域,面向对象思想的提出,就是为了解决可扩展性带来的问题;设计模式更是将可扩展性做到了极致。
设计具备良好可扩展性的系统,有两个基本条件:
“唯一不变的是变化”,按照这个标准衡量,架构师每个设计方案都要考虑可扩展性。预测变化的复杂性在于:
如何把握预测的程度和提升预测结果的准确性,是一件很复杂的事情,而且没有通用的标准,更多是靠经验、直觉。
预测变化是一回事,采取什么方案来应对变化,又是另外一个复杂的事情。即使预测很准确,如果方案不合适,则系统扩展一样很麻烦。
微服务架构中的各层进行封装和隔离也是一种应对变化的解决方式。
第一种应对变化的常见方案是将“变化”封装在一个“变化层”,将不变的部分封装在一个独立的“稳定层”。
无论是变化层依赖稳定层,还是稳定层依赖变化层都是可以的,需要根据具体业务情况来设计。
无论采取哪种形式,通过剥离变化层和稳定层的方式应对变化,都会带来两个主要的复杂性相关的问题。
第二种常见的应对变化的方案是提炼出一个“抽象层”和一个“实现层”。
抽象层是稳定的,实现层可以根据具体业务需要定制开发,当加入新的功能时,只需要增加新的实现,无须修改抽象层。这种方案典型的实践就是设计模式和规则引擎。
以设计模式的“装饰者”模式来分析,下面是装饰者模式的类关系图。
图中的 Component
和 Decorator
就是抽象出来的规则,这个规则包括几部分:
这个规则一旦抽象出来后就固定了,不能轻易修改。
规则引擎和设计模式类似,都是通过灵活的设计来达到可扩展的目的,但“灵活的设计”本身就是一件复杂的事情。
设计“高性能”“高可用”的架构时,通用的手段都是增加更多服务器;而低成本则需要减少服务器的数量才能达成低成本的目标。
低成本本质上是与高性能和高可用冲突的,低成本很多时候不是架构设计的首要目标,而是架构设计的附加约束。
低成本给架构设计带来的主要复杂度体现在,往往只有“创新”才能达到低成本目标。
这里的“创新”既包括开创一个全新的技术领域,也包括引入新技术,如果没有找到能够解决自己问题的新技术,那么就真的需要自己创造新技术了。
新技术的例子:
- NoSQL(Memcache、Redis 等)为了解决关系型数据库无法应对高并发访问带来的访问压力。
- 全文搜索引擎(Sphinx、Elasticsearch、Solr)为了解决关系型数据库
like
搜索的低效的问题。- Hadoop 为了解决传统文件系统无法应对海量数据存储和计算的问题。
业界的例子:
- Facebook解决PHP低效问题,研发HipHop PHP(将 PHP 语言翻译为 C++ 语言执行)、HHVM(将 PHP 翻译为字节码然后由虚拟机执行)。
- 新浪微博将传统的
Redis/MC
+MySQL
方式,扩展为Redis/MC
+SSD Cache
+MySQL
方式,SSD Cache
作为L2
缓存使用,既解决了MC/Redis
成本过高,容量小的问题,也解决了穿透DB
带来的数据库访问压力。- Linkedin 为了处理每天 5 千亿的事件,开发了高效的 Kafka 消息系统。
- Ruby on Rails 改为 Java。
- Lua + redis 改为 Go 语言实现。
从技术的角度来讲,安全可以分为两类:
常见的 XSS 攻击、CSRF 攻击、SQL 注入、Windows 漏洞、密码破解等,本质上是因为系统实现有漏洞,黑客有了可乘之机,功能安全其实就是“防小偷”。
从实现的角度来看,功能安全更多地是和具体的编码相关,与架构关系不大。开发框架会内嵌常见的安全功能,但是开发框架本身也可能存在安全漏洞和风险。
所以功能安全是一个逐步完善的过程,而且往往都是在问题出现后才能有针对性的提出解决方案,我们永远无法预测系统下一个漏洞在哪里,也不敢说自己的系统肯定没有任何问题。
换句话讲,功能安全其实也是一个“攻”与“防”的矛盾,只能在这种攻防大战中逐步完善,不可能在系统架构设计的时候一劳永逸地解决。
如果说功能安全是“防小偷”,那么架构安全就是“防强盗”。
架构设计时需要特别关注架构安全,尤其是互联网时代,理论上来说系统部署在互联网上时,全球任何地方都可以发起攻击。
传统的架构安全主要依靠防火墙,防火墙最基本的功能就是隔离网络,通过将网络划分成不同的区域,制定出不同区域之间的访问控制策略来控制不同信任程度区域间传送的数据流。
防火墙的功能虽然强大,但性能一般,所以在传统的银行和企业应用领域应用较多。但在互联网领域,防火墙的应用场景并不多。
互联网系统的架构安全目前并没有太好的设计手段来实现,更多地是依靠运营商或者云服务商强大的带宽和流量清洗的能力,较少自己来设计和实现。
规模带来复杂度的主要原因就是“量变引起质变”,当数量超过一定的阈值后,复杂度会发生质的变化。常见的规模带来的复杂度有:
对于编程来说,本质上是确定的,对于架构来说,本质上是不确定的。相比编程来说,架构设计并没有像编程语言那样的语法来进行约束,更多的时候是面对多种可能性时进行选择。
合适原则、简单原则、演化原则,架构设计时遵循这几个原则,有助于做出最好的选择。
合适优于业界领先。
真正优秀的架构都是在企业当前人力、条件、业务等各种约束下设计出来的,能够合理地将资源整合在一起并发挥出最大功效,并且能够快速落地。
简单优于复杂。
“复杂”在制造领域代表先进,在建筑领域代表领先,但在软件领域,却恰恰相反,代表的是“问题”。软件领域的复杂性体现在两个方面:
架构设计时如果简单的方案和复杂的方案都可以满足需求,最好选择简单的方案,遵循KISS(Keep It Simple,Stupid!)原则进行架构设计。
演化优于一步到位。
从和目的、主题、材料和结构的联系上来说,软件架构可以和建筑物的架构相比拟。然而,字面意思上的相似性却掩盖了一个本质上的差异:建筑一旦完成(甚至一旦开建)就不可再变,而软件却需要根据业务的发展不断地变化!
对于建筑来说,永恒是主题;而对于软件来说,变化才是主题,软件架构需要根据业务发展不断变化。软件架构设计更类似于大自然“设计”一个生物,通过演化让生物适应环境,逐步变得更加强大:
软件架构设计同样是类似的过程:
在进行架构设计时应该: