【从0开始学架构笔记】01 基础架构

文章目录

    • 一、架构的定义
      • 1. 系统与子系统
      • 2. 模块与组件
      • 3. 框架与架构
      • 4. 重新定义架构
    • 二、架构设计的目的
    • 三、复杂度来源:高性能
      • 1. 单机复杂度
      • 2. 集群复杂度
        • 2.1 任务分配
        • 2.2 任务分解(微服务)
    • 四、复杂度来源:高可用
      • 1. 计算高可用(业务)
      • 2. 存储高可用
      • 3. 高可用状态决策
        • 3.1 独裁式
        • 3.2 协商式
        • 3.3 民主式
    • 五、复杂度来源:可扩展性
      • 1. 预测变化
      • 2. 应对变化
        • 2.1 将“变化”封装在一个“变化层”,将不变的部分封装在一个独立的“稳定层”。
        • 2.2 提炼出一个“抽象层”和一个“实现层”
    • 六、复杂度来源:低成本、安全、规模
      • 1. 低成本
      • 2. 安全
        • 2.1 功能安全
        • 2.2 架构安全
      • 3. 规模
        • 3.1 功能越来越多,导致系统复杂度指数级上升
        • 3.2 数据越来越多,系统复杂度发生质变
    • 七、架构设计的三原则及案例
      • 1. 合适原则
      • 2. 简单原则
        • 2.1 结构的复杂性
        • 2.2 逻辑的复杂性
      • 3. 演化原则
      • 4. 案例
        • 4.1 案例一:淘宝
        • 4.2 案例二:手机QQ
    • 八、架构设计流程
      • 1. 识别复杂度
      • 2. 设计备选方案
        • 2.1 常见错误一:设计最优秀的方案
        • 2.2 常见错误二:只做一个方案
        • 2.3 常见错误三:备选方案过于详细
      • 3. 评估和选择备选方案
      • 4. 详细方案设计

一、架构的定义

1. 系统与子系统

  • 关联:系统是由一群有关联的个体组成的,没有关联的个体堆在一起不能成为一个系统。
  • 规则:系统内的个体需要按照指定的规则运作,而不是单个个体各自为政。规则规定了系统内个体分工和协作的方式。
  • 能力:系统能力与个体能力有本质的差别,系统能力不是个体能力之和,而是产生了新的能力。

子系统的定义和系统定义是一样的,只是观察的角度有差异,一个系统可能是另外一个更大系统的子系统。

2. 模块与组件

模块和组件都是系统的组成部分,只是从不同的角度拆分系统而已。
从逻辑的角度来拆分系统后,得到的单元就是“模块”;从物理的角度来拆分系统后,得到的单元就是“组件”。划分模块的主要目的是职责分离;划分组件的主要目的是单元复用。其实,“组件”的英文component也可翻译成中文的“零件”一词,“零件”更容易理解一些,“零件”是一个物理的概念,并且具备“独立且可替换”的特点。

3. 框架与架构

软件框架(Software framework)通常指的是为了实现某个业界标准或完成特定基本任务的软件组件规范,也指为了实现某个软件组件规范时,提供规范所要求之基础功能的软件产品
提炼一下其中关键部分:

  1. 框架是组件规范:例如,MVC就是一种最常见的开发规范,类似的还有MVP、MVVM、J2EE等框架。
  2. 框架提供基础功能的产品:例如,Spring MVC是MVC的开发框架,除了满足MVC的规范,Spring提供了很多基础功能来帮助我们实现功能,包括注解(@Controller等)、Spring Security、Spring JPA等很多基础功能。

软件架构指软件系统的“基础结构”,创造这些基础结构的准则,以及对这些结构的描述。

框架关注的是“规范”,架构关注的是“结构”。框架的英文是Framework,架构的英文是Architecture。

4. 重新定义架构

软件架构指软件系统的顶层结构

首先,“系统是一群关联个体组成”,这些“个体”可以是“子系统”“模块”“组件”等;架构需要明确系统包含哪些“个体”。
其次,系统中的个体需要“根据某种规则”运作,架构需要明确个体运作和协作的规则。
第三,维基百科定义的架构用到了“基础结构”这个说法,我改为“顶层结构”,可以更好地区分系统和子系统,避免将系统架构和子系统架构混淆在一起导致架构层次混乱。

二、架构设计的目的

架构设计的主要目的是为了解决软件系统复杂度带来的问题。

首先,遵循这条准则能够让“新手”架构师心中有数,而不是一头雾水。

其次,遵循这条准则能够让“老鸟”架构师有的放矢,而不是贪大求全。

复杂度分析的几个角度:

  • 高性能
  • 高可用
  • 可扩展性
  • 成本
  • 安全性
  • 规模

三、复杂度来源:高性能

软件系统中高性能带来的复杂度主要体现在两方面

  • 一方面是单台计算机内部为了高性能带来的复杂度
  • 另一方面是多台计算机集群为了高性能带来的复杂度

1. 单机复杂度

操作系统是软件系统的运行环境,操作系统的复杂度直接决定了软件系统的复杂度。操作系统和性能最相关的就是进程线程

操作系统发展到现在,如果我们要完成一个高性能的软件系统,需要考虑如多进程、多线程、进程间通信、多线程并发等技术点,而且这些技术并不是最新的就是最好的,也不是非此即彼的选择。在做架构设计的时候,需要花费很大的精力来结合业务进行分析、判断、选择、组合,这个过程同样很复杂。举一个最简单的例子:Nginx可以用多进程也可以用多线程,JBoss采用的是多线程;Redis采用的是单进程,Memcache采用的是多线程,这些系统都实现了高性能,但内部实现差异却很大。

2. 集群复杂度

2.1 任务分配

【从0开始学架构笔记】01 基础架构_第1张图片

