架构设计的关键思维是判断和取舍,程序设计的关键思维是逻辑和实现。
架构设计需要考虑的通用问题,性能、可用性、可扩展性、安全性、成本、规模。
架构设计的三大原则,合适优于业界领先、简单优于复杂、迭代优于一步到位。
基础概念
架构指软件系统的顶层结构,它定义了系统由哪些角色组成,角色之间的关系和运作规则。
框架是面向编程或配置的半成品,关注的是“规范”,架构关注的是“结构”。
组件是从技术维度上的复用。
模块是从业务维度上职责的划分。
系统是相互协同可运行的实体。
如下定义了一交易系统的顶层逻辑架构,橙色方块表示了系统内的角色,连线表示了各角色间的关系,箭头表示了运作规则:
架构图
如果你是业务系统的架构师,首先需要思考怎么从业务逻辑的角度把系统拆分成一个个模块角色,其次需要思考怎么从物理部署的角度把系统拆分成组件角色。从不同维护我们可以画出不同纬度的架构图,如学生管理系统,从业务逻辑的角度分解、从物理角度分解和从开发规范的角度分解会产生如下架构图:
- 业务逻辑架构图:
- 物理部署架构图:
- 开发规范架构图:
无论是架构设计还是画架构图,都应该采取“自顶向下,逐步细化”的方式。以微信为例:
注:L0\L1\L2 指层级,一个 L0 往下可以分解多个 L1,一个 L1 可以往下分解多个 L2,以此类推,一般建议不超过 5 层(L0~L4)。
RUP 4+1视图
逻辑视图:逻辑视图关注功能,不仅包括用户可见的功能,还包括为实现用户功能而必须提供的"辅助功能模块";它们可能是逻辑层、功能模块等。
开发视图:开发视图关注程序包,不仅包括要编写的源程序,还包括可以直接使用的第三方SDK和现成框架、类库,以及开发的系统将运行于其上的系统软件或中间件。开发视图和逻辑视图之间可能存在一定的映射关系:比如逻辑层一般会映射到多个程序包等。
处理视图:处理视图关注进程、线程、对象等运行时概念,以及相关的并发、同步、通信等问题。处理视图和开发视图的关系:开发视图一般偏重程序包在编译时期的静态依赖关系,而这些程序运行起来之后会表现为对象、线程、进程,处理视图比较关注的正是这些运行时单元的交互问题。
物理视图:物理视图关注"目标程序及其依赖的运行库和系统软件"最终如何安装或部署到物理机器,以及如何部署机器和网络来配合软件系统的可靠性、可伸缩性等要求。物理视图和处理视图的关系:处理视图特别关注目标程序的动态执行情况,而物理视图重视目标程序的静态位置问题;物理视图是综合考虑软件系统和整个IT系统相互影响的架构视图。
架构设计的目的
架构即(重要)决策,是为了应对软件系统复杂度而提出的一个解决方案,主要从性能、可用性、可扩展性、安全性、成本、规模等几个方面进行考虑,但和具体业务相结合,不需要面面俱到。
架构设计三原则
合适、简单、迭代为架构设计三原则。
合适
合适优于业界领先。
合适即资源整合,整合现有人力、业务场景、经验教训等资源,设计出符合当下业务场景的系统,主要参考依据:
- 根据目前业务情况预判2年后的业务容量,根据这个容量来进行设计。
- 目前资源是否足够。如人力资源、现有人员的知识储备情况、硬件资源、研发周期、业务范围。
- 业务积累是否足够。挑战和踩坑,都是架构设计非常关键的促进因素,单纯靠拍脑袋或者头脑风暴,是不可能和真正实战相比的。
- 业务场景是否足够。业界领先的方案其实都是“逼”出来的!简单来说,“业务”发展到一定阶段,量变导致了质变,出现了新的问题,已有的方式已经不能应对这些问题,需要用一种新的方案来解决,通过创新和尝试,才有了业界领先的方案。
真正优秀的架构都是在企业当前人力、条件、业务等各种约束下设计出来的,能够合理地将资源整合在一起并发挥出最大功效,并且能够快速落地。
简单
简单优于复杂。
“复杂”在制造和建筑领域都代表领先,但在软件领域,却恰恰相反,代表的是“问题”。软件领域的复杂性体现在两个方面:
- 结构的复杂性
- 逻辑的复杂性
结构的复杂性
结构复杂的系统几乎毫无例外具备两个特点:
- 组成复杂系统的组件数量更多;
- 这些组件之间的关系也更加复杂,多一个组件依赖关系复杂度,是按照『指数』级提升的。
结构的复杂性带来一下问题:
- 组件越多,就越有可能其中某个组件出现故障,影响系统可用性。如一个组件可用性是90%,那么有三个组件的系统可用性为0.90.90.9=72.9%
- 某个组件改动,会影响关联的所有组件,影响系统开发效率。因为一旦变更涉及外部系统,需要协调各方统一进行方案评估、资源协调、上线配合。
- 定位一个复杂系统中的问题总是比简单系统更加困难,影响可运维性。首先是组件多,每个组件都有嫌疑,因此要逐一排查;其次组件间的关系复杂,有可能表现故障的组件并不是真正问题的根源。
演化
演化原则宣言:“演化优于一步到位”。
对于建筑来说,永恒是主题;而对于软件来说,变化才是主题。软件架构设计其实更加类似于大自然“设计”一个生物,通过演化让生物适应环境,逐步变得更加强大:
- 首先,设计出来的架构要满足当时的业务需要。
- 其次,架构要不断地在实际应用过程中迭代,保留优秀的设计,修复有缺陷的设计,改正错误的设计,去掉无用的设计,使得架构逐渐完善。
- 第三,当业务发生变化时,架构要扩展、重构,甚至重写;代码也许会重写,但有价值的经验、教训、逻辑、设计等(类似生物体内的基因)却可以在新架构中延续。
复杂度来源
高性能
衡量软件性能包括了响应时间、TPS(意思是每秒事务数,事务可以是一个接口)、QPS、服务器资源利用率等。
提高系统并发能力的方式,方法论上主要有两种:垂直扩展(提升单机处理能力)与水平扩展(增加服务器数量),所以软件系统中高性能带来的复杂度主要体现在两方面,一方面是单台计算机内部为了高性能带来的复杂度;另一方面是多台计算机集群为了高性能带来的复杂度。
垂直扩展
- 增强单机硬件性能,例如:增加CPU核数如32核,升级更好的网卡如万兆,升级更好的硬盘如SSD,扩充硬盘容量如2T,扩充系统内存如128G;
- 提升单机架构性能,例如:使用Cache来减少IO次数,使用异步来增加单服务吞吐量,使用无锁数据结构来减少响应时间,使用多线程来提升业务处理效率;
水平扩展
- 功能分解:基于功能将系统分解为更小的子系统
- 多实例副本:同一组件重复部署到多台不同的服务器
- 数据分割:在每台机器上都只部署一部分数据如:缓存一致性hash算法
复杂度来源:高可用
可用性是指系统无中断地执行其功能的能力。
系统高可用方案:
- 单点冗余做故障转移
- 服务熔断做服务降级
- 限流
高性能增加机器目的在于“扩展”处理性能;高可用增加机器目的在于“冗余”处理单元。而高可用带来的系统复杂度主要来源于「冗余」,因为冗余就会涉及到服务状态判断、故障转移策略、任务分配策略、数据一致性问题等。
备份的分类
冷备:系统没启动,如离线直接拷贝数据库文件。
温备:系统启动,但是没法接管业务,如在数据库运行时加全局读锁然后备份数据。
热备:系统启动,随时可以接管业务,如数据库的主从同步。
计算高可用
计算是指无论在哪台机器上进行计算(执行业务逻辑),同样的算法和输入数据,产出的结果都是一样的。
计算高可用的难点在于,引入了任务分配器,任务分配器本身也存在高可用问题
存储高可用
存储高可用主要方案就是将相同数据存在在不同机器上做数据冗余,数据存储在不同机器上就一定会出现数据不一致的情况。
存储高可用的难点不在于如何备份数据,而在于如何减少或者规避数据不一致对业务造成的影响。
高可用状态决策
无论是计算高可用还是存储高可用,其基础都是“状态决策”,即系统需要能够判断当前的状态是正常还是异常。
- 独裁式:独裁式的决策方式不会出现决策混乱的问题,因为只有一个决策者。但是他本身存也存在单点状态问题,所以会陷入死循环。
- 协商式:协商式决策指的是两个独立的个体通过交流信息,然后根据规则进行决策,最常用的协商式决策就是主备决策。存在两个个体断线的情况,一单断线,系统就无法进行协商。
- 民主式:民主式决策指的是多个独立的个体通过投票的方式来进行状态决策。例如,ZooKeeper 集群在选举 leader 时就是采用这种方式。缺点主要是算法复杂,且存在脑裂情况。
可扩展性
可扩展性是指,系统为了应对将来需求变化而提供的一种扩展能力,当有新的需求出现时,系统不需要或者仅需要少量修改就可以支持,无须整个系统重构或者重建,设计具备良好可扩展性的系统,有两个基本条件:
- 正确预测变化
- 完美应对变化
可扩展设计方法论主要就是"分层"和"抽象":
- 提炼出“变化层”和“稳定层”,将不变的部分封装在一个独立的“稳定层”,将“变化”封装在一个“变化层”(也叫“适配层”)。这种方案的核心思想是通过变化层来隔离变化。
- 提炼出“抽象层”和“实现层”,这种方案的核心思想就是通过实现层来封装变化,典型的实践就是设计模式和规则引擎。
可扩展性具体实现方案:
使用面向对象思想、设计模式、服务分层、微服务化、基于消息的事件总线等。
设计模式的核心是封装变化、隔离可变性
可扩展性设计原则:
- 2年原则:只预测 2 年内的可能变化,不要试图预测 5 年甚至 10 年后的变化。
- 1 写 2 抄 3 重构原则:事不过三,三则重构。
成本
低成本是架构设计中需要考虑一个约束条件,但不会是首要目标。低成本本质上是与高性能和高可用冲突的,当无法设计出满足成本要求的方案,就只能协调并调整成本目标。
降低成本的常用方法:
- 引入新技术。主要复杂度在于需要去熟悉新技术,并且将新技术与已有技术结合;一般中小型公司基本采用该方式达到目标。
- 开创一个全新技术领域。主要复杂度在于需要去创造全新的理念和技术,并且与旧技术相比,需要有质的飞跃,复杂度更高;一般大公司拥有更多的资源、技术实力会采用该方式来达到低成本的目标。
安全
安全是一个庞大而又复杂的技术领域,一旦出问题,对业务和企业形象影响非常大。从技术的角度来讲,包括:
- 功能安全-“防小偷”,减少系统潜在的缺陷,阻止黑客破坏行为,常见的 XSS 攻击、CSRF 攻击、SQL 注入、Windows 漏洞、密码破解等;
- 架构安全-“防强盗”,保护系统不受恶意访问和攻击,保护系统的重要数据不被窃取。由于是蓄意破坏系统,因此对影响也大得多。
安全设计的常用方法:
- 功能安全:是一个逐步完善的过程,而且往往都是在问题出现后才能有针对性的提出解决方案,与编码实现有关。
- 架构安全:传统企业主要通过防火墙实现不同区域的访问控制,功能强大、性能一般,但是成本更高。互联网企业更多地是依靠运营商或者云服务商强大的带宽和流量清洗的能力,较少自己来设计和实现。
规模
规模带来复杂度的主要原因就是“量变引起质变”,当数量超过一定的阈值后,复杂度会发生质的变化。随着业务的发展,规模带来的常见复杂度有:
- 业务功能越来越多,调用逻辑越来越复杂;
- 数据容量、类型、关联关系越来越多。
规模设计的常用方法:
- 规模问题需要与高性能、高可用、高扩展、高伸缩性统一考虑。常采用“分而治之,各个击破”的方法策略。
架构设计流程
识别复杂度
架构设计的本质目的是为了解决软件系统的复杂性,所以在我们设计架构时,首先就要分析系统的复杂性。实际上大部分场景下,复杂度只是其中的某一个,少数情况下包含其中两个。如果确实涉及多个复杂度,正确的做法是将主要的复杂度问题列出来,然后根据业务、技术、团队等综合情况进行排序,优先解决当前面临的最主要的复杂度问题。
如果复杂度不好判断,我们可以根据性能、可用性、可扩展性、安全性、成本、规模等复杂度通用纬度做排除法。
设计备选方案
架构方案设计可以从四个方面考虑,第一非功能性诉求(性能,可靠性,容错性,一致性等),第二研发成本(设计复杂性,实现复杂性,前期人力投入,后期人力投入,短期重构风险,工期要求),第三运维成本(运营工具是否完善,纳入现有运维体系的难度等),第四软硬件成本。架构师需要对已经存在的技术非常熟悉,如:高可用的主备方案、集群方案,高性能的负载均衡、多路复用,可扩展的分层、插件化等技术。
几种常见的架构设计误区
- 设计最优秀的方案:不要面向“简历”进行架构设计,而是要根据“合适”、“简单”、“演进”的架构设计原则,决策出与需求、团队、技术能力相匹配的合适方案。
- 只做一个方案:一个方案容易陷入思考问题片面、自我坚持的认知陷阱。
- 备选方案过于详细:备选方案过于详细会导致耗费了大量的时间和精力;将注意力集中到细节中,忽略了整体的技术设计。
备选方案设计的注意事项
- 备选方案不要过于详细。备选阶段解决的是技术选型问题,而不是技术细节。
- 备选方案的数量以 3~5个为最佳。
- 备选方案的技术差异要明显。
- 备选方案不要只局限于已经熟悉的技术。
常用备选方案
- 直接使用商业化软件,如上云,性能、HA、伸缩性都有保证。
- 直接使用开源产品,成本低,但是出现问题排查难度高。
- 直接自研,成本高,但是解决问题快。
- 基于开源方案做二次开发。
评估和选择备选方案
列出我们需要关注的质量属性点,然后分别从这些质量属性的维度去评估每个方案,再综合挑选适合当时情况的最优方案。
360 度环评表:
详细方案设计
简单来说,详细方案设计就是将方案涉及的关键技术细节给确定下来。
- 假如我们确定使用 Elasticsearch 来做全文搜索,那么就需要确定 Elasticsearch 的索引是按照业务划分,还是一个大索引就可以了;副本数量是 2 个、3 个还是 4 个,集群节点数量是 3 个还是 6 个等。
- 假如我们确定使用 MySQL 分库分表,那么就需要确定哪些表要分库分表,按照什么维度来分库分表,分库分表后联合查询怎么处理等。
- 假如我们确定引入 Nginx 来做负载均衡,那么 Nginx 的主备怎么做,Nginx 的负载均衡策略用哪个(权重分配?轮询?ip_hash?)等。
为了保证所设计架构的可行性,我们可以采用以下方式进行设计:
- 架构师不但要进行备选方案设计和选型,还需要对备选方案的关键细节有较深入的理解。
- 通过分步骤、分阶段、分系统等方式,尽量降低方案复杂度。
- 如果方案本身就很复杂,那就采取设计团队的方式来进行设计,汇集大家的智慧和经验,防止只有 1~2 个架构师可能出现的思维盲点或者经验盲区。