为什么做架构?
为了高性能,高可用,高拓展
架构设计的真正目的
解决软件系统复杂度带来的问题
这么多需求,从哪里开始下手进行架构设计呢?”
——通过熟悉和理解需求,识别系统复杂性所在的地方,然后针对这些复杂点进行架构设计
“架构设计要考虑高性能、高可用、高扩展……这么多高 XX,全部设计完成估计要 1 个 月,但老大只给了 1 周时间”
架构设计并不是要面面俱到,不需要每个架构都具备高性能、高可用、高扩展等特 点,而是要识别出复杂点然后有针对性地解决问题。
——理解每个架构方案背后所需要解决的复杂点,然后才能对比自己的业务复杂点,参考 复杂点相似的方案。 其次,遵循这条准则能够让“老鸟”架构师有的放矢,而不是贪大求全。
技术人员往往都希望自己能够做出最牛的东西,架构师也不例外,尤其是一些“老鸟”架构 师,为了证明自己的技术牛,可能会陷入贪大求全的焦油坑而无法自拔。例如: “我们的系统一定要做到每秒 TPS 10 万”。 “淘宝的架构是这么做的,我们也要这么做”。 “Docker 现在很流行,我们的架构应该将 Docker 应用进来”。 以上这些想法,如果拿“架构设计是为了解决软件复杂度”这个原则来衡量,就很容易判 断。
——如果系统的复杂度不是在性能这部分,TPS 做到 10 万并没有什么用。
复杂度来源
-
高性能
-
单机复杂度
-
真正意义多任务并行
- SMP
- NUMA
- MPP
-
-
集群复杂度
- 简单的系统更容易做到高性能
- 可以针对单个任务进行拓展、
-
-
高可用
系统无中断的执行其功能的能力,代表系统的可用性程度,是进行系统设计的准则之一
-
计算高可用:无论哪台机器进行运算,同样的算法和输入数据,产出结果是相同的
-
存储高可用:将数据从一台机器搬到另一台机器,需要经过线路进行传输
- 难点不在于如何备份数据,在于如何减少或者规避数据不一致对业务造成的影响
-
高可用决策状态
-
独裁式
-
协商式
-
民主式:多个独立个体投票进行状态决策
- 脑裂
- 投票节点必须超过一半
-
-
高性能和高可用哪个更复杂?
相对而言还是高可用更难些,按照作者说的高性能其实就是容量,在负载均衡系统高可用的情况下加机器就可以了,而想做到各个环节的高可用不是靠加机器就能搞定的,通常需要复杂的算法、引入更多的中间件、牺牲一定的性能才能实现,这其中还要进行各种权衡取舍裁剪才可以
高拓展性
可扩展性指系统为了应对将来需求变化而提供的一种扩展能力,当有新的需求出现时,系统不需要或者仅需要少量修改就可以支持,无须整个系统重构或者重建。
预测变化
软件系统与硬件或者建筑相比,有一个很大的差异:软件系统在发布后还可以不断地修改和演进,这就意味着不断有新的需求需要实现。如果新需求能够不改代码甚至少改代码就可以实现,那当然是皆大欢喜的,否则来一个需求就要求系统大改一次,成本会非常高,程序员心里也不爽(改来改去),产品经理也不爽(做得那么慢),老板也不爽(那么多人就只能干这么点事)。因此作为架构师,我们总是试图去预测所有的变化,然后设计完美的方案来应对,当下一次需求真正来临时,架构师可以自豪地说:这个我当时已经预测到了,架构已经完美地支持,只需要一两天工作量就可以了!
然而理想是美好的,现实却是复杂的。有一句谚语,“唯一不变的是变化”,如果按照这个标准去衡量,架构师每个设计方案都要考虑可扩展性。例如,架构师准备设计一个简单的后台管理系统,当架构师考虑用 MySQL 存储数据时,是否要考虑后续需要用 Oracle 来存储?当架构师设计用 HTTP 做接口协议时,是否要考虑要不要支持 ProtocolBuffer?甚至更离谱一点,架构师是否要考虑 VR 技术对架构的影响从而提前做好可扩展性?如果每个点都考虑可扩展性,架构师会不堪重负,架构设计也会异常庞大且最终无法落地。但架构师也不能完全不做预测,否则可能系统刚上线,马上来新的需求就需要重构,这同样意着前期很多投入的工作量也白费了。
同时,“预测”这个词,本身就暗示了不可能每次预测都是准确的,如果预测的事情出错,我们期望中的需求迟迟不来,甚至被明确否定,那么基于预测做的架构设计就没什么作用,投入的工作量也就白费了。
综合分析,预测变化的复杂性在于:
- 不能每个设计点都考虑可扩展性。
- 不能完全不考虑可扩展性。
- 所有的预测都存在出错的可能性。
应对变化
第一种应对变化的常见方案是将“变化”封装在一个“变化层”,将不变的部分封装在一个独立的“稳定层” 。
无论采取哪种形式,通过剥离变化层和稳定层的方式应对变化,都会带来两个主要的复杂性相关的问题。
- 系统需要拆分出变化层和稳定层
对于哪些属于变化层,哪些属于稳定层,很多时候并不是像前面的示例(不同接口协议或者不同数据库)那样明确,不同的人有不同的理解,导致架构设计评审的时候可能吵翻天。
- 需要设计变化层和稳定层之间的接口
接口设计同样至关重要,对于稳定层来说,接口肯定是越稳定越好;但对于变化层来说,在有差异的多个实现方式中找出共同点,并且还要保证当加入新的功能时原有的接口设计不需要太大修改,这是一件很复杂的事情。例如,MySQL 的 REPLACE INTO 和 Oracle 的MERGE INTO 语法和功能有一些差异,那存储层如何向稳定层提供数据访问接口呢?是采取 MySQL 的方式,还是采取 Oracle 的方式,还是自适应判断?如果再考虑 DB2 的情况呢?
第二种常见的应对变化的方案是提炼出一个“抽象层”和一个“实现层” 。抽象层是稳定的,实现层可以根据具体业务需要定制开发,当加入新的功能时,只需要增加新的实现,无须修改抽象层。这种方案典型的实践就是设计模式和规则引擎
设计模式的核心就是,封装变化,隔离可变性
复杂度来源:低成本、安全、规模
低成本
低成本给架构设计带来的主要复杂度体现在,往往只有“创新”才能达到低成本目标类似的新技术例子很多:
NoSQL(Memcache、Redis 等)的出现是为了解决关系型数据库无法应对高并发访问带来的访问压力
全文搜索引擎(Sphinx、Elasticsearch、Solr)的出现是为了解决关系型数据库 like 搜索的低效的问题。
Hadoop 的出现是为了解决传统文件系统无法应对海量数据存储和计算的问题。
无论是引入新技术,还是自己创造新技术,都是一件复杂的事情。引入新技术的主要复杂度在于需要去熟悉新技术,并且将新技术与已有技术结合起来;创造新技术的主要复杂度在于需要自己去创造全新的理念和技术,并且新技术跟旧技术相比,需要有质的飞跃。
相比来说,创造新技术复杂度更高,因此一般中小公司基本都是靠引入新技术来达到低成本的目标;而大公司更有可能自己去创造新的技术来达到低成本的目标,因为大公司才有足够的资源、技术和时间去创造新技术。
安全
-
功能安全
例如,常见的 XSS 攻击、CSRF 攻击、SQL 注入、Windows 漏洞、密码破解等,本质上是因为系统实现有漏洞,黑客有了可乘之机。黑客会利用各种漏洞潜入系统,这种行为就像小偷一样,黑客和小偷的手法都是利用系统或家中不完善的地方潜入,并进行破坏或者盗取。因此形象地说,功能安全其实就是“防小偷” 。
-
架构安全
如果说功能安全是“防小偷”,那么架构安全就是“防强盗” 。强盗会直接用大锤将门砸开,或者用炸药将围墙炸倒;小偷是偷东西,而强盗很多时候就是故意搞破坏,对系统的影响也大得多。因此架构设计时需要特别关注架构安全,尤其是互联网时代,理论上来说系统部署在互联网上时,全球任何地方都可以发起攻击。
传统的架构安全主要依靠防火墙,防火墙最基本的功能就是隔离网络,通过将网络划分成不同的区域,制定出不同区域之间的访问控制策略来控制不同信任程度区域间传送的数据流。例如,下图是一个典型的银行系统的安全架构。
防火墙的功能虽然强大,但性能一般,所以在传统的银行和企业应用领域应用较多。但在互联网领域,防火墙的应用场景并不多。因为互联网的业务具有海量用户访问和高并发的特点,防火墙的性能不足以支撑;尤其是互联网领域的 DDoS 攻击,轻则几 GB,重则几十GB。2016 年知名安全研究人员布莱恩·克莱布斯(Brian Krebs)的安全博客网站遭遇DDoS 攻击,攻击带宽达 665Gbps,是目前在网络犯罪领域大的拒绝服务攻击。这种规模的攻击,如果用防火墙来防,则需要部署大量的防火墙,成本会很高。例如,中高端一些的防火墙价格 10 万元,每秒能抗住大约 25GB 流量,那么应对这种攻击就需要将近30 台防火墙,成本将近 300 万元,这还不包括维护成本,而这些防火墙设备在没有发生攻击的时候又没有什么作用。也就是说,如果花费几百万元来买这么一套设备,有可能几年都发挥不了任何作用。
就算是公司对钱不在乎,一般也不会堆防火墙来防 DDoS 攻击,因为 DDoS 攻击最大的影响是大量消耗机房的出口总带宽。不管防火墙处理能力有多强,当出口带宽被耗尽时,整个业务在用户看来就是不可用的,因为用户的正常请求已经无法到达系统了。防火墙能够保证内部系统不受冲击,但用户也是进不来的。对于用户来说,业务都已经受到影响了,至于是因为用户自己进不去,还是因为系统出故障,用户其实根本不会关心。 基于上述原因,互联网系统的架构安全目前并没有太好的设计手段来实现,更多地是依靠运营商或者云服务商强大的带宽和流量清洗的能力,较少自己来设计和实现。
规模
很多企业级的系统,既没有高性能要求,也没有双中心高可用要求,也不需要什么扩展性,但往往我们一说到这样的系统,很多人都会脱口而出:这个系统好复杂!为什么这样说呢? 关键就在于这样的系统往往功能特别多,逻辑分支特别多。特别是有的系统,发展时间比较长,不断地往上面叠加功能,后来的人由于不熟悉整个发展历史,可能连很多功能的应用场景都不清楚,或者细节根本无法掌握,面对的就是一个黑盒系统,看不懂、改不动、不敢改、修不了,复杂度自然就感觉很高了。规模带来复杂度的主要原因就是“量变引起质变”,当数量超过一定的阈值后,复杂度会发生质的变化。常见的规模带来的复杂度有:
-
功能越来越多,导致系统复杂度指数级上升
例如,某个系统开始只有 3 大功能,后来不断增加到 8 大功能,虽然还是同一个系统,但复杂度已经相差很大了,具体相差多大呢?我以一个简单的抽象模型来计算一下,假设系统间的功能都是两两相关的,系统的复杂度= 功能数量 + 功能之间的连接数量,通过计算我们可以看出:
3 个功能的系统复杂度 = 3 + 3 = 6 8 个功能的系统复杂度 = 8 + 28 = 36
可以看出,具备 8 个功能的系统的复杂度不是比具备 3 个功能的系统的复杂度多 5,而是多了 30,基本是指数级增长的,主要原因在于随着系统功能数量增多,功能之间的连接呈指数级增长。下图形象地展示了功能数量的增多带来了复杂度。
- 数据越来越多,系统复杂度发生质变
与功能类似,系统数据越来越多时,也会由量变带来质变,最近几年火热的“大数据”就是在这种背景下诞生的。大数据单独成为了一个热门的技术领域,主要原因就是数据太多以后,传统的数据收集、加工、存储、分析的手段和工具已经无法适应,必须应用新的技术才能解决。目前的大数据理论基础是 Google 发表的三篇大数据相关论文,其中 Google File System 是大数据文件存储的技术理论,Google Bigtable 是列式数据存储的技术理论,Google MapReduce 是大数据运算的技术理论,这三篇技术论文各自开创了一个新的技术领域。 即使我们的数据没有达到大数据规模,数据的增长也可能给系统带来复杂性。最典型的例子莫过于使用关系数据库存储数据,我以 MySQL 为例,MySQL 单表的数据因不同的业务和应用场景会有不同的最优值,但不管怎样都肯定是有一定的限度的,一般推荐在 5000 万行左右。如果因为业务的发展,单表数据达到了 10 亿行,就会产生很多问题,例如:
添加索引会很慢,可能需要几个小时,这几个小时内数据库表是无法插入数据的,相当于业务停机了。 修改表结构和添加索引存在类似的问题,耗时可能会很长。 即使有索引,索引的性能也可能会很低,因为数据量太大。 数据库备份耗时很长。
......
因此,当 MySQL 单表数据量太大时,我们必须考虑将单表拆分为多表,这个拆分过程也 会引入更多复杂性,例如: 以用户表为例:是按照用户 id 拆分表,还是按照用户注册时间拆表? 以用户表为例:假设按照用户 id 拆表,当业务需要查询学历为“本科”以上的用户时,要 去很多表查询才能得到最终结果,怎么保证性能? 拆表的规则是什么? 拆完表后查询如何处理?