“任务”涵盖的范围很广,可以指完整的业务处理,也可以单指某个具体的任务。例如,“存储”“运算”“缓存”等都可以作为一项任务,因此存储系统、运算系统、缓存系统都可以按照任务分配的方式来搭建架构。此外,“任务分配器”也并不一定只能是物理上存在的机器或者一个独立运行的程序,也可以是嵌入在其他程序中的算法,例如Memcache的集群架构。

2.2 任务分解(微服务)

【从0开始学架构笔记】01 基础架构_第2张图片

通过这种任务分解的方式,能够把原来大一统但复杂的业务系统,拆分成小而简单但需要多个系统配合的业务系统。从业务的角度来看,任务分解既不会减少功能,也不会减少代码量(事实上代码量可能还会增加,因为从代码内部调用改为通过服务器之间的接口调用),那为何通过任务分解就能够提升性能呢?

主要有几方面的因素:

  • 简单的系统更加容易做到高性能
  • 可以针对单个任务进行扩展

虽然系统拆分可能在某种程度上能提升业务处理性能,但提升性能也是有限的,因为最终决定业务处理性能的还是业务逻辑本身,业务逻辑本身没有发生大的变化下,理论上的性能是有一个上限的,系统拆分能够让性能逼近这个极限,但无法突破这个极限。因此,任务分解带来的性能收益是有一个度的,并不是任务分解越细越好,而对于架构设计来说,如何把握这个粒度就非常关键了。

四、复杂度来源:高可用

系统无中断地执行其功能的能力,代表系统的可用性程度,是进行系统设计时的准则之一。
这个定义的关键在于“无中断”,但恰好难点也在“无中断”上面。

系统的高可用方案五花八门,但万变不离其宗,本质上都是通过“冗余”来实现高可用。通俗点来讲,就是一台机器不够就两台,两台不够就四台;一个机房可能断电,那就部署两个机房;一条通道可能故障,那就用两条,两条不够那就用三条(移动、电信、联通一起上)。高可用的“冗余”解决方案,单纯从形式上来看,和之前讲的高性能是一样的,都是通过增加更多机器来达到目的,但其实本质上是有根本区别的:高性能增加机器目的在于“扩展”处理性能;高可用增加机器目的在于“冗余”处理单元

1. 计算高可用(业务)

这里的“计算”指的是业务的逻辑处理。计算有一个特点就是无论在哪台机器上进行计算,同样的算法和输入数据,产出的结果都是一样的

高可用集群具体应该采用哪种分配方式,需要结合实际业务需求来分析和判断,并不存在某种算法就一定优于另外的算法。例如,ZooKeeper采用的就是1主多备,而Memcached采用的就是全主0备。

2. 存储高可用

存储与计算相比,有一个本质上的区别:将数据从一台机器搬到到另一台机器,需要经过线路进行传输。线路传输的速度是毫秒级别,同一机房内部能够做到几毫秒;分布在不同地方的机房,传输耗时需要几十甚至上百毫秒。对于高可用系统来说,这意味着整个系统在某个时间点上,数据肯定是不一致的。

除了物理上的传输速度限制,传输线路本身也存在可用性问题,传输线路可能中断、可能拥塞、可能异常(错包、丢包),并且传输线路的故障时间一般都特别长,短的十几分钟,长的几个小时都是可能的。

综合分析,无论是正常情况下的传输延迟,还是异常情况下的传输中断,都会导致系统的数据在某个时间点或者时间段是不一致的,而数据的不一致又会导致业务问题;但如果完全不做冗余,系统的整体高可用又无法保证,所以存储高可用的难点不在于如何备份数据,而在于如何减少或者规避数据不一致对业务造成的影响

分布式领域里面著名的CAP定理,从理论上论证了存储高可用的复杂度。也就是说,存储高可用不可能同时满足“一致性、可用性、分区容错性”,最多满足其中两个,这就要求我们在做架构设计时结合业务进行取舍。

3. 高可用状态决策

无论是计算高可用还是存储高可用,其基础都是“状态决策”,即系统需要能够判断当前的状态是正常还是异常,如果出现了异常就要采取行动来保证高可用。如果状态决策本身都是有错误或者有偏差的,那么后续的任何行动和处理无论多么完美也都没有意义和价值。但在具体实践的过程中,恰好存在一个本质的矛盾:通过冗余来实现的高可用系统,状态决策本质上就不可能做到完全正确。下面基于几种常见的决策方式进行详细分析。

3.1 独裁式

独裁式决策指的是存在一个独立的决策主体称为“决策者”,负责收集信息然后进行决策;所有冗余的个体称为“上报者”,都将状态信息发送给决策者。

【从0开始学架构笔记】01 基础架构_第3张图片
当决策者本身故障时,整个系统就无法实现准确的状态决策。如果决策者本身又做一套状态决策,那就陷入一个递归的死循环了。

3.2 协商式

协商式决策指的是两个独立的个体通过交流信息,然后根据规则进行决策,最常用的协商式决策就是主备决策

这个架构的基本协商规则可以设计成:

  • 2台服务器启动时都是备机。
  • 2台服务器建立连接。
  • 2台服务器交换状态信息。
  • 某1台服务器做出决策,成为主机;另一台服务器继续保持备机身份。

如果两者的信息交换出现问题(比如主备连接中断),此时状态决策应该怎么做。

  1. 如果备机在连接中断的情况下认为主机故障,那么备机需要升级为主机,但实际上此时主机并没有故障,那么系统就出现了两个主机,这与设计初衷(1主1备)是不符合的;
  2. 如果备机在连接中断的情况下不认为主机故障,则此时如果主机真的发生故障,那么系统就没有主机了,这同样与设计初衷(1主1备)是不符合的;
  3. 如果为了规避连接中断对状态决策带来的影响,可以增加更多的连接。例如,双连接、三连接。这样虽然能够降低连接中断对状态带来的影响(注意:只能降低,不能彻底解决),但同时又引入了这几条连接之间信息取舍的问题,即如果不同连接传递的信息不同,应该以哪个连接为准?实际上这也是一个无解的答案,无论以哪个连接为准,在特定场景下都可能存在问题。

