从0开始学架构.基础篇
开篇词 | 照着做,你也能成为架构师!
架构设计的关键思维是判断和取舍,程序设计的关键思维是逻辑和实现。
内容
- 架构基础
- 高性能架构模式
- 高可用架构模式
- 可扩展架构模式
- 架构实战
01 | 架构到底是指什么?
系统与子系统
其实子系统的定义和系统定义是一样的,只是观察的角度有差异,一个系统可能是另外一个更大系统的子系统。
模块与组件
模块和组件都是系统的组成部分,只是从不同的角度拆分系统而已
从逻辑的角度来拆分系统后,得到的单元就是“模块”;从物理的角度来拆分系统后,得到的单元就是“组件”。划分模块的主要目的是职责分离;划分组件的主要目的是单元复用。其实,“组件”的英文 component 也可翻译成中文的“零件”一词,“零件”更容易理解一些,“零件”是一个物理的概念,并且具备“独立且可替换”的特点。
框架与架构
框架关注的是“规范”,架构关注的是“结构”
- 从业务逻辑的角度分解
- 从物理部署的角度分解
- 从开发规范的角度分解
软件架构指软件系统的顶层结构
你原来理解的架构是如何定义的?
架构是顶层设计;框架是面向编程或配置的半成品;组件是从技术维度上的复用;模块是从业务维度上职责的划分;系统是相互协同可运行的实体。
02 | 架构设计的历史背景
机器语言(1940 年之前)
太难写、太难读、太难改
汇编语言(20 世纪 40 年代)
汇编语言虽然解决了机器语言读写复杂的问题,但本质上还是***面向机器***的,因为***写汇编语言需要我们精确了解计算机底层的知识***。例如,CPU 指令、寄存器、段地址等底层的细节。这对于程序员来说同样很复杂,因为程序员需要将现实世界中的问题和需求按照机器的逻辑进行翻译。
高级语言(20 世纪 50 年代)
为什么称这些语言为“高级语言”呢?原因在于这些语言让程序员不需要关注机器底层的低级结构和逻辑,而只要关注具体的问题和业务即可。
第一次软件危机与结构化程序设计(20 世纪 60 年代~20 世纪 70 年代)
结构化程序设计方法。同时,第一个结构化的程序语言 Pascal 也在此时诞生,并迅速流行起来。
第二次软件危机与面向对象(20 世纪 80 年代)
第二次软件危机的根本原因还是在于软件生产力远远跟不上硬件和业务的发展。第一次软件危机的根源在于软件的“逻辑”变得非常复杂,而第二次软件危机主要体现在软件的“扩展”变得非常复杂。结构化程序设计虽然能够解决(也许用“缓解”更合适)软件逻辑的复杂性,但是对于业务变化带来的软件扩展却无能为力,软件领域迫切希望找到新的银弹来解决软件危机,在这种背景下,***面向对象的思想***开始流行起来。
面向对象真正开始流行是在 20 世纪 80 年代,主要得益于 C++ 的功劳,后来的 Java、C# 把面向对象推向了新的高峰。到现在为止,面向对象已经成为了主流的开发思想。
软件架构的历史背景
软件架构的出现有其历史必然性。20 世纪 60 年代第一次软件危机引出了“结构化编程”,创造了“模块”概念;20 世纪 80 年代第二次软件危机引出了“面向对象编程”,创造了“对象”概念;到了 20 世纪 90 年代“软件架构”开始流行,创造了“组件”概念。我们可以看到,“模块”“对象”“组件”本质上都是对达到一定规模的软件进行拆分,差别只是在于随着软件的复杂度不断增加,拆分的粒度越来越粗,拆分的层次越来越高。
03 | 架构设计的目的
目的
架构设计的主要目的是为了解决软件系统复杂度带来的问题
简单的复杂度分析案例
假设我们需要设计一个大学的***学生管理系统***,其基本功能包括登录、注册、成绩管理、课程管理等。当我们对这样一个系统进行架构设计的时候,首先应识别其复杂度到底体现在哪里。
性能:一个学校的学生大约 1 ~ 2 万人,学生管理系统的访问频率并不高,平均每天单个学生的访问次数平均不到 1 次,因此性能这部分并不复杂,存储用 MySQL 完全能够胜任,缓存都可以不用,Web 服务器用 Nginx 绰绰有余。
可扩展性:学生管理系统的功能比较稳定,可扩展的空间并不大,因此可扩展性也不复杂。
高可用:学生管理系统即使宕机 2 小时,对学生管理工作影响并不大,因此可以不做负载均衡,更不用考虑异地多活这类复杂的方案了。但是,如果学生的数据全部丢失,修复是非常麻烦的,只能靠人工逐条修复,这个很难接受,因此需要考虑存储高可靠,这里就有点复杂了。我们需要考虑多种异常情况:机器故障、机房故障,针对机器故障,我们需要设计 MySQL 同机房主备方案;针对机房故障,我们需要设计 MySQL 跨机房同步方案。
安全性:学生管理系统存储的信息有一定的隐私性,例如学生的家庭情况,但并不是和金融相关的,也不包含强隐私(例如玉照、情感)的信息,因此安全性方面只要做 3 个事情就基本满足要求了:Nginx 提供 ACL 控制、用户账号密码管理、数据库访问权限控制。
成本:由于系统很简单,基本上几台服务器就能够搞定,对于一所大学来说完全不是问题,可以无需太多关注。
对应的架构:
04 | 复杂度来源:高性能
软件系统中高性能带来的复杂度主要体现在两方面,一方面是单台计算机内部为了高性能带来的复杂度;另一方面是多台计算机集群为了高性能带来的复杂度。
单机复杂度
- 进程
- 线程
集群复杂度
- 任务分配
任务分配的意思是指每台机器都可以处理完整的业务任务,不同的任务分配到不同的机器上执行。
我从最简单的一台服务器变两台服务器开始,来讲任务分配带来的复杂性,整体架构示意图如下。
如果我们的性能要求继续提高,假设要求每秒提升到 10 万次,上面这个架构会出现什么问题呢?是不是将业务服务器增加到 25 台就可以了呢?显然不是,因为随着性能的增加,任务分配器本身又会成为性能瓶颈,当业务请求达到每秒 10 万次的时候,单台任务分配器也不够用了,任务分配器本身也需要扩展为多台机器,这时的架构又会演变成这个样子。
上面这两个例子都是以业务处理为例,实际上“任务”涵盖的范围很广,可以指完整的业务处理,也可以单指某个具体的任务。例如,“存储”“运算”“缓存”等都可以作为一项任务,因此存储系统、运算系统、缓存系统都可以按照任务分配的方式来搭建架构。此外,“任务分配器”也并不一定只能是物理上存在的机器或者一个独立运行的程序,也可以是嵌入在其他程序中的算法,例如 Memcache 的集群架构。
- 任务分解
微信的后台架构
通过这种任务分解的方式,能够把原来大一统但复杂的业务系统,拆分成小而简单但需要多个系统配合的业务系统。从业务的角度来看,任务分解既不会减少功能,也不会减少代码量(事实上代码量可能还会增加,因为从代码内部调用改为通过服务器之间的接口调用),那为何通过任务分解就能够提升性能呢?
- 简单的系统更加容易做到高性能
- 可以针对单个任务进行扩展
既然将一个大一统的系统分解为多个子系统能够提升性能,那是不是划分得越细越好呢?例如,上面的微信后台目前是 7 个逻辑子系统,如果我们把这 7 个逻辑子系统再细分,划分为 100 个逻辑子系统,性能是不是会更高呢?
其实不然,这样做性能不仅不会提升,反而还会下降,最主要的原因是如果系统拆分得太细,为了完成某个业务,系统间的调用次数会呈指数级别上升,而系统间的调用通道目前都是通过网络传输的方式,性能远比系统内的函数调用要低得多。
05 | 复杂度来源:高可用
系统无中断地执行其功能的能力,代表系统的可用性程度,是进行系统设计时的准则之一。
高性能增加机器目的在于“扩展”处理性能;高可用增加机器目的在于“冗余”处理单元。
计算高可用
单机变双机的简单架构:
复杂一点的高可用集群架构:
存储高可用
本质:将数据从一台机器搬到到另一台机器,需要经过线路进行传输。
数据 + 逻辑 = 业务
存储高可用的难点不在于如何备份数据,而在于如何减少或者规避数据不一致对业务造成的影响。
高可用状态决策
无论是计算高可用还是存储高可用,其基础都是“状态决策”,即系统需要能够判断当前的状态是正常还是异常,如果出现了异常就要采取行动来保证高可用。如果状态决策本身都是有错误或者有偏差的,那么后续的任何行动和处理无论多么完美也都没有意义和价值。但在具体实践的过程中,恰好存在一个本质的矛盾:通过冗余来实现的高可用系统,状态决策本质上就不可能做到完全正确。
- 独裁式
- 协商式:最常用的协商式决策就是主备决策。
- 民主式:按照“多数取胜”的规则来确定最终的状态
06 | 复杂度来源:可扩展性
设计具备良好可扩展性的系统,有两个基本条件:
- 正确预测变化
- 完美封装变化
预测变化
原因:不断有新的需求需要实现
应对变化
第一种应对变化的常见方案是将“变化”封装在一个“变化层”,将不变的部分封装在一个独立的“稳定层”
例如:
- 两个主要的复杂性相关的问题
- 系统需要拆分出变化层和稳定层
- 需要设计变化层和稳定层之间的接口
第二种常见的应对变化的方案是提炼出一个“抽象层”和一个“实现层”。抽象层是稳定的,实现层可以根据具体业务需要定制开发,当加入新的功能时,只需要增加新的实现,无须修改抽象层。这种方案典型的实践就是设计模式和规则引擎。
07 | 复杂度来源:低成本、安全、规模
关于复杂度来源,前面的专栏已经讲了高性能、高可用和可扩展性,今天我来聊聊复杂度另外三个来源低
低成本
当我们设计“高性能”“高可用”的架构时,通用的手段都是增加更多服务器来满足“高性能”和“高可用”的要求;而低成本正好与此相反,我们需要减少服务器的数量才能达成低成本的目标。因此,低成本本质上是与高性能和高可用冲突的,所以低成本很多时候不会是架构设计的首要目标,而是架构设计的附加约束。也就是说,我们首先设定一个成本目标,当我们根据高性能、高可用的要求设计出方案时,评估一下方案是否能满足成本目标,如果不行,就需要重新设计架构;如果无论如何都无法设计出满足成本要求的方案,那就只能找老板调整成本目标了。
低成本给架构设计带来的主要复杂度体现在,往往只有“创新”才能达到低成本目标。这里的“创新”既包括开创一个全新的技术领域(这个要求对绝大部分公司太高),也包括引入新技术,如果没有找到能够解决自己问题的新技术,那么就真的需要自己创造新技术了。
安全
从技术的角度来讲,安全可以分为两类:一类是功能上的安全,一类是架构上的安全。
- 功能安全
例如,常见的 XSS 攻击、CSRF 攻击、SQL 注入、Windows 漏洞、密码破解等,本质上是因为系统实现有漏洞,黑客有了可乘之机。
功能安全其实就是“防小偷”
- 架构安全
架构安全就是“防强盗”
传统的架构安全主要依靠防火墙,防火墙最基本的功能就是隔离网络,通过将网络划分成不同的区域,制定出不同区域之间的访问控制策略来控制不同信任程度区域间传送的数据流。
基于上述原因,互联网系统的架构安全目前并没有太好的设计手段来实现,更多地是依靠运营商或者云服务商强大的带宽和流量清洗的能力,较少自己来设计和实现。
规模
规模带来复杂度的主要原因就是“量变引起质变”
常见的规模带来的复杂度有:
-
功能越来越多,导致系统复杂度指数级上升。可以看出,具备 8 个功能的系统的复杂度不是比具备 3 个功能的系统的复杂度多 5,而是多了 30,基本是指数级增长的,主要原因在于随着系统功能数量增多,功能之间的连接呈指数级增长。下图形象地展示了功能数量的增多带来了复杂度。
-
数据越来越多,系统复杂度发生质变。大数据。目前的大数据理论基础是 Google 发表的三篇大数据相关论文,其中 Google File System 是大数据文件存储的技术理论,Google Bigtable 是列式数据存储的技术理论,Google MapReduce 是大数据运算的技术理论,这三篇技术论文各自开创了一个新的技术领域。
即使我们的数据没有达到大数据规模,数据的增长也可能给系统带来复杂性。最典型的例子莫过于使用关系数据库存储数据,我以 MySQL 为例,MySQL 单表的数据因不同的业务和应用场景会有不同的最优值,但不管怎样都肯定是有一定的限度的,一般推荐在 5000 万行左右。如果因为业务的发展,单表数据达到了 10 亿行,就会产生很多问题,例如:
- 添加索引会很慢,可能需要几个小时,这几个小时内数据库表是无法插入数据的,相当于业务停机了。
- 修改表结构和添加索引存在类似的问题,耗时可能会很长。
- 即使有索引,索引的性能也可能会很低,因为数据量太大。
- 数据库备份耗时很长。
因此,当 MySQL 单表数据量太大时,我们必须考虑将单表拆分为多表,这个拆分过程也会引入更多复杂性,例如:拆表的规则是什么?拆完表后查询如何处理?
小结
6大复杂度来源:
- 高性能
- 高可用
- 可扩展
- 低成本
- 安全
- 规模
08 | 架构设计三原则
合适原则
合适原则宣言:“合适优于业界领先”。
“亿级用户平台”失败的案例原因:
- 将军难打无兵之仗
- 罗马不是一天建成的
- 冰山下面才是关键
简单原则
简单原则宣言:“简单优于复杂”。
- 结构的复杂性
- 组件越多,就越有可能其中某个组件出现故障
- 某个组件改动,会影响关联的所有组件
- 定位一个复杂系统中的问题总是比简单系统更加困难
- 逻辑的复杂性
- 系统会很庞大,可能是上百万、上千万的代码规模,“clone”一次代码要 30 分钟。
- 几十、上百人维护这一套代码,某个“菜鸟”不小心改了一行代码,导致整站崩溃。
- 需求像雪片般飞来,为了应对,开几十个代码分支,然后各种分支合并、各种分支覆盖。
- 产品、研发、测试、项目管理不停地开会讨论版本计划,协调资源,解决冲突。
- 版本太多,每天都要上线几十个版本,系统每隔 1 个小时重启一次。
- 线上运行出现故障,几十个人扑上去定位和处理,一间小黑屋都装不下所有人,整个办公区闹翻天。
功能复杂的组件,另外一个典型特征就是采用了复杂的算法。复杂算法导致的问题主要是难以理解,进而导致难以实现、难以修改,并且出了问题难以快速解决。
KISS(Keep It Simple, Stupid!)
演化原则
演化原则宣言:“演化优于一步到位”。
对于建筑来说,永恒是主题;而对于软件来说,变化才是主题
软件架构设计其实更加类似于大自然“设计”一个生物,通过演化让生物适应环境,逐步变得更加强大
- 首先,设计出来的架构要满足当时的业务需要。
- 其次,架构要不断地在实际应用过程中迭代,保留优秀的设计,修复有缺陷的设计,改正错误的设计,去掉无用的设计,使得架构逐渐完善。
- 第三,当业务发生变化时,架构要扩展、重构,甚至重写;代码也许会重写,但有价值的经验、教训、逻辑、设计等(类似生物体内的基因)却可以在新架构中延续。
架构师在进行架构设计时需要牢记这个原则,时刻提醒自己不要贪大求全,或者盲目照搬大公司的做法。应该认真分析当前业务的特点,明确业务面临的主要问题,设计合理的架构,快速落地以满足业务需要,然后在运行过程中不断完善架构,不断随着业务演化架构。
09 | 架构设计原则案例
淘宝
- 第四代技术架构
手机 QQ
- IM 4.0 架构
存储架构
通信架构
10 | 架构设计流程:识别复杂度
架构设计第 1 步:识别复杂度
将主要的复杂度问题列出来,然后根据业务、技术、团队等综合情况进行排序,优先解决当前面临的最主要的复杂度问题
前浪微博案例
综合分析下来,消息队列的复杂性主要体现在这几个方面:高性能消息读取、高可用消息写入、高可用消息存储、高可用消息读取。
11 | 架构设计流程:设计备选方案
架构设计第 2 步:设计备选方案
技术的组合:新技术都是在现有技术的基础上发展起来的,现有技术又来源于先前的技术。将技术进行功能性分组,可以大大简化设计过程,这是技术“模块化”的首要原因。技术的“组合”和“递归”特征,将彻底改变我们对技术本质的认识。
如何设计最终的方案,并不是一件容易的事情,这个阶段也是很多架构师容易犯错的地方。
- 第一种常见的错误:设计最优秀的方案。
- 第二种常见的错误:只做一个方案。
- 备选方案的数量以 3 ~ 5 个为最佳
- 备选方案的差异要比较明显
- 备选方案的技术不要只局限于已经熟悉的技术
- 第三种常见的错误:备选方案过于详细。
设计备选方案实战
- 备选方案 1:采用开源的 Kafka
- 备选方案 2:集群 + MySQL 存储
- 备选方案 3:集群 + 自研存储方案
12 | 架构设计流程:评估和选择备选方案
正因为选择备选方案存在这些困难,所以实践中很多设计师或者架构师就采取了下面几种指导思想:
- 最简派
- 最牛派
- 最熟派
- 领导派
架构设计第 3 步:评估和选择备选方案
前面提到了那么多指导思想,真正应该选择哪种方法来评估和选择备选方案呢?我的答案就是“360 度环评”!具体的操作方式为:列出我们需要关注的质量属性点,然后分别从这些质量属性的维度去评估每个方案,再综合挑选适合当时情况的最优方案。
常见的方案质量属性点有:性能、可用性、硬件成本、项目投入、复杂度、安全性、可扩展性等。在评估这些质量属性时,需要遵循架构设计原则 1“合适原则”和原则 2“简单原则”,避免贪大求全,基本上某个质量属性能够满足一定时期内业务发展就可以了。
有几种看似正确但实际错误的做法:
- 数量对比法:简单地看哪个方案的优点多就选哪个。例如,总共 5 个质量属性的对比,其中 A 方案占优的有 3 个,B 方案占优的有 2 个,所以就挑选 A 方案。
- 加权法:每个质量属性给一个权重。例如,性能的权重高中低分别得 10 分、5 分、3 分,成本权重高中低分别是 5 分、3 分、1 分,然后将每个方案的权重得分加起来,最后看哪个方案的权重得分最高就选哪个。
正确的做法:
按优先级选择,即架构师综合当前的业务发展情况、团队人员规模和技能、业务发展预测等因素,将质量属性按照优先级排序,首先挑选满足第一优先级的,如果方案都满足,那就再看第二优先级……以此类推。那会不会出现两个或者多个方案,每个质量属性的优缺点都一样的情况呢?理论上是可能的,但实际上是不可能的。前面我提到,在做备选方案设计时,不同的备选方案之间的差异要比较明显,差异明显的备选方案不可能所有的优缺点都是一样的。
13 | 架构设计流程:详细方案设计
架构设计第 4 步:详细方案设计
简单来说,详细方案设计就是将方案涉及的关键技术细节给确定下来。
Nginx 的负载均衡策略
备选有轮询、权重分配、ip_hash、fair、url_hash 五个,具体选哪个呢?看起来和备选方案阶段面临的问题类似,但实际上这里的技术方案选择是
- 轮询(默认)。每个请求按时间顺序逐一分配到不同的后端服务器,后端服务器分配的请求数基本一致,如果后端服务器“down 掉”,能自动剔除。
- 加权轮询。根据权重来进行轮询,权重高的服务器分配的请求更多,主要适应于后端服务器性能不均的情况,如新老服务器混用。
- ip_hash。每个请求按访问 IP 的 hash 结果分配,这样每个访客固定访问一个后端服务器,主要用于解决 session 的问题,如购物车类的应用。
- fair。按后端服务器的响应时间来分配请求,响应时间短的优先分配,能够最大化地平衡各后端服务器的压力,可以适用于后端服务器性能不均衡的情况,也可以防止某台后端服务器性能不足的情况下还继续接收同样多的请求从而造成雪崩效应。
- url_hash。按访问 URL 的 hash 结果来分配请求,每个 URL 定向到同一个后端服务器,适用于后端服务器能够将 URL 的响应结果缓存的情况。
详细设计方案阶段可能遇到的一种极端情况就是在详细设计阶段发现备选方案不可行,一般情况下主要的原因是备选方案设计时遗漏了某个关键技术点或者关键的质量属性
- 架构师不但要进行备选方案设计和选型,还需要对备选方案的关键细节有较深入的理解。
- 通过分步骤、分阶段、分系统等方式,尽量降低方案复杂度。
- 如果方案本身就很复杂,那就采取设计团队的方式来进行设计,博采众长,汇集大家的智慧和经验,防止只有 1~2 个架构师可能出现的思维盲点或者经验盲区。