《从0开始学架构》学习笔记(一)
最近购买了极客时间推出的李运华的课程——《从0开始学架构》,本人通过听音频和文字阅读,整理出相关笔记,目的是方便今后再次阅读。再次感谢李运华的讲解,购买链接:
开篇词 | 照着做,你也能成为架构师
想成为架构师,梦想是美好的,但道路是曲折的,这应该不是个人天资的问题,而是架构设计本身的一些特性所致。
- 架构设计的关键思维是判断和取舍,程序设计的关键思维是逻辑和实现。
- 架构设计没有体系化的培训和训练机制
- 程序员对架构设计的理解存在很多舞曲(比如,架构一定具备高可用、高性能等)
这个专栏涵盖:
- 架构基础
- 高性能架构模式
- 高可用架构模式
- 可扩展架构模式
- 架构实战
通过本专栏的学习,你会收获:
- 清楚地理解架构的相关的概念、本质、目的
- 掌握通用的架构设计原则
- 掌握架构标准的设计流程
- 深入理解已有的架构模式
- 掌握架构演进和开源系统使用的一些技巧
只要你努力,技术的梦想一定会实现。
精彩留言:
一、架构基础
01 | 架构到底是指什么?
梳理几个有关系而又相似的概念:
a. 系统与子系统
系统泛指由一群有关联的个体组成,根据某种规则运作,能完成个别元件不能单独完成的工作的群体。它的意思是“总体”“整体”或“联盟”。(*)
(注意:这里的“能”指的是“能力”,系统能力与个体能力又本质差别,系统能力不是个体同理之和,而是生产了新的能力。比如,汽车能够载重前行,发动机不能。)
b. 模块与组件
软件模块(module)是一套一致而互相有紧密关连的软件组织。它分别包含了程序和数据结构两部分。现代软件开发往往利用模块作为合成的单位。模块的接口表达了由该模块提供的功能和调用它时所需的元素。模块是可能分开被编写的单位。这使它们可再用和允许人员同时协作、编写及研究不同的模块。软件组件定义为自包含的、可编程的、可重用的、与语言无关的软件单元,软件组件可以很容易被用于组装应用程序中。(*)
总结:模块和组件都是系统的组成部分,只是从不同的角度拆分系统而已。
c. 框架与架构
软件框架(software framework)通常指的是为了实现某个业界标准或完成特定基本任务的软件组件规范,也指为了实现某个软件组件规范时,提供规范所要求之基础功能的软件产品。
提炼一下其中关键部分:
- 框架是组件规范:例如,mvc 就是一种最常见的开发规范,类似的还有 mvp、mvvm、j2ee 等框架。
- 框架提供基础功能的产品:例如,spring mvc 是 mvc 的开发框架,除了满足 mvc 的规范,spring 提供了很多基础功能来帮助我们实现功能,包括注解(@controller 等)、spring security、spring jpa 等很多基础功能。
软件架构指软件系统的“基础结构”,创造这些基础结构的准则,以及对这些结构的描述。
单纯从定义的角度来看,框架和架构的区别还是比较明显的,框架关注的是“规范”,架构关注的是“结构”。框架的英文是 framework,架构的英文是 architecture。spring mvc 的英文文档标题就是“web mvc framework”。
“从业务逻辑的角度分解,“学生管理系统”的机构是:
从物理部署的角度分解,“学生管理系统”的架构是:
从开发规范的角度分解,“学生管理系统”可以采用标准的 mvc 框架来开发,因此架构又变成了 mvc 架构:
这些“架构”,都是“学生管理系统”正确的架构,只是从不同的角度来分解而已,这也是 ibm 的 rup 将软件架构视图分为著名的“4+1 视图”的原因。
重新定义架构
“软件架构指软件系统的顶层结构”——李运华
首先,“系统是一群关联个体组成”,这些“个体”可以是“子系统”“模块”“组件”等;架构需要明确系统包含哪些“个体”。
其次,系统中的个体需要“根据某种规则”运作,架构需要明确个体运作和协作的规则。
第三,*定义的架构用到了“基础结构”这个说法,我改为“顶层结构”,可以更好地区分系统和子系统,避免将系统架构和子系统架构混淆在一起导致架构层次混乱。
精选留言:
02 | 架构设计的历史背景
1.机器语言(1940 年之前)
101100000000000000000011 000001010000000000110000 001011010000000000000101
2.汇编语言(20 世纪 40 年代)
为了解决机器语言编写、阅读、修改复杂的问题,汇编语言应运而生。汇编语言又叫“符号语言”,用助记符代替机器指令的操作码,用地址符号(symbol)或标号(label)代替指令或操作数的地址。
机器语言:1000100111011000 汇编语言:mov ax,bx
.section .data a: .int 10 b: .int 20 format: .asciz "%d\n" .section .text .global _start _start: movl a, %edx addl b, %edx pushl %edx pushl $format call printf movl $0, (%esp) call exit
除了编写本身复杂,还有另外一个复杂的地方在于:不同 cpu 的汇编指令和结构是不同的。
3.高级语言(20 世纪 50 年代)
- fortran:1955 年,名称取自”formula translator”,即公式翻译器,由约翰·巴科斯(john backus)等人发明。
- lisp:1958 年,名称取自”list processor”,即枚举处理器,由约翰·麦卡锡(john mccarthy)等人发明。
- cobol:1959 年,名称取自”common business oriented language”,即通用商业导向语言,由葛丽丝·霍普(grace hopper)发明。
这些语言让程序员不需要关注机器底层的低级结构和逻辑,而只要关注具体的问题和业务即可。
4.第一次软件危机与结构化程序设计(20 世纪 60 年代~20 世纪 70 年代)
20 世纪 60 年代中期开始爆发了第一次软件危机,典型表现有软件质量低下、项目无法如期完成、项目严重超支等,因为软件而导致的重大事故时有发生。例如,1963 年美国(http://en.wikipedia.org/wiki/mariner_1)的水手一号火箭发射失败事故,就是因为一行 fortran 代码错误导致的。
结构化程序方法成为了 20 世纪 70 年代软件开发的潮流。
5.第二次软件危机与面向对象(20 世纪 80 年代)
结构化编程的风靡在一定程度上缓解了软件危机,然而随着硬件的快速发展,业务需求越来越复杂,以及编程应用领域越来越广泛,第二次软件危机很快就到来了。第二次软件危机的根本原因还是在于软件生产力远远跟不上硬件和业务的发展。
第一次软件危机的根源在于软件的“逻辑”变得非常复杂,而第二次软件危机主要体现在软件的“扩展”变得非常复杂。
结构化程序设计虽然能够解决(也许用“缓解”更合适)软件逻辑的复杂性,但是对于业务变化带来的软件扩展却无能为力,软件领域迫切希望找到新的银弹来解决软件危机,在这种背景下,面向对象的思想开始流行起来。
软件架构的历史背景
虽然早在 20 世纪 60 年代,戴克斯特拉这位上古大神就已经涉及软件架构这个概念了,但软件架构真正流行却是从 20 世纪 90 年代开始的,由于在 rational 和 microsoft 内部的相关活动,软件架构的概念开始越来越流行了。
软件架构的出现有其历史必然性。
20 世纪 60 年代第一次软件危机引出了“结构化编程”,创造了“模块”概念;
20 世纪 80 年代第二次软件危机引出了“面向对象编程”,创造了“对象”概念;
20 世纪 90 年代“软件架构”开始流行,创造了“组件”概念。
我们可以看到,“模块”“对象”“组件”本质上都是对达到一定规模的软件进行拆分,差别只是在于随着软件的复杂度不断增加,拆分的粒度越来越粗,拆分的层次越来越高。《人月神话》中提到的 ibm 360 大型系统,开发时间是 1964 年,那个时候结构化编程都还没有提出来,更不用说软件架构了。如果 ibm 360 系统放在 20 世纪 90 年代开发,不管是质量还是效率、成本,都会比 1964 年开始做要好得多,当然,这样的话我们可能就看不到《人月神话》了。
03 | 架构设计的目的
架构设计的真正目的究竟是什么?
架构设计的主要目的是为了解决软件系统复杂度带来的问题。
架构设计并不是要面面俱到,不需要每个架构都具备高性能、高可用、高扩展等特点,而是要识别出复杂点然后有针对性地解决问题。
简单的复杂度分析案例:
假设我们需要设计一个学生管理系统:
- 性能:一个学校大约1~2万人,学生管理系统的访问率并不高,因此性能要求并不高,存储用mysql完全能胜任,缓存可以不用,web服务器用nginx绰绰有余。
- 可扩展性:学生管理系统的功比较稳定,可扩展性不强。
- 高可用:宕机2小时对学生影响可能不大,可以不做负载均衡,不用考虑异地多活这类复杂的方案。学生信息的存储比较重要,因此需要考虑存储高可靠。还要考虑:机器故障、机房故障,针对机器故障可设计mysql同机房主备方案;针对机房故障,可考虑设计mysql跨机房同步方案。
- 安全性:基本满足:nginx提供acl控制、用户账号密码管理、数据库访问权限控制。
- 成本:服务器使用数量不多。
精选留言:
04 | 复杂度来源:高性能
软件系统中高性能带来的复杂度主要体现在两个方面:
- 单台计算机内部为了高性能带来的复杂度
- 多太计算机集群为了高性能带来的复杂度
单机复杂度
计算机内部复杂度最关键的地方就是操作系统。计算机性能的发展本质上是悠硬件发展驱动的,尤其是cpu的性能发展。
操作系统和性能相关的就是进程和线程。
- 最早的计算机没有操作系统,只有输入、计算和输出功能。这样的处理性能效率很低。
- 为解决手工操作带来的低效,批处理应运而生,性能就有了很大的提升。(缺点:计算机一次只能执行一个任务,如果某个任务需要从i/o设备(例如磁带)读取大量的数据,在i/o操作的过程中,cpu其实是空闲的,浪费了部分资源)
- 为进一步提升性能,人们发明了“进程”,用进程来对应一个任务,每个任务都有自己的独立内存空间,进程间互不相关,由操作系统来进行调度。(此时的cpu还没有多核和多线程的概念,为了达到多进程并行的目的,采取了分时的方式。同时,进程间通信的各种方式被设计出来,包括管道、消息队列、信号量、共享存储等。多进程让多任务能够并行处理,但本身缺点:单个进程内部只能串行处理,而实际上很多进程内部的子任务并不要求是严格按照时间顺序来执行的,也需要并行处理。)
- 为解决进程的缺点,人们发明了线程。(同时,为保证数据的正确性,又发明了互斥锁机制。有了多线程后,操作系统调度的最小单位就变成了线程,而进程变成了操作系统分配资源的最小单位。多进程多线程虽让多任务并行处理的性能大大提升,但本质还是分时系统,并不能实现真正意义上的多任务并行)
- 多个cpu能够同时执行计算任务,实现真正意义上的多任务并行:目前这样的解决方案有 3 种:smp(symmetric multi-processor,对称多处理器结构)、numa(non-uniform memory access,非一致存储访问结构)、mpp(massive parallel processing,海量并行处理结构)。
操作系统发展到现在,如果我们要完成一个高性能的软件系统,需要考虑如多进程、多线程、进程间通信、多线程并发等技术点,而且这些技术并不是最新的就是最好的,也不是非此即彼的选择。在做架构设计的时候,需要花费很大的精力来结合业务进行分析、判断、选择、组合,这个过程同样很复杂。
虽然,计算机操作系统和硬件的发展已经很快了,但是在进入互联网时代后,业务的发展速度远远更超前了。例如:
- 2016 年“双 11”支付宝每秒峰值达 12 万笔支付。
- 2017 年春节微信红包收发红包每秒达到 76 万个
单机的性能无法支撑业务需求的增长,必须采用机器集群的方式来达到高性能。但是,通过大量的机器来提升性能,并不仅仅是增加机器这么简单,下面是针对几种方式的加单分析:
1.任务分配:
任务分配的意思是指,每台机器都可以处理完整的业务任务,不同的任务分配到不同的机器上执行。
例如:从最简单的一台服务器变两台服务器:
此时架构上明显要复杂多了,主要体现在:
- 需要增加一个任务分配器,这个分配器可能是硬件网络设备(例如,f5、交换机等),可能是软件网络设备(例如,lvs),也可能是负载均衡软件(例如,nginx、haproxy),还可能是自己开发的系统。选择合适的任务分配器也是一件复杂的事情,需要综合考虑性能、成本、可维护性、可用性等各方面的因素。
- 任务分配器和真正的业务服务器之间有连接和交互(即图中任务分配器到业务服务器的连接线),需要选择合适的连接方式,并且对连接进行管理。例如,连接建立、连接检测、连接中断后如何处理等。
- 任务分配器需要增加分配算法。例如,是采用轮询算法,还是按权重分配,又或者按照负载进行分配。如果按照服务器的负载进行分配,则业务服务器还要能够上报自己的状态给任务分配器。
假设性能要求继续提高,要求每秒提升到10万次:
这个架构比 2 台业务服务器的架构要复杂,主要体现在:
- 任务分配器从 1 台变成了多台(对应图中的任务分配器 1 到任务分配器 m),这个变化带来的复杂度就是需要将不同的用户分配到不同的任务分配器上(即图中的虚线“用户分配”部分),常见的方法包括 dns 轮询、智能 dns、cdn(content delivery network,内容分发网络)、gslb 设备(global server load balance,全局负载均衡)等。
- 任务分配器和业务服务器的连接从简单的“1 对多”(1 台任务分配器连接多台业务服务器)变成了“多对多”(多台任务分配器连接多台业务服务器)的网状结构。
- 机器数量从 3 台扩展到 30 台(一般任务分配器数量比业务服务器要少,这里我们假设业务服务器为 25 台,任务分配器为 5 台),状态管理、故障处理复杂度也大大增加。
上面这两个例子都是以业务处理为例,实际上“任务”涵盖的范围很广,可以指完整的业务处理,也可以单指某个具体的任务。例如,“存储”“运算”“缓存”等都可以作为一项任务,因此存储系统、运算系统、缓存系统都可以按照任务分配的方式来搭建架构。此外,“任务分配器”也并不一定只能是物理上存在的机器或者者一个独立运行的程序,也可以是嵌入在其他程序中的算法,例如 memcache 的集群架构。
2.任务分解
通过任务分配的方式,能够突破单台机器处理性能的瓶颈,通过增加更多的机器来满足业务的性能需求,但如果业务本身也越来越复杂,单纯只通过任务分配的方式来扩展性能,收益会越来越低。
为了能够继续提升性能,我们需要采取第二种方式:任务分解。
那为何通过任务分解就能够提升性能呢?
1.简单的系统更加容易做到高性能
系统的功能越简单,影响性能的点就越少,就更加容易进行有针对性的优化。而系统很复杂的情况下,首先是比较难以找到关键性能点,因为需要考虑和验证的点太多;其次是即使花费很大力气找到了,修改起来也不容易。
2.可以针对单个任务进行扩展
当各个逻辑任务分解到独立的子系统后,整个系统的性能瓶颈更加容易发现,而且发现后只需要针对有瓶颈的子系统进行性能优化或者提升,不需要改动整个系统,风险会小很多。
虽然系统拆分可能在某种程度上能提升业务处理性能,但提升性能也是有限的。理论上的性能是有一个上限的,系统拆分能够让性能逼近这个极限,但无法突破这个极限。因此,任务分解带来的性能收益是有一个度的,并不是任务分解越细越好,而对于架构设计来说,如何把握这个粒度就非常关键了。
精选留言:
05 | 复杂度来源:高可用
高可用:系统无中断地执行其功能的能力,代表系统的可用性程度,是进行系统设计时的准则之一。
“无中断”的干扰因素有很多:硬件出现故障、软件bug、外部环境的不可控,不可避免性,地震水灾等。所以,系统的高可用方案五花八门,但是本质都是通过“冗余”来实现高可用。
通俗点来讲,就是一台机器不够就两台,两台不够就四台;一个机房可能断电,那就部署两个机房;一条通道可能故障,那就用两条,两条不够那就用三条(移动、电信、联通一起上)。
高可用的“冗余”解决方案,单纯从形式上来看,和之前讲的高性能是一样的,都是通过增加更多机器来达到目的,但其实本质上是有根本区别的:高性能增加机器目的在于“扩展”处理性能;高可用增加机器目的在于“冗余”处理单元。
1.计算高可用
这里的“计算”指的是业务的逻辑处理。计算有一个特点就是无论在哪台机器上进行计算,同样的算法和输入数据,产出的结果都是一样的,所以将计算从一台机器迁移到另外一台机器,对业务并没有什么影响。
单机变双机的简单架构示意图:
这个双机的架构图和上期“高性能”讲到的双机架构图是一样的,因此复杂度也是类似的,具体表现为:
- 需要增加一个任务分配器,选择合适的任务分配器也是一件复杂的事情,需要综合考虑性能、成本、可维护性、可用性等各方面因素。
- 任务分配器和真正的业务服务器之间有连接和交互,需要选择合适的连接方式,并且对连接进行管理。例如,连接建立、连接检测、连接中断后如何处理等。
- 任务分配器需要增加分配算法。例如,常见的双机算法有主备、主主,主备方案又可以细分为冷备、温备、热备。
上面这个示意图只是简单的双机架构,再看一个复杂一点的高可用集群架构:
这个高可用集群相比双机来说,分配算法更加复杂,可以是 1 主 3 备、2 主 2 备、3 主 1 备、4 主 0 备,具体应该采用哪种方式,需要结合实际业务需求来分析和判断,并不存在某种算法就一定优于另外的算法。例如,zookeeper 采用的就是 1 主多备,而 memcached 采用的就是全主 0 备。
2.存储高可用
存储与计算相比,有一个本质上的区别:将数据从一台机器搬到到另一台机器,需要经过线路进行传输。
- 正常情况下的传输延迟:线路传输的速度是毫秒级别,同一机房内部能够做到几毫秒;分布在不同地方的机房,传输耗时需要几十甚至上百毫秒。(例如,从广州机房到北京机房,稳定情况下 ping 延时大约是 50ms,不稳定情况下可能达到 1s 甚至更多。)
- 异常情况下的传输中断:传输线路可能中断、可能拥塞、可能异常(错包、丢包),并且传输线路的故障时间一般都特别长,短的十几分钟,长的几个小时都是可能的。
综合分析,以上两点都会导致系统的数据在某个时间点或者时间段是不一致的,而数据的不一致又会导致业务问题;但如果完全不做冗余,系统的整体高可用又无法保证,所以存储高可用的难点不在于如何备份数据,而在于如何减少或者规避数据不一致对业务造成的影响。分布式领域里面有一个著名的 cap 定理,从理论上论证了存储高可用的复杂度。也就是说,存储高可用不可能同时满足“一致性、可用性、分区容错性”,最多满足其中两个,这就要求我们在做架构设计时结合业务进行取舍。
高可用状态决策
一个本质的矛盾:通过冗余来实现的高可用系统,状态决策本质上就不可能做到完全正确。下面我基于几种常见的决策方式进行详细分析。
1. *式
*式的决策方式:
- 优点:不会出现决策混乱的问题,因为只有一个决策者。
- 缺点:当决策者本身故障时,整个系统就无法实现准确的状态决策。如果决策者本身又做一套状态决策,那就陷入一个递归的死循环了。
2. 协商式
协商式决策指的是两个独立的个体通过交流信息,然后根据规则进行决策,最常用的协商式决策就是主备决策。
这个架构的基本协商规则可以设计成:
- 2 台服务器启动时都是备机。
- 2 台服务器建立连接。
- 2 台服务器交换状态信息。
- 某 1 台服务器做出决策,成为主机;另一台服务器继续保持备机身份。
协商式决策的架构不复杂,规则也不复杂,其难点在于,如果两者的信息交换出现问题(比如主备连接中断),此时状态决策应该怎么做。如果备机在连接中断的情况下认为主机故障,那么备机需要升级为主机。
下面分为三种情况:
第一种情况:如果备机在连接中断的情况下,实际上主机并没有故障,那么系统就出现了两个主机,这与设计初衷(1 主 1 备)是不符合的。
第二种情况:如果备机在连接中断的情况下不认为主机故障,则此时如果主机真的发生故障,那么系统就没有主机了,这同样与设计初衷(1 主 1 备)是不符合的。
第三种情况:如果为了规避连接中断对状态决策带来的影响,可以增加更多的连接。
例如,双连接、三连接。这样虽然能够降低连接中断对状态带来的影响(注意:只能降低,不能彻底解决),但同时又引入了这几条连接之间信息取舍的问题,即如果不同连接传递的信息不同,应该以哪个连接为准?实际上这也是一个无解的答案,无论以哪个连接为准,在特定场景下都可能存在问题。
综合分析,协商式状态决策在某些场景总是存在一些问题的。
3. *式
*式决策指的是多个独立的个体通过投票的方式来进行状态决策。例如,zookeeper 集群在选举 leader 时就是采用这种方式。
*式决策和协商式决策比较类似,其基础都是独立的个体之间交换信息,每个个体做出自己的决策,然后按照“多数取胜”的规则来确定最终的状态。不同点在于*式决策比协商式决策要复杂得多,zookeeper 的选举算法 paxos,绝大部分人都看得云里雾里,更不用说用代码来实现这套算法了。
除了算法复杂,*式决策还有一个固有的缺陷:脑裂。
从图中可以看到,正常状态的时候,节点 5 作为主节点,其他节点作为备节点;当连接发生故障时,节点 1、节点 2、节点 3 形成了一个子集群,节点 4、节点 5 形成了另外一个子集群,这两个子集群的连接已经中断,无法进行信息交换。按照*决策的规则和算法,两个子集群分别选出了节点 2 和节点 5 作为主节点,此时整个系统就出现了两个主节点。这个状态违背了系统设计的初衷,两个主节点会各自做出自己的决策,整个系统的状态就混乱了。
为了解决脑裂问题,*式决策的系统一般都采用“投票节点数必须超过系统总节点数一半”规则来处理。
如图中那种情况,节点 4 和节点 5 形成的子集群总节点数只有 2 个,没有达到总节点数 5 个的一半,因此这个子集群不会进行选举。这种方式虽然解决了脑裂问题,但同时降低了系统整体的可用性,即如果系统不是因为脑裂问题导致投票节点数过少,而真的是因为节点故障(例如,节点 1、节点 2、节点 3 真的发生了故障),此时系统也不会选出主节点,整个系统就相当于宕机了,尽管此时还有节点 4 和节点 5 是正常的。
综合分析,无论采取什么样的方案,状态决策都不可能做到任何场景下都没有问题,但完全不做高可用方案又会产生更大的问题,如何选取适合系统的高可用方案,也是一个复杂的分析、判断和选择的过程。
精选留言:
06 | 复杂度来源:可扩展性
可扩展性指系统为了应对将来需求变化而提供的一种扩展能力,当有新的需求出现时,系统不需要或者仅需要少量修改就可以支持,无须整个系统重构或者重建。
- 软件系统固有的多变性,新的需求总会不断提出来,因此可扩展性显得尤其重要。
- 在软件开发领域,面向对象思想的提出,就是为了解决可扩展性带来的问题。
- 设计模式,更是将可扩展性做到了极致。
设计具备良好可扩展性的系统,有两个基本条件:正确预测变化、完美封装变化。
预测变化的复杂性在于:
- 不能每个设计点都考虑可扩展性。
- 不能完全不考虑可扩展性。
- 所有的预测都存在出错的可能性。
对于架构师来说,如何把握预测的程度和提升预测结果的准确性,是一件很复杂的事情,而且没有通用的标准可以简单套上去,更多是靠自己的经验、直觉。没有明确标准,不同的人理解和判断有偏差,而最终又只能选择一个判断。
应对变化
即使是经验丰富的架构师,在预测到所有的变化的可能性,也不能保证可扩展性就很容易得到实现,预测准确,方案不适合,也是一件很麻烦的事情。
第一种应对变化的常见方案是将“变化”封装在一个“变化层”,将不变的部分封装在一个独立的“稳定层”。
无论是变化层依赖稳定层,还是稳定层依赖变化层都是可以的,需要根据具体业务情况来设计。例如,如果系统需要支持 xml、json、protocolbuffer 三种接入方式,那么最终的架构就是上面图中的“形式 1”架构,也就是下面这样。
如果系统需要支持 mysql、oracle、db2 数据库存储,那么最终的架构就变成了“形式 2”的架构了,你可以看下面这张图。
无论采取哪种形式,通过剥离变化层和稳定层的方式应对变化,都会带来两个主要的复杂性相关的问题。
- 系统需要拆分出变化层和稳定层
- 需要设计变化层和稳定层之间的接口
第二种常见的应对变化的方案是提炼出一个“抽象层”和一个“实现层”。抽象层是稳定的,实现层可以根据具体业务需要定制开发,当加入新的功能时,只需要增加新的实现,无须修改抽象层。这种方案典型的实践就是设计模式和规则引擎。考虑到绝大部分技术人员对设计模式都非常熟悉,我以设计模式为例来说明这种方案的复杂性。以设计模式的“装饰者”模式来分析,下面是装饰者模式的类关系图。
图中的 component 和 decorator 就是抽象出来的规则,这个规则包括几部分:
- component 和 decorator 类。
- decorator 类继承 component 类。
- decorator 类聚合了 component 类。
这个规则一旦抽象出来后就固定了,不能轻易修改。例如,把规则 3 去掉,就无法实现装饰者模式的目的了。装饰者模式相比传统的继承来实现功能,确实灵活很多。
例如,《设计模式》中装饰者模式的样例“textview”类的实现,用了装饰者之后,能够灵活地给 textview 增加额外更多功能,比如可以增加边框、滚动条、背景图片等,这些功能上的组合不影响规则,只需要按照规则实现即可。但装饰者模式相对普通的类实现模式,明显要复杂多了。本来一个函数或者一个类就能搞定的事情,现在要拆分成多个类,而且多个类之间必须按照装饰者模式来设计和调用。
精选留言:
07 | 复杂度来源:低成本、安全、规模
前面已经讲了高性能、高可用和可扩展性,今天我来聊聊复杂度另外三个来源低成本、安全和规模。
1.低成本
当我们设计“高性能”“高可用”的架构时,通用的手段都是增加更多服务器来满足“高性能”和“高可用”的要求;而低成本正好与此相反,我们需要减少服务器的数量才能达成低成本的目标。因此,低成本本质上是与高性能和高可用冲突的,所以低成本很多时候不会是架构设计的首要目标,而是架构设计的附加约束。
低成本给架构设计带来的主要复杂度体现在,往往只有“创新”才能达到低成本目标。这里的“创新”既包括开创一个全新的技术领域(这个要求对绝大部分公司太高),也包括引入新技术,如果没有找到能够解决自己问题的新技术,那么就真的需要自己创造新技术了。
类似的新技术例子很多:
- nosql(memcache、redis 等)的出现是为了解决关系型数据库无法应对高并发访问带来的访问压力。
- 全文搜索引擎(sphinx、elasticsearch、solr)的出现是为了解决关系型数据库 like 搜索的低效的问题。
- hadoop 的出现是为了解决传统文件系统无法应对海量数据存储和计算的问题。
再来举几个业界类似的例子:
- facebook 为了解决 php 的低效问题,刚开始的解决方案是 hiphop php,可以将 php 语言翻译为 c++ 语言执行,后来改为 hhvm,将 php 翻译为字节码然后由虚拟机执行,和 java 的 jvm 类似。
- 新浪微博将传统的 redis/mc + mysql 方式,扩展为 redis/mc + ssd cache + mysql 方式,ssd cache 作为 l2 缓存使用,既解决了 mc/redis 成本过高,容量小的问题,也解决了穿透 db 带来的数据库访问压力(来源:http://www.infoq.com/cn/articles/weibo-platform-archieture )。
- linkedin 为了处理每天 5 千亿的事件,开发了高效的 kafka 消息系统。
- 其他类似将 ruby on rails 改为 java、lua + redis 改为 go 语言实现的例子还有很多。
无论是引入新技术,还是自己创造新技术,都是一件复杂的事情。引入新技术的主要复杂度在于需要去熟悉新技术,并且将新技术与已有技术结合起来;创造新技术的主要复杂度在于需要自己去创造全新的理念和技术,并且新技术跟旧技术相比,需要有质的飞跃。
2.安全
安全本身是一个庞大而又复杂的技术领域,并且一旦出问题,对业务和企业形象影响非常大。例如:
- 2016 年雅虎爆出史上最大规模信息泄露事件,逾 5 亿用户资料在 2014 年被窃取。2
- 016 年 10 月美国遭史上最大规模 ddos 攻击,东海岸网站集体瘫痪。
- 2013 年 10 月,为全国 4500 多家酒店提供网络服务的浙江慧达驿站网络有限公司,因安全漏洞问题,致 2 千万条入住酒店的客户信息泄露,由此导致很多敲诈、家庭破裂的后续事件。
正因为经常能够看到或者听到各类安全事件,所以大部分技术人员和架构师,对安全这部分会多一些了解和考虑。
从技术的角度来讲,安全可以分为两类:
1.功能上的安全:
例如,常见的 xss 攻击、csrf 攻击、sql 注入、windows 漏洞、密码破解等,本质上是因为系统实现有漏洞,黑客有了可乘之机。
从实现的角度来看,功能安全更多地是和具体的编码相关,与架构关系不大。现在很多开发框架都内嵌了常见的安全功能,能够大大减少安全相关功能的重复开发,但框架只能预防常见的安全漏洞和风险(常见的 xss 攻击、csrf 攻击、sql 注入等),无法预知新的安全问题,而且框架本身很多时候也存在漏洞(例如,流行的 apache struts2 就多次爆出了调用远程代码执行的高危漏洞,给整个互联网都造成了一定的恐慌)。
所以功能安全是一个逐步完善的过程,而且往往都是在问题出现后才能有针对性的提出解决方案,我们永远无法预测系统下一个漏洞在哪里,也不敢说自己的系统肯定没有任何问题。换句话讲,功能安全其实也是一个“攻”与“防”的矛盾,只能在这种攻防大战中逐步完善,不可能在系统架构设计的时候一劳永逸地解决。
2.架构上的安全:
架构设计时需要特别关注架构安全,尤其是互联网时代,理论上来说系统部署在互联网上时,全球任何地方都可以发起攻击。
传统的架构安全主要依靠防火墙,防火墙最基本的功能就是隔离网络,通过将网络划分成不同的区域,制定出不同区域之间的访问控制策略来控制不同信任程度区域间传送的数据流。例如,下图是一个典型的银行系统的安全架构。
从图中你可以看到,整个系统根据不同的分区部署了多个防火墙来保证系统的安全。
防火墙的功能虽然强大,但性能一般,所以在传统的银行和企业应用领域应用较多。但在互联网领域,防火墙的应用场景并不多。因为互联网的业务具有海量用户访问和高并发的特点,防火墙的性能不足以支撑;尤其是互联网领域的 ddos 攻击,轻则几 gb,重则几十 gb。2016 年知名安全研究人员布莱恩·克莱布斯(brian krebs)的安全博客网站遭遇 ddos 攻击,攻击带宽达 665gbps,是目前在网络犯罪领域已知的最大的拒绝服务攻击。这种规模的攻击,如果用防火墙来防,则需要部署大量的防火墙,成本会很高。例如,中高端一些的防火墙价格 10 万元,每秒能抗住大约 25gb 流量,那么应对这种攻击就需要将近 30 台防火墙,成本将近 300 万元,这还不包括维护成本,而这些防火墙设备在没有发生攻击的时候又没有什么作用。也就是说,如果花费几百万元来买这么一套设备,有可能几年都发挥不了任何作用。就算是公司对钱不在乎,一般也不会堆防火墙来防 ddos 攻击,因为 ddos 攻击最大的影响是大量消耗机房的出口总带宽。不管防火墙处理能力有多强,当出口带宽被耗尽时,整个业务在用户看来就是不可用的,因为用户的正常请求已经无法到达系统了。防火墙能够保证内部系统不受冲击,但用户也是进不来的。对于用户来说,业务都已经受到影响了,至于是因为用户自己进不去,还是因为系统出故障,用户其实根本不会关心。
基于上述原因,互联网系统的架构安全目前并没有太好的设计手段来实现,更多地是依靠运营商或者云服务商强大的带宽和流量清洗的能力,较少自己来设计和实现。
3.规模
很多企业级的系统,既没有高性能要求,也没有双中心高可用要求,也不需要什么扩展性,但往往我们一说到这样的系统,很多人都会脱口而出:这个系统好复杂!为什么这样说呢?关键就在于这样的系统往往功能特别多,逻辑分支特别多。特别是有的系统,发展时间比较长,不断地往上面叠加功能,后来的人由于不熟悉整个发展历史,可能连很多功能的应用场景都不清楚,或者细节根本无法掌握,面对的就是一个黑盒系统,看不懂、改不动、不敢改、修不了,复杂度自然就感觉很高了。
规模带来复杂度的主要原因就是“量变引起质变”,当数量超过一定的阈值后,复杂度会发生质的变化。常见的规模带来的复杂度有:
1. 功能越来越多,导致系统复杂度指数级上升
例如,某个系统开始只有 3 大功能,后来不断增加到 8 大功能,虽然还是同一个系统,但复杂度已经相差很大了,具体相差多大呢?我以一个简单的抽象模型来计算一下,假设系统间的功能都是两两相关的,系统的复杂度 = 功能数量 + 功能之间的连接数量,通过计算我们可以看出:
- 3 个功能的系统复杂度 = 3 + 3 = 6
- 8 个功能的系统复杂度 = 8 + 28 = 36
可以看出,具备 8 个功能的系统的复杂度不是比具备 3 个功能的系统的复杂度多 5,而是多了 30,基本是指数级增长的,主要原因在于随着系统功能数量增多,功能之间的连接呈指数级增长。下图形象地展示了功能数量的增多带来了复杂度。
通过肉眼就可以很直观地看出,具备 8 个功能的系统复杂度要高得多。
2. 数据越来越多,系统复杂度发生质变
与功能类似,系统数据越来越多时,也会由量变带来质变,最近几年火热的“大数据”就是在这种背景下诞生的。大数据单独成为了一个热门的技术领域,主要原因就是数据太多以后,传统的数据收集、加工、存储、分析的手段和工具已经无法适应,必须应用新的技术才能解决。目前的大数据理论基础是 google 发表的三篇大数据相关论文,其中 google file system 是大数据文件存储的技术理论,google bigtable 是列式数据存储的技术理论,google mapreduce 是大数据运算的技术理论,这三篇技术论文各自开创了一个新的技术领域。
即使我们的数据没有达到大数据规模,数据的增长也可能给系统带来复杂性。最典型的例子莫过于使用关系数据库存储数据,我以 mysql 为例,mysql 单表的数据因不同的业务和应用场景会有不同的最优值,但不管怎样都肯定是有一定的限度的,一般推荐在 5000 万行左右。如果因为业务的发展,单表数据达到了 10 亿行,就会产生很多问题,例如:
- 添加索引会很慢,可能需要几个小时,这几个小时内数据库表是无法插入数据的,相当于业务停机了。
- 修改表结构和添加索引存在类似的问题,耗时可能会很长。
- 即使有索引,索引的性能也可能会很低,因为数据量太大。
- 数据库备份耗时很长。
- ……
因此,当 mysql 单表数据量太大时,我们必须考虑将单表拆分为多表,这个拆分过程也会引入更多复杂性,例如:
拆表的规则是什么?以用户表为例:是按照用户 id 拆分表,还是按照用户注册时间拆表?拆完表后查询如何处理?以用户表为例:假设按照用户 id 拆表,当业务需要查询学历为“本科”以上的用户时,要去很多表查询才能得到最终结果,怎么保证性能?
推荐阅读
-
自己从0开始学习Unity的笔记 I (C#字符串转换)
-
【学习笔记】C语言习题:有n个人围成一圈,顺序排号。从第一个人开始报数(从1到3报数),凡报到3的人退出圈子,问最后留下的是原来第几号的那位。
-
从0开始接触html--第一天学习内容总结
-
《从零开始学Swift》学习笔记(Day1)——我的第一行Swift代码
-
《从零开始学架构》笔记——第一部分:概念和基础
-
《从0开始学架构》学习笔记(一)
-
从0开始的微服务架构:(一)重识微服务架构
-
自己从0开始学习Unity的笔记 I (C#字符串转换)
-
【学习笔记】C语言习题:有n个人围成一圈,顺序排号。从第一个人开始报数(从1到3报数),凡报到3的人退出圈子,问最后留下的是原来第几号的那位。
-
从0开始的Python学习013编写一个Python脚本