3.3 民主式

民主式决策指的是多个独立的个体通过投票的方式来进行状态决策。例如,ZooKeeper集群在选举leader时就是采用这种方式。

民主式决策和协商式决策比较类似,其基础都是独立的个体之间交换信息,每个个体做出自己的决策,然后按照“多数取胜”的规则来确定最终的状态。不同点在于民主式决策比协商式决策要复杂得多。

除了算法复杂,民主式决策还有一个固有的缺陷:脑裂。脑裂的根本原因是,原来统一的集群因为连接中断,造成了两个独立分隔的子集群,每个子集群单独进行选举,于是选出了2个主机,相当于人体有两个大脑了。
【从0开始学架构笔记】01 基础架构_第4张图片

为了解决脑裂问题,民主式决策的系统一般都采用“投票节点数必须超过系统总节点数一半”规则来处理。如图中那种情况,节点4和节点5形成的子集群总节点数只有2个,没有达到总节点数5个的一半,因此这个子集群不会进行选举。这种方式虽然解决了脑裂问题,但同时降低了系统整体的可用性,即如果系统不是因为脑裂问题导致投票节点数过少,而真的是因为节点故障(例如,节点1、节点2、节点3真的发生了故障),此时系统也不会选出主节点,整个系统就相当于宕机了,尽管此时还有节点4和节点5是正常的。

五、复杂度来源:可扩展性

设计具备良好可扩展性的系统,有两个基本条件:正确预测变化完美封装变化

1. 预测变化

预测变化的复杂性在于:

  • 不能每个设计点都考虑可扩展性。
  • 不能完全不考虑可扩展性。
  • 所有的预测都存在出错的可能性。

对于架构师来说,如何把握预测的程度和提升预测结果的准确性,是一件很复杂的事情,而且没有通用的标准可以简单套上去,更多是靠自己的经验、直觉,所以架构设计评审的时候经常会出现两个设计师对某个判断争得面红耳赤的情况,原因就在于没有明确标准,不同的人理解和判断有偏差,而最终又只能选择一个判断。

2. 应对变化

2.1 将“变化”封装在一个“变化层”,将不变的部分封装在一个独立的“稳定层”。

【从0开始学架构笔记】01 基础架构_第5张图片
无论是变化层依赖稳定层,还是稳定层依赖变化层都是可以的,需要根据具体业务情况来设计。例如,如果系统需要支持XML、JSON、ProtocolBuffer三种接入方式,那么最终的架构就是上面图中的“形式1”架构,也就是下面这样。
【从0开始学架构笔记】01 基础架构_第6张图片
如果系统需要支持MySQL、Oracle、DB2数据库存储,那么最终的架构就变成了“形式2”的架构了,可以看下面这张图。

【从0开始学架构笔记】01 基础架构_第7张图片
通过剥离变化层和稳定层的方式应对变化,都会带来两个主要的复杂性相关的问题。

  1. 系统需要拆分出变化层和稳定层
    对于哪些属于变化层,哪些属于稳定层,很多时候并不是像前面的示例(不同接口协议或者不同数据库)那样明确,不同的人有不同的理解,导致架构设计评审的时候可能吵翻天。
  2. 需要设计变化层和稳定层之间的接口
    接口设计同样至关重要,对于稳定层来说,接口肯定是越稳定越好;但对于变化层来说,在有差异的多个实现方式中找出共同点,并且还要保证当加入新的功能时原有的接口设计不需要太大修改,这是一件很复杂的事情。

2.2 提炼出一个“抽象层”和一个“实现层”

抽象层是稳定的,实现层可以根据具体业务需要定制开发,当加入新的功能时,只需要增加新的实现,无须修改抽象层。这种方案典型的实践就是设计模式和规则引擎。

六、复杂度来源:低成本、安全、规模

1. 低成本

低成本本质上是与高性能和高可用冲突的,所以低成本很多时候不会是架构设计的首要目标,而是架构设计的附加约束。也就是说,我们首先设定一个成本目标,当我们根据高性能、高可用的要求设计出方案时,评估一下方案是否能满足成本目标,如果不行,就需要重新设计架构;如果无论如何都无法设计出满足成本要求的方案,那就只能找老板调整成本目标了。

低成本给架构设计带来的主要复杂度体现在,往往只有“创新”才能达到低成本目标。这里的“创新”既包括开创一个全新的技术领域(这个要求对绝大部分公司太高),也包括引入新技术,如果没有找到能够解决自己问题的新技术,那么就真的需要自己创造新技术了。

相比来说,创造新技术复杂度更高,因此一般中小公司基本都是靠引入新技术来达到低成本的目标;而大公司更有可能自己去创造新的技术来达到低成本的目标,因为大公司才有足够的资源、技术和时间去创造新技术。

2. 安全

从技术的角度来讲,安全可以分为两类:一类是功能上的安全,一类是架构上的安全。

2.1 功能安全

例如,常见的XSS攻击、CSRF攻击、SQL注入、Windows漏洞、密码破解等,本质上是因为系统实现有漏洞,黑客有了可乘之机。黑客会利用各种漏洞潜入系统,这种行为就像小偷一样,黑客和小偷的手法都是利用系统或家中不完善的地方潜入,并进行破坏或者盗取。因此形象地说,功能安全其实就是“防小偷”
从实现的角度来看,功能安全更多地是和具体的编码相关,与架构关系不大。现在很多开发框架都内嵌了常见的安全功能,能够大大减少安全相关功能的重复开发,但框架只能预防常见的安全漏洞和风险(常见的XSS攻击、CSRF攻击、SQL注入等),无法预知新的安全问题,而且框架本身很多时候也存在漏洞(例如,流行的Apache Struts2就多次爆出了调用远程代码执行的高危漏洞,给整个互联网都造成了一定的恐慌)。所以功能安全是一个逐步完善的过程,而且往往都是在问题出现后才能有针对性的提出解决方案,我们永远无法预测系统下一个漏洞在哪里,也不敢说自己的系统肯定没有任何问题。换句话讲,功能安全其实也是一个“攻”与“防”的矛盾,只能在这种攻防大战中逐步完善,不可能在系统架构设计的时候一劳永逸地解决。

2.2 架构安全

架构安全就是“防强盗”,强盗会直接用大锤将门砸开,或者用炸药将围墙炸倒;小偷是偷东西,而强盗很多时候就是故意搞破坏,对系统的影响也大得多。因此架构设计时需要特别关注架构安全,尤其是互联网时代,理论上来说系统部署在互联网上时,全球任何地方都可以发起攻击。
传统的架构安全主要依靠防火墙,防火墙最基本的功能就是隔离网络,通过将网络划分成不同的区域,制定出不同区域之间的访问控制策略来控制不同信任程度区域间传送的数据流。
防火墙的功能虽然强大,但性能一般,所以在传统的银行和企业应用领域应用较多。但在互联网领域,防火墙的应用场景并不多。因为互联网的业务具有海量用户访问和高并发的特点,防火墙的性能不足以支撑;尤其是互联网领域的DDoS攻击,轻则几GB,重则几十GB。
公司一般不会堆防火墙来防DDoS攻击,因为DDoS攻击最大的影响是大量消耗机房的出口总带宽。不管防火墙处理能力有多强,当出口带宽被耗尽时,整个业务在用户看来就是不可用的,因为用户的正常请求已经无法到达系统了。防火墙能够保证内部系统不受冲击,但用户也是进不来的。对于用户来说,业务都已经受到影响了,至于是因为用户自己进不去,还是因为系统出故障,用户其实根本不会关心。
基于上述原因,互联网系统的架构安全目前并没有太好的设计手段来实现,更多地是依靠运营商或者云服务商强大的带宽和流量清洗的能力,较少自己来设计和实现。

3. 规模

规模带来复杂度的主要原因就是“量变引起质变”,当数量超过一定的阈值后,复杂度会发生质的变化。常见的规模带来的复杂度有:

3.1 功能越来越多,导致系统复杂度指数级上升

随着系统功能数量增多,功能之间的连接呈指数级增长。下图形象地展示了功能数量的增多带来了复杂度。
【从0开始学架构笔记】01 基础架构_第8张图片
【从0开始学架构笔记】01 基础架构_第9张图片

3.2 数据越来越多,系统复杂度发生质变

数据太多以后,传统的数据收集、加工、存储、分析的手段和工具已经无法适应,必须应用新的技术才能解决。目前的大数据理论基础是Google发表的三篇大数据相关论文,其中Google File System是大数据文件存储的技术理论,Google Bigtable是列式数据存储的技术理论,Google MapReduce是大数据运算的技术理论,这三篇技术论文各自开创了一个新的技术领域。

即使数据没有达到大数据规模,数据的增长也可能给系统带来复杂性。最典型的例子莫过于使用关系数据库存储数据,以MySQL为例,MySQL单表的数据因不同的业务和应用场景会有不同的最优值,但不管怎样都肯定是有一定的限度的,一般推荐在5000万行左右。如果因为业务的发展,单表数据达到了10亿行,就会产生很多问题,例如:

  • 添加索引会很慢,可能需要几个小时,这几个小时内数据库表是无法插入数据的,相当于业务停机了。
  • 修改表结构和添加索引存在类似的问题,耗时可能会很长。
  • 即使有索引,索引的性能也可能会很低,因为数据量太大。
  • 数据库备份耗时很长。
  • ……

因此,当MySQL单表数据量太大时,我们必须考虑将单表拆分为多表,这个拆分过程也会引入更多复杂性,例如:

  • 拆表的规则是什么?
    以用户表为例:是按照用户id拆分表,还是按照用户注册时间拆表?
  • 拆完表后查询如何处理?
    以用户表为例:假设按照用户id拆表,当业务需要查询学历为“本科”以上的用户时,要去很多表查询才能得到最终结果,怎么保证性能?

七、架构设计的三原则及案例

优秀程序员和架构师之间还有一个明显的鸿沟需要跨越,这个鸿沟就是“不确定性”。

对于编程来说,本质上是不能存在不确定的。而对于架构设计来说,本质上是不确定的。相比编程来说,架构设计并没有像编程语言那样的语法来进行约束,更多的时候是面对多种可能性时进行选择。这个问题的关键原因在于架构设计领域并没有一套通用的规范来指导架构师进行架构设计,更多是依赖架构师的经验和直觉,因此架构设计有时候也会被看作一项比较神秘的工作。

业务千变万化,技术层出不穷,设计理念也是百花齐放,看起来似乎很难有一套通用的规范来适用所有的架构设计场景。但是有几个共性的原则隐含其中,这就是:合适原则、简单原则、演化原则,架构设计时遵循这几个原则,有助于做出最好的选择。

面对“不确定性”时架构设计的三原则,分别是合适优于业界领先简单优于复杂演化优于一步到位

1. 合适原则

合适原则宣言:“合适优于业界领先”

再好的梦想,也需要脚踏实地实现!这里的“脚踏实地”主要体现在下面几个方面。

  1. 将军难打无兵之仗:没那么多人,却想干那么多活,是失败的第一个主要原因。
  2. 罗马不是一天建成的:没有那么多积累,却想一步登天,是失败的第二个主要原因。
  3. 冰山下面才是关键:没有那么卓越的业务场景,却幻想灵光一闪成为天才,是失败的第三个主要原因。

真正优秀的架构都是在企业当前人力、条件、业务等各种约束下设计出来的,能够合理地将资源整合在一起并发挥出最大功效,并且能够快速落地。这也是很多BAT出来的架构师到了小公司或者创业团队反而做不出成绩的原因,因为没有了大公司的平台、资源、积累,只是生搬硬套大公司的做法,失败的概率非常高。

2. 简单原则

简单原则宣言:“简单优于复杂”。

“复杂”在制造领域代表先进,在建筑领域代表领先,但在软件领域,却恰恰相反,代表的是“问题”。软件领域的复杂性体现在两个方面:

2.1 结构的复杂性

结构复杂的系统几乎毫无例外具备两个特点:

  • 组成复杂系统的组件数量更多;
  • 同时这些组件之间的关系也更加复杂。

结构上的复杂性存在的第一个问题是,组件越多,就越有可能其中某个组件出现故障,从而导致系统故障。

结构上的复杂性存在的第二个问题是,某个组件改动,会影响关联的所有组件,这些被影响的组件同样会继续递归影响更多的组件。这个问题会影响整个系统的开发效率,因为一旦变更涉及外部系统,需要协调各方统一进行方案评估、资源协调、上线配合。

结构上的复杂性存在的第三个问题是,定位一个复杂系统中的问题总是比简单系统更加困难。首先是组件多,每个组件都有嫌疑,因此要逐一排查;其次组件间的关系复杂,有可能表现故障的组件并不是真正问题的根源。

2.2 逻辑的复杂性

意识到结构的复杂性后,我们的第一反应可能就是“降低组件数量”,毕竟组件数量越少,系统结构越简单。最简单的结构就是整个系统只有一个组件,即系统本身,所有的功能和逻辑都在这一个组件中实现。

不过这样做是行不通的,原因在于除了结构的复杂性,还有逻辑的复杂性,即如果某个组件的逻辑太复杂,一样会带来各种问题。逻辑复杂几乎会导致软件工程的每个环节都有问题。

功能复杂的组件,另外一个典型特征就是采用了复杂的算法。复杂算法导致的问题主要是难以理解,进而导致难以实现、难以修改,并且出了问题难以快速解决。

综合来看,无论是结构的复杂性,还是逻辑的复杂性,都会存在各种问题,所以架构设计时如果简单的方案和复杂的方案都可以满足需求,最好选择简单的方案。《UNIX编程艺术》总结的KISS(Keep It Simple, Stupid!)原则一样适应于架构设计。

3. 演化原则

演化原则宣言:“演化优于一步到位”

对于建筑来说,永恒是主题;而对于软件来说,变化才是主题。软件架构需要根据业务的发展而不断变化。

如果没有把握“软件架构需要根据业务发展不断变化”这个本质,在做架构设计的时候就很容易陷入一个误区:试图一步到位设计一个软件架构,期望不管业务如何变化,架构都稳如磐石。

考虑到软件架构需要根据业务发展不断变化这个本质特点,软件架构设计其实更加类似于大自然“设计”一个生物,通过演化让生物适应环境,逐步变得更加强大,软件架构设计同样是类似的过程:

  • 首先,设计出来的架构要满足当时的业务需要。
  • 其次,架构要不断地在实际应用过程中迭代,保留优秀的设计,修复有缺陷的设计,改正错误的设计,去掉无用的设计,使得架构逐渐完善。
  • 第三,当业务发生变化时,架构要扩展、重构,甚至重写;代码也许会重写,但有价值的经验、教训、逻辑、设计等(类似生物体内的基因)却可以在新架构中延续。

架构师在进行架构设计时需要牢记这个原则,时刻提醒自己不要贪大求全,或者盲目照搬大公司的做法。应该认真分析当前业务的特点,明确业务面临的主要问题,设计合理的架构,快速落地以满足业务需要,然后在运行过程中不断完善架构,不断随着业务演化架构。

即使是大公司的团队,在设计一个新系统的架构时,也需要遵循演化的原则,而不应该认为团队人员多、资源多,不管什么系统上来就要一步到位,因为业务的发展和变化是很快的,不管多牛的团队,也不可能完美预测所有的业务发展和变化路径。

4. 案例

即使是现在非常复杂、非常强大的架构,也并不是一开始就进行了复杂设计,而是首先采取了简单的方式(简单原则),满足了当时的业务需要(合适原则),随着业务的发展逐步演化而来的(演化原则)。

4.1 案例一:淘宝

淘宝技术发展主要经历了“个人网站”→“Oracle/支付宝/旺旺”→“Java时代1.0”→“Java时代2.0”→“Java时代3.0”→“分布式时代”。

  1. 个人网站

买一个系统是为了“快速可用”,而买一个轻量级的系统是为了“快速开发”。因为系统上线后肯定有大量的需求需要做,这时能够快速开发就非常重要。

从这个实例我们可以看到:淘宝最开始的时候业务要求就是“快”,因此反过来要求技术同样要“快”,业务决定技术,这里架构设计和选择主要遵循的是“合适原则”和“简单原则”。

  1. Oracle/支付宝/旺旺

技术的替代方案非常简单,就是换成Oracle。换Oracle的原因除了它容量大、稳定、安全、性能高,还有人才方面的原因。

此时离刚上线才半年不到,业务飞速发展,最快的方式支撑业务的发展还是去买。如果说第一阶段买的是“方案”,这个阶段买的就是“性能”,这里架构设计和选择主要遵循的还是“合适原则”和“简单原则”。

  1. Java时代1.0

淘宝切换到Java的原因很有趣,主要因为找了一个PHP的开源连接池SQL Relay连接到Oracle,而这个代理经常死锁,死锁了就必须重启,而数据库又必须用Oracle,于是决定换个开发语言。

这次切换的最主要原因是因为技术影响了业务的发展,频繁的死锁和重启对用户业务产生了严重的影响,从业务的角度来看这是不得不解决的技术问题。而且Java是当时最成熟的网站开发语言,它有比较良好的企业开发框架,被世界上主流的大规模网站普遍采用,另外有Java开发经验的人才也比较多,后续维护成本会比较低。

综合来看,这次架构的变化没有再简单通过“买”来解决,而是通过重构来解决,架构设计和选择遵循了“演化原则”。

  1. Java时代2.0

随着数据量的继续增长,采取“买”的方式已无法解决容量、性能、成本问题。

另外,随着规模的增大,纯粹靠买的一个典型问题开始成为重要的考虑因素,那就是成本。这就是“量变带来质变”的一个典型案例,业务和系统发生质变后,架构设计遵循“演化原则”的思想,需要再一次重构甚至重写。

  1. Java 时代3.0和分布式时代

到了这个阶段,业务规模急剧上升后,原来并不是主要复杂度的IOE成本开始成为了主要的问题,因此通过自研系统来降低IOE的成本,去IOE也是系统架构的再一次演化。

4.2 案例二:手机QQ

手机QQ的发展历程按照用户规模可以粗略划分为4个阶段:十万级、百万级、千万级、亿级,不同的用户规模,IM后台的架构也不同,而且基本上都是用户规模先上去,然后产生各种问题,倒逼技术架构升级。

  1. 十万级IM 1.X
    【从0开始学架构笔记】01 基础架构_第10张图片

当时业务刚开始,架构设计遵循的是“合适原则”和“简单原则”。

  1. 百万级IM 2.X

【从0开始学架构笔记】01 基础架构_第11张图片
按照“演化原则”的指导进行了重构,重构的方案相比现在来说也还是简单得多,因此当时做架构设计时也遵循了“合适原则”和“简单原则”。

  1. 千万级IM 3.X
    【从0开始学架构笔记】01 基础架构_第12张图片
    架构需要继续改造升级,再一次“演化”。可以看到这次的方案相比之前的方案来说并不简单了,这是业务特性决定的。

  2. 亿级IM 4.X

IM后台从1.0到3.5都是在原来基础上做改造升级的,但是持续打补丁已经难以支撑亿级在线,IM后台4.0必须从头开始,重新设计实现!这里再次遵循了“演化原则”。
重新设计的IM 4.0架构如图所示,和之前的架构相比,架构本身都拆分为两个主要的架构:存储架构和通信架构。

(1)存储架构

(2)通信架构

【从0开始学架构笔记】01 基础架构_第13张图片

八、架构设计流程

1. 识别复杂度

在设计架构时,首先就要分析系统的复杂性。只有正确分析出了系统的复杂性,后续的架构设计方案才不会偏离方向;否则,如果对系统的复杂性判断错误,即使后续的架构设计方案再完美再先进,都是南辕北辙,做的越好,错的越多、越离谱。

架构的复杂度主要来源于“高性能”“高可用”“可扩展”等几个方面,但架构师在具体判断复杂性的时候,不能生搬硬套,认为任何时候架构都必须同时满足这三方面的要求。实际上大部分场景下,复杂度只是其中的某一个,少数情况下包含其中两个,如果真的出现同时需要解决三个或者三个以上的复杂度,要么说明这个系统之前设计的有问题,要么可能就是架构师的判断出现了失误,即使真的认为要同时满足这三方面的要求,也必须要进行优先级排序。

如果运气真的不好,接手了一个每个复杂度都存在问题的系统,也要一个个来解决问题,不要幻想一次架构重构解决所有问题。如果同时要解决这些问题,就可能会面临这些困境:

  • 要做的事情太多,反而感觉无从下手。
  • 设计方案本身太复杂,落地时间遥遥无期。
  • 同一个方案要解决不同的复杂性,有的设计点是互相矛盾的。例如,要提升系统可用性,就需要将数据及时存储到硬盘上,而硬盘刷盘反过来又会影响系统性能。

因此,正确的做法是将主要的复杂度问题列出来,然后根据业务、技术、团队等综合情况进行排序,优先解决当前面临的最主要的复杂度问题

对于按照复杂度优先级解决的方式,存在一个普遍的担忧:如果按照优先级来解决复杂度,可能会出现解决了优先级排在前面的复杂度后,解决后续复杂度的方案需要将已经落地的方案推倒重来。这个担忧理论上是可能的,但现实中几乎是不可能出现的,原因在于软件系统的可塑性和易变性。对于同一个复杂度问题,软件系统的方案可以有多个,总是可以挑出综合来看性价比最高的方案。

即使架构师决定要推倒重来,这个新的方案也必须能够同时解决已经被解决的复杂度问题,一般来说能够达到这种理想状态的方案基本都是依靠新技术的引入。例如,Hadoop能够将高可用、高性能、大容量三个大数据处理的复杂度问题同时解决。

识别复杂度对架构师来说是一项挑战,因为原始的需求中并没有哪个地方会明确地说明复杂度在哪里,需要架构师在理解需求的基础上进行分析。有经验的架构师可能一看需求就知道复杂度大概在哪里;如果经验不足,那只能采取“排查法”,从不同的角度逐一进行分析。

2. 设计备选方案

架构师的工作并不神秘,成熟的架构师需要对已经存在的技术非常熟悉,对已经经过验证的架构模式烂熟于心,然后根据自己对业务的理解,挑选合适的架构模式进行组合,再对组合后的方案进行修改和调整。

虽说基于已有的技术或者架构模式进行组合,然后调整,大部分情况下就能够得到我们需要的方案,但并不意味着架构设计是一件很简单的事情。因为可选的模式有很多,组合的方案更多,往往一个问题的解决方案有很多个;如果再在组合的方案上进行一些创新,解决方案会更多。因此,如何设计最终的方案,并不是一件容易的事情,这个阶段也是很多架构师容易犯错的地方。

2.1 常见错误一:设计最优秀的方案

根据架构设计原则中“合适原则”和“简单原则“的要求,挑选合适自己业务、团队、技术能力的方案才是好方案;否则要么浪费大量资源开发了无用的系统(例如,之前提过的“亿级用户平台”的案例,设计了TPS 50000的系统,实际TPS只有500),要么根本无法实现(例如,10个人的团队要开发现在的整个淘宝系统)。

2.2 常见错误二:只做一个方案

这样做有很多弊端:

  • 心里评估过于简单,可能没有想得全面,只是因为某一个缺点就把某个方案给否决了,而实际上没有哪个方案是完美的,某个地方有缺点的方案可能是综合来看最好的方案。
  • 架构师再怎么牛,经验知识和技能也有局限,有可能某个评估的标准或者经验是不正确的,或者是老的经验不适合新的情况,甚至有的评估标准是架构师自己原来就理解错了。
  • 单一方案设计会出现过度辩护的情况,即架构评审时,针对方案存在的问题和疑问,架构师会竭尽全力去为自己的设计进行辩护,经验不足的设计人员可能会强词夺理。

合理的做法是:

  • 备选方案的数量以3 ~ 5个为最佳。少于3个方案可能是因为思维狭隘,考虑不周全;多于5个则需要耗费大量的精力和时间,并且方案之间的差别可能不明显。
  • 备选方案的差异要比较明显。例如,主备方案和集群方案差异就很明显,或者同样是主备方案,用ZooKeeper做主备决策和用Keepalived做主备决策的差异也很明显。但是都用ZooKeeper做主备决策,一个检测周期是1分钟,一个检测周期是5分钟,这就不是架构上的差异,而是细节上的差异了,不适合做成两个方案。
  • 备选方案的技术不要只局限于已经熟悉的技术。设计架构时,架构师需要将视野放宽,考虑更多可能性。很多架构师或者设计师积累了一些成功的经验,出于快速完成任务和降低风险的目的,可能自觉或者不自觉地倾向于使用自己已经熟悉的技术,对于新的技术有一种不放心的感觉。就像那句俗语说的:“如果你手里有一把锤子,所有的问题在你看来都是钉子”。例如,架构师对MySQL很熟悉,因此不管什么存储都基于MySQL去设计方案,系统性能不够了,首先考虑的就是MySQL分库分表,而事实上也许引入一个Memcache缓存就能够解决问题。

2.3 常见错误三:备选方案过于详细

有的架构师或者设计师在写备选方案时,错误地将备选方案等同于最终的方案,每个备选方案都写得很细。这样做的弊端显而易见:

  • 耗费了大量的时间和精力。
  • 将注意力集中到细节中,忽略了整体的技术设计,导致备选方案数量不够或者差异不大。
  • 评审的时候其他人会被很多细节给绕进去,评审效果很差。例如,评审的时候针对某个定时器应该是1分钟还是30秒,争论得不可开交。

正确的做法是备选阶段关注的是技术选型,而不是技术细节,技术选型的差异要比较明显。

3. 评估和选择备选方案

在完成备选方案设计后,如何挑选出最终的方案也是一个很大的挑战,主要原因有:

  • 每个方案都是可行的,如果方案不可行就根本不应该作为备选方案。
  • 没有哪个方案是完美的。例如,A方案有性能的缺点,B方案有成本的缺点,C方案有新技术不成熟的风险。
  • 评价标准主观性比较强,比如设计师说A方案比B方案复杂,但另外一个设计师可能会认为差不多,因为比较难将“复杂”一词进行量化。因此,方案评审的时候我们经常会遇到几个设计师针对某个方案或者某个技术点争论得面红耳赤。

实践中很多设计师或者架构师采取了下面几种指导思想:

  • 最简派
    设计师挑选一个看起来最简单的方案。例如,我们要做全文搜索功能,方案1基于MySQL,方案2基于Elasticsearch。MySQL的查询功能比较简单,而Elasticsearch的倒排索引设计要复杂得多,写入数据到Elasticsearch,要设计Elasticsearch的索引,要设计Elasticsearch的分布式……全套下来复杂度很高,所以干脆就挑选MySQL来做吧。
  • 最牛派
    最牛派的做法和最简派正好相反,设计师会倾向于挑选技术上看起来最牛的方案。例如,性能最高的、可用性最好的、功能最强大的,或者淘宝用的、微信开源的、Google出品的等。
    我们以缓存方案中的Memcache和Redis为例,假如我们要挑选一个搭配MySQL使用的缓存,Memcache是纯内存缓存,支持基于一致性hash的集群;而Redis同时支持持久化、支持数据字典、支持主备、支持集群,看起来比Memcache好很多啊,所以就选Redis好了。
  • 最熟派
    设计师基于自己的过往经验,挑选自己最熟悉的方案。我以编程语言为例,假如设计师曾经是一个C++经验丰富的开发人员,现在要设计一个运维管理系统,由于对Python或者Ruby on Rails不熟悉,因此继续选择C++来做运维管理系统。
  • 领导派
    领导派就更加聪明了,列出备选方案,设计师自己拿捏不定,然后就让领导来定夺,反正最后方案选的对那是领导厉害,方案选的不对怎么办?那也是领导“背锅”。

其实这些不同的做法本身并不存在绝对的正确或者绝对的错误,关键是不同的场景应该采取不同的方式。也就是说,有时候我们要挑选最简单的方案,有时候要挑选最优秀的方案,有时候要挑选最熟悉的方案,甚至有时候真的要领导拍板。

应该选择哪种方法来评估和选择备选方案呢?答案就是“360度环评”!具体的操作方式为:列出我们需要关注的质量属性点,然后分别从这些质量属性的维度去评估每个方案,再综合挑选适合当时情况的最优方案

常见的方案质量属性点有:性能、可用性、硬件成本、项目投入、复杂度、安全性、可扩展性等。在评估这些质量属性时,需要遵循架构设计原则1“合适原则”和原则2“简单原则”,避免贪大求全,基本上某个质量属性能够满足一定时期内业务发展就可以了。

360度环评表示例:

有的设计师会有这样的担心:如果我们运气真的很好,业务直接一年翻了10倍,之前的方案不合适了,又要重新做方案?这种情况确实有可能存在,但概率很小,如果每次做方案都考虑这种小概率事件,我们的方案会出现过度设计,导致投入浪费。考虑这个问题的时候,需要遵循架构设计原则3“演化原则”,避免过度设计、一步到位的想法。按照原则3的思想,即使真的出现这种情况,那就算是重新做方案,代价也是可以接受的,因为业务如此迅猛发展,钱和人都不是问题。通常情况下,如果某个质量属性评估和业务发展有关系(例如,性能、硬件成本等),需要评估未来业务发展的规模时,一种简单的方式是将当前的业务规模乘以2 ~4即可,如果现在的基数较低,可以乘以4;如果现在基数较高,可以乘以2。

最理想的情况是设计一个方案,能够简单地扩容就能够跟上业务的发展。但现实往往没那么理想,因为量变会引起质变,具体哪些地方质变,是很难提前很长时间能预判到的。

完成方案的360度环评后,可以基于评估结果整理出360度环评表,一目了然地看到各个方案的优劣点。但是360度环评表也只能帮助我们分析各个备选方案,还是没有告诉我们具体选哪个方案,原因就在于没有哪个方案是完美的,极少出现某个方案在所有对比维度上都是最优的。

!!面临这种选择上的困难,有几种看似正确但实际错误的做法

  • 数量对比法:简单地看哪个方案的优点多就选哪个。例如,总共5个质量属性的对比,其中A方案占优的有3个,B方案占优的有2个,所以就挑选A方案。
    这种方案主要的问题在于把所有质量属性的重要性等同,而没有考虑质量属性的优先级。其次,有时候会出现两个方案的优点数量是一样的情况。如果为了数量上的不对称,强行再增加一个质量属性进行对比,这个最后增加的不重要的属性反而成了影响方案选择的关键因素,这又犯了没有区分质量属性的优先级的问题。
  • 加权法:每个质量属性给一个权重。例如,性能的权重高中低分别得10分、5分、3分,成本权重高中低分别是5分、3分、1分,然后将每个方案的权重得分加起来,最后看哪个方案的权重得分最高就选哪个。
    这种方案主要的问题是无法客观地给出每个质量属性的权重得分。这个分数是很难确定的,没有明确的标准,甚至会出现为了选某个方案,设计师故意将某些权重分值调高而降低另外一些权重分值,最后方案的选择就变成了一个数字游戏了。

正确的做法是按优先级选择,即架构师综合当前的业务发展情况、团队人员规模和技能、业务发展预测等因素,将质量属性按照优先级排序,首先挑选满足第一优先级的,如果方案都满足,那就再看第二优先级……以此类推。那会不会出现两个或者多个方案,每个质量属性的优缺点都一样的情况呢?理论上是可能的,但实际上是不可能的。前面提到在做备选方案设计时,不同的备选方案之间的差异要比较明显,差异明显的备选方案不可能所有的优缺点都是一样的。

备选方案的选择和很多因素相关,并不单单考虑性能高低、技术是否优越这些纯技术因素。业务的需求特点、运维团队的经验、已有的技术体系、团队人员的技术水平都会影响备选方案的选择。

4. 详细方案设计

详细方案设计就是将方案涉及的关键技术细节给确定下来。

  • 假如我们确定使用Elasticsearch来做全文搜索,那么就需要确定Elasticsearch的索引是按照业务划分,还是一个大索引就可以了;副本数量是2个、3个还是4个,集群节点数量是3个还是6个等。
  • 假如我们确定使用MySQL分库分表,那么就需要确定哪些表要分库分表,按照什么维度来分库分表,分库分表后联合查询怎么处理等。
  • 假如我们确定引入Nginx来做负载均衡,那么Nginx的主备怎么做,Nginx的负载均衡策略用哪个(权重分配?轮询?ip_hash?)等。
    可以看到,详细设计方案里面其实也有一些技术点和备选方案类似。例如,Nginx的负载均衡策略,备选有轮询、权重分配、ip_hash、fair、url_hash五个,具体选哪个呢?看起来和备选方案阶段面临的问题类似,但实际上这里的技术方案选择是很轻量级的,我们无须像备选方案阶段那样操作,而只需要简单根据这些技术的适用场景选择就可以了。

详细设计方案阶段可能遇到的一种极端情况就是在详细设计阶段发现备选方案不可行,一般情况下主要的原因是备选方案设计时遗漏了某个关键技术点或者关键的质量属性。 这种情况可以通过下面方式有效地避免:

  • 架构师不但要进行备选方案设计和选型,还需要对备选方案的关键细节有较深入的理解。例如,架构师选择了Elasticsearch作为全文搜索解决方案,前提必须是架构师自己对Elasticsearch的设计原理有深入的理解,比如索引、副本、集群等技术点;而不能道听途说Elasticsearch很牛,所以选择它,更不能成为把“细节我们不讨论”这句话挂在嘴边的“PPT架构师”。
  • 通过分步骤、分阶段、分系统等方式,尽量降低方案复杂度,方案本身的复杂度越高,某个细节推翻整个方案的可能性就越高,适当降低复杂性,可以减少这种风险。
  • 如果方案本身就很复杂,那就采取设计团队的方式来进行设计,博采众长,汇集大家的智慧和经验,防止只有1~2个架构师可能出现的思维盲点或者经验盲区。

你可能感兴趣的:(架构,架构,笔记)