This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,151 @@
<audio id="audio" title="01 | 架构到底是指什么?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0d/dc/0de96702b98219cd28de5ae2096a5bdc.mp3"></audio>
对于技术人员来说“架构”是一个再常见不过的词了。我们会对新员工培训整个系统的架构参加架构设计评审学习业界开源系统例如MySQL、Hadoop的架构研究大公司的架构实现例如微信架构、淘宝架构……虽然“架构”这个词常见但如果深究一下“架构”到底指什么大部分人也许并不一定能够准确地回答。例如
<li>
架构和框架是什么关系?有什么区别?
</li>
<li>
Linux有架构MySQL有架构JVM也有架构使用Java开发、MySQL存储、跑在Linux上的业务系统也有架构应该关注哪个架构呢
</li>
<li>
微信有架构,微信的登录系统也有架构,微信的支付系统也有架构,当我们谈微信架构时,到底是在谈什么架构?
</li>
要想准确地回答这几个问题,关键在于梳理几个有关系而又相似的概念,包括:系统与子系统、模块与组件、框架与架构。
## 系统与子系统
我们先来看维基百科定义的“系统”。
>
系统泛指由一群有关联的个体组成,根据某种规则运作,能完成个别元件不能单独完成的工作的群体。它的意思是“总体”“整体”或“联盟”。
我来提炼一下里面的关键内容:
<li>
**关联**系统是由一群有关联的个体组成的没有关联的个体堆在一起不能成为一个系统。例如把一个发动机和一台PC放在一起不能称之为一个系统把发动机、底盘、轮胎、车架组合起来才能成为一台汽车。
</li>
<li>
**规则**:系统内的个体需要按照指定的规则运作,而不是单个个体各自为政。规则规定了系统内个体分工和协作的方式。例如,汽车发动机负责产生动力,然后通过变速器和传动轴,将动力输出到车轮上,从而驱动汽车前进。
</li>
<li>
**能力**:系统能力与个体能力有本质的差别,系统能力不是个体能力之和,而是产生了新的能力。例如,汽车能够载重前进,而发动机、变速器、传动轴、车轮本身都不具备这样的能力。
</li>
我们再来看子系统的定义。
>
子系统也是由一群有关联的个体所组成的系统,多半会是更大系统中的一部分。
其实子系统的定义和系统定义是一样的,只是观察的角度有差异,一个系统可能是另外一个更大系统的子系统。
按照这个定义,系统和子系统比较容易理解。我们以微信为例来做一个分析。
<li>
微信本身是一个系统,包含聊天、登录、支付、朋友圈等子系统。
</li>
<li>
朋友圈这个系统又包括动态、评论、点赞等子系统。
</li>
<li>
评论这个系统可能又包括防刷子系统、审核子系统、发布子系统、存储子系统。
</li>
<li>
评论审核子系统不再包含业务意义上的子系统而是包括各个模块或者组件这些模块或者组件本身也是另外一个维度上的系统。例如MySQL、Redis等是存储系统但不是业务子系统。
</li>
## 模块与组件
模块和组件两个概念在实际工作中很容易混淆,我们经常能够听到类似这样的说法:
<li>
MySQL模块主要负责存储数据而ElasticSearch模块主要负责数据搜索。
</li>
<li>
我们有安全加密组件、有审核组件。
</li>
<li>
App的下载模块使用了第三方的组件。
</li>
造成这种现象的主要原因是,模块与组件的定义并不好理解,也不能很好地进行区分。我们来看看这两者在维基百科上的定义。
>
软件模块Module是一套一致而互相有紧密关连的软件组织。它分别包含了程序和数据结构两部分。现代软件开发往往利用模块作为合成的单位。模块的接口表达了由该模块提供的功能和调用它时所需的元素。模块是可能分开被编写的单位。这使它们可再用和允许人员同时协作、编写及研究不同的模块。
>
软件组件定义为自包含的、可编程的、可重用的、与语言无关的软件单元,软件组件可以很容易被用于组装应用程序中。
可能你看完这两个定义后一头雾水,还是不知道这两者有什么区别。造成这种现象的根本原因是,**模块和组件都是系统的组成部分,只是从不同的角度拆分系统而已**。
从逻辑的角度来拆分系统后得到的单元就是“模块”从物理的角度来拆分系统后得到的单元就是“组件”。划分模块的主要目的是职责分离划分组件的主要目的是单元复用。其实“组件”的英文component也可翻译成中文的“零件”一词“零件”更容易理解一些“零件”是一个物理的概念并且具备“独立且可替换”的特点。
我以一个最简单的网站系统来为例。假设我们要做一个学生信息管理系统这个系统从逻辑的角度来拆分可以分为“登录注册模块”“个人信息模块”“个人成绩模块”从物理的角度来拆分可以拆分为Nginx、Web服务器、MySQL。
## 框架与架构
框架是和架构比较相似的概念,且两者有较强的关联关系,所以在实际工作中,这两个概念有时我们容易分不清楚。参考维基百科上框架与架构的定义,我来解释两者的区别。
>
软件框架Software framework通常指的是为了实现某个业界标准或完成特定基本任务的软件组件规范也指为了实现某个软件组件规范时提供规范所要求之基础功能的软件产品。
我来提炼一下其中关键部分:
<li>
框架是组件规范例如MVC就是一种最常见的开发规范类似的还有MVP、MVVM、J2EE等框架。
</li>
<li>
框架提供基础功能的产品例如Spring MVC是MVC的开发框架除了满足MVC的规范Spring提供了很多基础功能来帮助我们实现功能包括注解@Controller等、Spring Security、Spring JPA等很多基础功能。
</li>
>
软件架构指软件系统的“基础结构”,创造这些基础结构的准则,以及对这些结构的描述。
单纯从定义的角度来看,框架和架构的区别还是比较明显的,**框架关注的是“规范”,架构关注的是“结构”**。框架的英文是Framework架构的英文是Architecture。Spring MVC的英文文档标题就是“Web MVC framework”。
虽然如此在实际工作中我们却经常碰到一些似是而非的说法。例如“我们的系统是MVC架构”“我们需要将android app重构为MVP架构”“我们的系统基于SSH框架开发”“我们是SSH的架构”“XX系统是基于Spring MVC框架开发标准的MVC架构”……
究竟什么说法是对的,什么说法是错的呢?
其实这些说法都是对的,造成这种现象的根本原因隐藏于架构的定义中,关键就是“基础结构”这个概念并没有明确说是从什么角度来分解的。采用不同的角度或者维度,可以将系统划分为不同的结构,其实我在“模块与组件”中的“学生管理系统”示例已经包含了这点。
从业务逻辑的角度分解,“学生管理系统”的架构是:
<img src="https://static001.geekbang.org/resource/image/74/0c/746f547767d94a5a7b8a9a130fcefc0c.jpg" alt="" />
从物理部署的角度分解,“学生管理系统”的架构是:
<img src="https://static001.geekbang.org/resource/image/06/ed/0682867076f29d8f48c4021dabfd98ed.jpg" alt="" />
从开发规范的角度分解“学生管理系统”可以采用标准的MVC框架来开发因此架构又变成了MVC架构
<img src="https://static001.geekbang.org/resource/image/e1/1d/e1b415fd316dc3f487a75f228c5fcf1d.jpg" alt="" />
这些“架构”都是“学生管理系统”正确的架构只是从不同的角度来分解而已这也是IBM的RUP将软件架构视图分为著名的“**4+1视图**”的原因。
## 重新定义架构
参考维基百科的定义,我将架构重新定义为:**软件架构指软件系统的顶层结构**。
这个定义看似很简单,但包含的信息很丰富,基本上把系统、子系统、模块、组件、架构等概念都串起来了,我来详细解释一下。
首先,“系统是一群关联个体组成”,这些“个体”可以是“子系统”“模块”“组件”等;架构需要明确系统包含哪些“个体”。
其次,系统中的个体需要“根据某种规则”运作,架构需要明确个体运作和协作的规则。
第三,维基百科定义的架构用到了“基础结构”这个说法,我改为“顶层结构”,可以更好地区分系统和子系统,避免将系统架构和子系统架构混淆在一起导致架构层次混乱。
## 小结
今天我为你梳理了与架构有关的几个容易混淆的概念,包括系统与子系统、模块与组件、框架与架构,解释了架构的定义,希望对你有所帮助。
这就是今天的全部内容,留一道思考题给你吧。你原来理解的架构是如何定义的?对比我今天讲的架构定义,你觉得差异在哪里?
欢迎你把答案写到留言区,和我一起讨论。相信经过深度思考的回答,也会让你对知识的理解更加深刻。(编辑乱入:精彩的留言有机会获得丰厚福利哦!)

View File

@@ -0,0 +1,148 @@
<audio id="audio" title="02 | 架构设计的历史背景" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7d/80/7d858422ae749d95666e7f658632ad80.mp3"></audio>
理解了架构的有关概念和定义之后,今天,我会给你讲讲架构设计的历史背景。我认为,如果想要深入理解一个事物的本质,最好的方式就是去追寻这个事物出现的历史背景和推动因素。我们先来简单梳理一下软件开发进化的历史,探索一下软件架构出现的历史背景。
## 机器语言1940年之前
最早的软件开发使用的是“**机器语言**”直接使用二进制码0和1来表示机器可以识别的指令和数据。例如在8086机器上完成“s=768+12288-1280”的数学运算机器码如下
```
101100000000000000000011
000001010000000000110000
001011010000000000000101
```
不用多说不管是当时的程序员还是现在的程序员第一眼看到这样一串东西时肯定是一头雾水因为这实在是太难看懂了这还只是一行运算如果要输出一个“hello world”面对几十上百行这样的0/1串眼睛都要花了
看都没法看更何况去写这样的程序如果不小心哪个地方敲错了将1敲成了0例如
```
101100000000000000000011
000001010000000000110000
001011000000000000000101
```
如果要找出这个程序中的错误,程序员的心里阴影面积有多大?
归纳一下,机器语言的主要问题是三难:**太难写、太难读、太难改**
## 汇编语言20世纪40年代
为了解决机器语言编写、阅读、修改复杂的问题,**汇编语言**应运而生。汇编语言又叫“**符号语言**”用助记符代替机器指令的操作码用地址符号Symbol或标号Label代替指令或操作数的地址。
例如为了完成“将寄存器BX的内容送到AX中”的简单操作汇编语言和机器语言分别如下。
```
机器语言1000100111011000
汇编语言mov ax,bx
```
相比机器语言来说汇编语言就清晰得多了。mov是操作ax和bx是寄存器代号mov ax,bx语句基本上就是“将寄存器BX的内容送到AX”的简化版的翻译即使不懂汇编单纯看到这样一串语言至少也能明白大概意思。
汇编语言虽然解决了机器语言读写复杂的问题,但本质上还是**面向机器**的因为写汇编语言需要我们精确了解计算机底层的知识。例如CPU指令、寄存器、段地址等底层的细节。这对于程序员来说同样很复杂因为程序员需要将现实世界中的问题和需求按照机器的逻辑进行翻译。例如对于程序员来说在现实世界中面对的问题是4 + 6 = ?。而要用汇编语言实现一个简单的加法运算,代码如下:
```
.section .data
a: .int 10
b: .int 20
format: .asciz &quot;%d\n&quot;
.section .text
.global _start
_start:
movl a, %edx  
addl b, %edx  
pushl %edx
pushl $format
call printf
movl $0, (%esp)
call exit
```
这还只是实现一个简单的加法运算所需要的汇编程序,可以想象一下,实现一个四则运算的程序会更加复杂,更不用说用汇编写一个操作系统了!
除了编写本身复杂还有另外一个复杂的地方在于不同CPU的汇编指令和结构是不同的。例如Intel的CPU和Motorola的CPU指令不同同样一个程序为Intel的CPU写一次还要为Motorola的CPU再写一次而且指令完全不同。
## 高级语言20世纪50年代
为了解决汇编语言的问题计算机前辈们从20世纪50年代开始又设计了多个**高级语言**,最初的高级语言有下面几个,并且这些语言至今还在特定的领域继续使用。
<li>Fortran1955年名称取自”FORmula TRANslator”即公式翻译器由约翰·巴科斯John Backus等人发明。
</li>
<li>LISP1958年名称取自”LISt Processor”即枚举处理器由约翰·麦卡锡John McCarthy等人发明。
</li>
<li>Cobol1959年名称取自”Common Business Oriented Language”即通用商业导向语言由葛丽丝·霍普Grace Hopper发明。
</li>
为什么称这些语言为“高级语言”呢?原因在于这些语言让程序员不需要关注机器底层的低级结构和逻辑,而只要关注具体的问题和业务即可。
还是以4 + 6=这个加法为例如果用LISP语言实现只需要简单一行代码即可
```
(+ 4 6)
```
除此以外通过编译程序的处理高级语言可以被编译为适合不同CPU指令的机器语言。程序员只要写一次程序就可以在多个不同的机器上编译运行无须根据不同的机器指令重写整个程序。
## 第一次软件危机与结构化程序设计20世纪60年代~20世纪70年代
高级语言的出现解放了程序员但好景不长随着软件的规模和复杂度的大大增加20世纪60年代中期开始爆发了第一次软件危机典型表现有软件质量低下、项目无法如期完成、项目严重超支等因为软件而导致的重大事故时有发生。例如1963年美国[http://en.wikipedia.org/wiki/Mariner_1](http://en.wikipedia.org/wiki/Mariner_1)的水手一号火箭发射失败事故就是因为一行FORTRAN代码错误导致的。
软件危机最典型的例子莫过于IBM的System/360的操作系统开发。佛瑞德·布鲁克斯Frederick P. Brooks, Jr.作为项目主管率领2000多个程序员夜以继日地工作共计花费了5000人一年的工作量写出将近100万行的源码总共投入5亿美元是美国的“曼哈顿”原子弹计划投入的1/4。尽管投入如此巨大但项目进度却一再延迟软件质量也得不到保障。布鲁克斯后来基于这个项目经验而总结的《人月神话》一书成了畅销的软件工程书籍。
为了解决问题在1968、1969年连续召开两次著名的NATO会议会议正式创造了“软件危机”一词并提出了针对性的解决方法“软件工程”。虽然“软件工程”提出之后也曾被视为软件领域的银弹但后来事实证明软件工程同样无法根除软件危机只能在一定程度上缓解软件危机。
差不多同一时间“结构化程序设计”作为另外一种解决软件危机的方案被提了出来。艾兹赫尔·戴克斯特拉Edsger Dijkstra于1968年发表了著名的《GOTO有害论》论文引起了长达数年的论战并由此产生了**结构化程序设计方法**。同时第一个结构化的程序语言Pascal也在此时诞生并迅速流行起来。
结构化程序设计的主要特点是抛弃goto语句采取“自顶向下、逐步细化、模块化”的指导思想。结构化程序设计本质上还是一种面向过程的设计思想但通过“自顶向下、逐步细化、模块化”的方法将软件的复杂度控制在一定范围内从而从整体上降低了软件开发的复杂度。结构化程序方法成为了20世纪70年代软件开发的潮流。
## 第二次软件危机与面向对象20世纪80年代
结构化编程的风靡在一定程度上缓解了软件危机,然而随着硬件的快速发展,业务需求越来越复杂,以及编程应用领域越来越广泛,第二次软件危机很快就到来了。
第二次软件危机的根本原因还是在于软件生产力远远跟不上硬件和业务的发展。第一次软件危机的根源在于软件的“逻辑”变得非常复杂,而第二次软件危机主要体现在软件的“扩展”变得非常复杂。结构化程序设计虽然能够解决(也许用“缓解”更合适)软件逻辑的复杂性,但是对于业务变化带来的软件扩展却无能为力,软件领域迫切希望找到新的银弹来解决软件危机,在这种背景下,**面向对象的思想**开始流行起来。
面向对象的思想并不是在第二次软件危机后才出现的早在1967年的Simula语言中就开始提出来了但第二次软件危机促进了面向对象的发展。**面向对象真正开始流行是在20世纪80年代主要得益于C++的功劳后来的Java、C#把面向对象推向了新的高峰。到现在为止,面向对象已经成为了主流的开发思想。**
虽然面向对象开始也被当作解决软件危机的银弹,但事实证明,和软件工程一样,面向对象也不是银弹,而只是一种新的软件方法而已。
## 软件架构的历史背景
虽然早在20世纪60年代戴克斯特拉这位上古大神就已经涉及软件架构这个概念了但软件架构真正流行却是从20世纪90年代开始的由于在Rational和Microsoft内部的相关活动软件架构的概念开始越来越流行了。
与之前的各种新方法或者新理念不同的是,“软件架构”出现的背景并不是整个行业都面临类似相同的问题,“软件架构”也不是为了解决新的软件危机而产生的,这是怎么回事呢?
卡内基·梅隆大学的玛丽·肖Mary Shaw和戴维·加兰David Garlan对软件架构做了很多研究他们在1994年的一篇文章《软件架构介绍》An Introduction to Software Architecture中写到
>
“When systems are constructed from many components, the organization of the overall system-the software architecture-presents a new set of design problems.”
简单翻译一下:随着软件系统规模的增加,计算相关的算法和数据结构不再构成主要的设计问题;当系统由许多部分组成时,整个系统的组织,也就是所说的“软件架构”,导致了一系列新的设计问题。
这段话很好地解释了“软件架构”为何先在Rational或者Microsoft这样的大公司开始逐步流行起来。因为只有大公司开发的软件系统才具备较大规模而只有规模较大的软件系统才会面临软件架构相关的问题例如
<li>系统规模庞大,内部耦合严重,开发效率低;
</li>
<li>系统耦合严重,牵一发动全身,后续修改和扩展困难;
</li>
<li>系统逻辑复杂,容易出问题,出问题后很难排查和修复。
</li>
软件架构的出现有其历史必然性。20世纪60年代第一次软件危机引出了“结构化编程”创造了“模块”概念20世纪80年代第二次软件危机引出了“面向对象编程”创造了“对象”概念到了20世纪90年代“软件架构”开始流行创造了“组件”概念。我们可以看到“模块”“对象”“组件”本质上都是对达到一定规模的软件进行拆分差别只是在于随着软件的复杂度不断增加拆分的粒度越来越粗拆分的层次越来越高。
《人月神话》中提到的IBM 360大型系统开发时间是1964年那个时候结构化编程都还没有提出来更不用说软件架构了。如果IBM 360系统放在20世纪90年代开发不管是质量还是效率、成本都会比1964年开始做要好得多当然这样的话我们可能就看不到《人月神话》了。
## 小结
今天我为你回顾了软件开发进化的历史,以及软件架构出现的历史背景,从历史发展的角度,希望对你深入了解架构设计的本质有所帮助。
这就是今天的全部内容,留一道思考题给你吧。为何结构化编程、面向对象编程、软件工程、架构设计最后都没有成为软件领域的银弹?
欢迎你把答案写到留言区,和我一起讨论。相信经过深度思考的回答,也会让你对知识的理解更加深刻。(编辑乱入:精彩的留言有机会获得丰厚福利哦!)

View File

@@ -0,0 +1,129 @@
<audio id="audio" title="03 | 架构设计的目的" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/62/62/62f6921e7fe32da903744cf34c7a7662.mp3"></audio>
周二,我们聊了架构出现的历史背景和推动因素。以史为鉴,对我们了解架构设计的目的很有帮助。谈到架构设计,相信每个技术人员都是耳熟能详,但如果深入探讨一下,“为何要做架构设计?”或者“架构设计目的是什么?”类似的问题,大部分人可能从来没有思考过,或者即使有思考,也没有太明确可信的答案。
## 架构设计的误区
关于架构设计的目的,常见的误区有:
- 因为架构很重要,所以要做架构设计
这是一句正确的废话,架构是很重要,但架构为何重要呢?
例如:不做架构设计系统就跑不起来么?
其实不然,很多朋友尤其是经历了创业公司的朋友可能会发现,公司的初始产品可能没有架构设计,大伙撸起袖子简单讨论一下就开始编码了,根本没有正规的架构设计过程,而且也许产品开发速度还更快,上线后运行也还不错。
例如:做了架构设计就能提升开发效率么?
也不尽然,实际上有时候最简单的设计开发效率反而是最高的,架构设计毕竟需要投入时间和人力,这部分投入如果用来尽早编码,项目也许会更快。
例如:设计良好的架构能促进业务发展么?
好像有一定的道理例如设计高性能的架构能够让用户体验更好但反过来想我们照抄微信的架构业务就能达到微信的量级么肯定不可能不要说达到微信的量级达到微信的1/10做梦都要笑醒了。
- 不是每个系统都要做架构设计吗
这其实是知其然不知其所以然,系统确实要做架构设计,但还是不知道为何要做架构设计,反正大家都要做架构设计,所以做架构设计肯定没错。
这样的架构师或者设计师很容易走入生搬硬套业界其他公司已有架构的歧路,美其名曰“参考”“微改进”。一旦强行引入其他公司架构后,很可能会发现架构水土不服,或者运行起来很别扭等各种情况,最后往往不得不削足适履,或者不断重构,甚至无奈推倒重来。
- 公司流程要求系统开发过程中必须有架构设计
与此答案类似还有因为“架构师总要做点事情”,所以要做架构设计,其实都是舍本逐末。因为流程有规定,所以要做架构设计;因为架构师要做事,所以要做架构设计,这都是很表面地看问题,并没有真正理解为何要做架构设计,而且很多需求并不一定要进行架构设计。如果认为架构师一定要找点事做,流程一定要进行架构设计,就会出现事实上不需要架构设计但形式上却继续去做架构设计,不但浪费时间和人力,还会拖慢整体的开发进度。
- 为了高性能、高可用、可扩展,所以要做架构设计
能够给出这个答案说明已经有了一定的架构经历或者基础毕竟确实很多架构设计都是冲着高性能、高可用……等“高XX”的目标去的。
但往往持有这类观点的架构师和设计师会给项目带来巨大的灾难这绝不是危言耸听而是很多实际发生的事情为什么会这样呢因为这类架构师或者设计师不管三七二十一不管什么系统也不管什么业务上来就要求“高性能、高可用、高扩展”结果就会出现架构设计复杂无比项目落地遥遥无期团队天天吵翻天……等各种让人抓狂的现象费尽九牛二虎之力将系统整上线却发现运行不够稳定经常出问题出了问题很难解决加个功能要改1个月……等各种继续让人抓狂的事件。
## 架构设计的真正目的
那架构设计的真正目的究竟是什么?
从周二与你分享的架构设计的历史背景,可以看到,整个软件技术发展的历史,其实就是一部与“复杂度”斗争的历史,架构的出现也不例外。简而言之,架构也是为了应对软件系统复杂度而提出的一个解决方案,通过回顾架构产生的历史背景和原因,我们可以基本推导出答案:**架构设计的主要目的是为了解决软件系统复杂度带来的问题**。
这个结论虽然很简洁,但却是架构设计过程中需要时刻铭记在心的一条准则,为什么这样说呢?
首先,遵循这条准则能够让“新手”架构师**心中有数,而不是一头雾水**。
新手架构师开始做架构设计的时候心情都很激动希望大显身手甚至恨不得一出手就设计出世界上最牛的XX架构从此走上人生巅峰但真的面对具体的需求时往往都会陷入一头雾水的状态
“这么多需求,从哪里开始下手进行架构设计呢?”。
“架构设计要考虑高性能、高可用、高扩展……这么多高XX全部设计完成估计要1个月但老大只给了1周时间”。
“业界A公司的架构是XB公司的方案是Y两个差别比较大该参考哪一个呢”。
以上类似问题,如果明确了“架构设计是为了解决软件复杂度”原则后,就很好回答。
- “这么多需求,从哪里开始下手进行架构设计呢?”
——通过熟悉和理解需求,识别系统复杂性所在的地方,然后针对这些复杂点进行架构设计。
- “架构设计要考虑高性能、高可用、高扩展……这么多高XX全部设计完成估计要1个月但老大只给了1周时间”
——架构设计并不是要面面俱到,不需要每个架构都具备高性能、高可用、高扩展等特点,而是要识别出复杂点然后有针对性地解决问题。
- “业界A公司的架构是XB公司的方案是Y两个差别比较大该参考哪一个呢
——理解每个架构方案背后所需要解决的复杂点,然后才能对比自己的业务复杂点,参考复杂点相似的方案。
其次,遵循这条准则能够让“老鸟”架构师**有的放矢,而不是贪大求全**。
技术人员往往都希望自己能够做出最牛的东西,架构师也不例外,尤其是一些“老鸟”架构师,为了证明自己的技术牛,可能会陷入贪大求全的焦油坑而无法自拔。例如:
“我们的系统一定要做到每秒TPS 10万”。
“淘宝的架构是这么做的,我们也要这么做”。
“Docker现在很流行我们的架构应该将Docker应用进来”。
以上这些想法,如果拿“架构设计是为了解决软件复杂度”这个原则来衡量,就很容易判断。
- “我们的系统一定要做到每秒TPS 10万”
——如果系统的复杂度不是在性能这部分TPS做到10万并没有什么用。
- “淘宝的架构是这么做的,我们也要这么做”
——淘宝的架构是为了解决淘宝业务的复杂度而设计的,淘宝的业务复杂度并不就是我们的业务复杂度,绝大多数业务的用户量都不可能有淘宝那么大。
- “Docker现在很流行我们的架构应该将Docker应用进来”
——Docker不是万能的只是为了解决资源重用和动态分配而设计的如果我们的系统复杂度根本不是在这方面引入Docker没有什么意义。
## 简单的复杂度分析案例
我来分析一个简单的案例,一起来看看如何将“架构设计的真正目的是为了解决软件系统复杂度带来的问题”这个指导思想应用到实践中。
假设我们需要设计一个大学的学生管理系统,其基本功能包括登录、注册、成绩管理、课程管理等。当我们对这样一个系统进行架构设计的时候,首先应识别其复杂度到底体现在哪里。
性能一个学校的学生大约1 ~ 2万人学生管理系统的访问频率并不高平均每天单个学生的访问次数平均不到1次因此性能这部分并不复杂存储用MySQL完全能够胜任缓存都可以不用Web服务器用Nginx绰绰有余。
可扩展性:学生管理系统的功能比较稳定,可扩展的空间并不大,因此可扩展性也不复杂。
高可用学生管理系统即使宕机2小时对学生管理工作影响并不大因此可以不做负载均衡更不用考虑异地多活这类复杂的方案了。但是如果学生的数据全部丢失修复是非常麻烦的只能靠人工逐条修复这个很难接受因此需要考虑存储高可靠这里就有点复杂了。我们需要考虑多种异常情况机器故障、机房故障针对机器故障我们需要设计MySQL同机房主备方案针对机房故障我们需要设计MySQL跨机房同步方案。
安全性学生管理系统存储的信息有一定的隐私性例如学生的家庭情况但并不是和金融相关的也不包含强隐私例如玉照、情感的信息因此安全性方面只要做3个事情就基本满足要求了Nginx提供ACL控制、用户账号密码管理、数据库访问权限控制。
成本:由于系统很简单,基本上几台服务器就能够搞定,对于一所大学来说完全不是问题,可以无需太多关注。
还有其他方面,如果有兴趣,你可以自行尝试去分析。通过我上面的分析,可以看到这个方案的主要复杂性体现在存储可靠性上,需要保证异常的时候,不要丢失所有数据即可(丢失几个或者几十个学生的信息问题不大),对应的架构如下:
<img src="https://static001.geekbang.org/resource/image/97/d4/970f83d548b6b4a5c7903b3fc1f3b8d4.jpg" alt="" />
学生管理系统虽然简单,但麻雀虽小五脏俱全,基本上能涵盖软件系统复杂度分析的各个方面,而且绝大部分技术人员都曾经自己设计或者接触过类似的系统,如果将这个案例和自己的经验对比,相信会有更多的收获。
## 小结
今天我为你分析了架构设计的误区,结合周二讲的架构设计的历史背景,给出架构设计的主要目的是为了解决软件系统复杂度带来的问题,并分析了一个简单复杂度的案例,希望对你有所帮助。
这就是今天的全部内容,留一道思考题给你吧。请按照“架构设计的主要目的是为了解决软件复杂度带来的问题”这个指导思想来分析一下你目前的业务系统架构,看看是否和你当时分析的结果一样?
欢迎你把答案写到留言区,和我一起讨论。相信经过深度思考的回答,也会让你对知识的理解更加深刻。(编辑乱入:精彩的留言有机会获得丰厚福利哦!)
最后给你推荐一个课程极客时间新上线了《Java核心技术36讲》由Oracle首席工程师杨晓峰老师给你精讲大厂Java面试题帮你构建Java知识体系你可以点击下方图片进入课程。
[<img src="https://static001.geekbang.org/resource/image/89/6c/891e4be4057f6b3ab7e43979a0b1286c.jpg" alt="" />](http://time.geekbang.org/column/intro/82?utm_source=app&amp;utm_medium=81&amp;utm_campaign=82-presell&amp;utm_content=article)

View File

@@ -0,0 +1,128 @@
<audio id="audio" title="04 | 复杂度来源:高性能" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/dc/3c/dc84b96f46c7a9abd15c9d962098973c.mp3"></audio>
周四我为你讲了架构设计的主要目的是为了解决软件系统复杂度带来的问题。那么从今天开始我将为你深入分析复杂度的6个来源先来聊聊复杂度的来源之一高性能。
对性能孜孜不倦的追求是整个人类技术不断发展的根本驱动力。例如计算机从电子管计算机到晶体管计算机再到集成电路计算机运算性能从每秒几次提升到每秒几亿次。但伴随性能越来越高相应的方法和系统复杂度也是越来越高。现代的计算机CPU集成了几亿颗晶体管逻辑复杂度和制造复杂度相比最初的晶体管计算机根本不可同日而语。
软件系统也存在同样的现象。最近几十年软件系统性能飞速发展从最初的计算机只能进行简单的科学计算到现在Google能够支撑每秒几万次的搜索。与此同时软件系统规模也从单台计算机扩展到上万台计算机从最初的单用户单工的字符界面Dos操作系统到现在的多用户多工的Windows 10图形操作系统。
当然技术发展带来了性能上的提升不一定带来复杂度的提升。例如硬件存储从纸带→磁带→磁盘→SSD并没有显著带来系统复杂度的增加。因为新技术会逐步淘汰旧技术这种情况下我们直接用新技术即可不用担心系统复杂度会随之提升。只有那些并不是用来取代旧技术而是开辟了一个全新领域的技术才会给软件系统带来复杂度因为软件系统在设计的时候就需要在这些技术之间进行判断选择或者组合。就像汽车的发明无法取代火车飞机的出现也并不能完全取代火车所以我们在出行的时候需要考虑选择汽车、火车还是飞机这个选择的过程就比较复杂了要考虑价格、时间、速度、舒适度等各种因素。
软件系统中高性能带来的复杂度主要体现在两方面,一方面是**单台计算机内部为了高性能带来的复杂度**;另一方面是**多台计算机集群为了高性能带来的复杂度**。
## 单机复杂度
计算机内部复杂度最关键的地方就是操作系统。计算机性能的发展本质上是由硬件发展驱动的尤其是CPU的性能发展。著名的“摩尔定律”表明了CPU的处理能力每隔18个月就翻一番而将硬件性能充分发挥出来的关键就是操作系统所以操作系统本身其实也是跟随硬件的发展而发展的操作系统是软件系统的运行环境操作系统的复杂度直接决定了软件系统的复杂度。
操作系统和性能最相关的就是**进程**和**线程**。最早的计算机其实是没有操作系统的,只有输入、计算和输出功能,用户输入一个指令,计算机完成操作,大部分时候计算机都在等待用户输入指令,这样的处理性能很显然是很低效的,因为人的输入速度是远远比不上计算机的运算速度的。
为了解决手工操作带来的低效,批处理操作系统应运而生。批处理简单来说就是先把要执行的指令预先写下来(写到纸带、磁带、磁盘等),形成一个指令清单,这个指令清单就是我们常说的“任务”,然后将任务交给计算机去执行,批处理操作系统负责读取“任务”中的指令清单并进行处理,计算机执行的过程中无须等待人工手工操作,这样性能就有了很大的提升。
批处理程序大大提升了处理性能但有一个很明显的缺点计算机一次只能执行一个任务如果某个任务需要从I/O设备例如磁带读取大量的数据在I/O操作的过程中CPU其实是空闲的而这个空闲时间本来是可以进行其他计算的。
为了进一步提升性能人们发明了“进程”用进程来对应一个任务每个任务都有自己独立的内存空间进程间互不相关由操作系统来进行调度。此时的CPU还没有多核和多线程的概念为了达到多进程并行运行的目的采取了分时的方式即把CPU的时间分成很多片段每个片段只能执行某个进程中的指令。虽然从操作系统和CPU的角度来说还是串行处理的但是由于CPU的处理速度很快从用户的角度来看感觉是多进程在并行处理。
多进程虽然要求每个任务都有独立的内存空间进程间互不相关但从用户的角度来看两个任务之间能够在运行过程中就进行通信会让任务设计变得更加灵活高效。否则如果两个任务运行过程中不能通信只能是A任务将结果写到存储B任务再从存储读取进行处理不仅效率低而且任务设计更加复杂。为了解决这个问题进程间通信的各种方式被设计出来了包括管道、消息队列、信号量、共享存储等。
多进程让多任务能够并行处理任务,但本身还有缺点,单个进程内部只能串行处理,而实际上很多进程内部的子任务并不要求是严格按照时间顺序来执行的,也需要并行处理。例如,一个餐馆管理进程,排位、点菜、买单、服务员调度等子任务必须能够并行处理,否则就会出现某个客人买单时间比较长(比如说信用卡刷不出来),其他客人都不能点菜的情况。为了解决这个问题,人们又发明了线程,线程是进程内部的子任务,但这些子任务都共享同一份进程数据。为了保证数据的正确性,又发明了互斥锁机制。有了多线程后,操作系统调度的最小单位就变成了线程,而进程变成了操作系统分配资源的最小单位。
多进程多线程虽然让多任务并行处理的性能大大提升但本质上还是分时系统并不能做到时间上真正的并行。解决这个问题的方式显而易见就是让多个CPU能够同时执行计算任务从而实现真正意义上的多任务并行。目前这样的解决方案有3种SMPSymmetric Multi-Processor对称多处理器结构、NUMANon-Uniform Memory Access非一致存储访问结构、MPPMassive Parallel Processing海量并行处理结构。其中SMP是我们最常见的目前流行的多核处理器就是SMP方案。
操作系统发展到现在,如果我们要完成一个高性能的软件系统,需要考虑如多进程、多线程、进程间通信、多线程并发等技术点,而且这些技术**并不是最新的就是最好的,也不是非此即彼的选择**。在做架构设计的时候需要花费很大的精力来结合业务进行分析、判断、选择、组合这个过程同样很复杂。举一个最简单的例子Nginx可以用多进程也可以用多线程JBoss采用的是多线程Redis采用的是单进程Memcache采用的是多线程这些系统都实现了高性能但内部实现差异却很大。
## 集群的复杂度
虽然计算机硬件的性能快速发展,但和业务的发展速度相比,还是小巫见大巫了,尤其是进入互联网时代后,业务的发展速度远远超过了硬件的发展速度。例如:
- 2016年“双11”支付宝每秒峰值达12万笔支付。
- 2017年春节微信红包收发红包每秒达到76万个。
要支持支付和红包这种复杂的业务,单机的性能无论如何是无法支撑的,必须采用机器集群的方式来达到高性能。例如,支付宝和微信这种规模的业务系统,后台系统的机器数量都是万台级别的。
通过大量机器来提升性能,并不仅仅是增加机器这么简单,让多台机器配合起来达到高性能的目的,是一个复杂的任务,我针对常见的几种方式简单分析一下。
1.任务分配
任务分配的意思是指每台机器都可以处理完整的业务任务,不同的任务分配到不同的机器上执行。
我从最简单的一台服务器变两台服务器开始,来讲任务分配带来的复杂性,整体架构示意图如下。
<img src="https://static001.geekbang.org/resource/image/8e/c0/8ef42bd2536b3f1860f4a879223c2dc0.jpg" alt="" />
从图中可以看到1台服务器演变为2台服务器后架构上明显要复杂多了主要体现在
<li>
需要增加一个任务分配器这个分配器可能是硬件网络设备例如F5、交换机等可能是软件网络设备例如LVS也可能是负载均衡软件例如Nginx、HAProxy还可能是自己开发的系统。选择合适的任务分配器也是一件复杂的事情需要综合考虑性能、成本、可维护性、可用性等各方面的因素。
</li>
<li>
任务分配器和真正的业务服务器之间有连接和交互(即图中任务分配器到业务服务器的连接线),需要选择合适的连接方式,并且对连接进行管理。例如,连接建立、连接检测、连接中断后如何处理等。
</li>
<li>
任务分配器需要增加分配算法。例如,是采用轮询算法,还是按权重分配,又或者按照负载进行分配。如果按照服务器的负载进行分配,则业务服务器还要能够上报自己的状态给任务分配器。
</li>
这一大段描述,即使你可能还看不懂,但也应该感受到其中的复杂度了,更何况还要真正去实践和实现。
上面这个架构只是最简单地增加1台业务机器我们假设单台业务服务器每秒能够处理5000次业务请求那么这个架构理论上能够支撑10000次请求实际上的性能一般按照8折计算大约是8000次左右。
如果我们的性能要求继续提高假设要求每秒提升到10万次上面这个架构会出现什么问题呢是不是将业务服务器增加到25台就可以了呢显然不是因为随着性能的增加任务分配器本身又会成为性能瓶颈当业务请求达到每秒10万次的时候单台任务分配器也不够用了任务分配器本身也需要扩展为多台机器这时的架构又会演变成这个样子。
<img src="https://static001.geekbang.org/resource/image/ac/03/ac0e9979025df3dd7b8f6588860a9203.jpg" alt="" />
这个架构比2台业务服务器的架构要复杂主要体现在
<li>
任务分配器从1台变成了多台对应图中的任务分配器1到任务分配器M这个变化带来的复杂度就是需要将不同的用户分配到不同的任务分配器上即图中的虚线“用户分配”部分常见的方法包括DNS轮询、智能DNS、CDNContent Delivery Network内容分发网络、GSLB设备Global Server Load Balance全局负载均衡等。
</li>
<li>
任务分配器和业务服务器的连接从简单的“1对多”1台任务分配器连接多台业务服务器变成了“多对多”多台任务分配器连接多台业务服务器的网状结构。
</li>
<li>
机器数量从3台扩展到30台一般任务分配器数量比业务服务器要少这里我们假设业务服务器为25台任务分配器为5台状态管理、故障处理复杂度也大大增加。
</li>
上面这两个例子都是以业务处理为例,实际上“任务”涵盖的范围很广,**可以指完整的业务处理,也可以单指某个具体的任务**。例如“存储”“运算”“缓存”等都可以作为一项任务因此存储系统、运算系统、缓存系统都可以按照任务分配的方式来搭建架构。此外“任务分配器”也并不一定只能是物理上存在的机器或者一个独立运行的程序也可以是嵌入在其他程序中的算法例如Memcache的集群架构。
<img src="https://static001.geekbang.org/resource/image/d2/81/d2c94ac2aedbd4ayy5852d2be77b4081.jpg" alt="" />
2.任务分解
通过任务分配的方式我们能够突破单台机器处理性能的瓶颈通过增加更多的机器来满足业务的性能需求但如果业务本身也越来越复杂单纯只通过任务分配的方式来扩展性能收益会越来越低。例如业务简单的时候1台机器扩展到10台机器性能能够提升8倍需要扣除机器群带来的部分性能损耗因此无法达到理论上的10倍那么高但如果业务越来越复杂1台机器扩展到10台性能可能只能提升5倍。造成这种现象的主要原因是业务越来越复杂单台机器处理的性能会越来越低。为了能够继续提升性能我们需要采取第二种方式**任务分解**。
继续以上面“任务分配”中的架构为例,“业务服务器”如果越来越复杂,我们可以将其拆分为更多的组成部分,我以微信的后台架构为例。
<img src="https://static001.geekbang.org/resource/image/72/11/727f995c45cyy1652e135175c0f6b411.jpg" alt="" />
通过上面的架构示意图可以看出微信后台架构从逻辑上将各个子业务进行了拆分包括接入、注册登录、消息、LBS、摇一摇、漂流瓶、其他业务聊天、视频、朋友圈等
通过这种任务分解的方式,能够把原来大一统但复杂的业务系统,拆分成小而简单但需要多个系统配合的业务系统。从业务的角度来看,任务分解既不会减少功能,也不会减少代码量(事实上代码量可能还会增加,因为从代码内部调用改为通过服务器之间的接口调用),那为何通过任务分解就能够提升性能呢?
主要有几方面的因素:
- **简单的系统更加容易做到高性能**
系统的功能越简单影响性能的点就越少就更加容易进行有针对性的优化。而系统很复杂的情况下首先是比较难以找到关键性能点因为需要考虑和验证的点太多其次是即使花费很大力气找到了修改起来也不容易因为可能将A关键性能点提升了但却无意中将B点的性能降低了整个系统的性能不但没有提升还有可能会下降。
- **可以针对单个任务进行扩展**
当各个逻辑任务分解到独立的子系统后整个系统的性能瓶颈更加容易发现而且发现后只需要针对有瓶颈的子系统进行性能优化或者提升不需要改动整个系统风险会小很多。以微信的后台架构为例如果用户数增长太快注册登录子系统性能出现瓶颈的时候只需要优化登录注册子系统的性能可以是代码优化也可以简单粗暴地加机器消息逻辑、LBS逻辑等其他子系统完全不需要改动。
既然将一个大一统的系统分解为多个子系统能够提升性能那是不是划分得越细越好呢例如上面的微信后台目前是7个逻辑子系统如果我们把这7个逻辑子系统再细分划分为100个逻辑子系统性能是不是会更高呢
其实不然,这样做性能不仅不会提升,反而还会下降,最主要的原因是如果系统拆分得太细,为了完成某个业务,系统间的调用次数会呈指数级别上升,而系统间的调用通道目前都是通过网络传输的方式,性能远比系统内的函数调用要低得多。我以一个简单的图示来说明。
<img src="https://static001.geekbang.org/resource/image/e7/f7/e7f71f230bb525e48ee3d62fa938cef7.jpg" alt="" />
从图中可以看到当系统拆分2个子系统的时候用户访问需要1次系统间的请求和1次响应当系统拆分为4个子系统的时候系统间的请求次数从1次增长到3次假如继续拆分下去为100个子系统为了完成某次用户访问系统间的请求次数变成了99次。
为了描述简单我抽象出来一个最简单的模型假设这些系统采用IP网络连接理想情况下一次请求和响应在网络上耗费为1ms业务处理本身耗时为50ms。我们也假设系统拆分对单个业务请求性能没有影响那么系统拆分为2个子系统的时候处理一次用户访问耗时为51ms而系统拆分为100个子系统的时候处理一次用户访问耗时竟然达到了149ms。
虽然系统拆分可能在某种程度上能提升业务处理性能但提升性能也是有限的不可能系统不拆分的时候业务处理耗时为50ms系统拆分后业务处理耗时只要1ms因为最终决定业务处理性能的还是业务逻辑本身业务逻辑本身没有发生大的变化下理论上的性能是有一个上限的系统拆分能够让性能逼近这个极限但无法突破这个极限。因此任务分解带来的性能收益是有一个度的并不是任务分解越细越好而对于架构设计来说如何把握这个粒度就非常关键了。
## 小结
今天我给你讲了软件系统中高性能带来的复杂度主要体现的两方面,一是单台计算机内部为了高性能带来的复杂度;二是多台计算机集群为了高性能带来的复杂度,希望对你有所帮助。
这就是今天的全部内容,留一道思考题给你吧。你所在的业务体系中,高性能的系统采用的是哪种方式?目前是否有改进和提升的空间?
欢迎你把答案写到留言区,和我一起讨论。相信经过深度思考的回答,也会让你对知识的理解更加深刻。(编辑乱入:精彩的留言有机会获得丰厚福利哦!)

View File

@@ -0,0 +1,122 @@
<audio id="audio" title="05 | 复杂度来源:高可用" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b2/ff/b25ee095c38ce623ce3e4453c71bbfff.mp3"></audio>
今天,我们聊聊复杂度的第二个来源高可用。
参考维基百科,先来看看高可用的定义。
>
系统无中断地执行其功能的能力,代表系统的可用性程度,是进行系统设计时的准则之一。
这个定义的关键在于“**无中断**”但恰好难点也在“无中断”上面因为无论是单个硬件还是单个软件都不可能做到无中断硬件会出故障软件会有bug硬件会逐渐老化软件会越来越复杂和庞大……
除了硬件和软件本质上无法做到“无中断”,外部环境导致的不可用更加不可避免、不受控制。例如,断电、水灾、地震,这些事故或者灾难也会导致系统不可用,而且影响程度更加严重,更加难以预测和规避。
所以,系统的高可用方案五花八门,但万变不离其宗,本质上都是通过“**冗余**”来实现高可用。通俗点来讲,就是一台机器不够就两台,两台不够就四台;一个机房可能断电,那就部署两个机房;一条通道可能故障,那就用两条,两条不够那就用三条(移动、电信、联通一起上)。高可用的“冗余”解决方案,单纯从形式上来看,和之前讲的高性能是一样的,都是通过增加更多机器来达到目的,但其实本质上是有根本区别的:**高性能增加机器目的在于“扩展”处理性能;高可用增加机器目的在于“冗余”处理单元**。
通过冗余增强了可用性,但同时也带来了复杂性,我会根据不同的应用场景逐一分析。
## 计算高可用
这里的“计算”指的是业务的逻辑处理。计算有一个特点就是**无论在哪台机器上进行计算,同样的算法和输入数据,产出的结果都是一样的**,所以将计算从一台机器迁移到另外一台机器,对业务并没有什么影响。既然如此,计算高可用的复杂度体现在哪里呢?我以最简单的单机变双机为例进行分析。先来看一个单机变双机的简单架构示意图。
<img src="https://static001.geekbang.org/resource/image/96/97/9616057cea1365eacf5f6c9c0091yy97.jpg" alt="" />
你可能会发现,这个双机的架构图和上期“高性能”讲到的双机架构图是一样的,因此复杂度也是类似的,具体表现为:
<li>
需要增加一个任务分配器,选择合适的任务分配器也是一件复杂的事情,需要综合考虑性能、成本、可维护性、可用性等各方面因素。
</li>
<li>
任务分配器和真正的业务服务器之间有连接和交互,需要选择合适的连接方式,并且对连接进行管理。例如,连接建立、连接检测、连接中断后如何处理等。
</li>
<li>
任务分配器需要增加分配算法。例如,常见的双机算法有主备、主主,主备方案又可以细分为冷备、温备、热备。
</li>
上面这个示意图只是简单的双机架构,我们再看一个复杂一点的高可用集群架构。
<img src="https://static001.geekbang.org/resource/image/e1/8d/e1e003e99efe63669d8137782d5fe18d.jpg" alt="" />
这个高可用集群相比双机来说分配算法更加复杂可以是1主3备、2主2备、3主1备、4主0备具体应该采用哪种方式需要结合实际业务需求来分析和判断并不存在某种算法就一定优于另外的算法。例如ZooKeeper采用的就是1主多备而Memcached采用的就是全主0备。
## 存储高可用
对于需要存储数据的系统来说,整个系统的高可用设计关键点和难点就在于“存储高可用”。存储与计算相比,有一个本质上的区别:**将数据从一台机器搬到到另一台机器,需要经过线路进行传输**。线路传输的速度是毫秒级别同一机房内部能够做到几毫秒分布在不同地方的机房传输耗时需要几十甚至上百毫秒。例如从广州机房到北京机房稳定情况下ping延时大约是50ms不稳定情况下可能达到1s甚至更多。
虽然毫秒对于人来说几乎没有什么感觉,但是对于高可用系统来说,就是本质上的不同,这意味着整个系统在某个时间点上,数据肯定是不一致的。按照“**数据+ 逻辑= 业务**”这个公式来套的话数据不一致即使逻辑一致最后的业务表现就不一样了。以最经典的银行储蓄业务为例假设用户的数据存在北京机房用户存入了1万块钱然后他查询的时候被路由到了上海机房北京机房的数据没有同步到上海机房用户会发现他的余额并没有增加1万块。想象一下此时用户肯定会背后一凉马上会怀疑自己的钱被盗了然后赶紧打客服电话投诉甚至打110报警即使最后发现只是因为传输延迟导致的问题站在用户的角度来说这个过程的体验肯定很不好。
<img src="https://static001.geekbang.org/resource/image/0b/5d/0bcb547c1f2yyc8c1761cd203656765d.jpg" alt="" />
除了物理上的传输速度限制传输线路本身也存在可用性问题传输线路可能中断、可能拥塞、可能异常错包、丢包并且传输线路的故障时间一般都特别长短的十几分钟长的几个小时都是可能的。例如2015年支付宝因为光缆被挖断业务影响超过4个小时2016年中美海底光缆中断3小时等。在传输线路中断的情况下就意味着存储无法进行同步在这段时间内整个系统的数据是不一致的。
综合分析,无论是正常情况下的传输延迟,还是异常情况下的传输中断,都会导致系统的数据在某个时间点或者时间段是不一致的,而数据的不一致又会导致业务问题;但如果完全不做冗余,系统的整体高可用又无法保证,所以**存储高可用的难点不在于如何备份数据,而在于如何减少或者规避数据不一致对业务造成的影响**。
分布式领域里面有一个著名的CAP定理从理论上论证了存储高可用的复杂度。也就是说存储高可用不可能同时满足“一致性、可用性、分区容错性”最多满足其中两个这就要求我们在做架构设计时结合业务进行取舍。
## 高可用状态决策
无论是计算高可用还是存储高可用,其基础都是“**状态决策**”,即系统需要能够判断当前的状态是正常还是异常,如果出现了异常就要采取行动来保证高可用。如果状态决策本身都是有错误或者有偏差的,那么后续的任何行动和处理无论多么完美也都没有意义和价值。但在具体实践的过程中,恰好存在一个本质的矛盾:**通过冗余来实现的高可用系统,状态决策本质上就不可能做到完全正确**。下面我基于几种常见的决策方式进行详细分析。
1.独裁式
独裁式决策指的是存在一个独立的决策主体,我们姑且称它为“决策者”,负责收集信息然后进行决策;所有冗余的个体,我们姑且称它为“上报者”,都将状态信息发送给决策者。
<img src="https://static001.geekbang.org/resource/image/86/cd/86083402e7fd928782350e6f7c109ccd.jpg" alt="" />
独裁式的决策方式不会出现决策混乱的问题,因为只有一个决策者,但问题也正是在于只有一个决策者。当决策者本身故障时,整个系统就无法实现准确的状态决策。如果决策者本身又做一套状态决策,那就陷入一个递归的死循环了。
2.协商式
协商式决策指的是两个独立的个体通过交流信息,然后根据规则进行决策,**最常用的协商式决策就是主备决策**。
<img src="https://static001.geekbang.org/resource/image/57/8a/57ed8efdb316727f99217d8cca11528a.jpg" alt="" />
这个架构的基本协商规则可以设计成:
- 2台服务器启动时都是备机。
- 2台服务器建立连接。
- 2台服务器交换状态信息。
- 某1台服务器做出决策成为主机另一台服务器继续保持备机身份。
协商式决策的架构不复杂,规则也不复杂,其难点在于,如果两者的信息交换出现问题(比如主备连接中断),此时状态决策应该怎么做。
- 如果备机在连接中断的情况下认为主机故障那么备机需要升级为主机但实际上此时主机并没有故障那么系统就出现了两个主机这与设计初衷1主1备是不符合的。
<img src="https://static001.geekbang.org/resource/image/d2/37/d2469cbb833a01618a8a783ee2674337.jpg" alt="" />
- 如果备机在连接中断的情况下不认为主机故障则此时如果主机真的发生故障那么系统就没有主机了这同样与设计初衷1主1备是不符合的。
<img src="https://static001.geekbang.org/resource/image/da/3c/da340fffcb7e33ffc0f3431856f7403c.jpg" alt="" />
- 如果为了规避连接中断对状态决策带来的影响,可以增加更多的连接。例如,双连接、三连接。这样虽然能够降低连接中断对状态带来的影响(注意:只能降低,不能彻底解决),但同时又引入了这几条连接之间信息取舍的问题,即如果不同连接传递的信息不同,应该以哪个连接为准?实际上这也是一个无解的答案,无论以哪个连接为准,在特定场景下都可能存在问题。
<img src="https://static001.geekbang.org/resource/image/4f/ef/4fb17b9b33d2ce2bf94269a2f78ffaef.jpg" alt="" />
综合分析,协商式状态决策在某些场景总是存在一些问题的。
3.民主式
民主式决策指的是多个独立的个体通过投票的方式来进行状态决策。例如ZooKeeper集群在选举leader时就是采用这种方式。
<img src="https://static001.geekbang.org/resource/image/b6/8c/b681373246bb52bc4c48801a82cb588c.jpg" alt="" />
民主式决策和协商式决策比较类似,其基础都是独立的个体之间交换信息,每个个体做出自己的决策,然后按照“**多数取胜**”的规则来确定最终的状态。不同点在于民主式决策比协商式决策要复杂得多ZooKeeper的选举算法ZAB绝大部分人都看得云里雾里更不用说用代码来实现这套算法了。
除了算法复杂民主式决策还有一个固有的缺陷脑裂。这个词来源于医学指人体左右大脑半球的连接被切断后左右脑因为无法交换信息导致各自做出决策然后身体受到两个大脑分别控制会做出各种奇怪的动作。例如当一个脑裂患者更衣时他有时会一只手将裤子拉起另一只手却将裤子往下脱。脑裂的根本原因是原来统一的集群因为连接中断造成了两个独立分隔的子集群每个子集群单独进行选举于是选出了2个主机相当于人体有两个大脑了。
<img src="https://static001.geekbang.org/resource/image/0f/74/0fd72dd8fe80dd19c562b8825d25e174.jpg" alt="" />
从图中可以看到正常状态的时候节点5作为主节点其他节点作为备节点当连接发生故障时节点1、节点2、节点3形成了一个子集群节点4、节点5形成了另外一个子集群这两个子集群的连接已经中断无法进行信息交换。按照民主决策的规则和算法两个子集群分别选出了节点2和节点5作为主节点此时整个系统就出现了两个主节点。这个状态违背了系统设计的初衷两个主节点会各自做出自己的决策整个系统的状态就混乱了。
为了解决脑裂问题民主式决策的系统一般都采用“投票节点数必须超过系统总节点数一半”规则来处理。如图中那种情况节点4和节点5形成的子集群总节点数只有2个没有达到总节点数5个的一半因此这个子集群不会进行选举。这种方式虽然解决了脑裂问题但同时降低了系统整体的可用性即如果系统不是因为脑裂问题导致投票节点数过少而真的是因为节点故障例如节点1、节点2、节点3真的发生了故障此时系统也不会选出主节点整个系统就相当于宕机了尽管此时还有节点4和节点5是正常的。
综合分析,无论采取什么样的方案,状态决策都不可能做到任何场景下都没有问题,但完全不做高可用方案又会产生更大的问题,如何选取适合系统的高可用方案,也是一个复杂的分析、判断和选择的过程。
## 小结
今天我给你讲了复杂度来源之一的高可用,分析了计算高可用和存储高可用两个场景,给出了几种高可用状态决策方式,希望对你有所帮助。
这就是今天的全部内容,留一道思考题给你吧。高性能和高可用是很多系统的核心复杂度,你认为哪个会更复杂一些?理由是什么?
欢迎你把答案写到留言区,和我一起讨论。相信经过深度思考的回答,也会让你对知识的理解更加深刻。(编辑乱入:精彩的留言有机会获得丰厚福利哦!)

View File

@@ -0,0 +1,77 @@
<audio id="audio" title="06 | 复杂度来源:可扩展性" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0b/fc/0b306b956b2391cdb241e278ab0250fc.mp3"></audio>
复杂度来源前面已经讲了高性能和高可用,今天来聊聊可扩展性。
可扩展性指系统为了应对将来需求变化而提供的一种扩展能力,当有新的需求出现时,系统不需要或者仅需要少量修改就可以支持,无须整个系统重构或者重建。
由于软件系统固有的多变性,新的需求总会不断提出来,因此可扩展性显得尤其重要。在软件开发领域,面向对象思想的提出,就是为了解决可扩展性带来的问题;后来的设计模式,更是将可扩展性做到了极致。得益于设计模式的巨大影响力,几乎所有的技术人员对于可扩展性都特别重视。
设计具备良好可扩展性的系统,有两个基本条件:**正确预测变化**、**完美封装变化**。但要达成这两个条件,本身也是一件复杂的事情,我来具体分析一下。
## 预测变化
软件系统与硬件或者建筑相比,有一个很大的差异:软件系统在发布后还可以不断地修改和演进,这就意味着**不断有新的需求需要实现**。如果新需求能够不改代码甚至少改代码就可以实现,那当然是皆大欢喜的,否则来一个需求就要求系统大改一次,成本会非常高,程序员心里也不爽(改来改去),产品经理也不爽(做得那么慢),老板也不爽(那么多人就只能干这么点事)。因此作为架构师,我们总是试图去预测所有的变化,然后设计完美的方案来应对,当下一次需求真正来临时,架构师可以自豪地说:这个我当时已经预测到了,架构已经完美地支持,只需要一两天工作量就可以了!
然而理想是美好的现实却是复杂的。有一句谚语“唯一不变的是变化”如果按照这个标准去衡量架构师每个设计方案都要考虑可扩展性。例如架构师准备设计一个简单的后台管理系统当架构师考虑用MySQL存储数据时是否要考虑后续需要用Oracle来存储当架构师设计用HTTP做接口协议时是否要考虑要不要支持ProtocolBuffer甚至更离谱一点架构师是否要考虑VR技术对架构的影响从而提前做好可扩展性如果每个点都考虑可扩展性架构师会不堪重负架构设计也会异常庞大且最终无法落地。但架构师也不能完全不做预测否则可能系统刚上线马上来新的需求就需要重构这同样意味着前期很多投入的工作量也白费了。
同时,“预测”这个词,本身就暗示了不可能每次预测都是准确的,如果预测的事情出错,我们期望中的需求迟迟不来,甚至被明确否定,那么基于预测做的架构设计就没什么作用,投入的工作量也就白费了。
综合分析,预测变化的复杂性在于:
- 不能每个设计点都考虑可扩展性。
- 不能完全不考虑可扩展性。
- 所有的预测都存在出错的可能性。
对于架构师来说,如何把握预测的程度和提升预测结果的准确性,是一件很复杂的事情,而且没有通用的标准可以简单套上去,更多是靠自己的经验、直觉,所以架构设计评审的时候经常会出现两个设计师对某个判断争得面红耳赤的情况,原因就在于没有明确标准,不同的人理解和判断有偏差,而最终又只能选择一个判断。
## 应对变化
假设架构师经验非常丰富,目光非常敏锐,看问题非常准,所有的变化都能准确预测,是否意味着可扩展性就很容易实现了呢?也没那么理想!因为预测变化是一回事,采取什么方案来应对变化,又是另外一个复杂的事情。即使预测很准确,如果方案不合适,则系统扩展一样很麻烦。
<img src="https://static001.geekbang.org/resource/image/30/4f/308b3819fyy4d4a5dfb934492effeb4f.jpg" alt="" />
第一种应对变化的常见方案是**将“变化”封装在一个“变化层”,将不变的部分封装在一个独立的“稳定层”**。
无论是变化层依赖稳定层还是稳定层依赖变化层都是可以的需要根据具体业务情况来设计。例如如果系统需要支持XML、JSON、ProtocolBuffer三种接入方式那么最终的架构就是上面图中的“形式1”架构也就是下面这样。
<img src="https://static001.geekbang.org/resource/image/0y/d3/0yy87c891c7ba0fd16826e8a91aa6ed3.jpg" alt="" />
如果系统需要支持MySQL、Oracle、DB2数据库存储那么最终的架构就变成了“形式2”的架构了你可以看下面这张图。
<img src="https://static001.geekbang.org/resource/image/87/c4/87c436de169b45f4b2909a7495c4eec4.jpg" alt="" />
无论采取哪种形式,通过剥离变化层和稳定层的方式应对变化,都会带来两个主要的复杂性相关的问题。
1.系统需要拆分出变化层和稳定层
对于哪些属于变化层,哪些属于稳定层,很多时候并不是像前面的示例(不同接口协议或者不同数据库)那样明确,不同的人有不同的理解,导致架构设计评审的时候可能吵翻天。
2.需要设计变化层和稳定层之间的接口
接口设计同样至关重要对于稳定层来说接口肯定是越稳定越好但对于变化层来说在有差异的多个实现方式中找出共同点并且还要保证当加入新的功能时原有的接口设计不需要太大修改这是一件很复杂的事情。例如MySQL的REPLACE INTO和Oracle的MERGE INTO语法和功能有一些差异那存储层如何向稳定层提供数据访问接口呢是采取MySQL的方式还是采取Oracle的方式还是自适应判断如果再考虑DB2的情况呢相信你看到这里就已经能够大致体会到接口设计的复杂性了。
第二种常见的应对变化的方案是**提炼出一个“抽象层”和一个“实现层”**。抽象层是稳定的,实现层可以根据具体业务需要定制开发,当加入新的功能时,只需要增加新的实现,无须修改抽象层。这种方案典型的实践就是设计模式和规则引擎。考虑到绝大部分技术人员对设计模式都非常熟悉,我以设计模式为例来说明这种方案的复杂性。
以设计模式的“装饰者”模式来分析,下面是装饰者模式的类关系图。
<img src="https://static001.geekbang.org/resource/image/69/88/69fd1fbd54fce48784cbb0e05e304888.jpg" alt="" />
图中的Component和Decorator就是抽象出来的规则这个规则包括几部分
1. Component和Decorator类。
1. Decorator类继承Component类。
1. Decorator类聚合了Component类。
这个规则一旦抽象出来后就固定了不能轻易修改。例如把规则3去掉就无法实现装饰者模式的目的了。
装饰者模式相比传统的继承来实现功能确实灵活很多。例如《设计模式》中装饰者模式的样例“TextView”类的实现用了装饰者之后能够灵活地给TextView增加额外更多功能比如可以增加边框、滚动条、背景图片等这些功能上的组合不影响规则只需要按照规则实现即可。但装饰者模式相对普通的类实现模式明显要复杂多了。本来一个函数或者一个类就能搞定的事情现在要拆分成多个类而且多个类之间必须按照装饰者模式来设计和调用。
规则引擎和设计模式类似都是通过灵活的设计来达到可扩展的目的但“灵活的设计”本身就是一件复杂的事情不说别的光是把23种设计模式全部理解和备注都是一件很困难的事情。
## 小结
今天我从预测变化和应对变化这两个设计可扩展性系统的条件,以及它们实现起来本身的复杂性,为你讲了复杂度来源之一的可扩展性,希望对你有所帮助。
这就是今天的全部内容,留一道思考题给你吧。你在具体代码中使用过哪些可扩展的技术?最终的效果如何?
欢迎你把答案写到留言区,和我一起讨论。相信经过深度思考的回答,也会让你对知识的理解更加深刻。(编辑乱入:精彩的留言有机会获得丰厚福利哦!)

View File

@@ -0,0 +1,136 @@
<audio id="audio" title="07 | 复杂度来源:低成本、安全、规模" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/68/93/6840d45f6ce27149b1aadfb6b01df193.mp3"></audio>
关于复杂度来源,前面的专栏已经讲了高性能、高可用和可扩展性,今天我来聊聊复杂度另外三个来源低成本、安全和规模。
## 低成本
当我们的架构方案只涉及几台或者十几台服务器时一般情况下成本并不是我们重点关注的目标但如果架构方案涉及几百上千甚至上万台服务器成本就会变成一个非常重要的架构设计考虑点。例如A方案需要10000台机器B方案只需要8000台机器单从比例来看也就节省了20%的成本但从数量来看B方案能节省2000台机器1台机器成本预算每年大约2万元这样一年下来就能节省4000万元4000万元成本不是小数目给100人的团队发奖金每人可以发40万元了这可是算得上天价奖金了。通过一个架构方案的设计就能轻松节约几千万元不但展现了技术的强大力量也带来了可观的收益对于技术人员来说最有满足感的事情莫过于如此了。
当我们设计“高性能”“高可用”的架构时,通用的手段都是增加更多服务器来满足“高性能”和“高可用”的要求;而低成本正好与此相反,我们需要减少服务器的数量才能达成低成本的目标。因此,低成本本质上是与高性能和高可用冲突的,所以低成本很多时候不会是架构设计的首要目标,而是架构设计的附加约束。也就是说,我们首先设定一个成本目标,当我们根据高性能、高可用的要求设计出方案时,评估一下方案是否能满足成本目标,如果不行,就需要重新设计架构;如果无论如何都无法设计出满足成本要求的方案,那就只能找老板调整成本目标了。
低成本给架构设计带来的主要复杂度体现在,**往往只有“创新”才能达到低成本目标**。这里的“创新”既包括开创一个全新的技术领域(这个要求对绝大部分公司太高),也包括引入新技术,如果没有找到能够解决自己问题的新技术,那么就真的需要自己创造新技术了。
类似的新技术例子很多,我来举几个。
<li>NoSQLMemcache、Redis等的出现是为了解决关系型数据库无法应对高并发访问带来的访问压力。
</li>
<li>全文搜索引擎Sphinx、Elasticsearch、Solr的出现是为了解决关系型数据库like搜索的低效的问题。
</li>
<li>Hadoop的出现是为了解决传统文件系统无法应对海量数据存储和计算的问题。
</li>
我再来举几个业界类似的例子。
<li>Facebook为了解决PHP的低效问题刚开始的解决方案是HipHop PHP可以将PHP语言翻译为C++语言执行后来改为HHVM将PHP翻译为字节码然后由虚拟机执行和Java的JVM类似。
</li>
<li>新浪微博将传统的Redis/MC + MySQL方式扩展为Redis/MC + SSD Cache + MySQL方式SSD Cache作为L2缓存使用既解决了MC/Redis成本过高容量小的问题也解决了穿透DB带来的数据库访问压力来源[http://www.infoq.com/cn/articles/weibo-platform-archieture](http://www.infoq.com/cn/articles/weibo-platform-archieture) )。
</li>
<li>Linkedin为了处理每天5千亿的事件开发了高效的Kafka消息系统。
</li>
<li>其他类似将Ruby on Rails改为Java、Lua + redis改为Go语言实现的例子还有很多。
</li>
无论是引入新技术,还是自己创造新技术,都是一件复杂的事情。引入新技术的主要复杂度在于需要去熟悉新技术,并且将新技术与已有技术结合起来;创造新技术的主要复杂度在于需要自己去创造全新的理念和技术,并且新技术跟旧技术相比,需要有质的飞跃。
相比来说,创造新技术复杂度更高,因此一般中小公司基本都是靠引入新技术来达到低成本的目标;而大公司更有可能自己去创造新的技术来达到低成本的目标,因为大公司才有足够的资源、技术和时间去创造新技术。
## 安全
安全本身是一个庞大而又复杂的技术领域,并且一旦出问题,对业务和企业形象影响非常大。例如:
<li>2016年雅虎爆出史上最大规模信息泄露事件逾5亿用户资料在2014年被窃取。
</li>
<li>2016年10月美国遭史上最大规模DDoS攻击东海岸网站集体瘫痪。
</li>
<li>2013年10月为全国4500多家酒店提供网络服务的浙江慧达驿站网络有限公司因安全漏洞问题致2千万条入住酒店的客户信息泄露由此导致很多敲诈、家庭破裂的后续事件。
</li>
正因为经常能够看到或者听到各类安全事件,所以大部分技术人员和架构师,对安全这部分会多一些了解和考虑。
从技术的角度来讲,安全可以分为两类:一类是功能上的安全,一类是架构上的安全。
1.功能安全
例如常见的XSS攻击、CSRF攻击、SQL注入、Windows漏洞、密码破解等本质上是因为系统实现有漏洞黑客有了可乘之机。黑客会利用各种漏洞潜入系统这种行为就像小偷一样黑客和小偷的手法都是利用系统或家中不完善的地方潜入并进行破坏或者盗取。因此形象地说**功能安全其实就是“防小偷”**。
从实现的角度来看功能安全更多地是和具体的编码相关与架构关系不大。现在很多开发框架都内嵌了常见的安全功能能够大大减少安全相关功能的重复开发但框架只能预防常见的安全漏洞和风险常见的XSS攻击、CSRF攻击、SQL注入等无法预知新的安全问题而且框架本身很多时候也存在漏洞例如流行的Apache Struts2就多次爆出了调用远程代码执行的高危漏洞给整个互联网都造成了一定的恐慌。所以功能安全是一个逐步完善的过程而且往往都是在问题出现后才能有针对性的提出解决方案我们永远无法预测系统下一个漏洞在哪里也不敢说自己的系统肯定没有任何问题。换句话讲功能安全其实也是一个“攻”与“防”的矛盾只能在这种攻防大战中逐步完善不可能在系统架构设计的时候一劳永逸地解决。
2.架构安全
如果说功能安全是“防小偷”,那么**架构安全就是“防强盗”**。强盗会直接用大锤将门砸开,或者用炸药将围墙炸倒;小偷是偷东西,而强盗很多时候就是故意搞破坏,对系统的影响也大得多。因此架构设计时需要特别关注架构安全,尤其是互联网时代,理论上来说系统部署在互联网上时,全球任何地方都可以发起攻击。
传统的架构安全主要依靠防火墙,防火墙最基本的功能就是隔离网络,通过将网络划分成不同的区域,制定出不同区域之间的**访问控制策略**来控制不同信任程度区域间传送的数据流。例如,下图是一个典型的银行系统的安全架构。
<img src="https://static001.geekbang.org/resource/image/28/6b/28e72e72d8691f1c869ea0db283e156b.png" alt="">
从图中你可以看到,整个系统根据不同的分区部署了多个防火墙来保证系统的安全。
防火墙的功能虽然强大但性能一般所以在传统的银行和企业应用领域应用较多。但在互联网领域防火墙的应用场景并不多。因为互联网的业务具有海量用户访问和高并发的特点防火墙的性能不足以支撑尤其是互联网领域的DDoS攻击轻则几GB重则几十GB。2016年知名安全研究人员布莱恩·克莱布斯Brian Krebs的安全博客网站遭遇DDoS攻击攻击带宽达665Gbps是目前在网络犯罪领域已知的最大的拒绝服务攻击。这种规模的攻击如果用防火墙来防则需要部署大量的防火墙成本会很高。例如中高端一些的防火墙价格10万元每秒能抗住大约25GB流量那么应对这种攻击就需要将近30台防火墙成本将近300万元这还不包括维护成本而这些防火墙设备在没有发生攻击的时候又没有什么作用。也就是说如果花费几百万元来买这么一套设备有可能几年都发挥不了任何作用。
就算是公司对钱不在乎一般也不会堆防火墙来防DDoS攻击因为DDoS攻击最大的影响是大量消耗机房的出口总带宽。不管防火墙处理能力有多强当出口带宽被耗尽时整个业务在用户看来就是不可用的因为用户的正常请求已经无法到达系统了。防火墙能够保证内部系统不受冲击但用户也是进不来的。对于用户来说业务都已经受到影响了至于是因为用户自己进不去还是因为系统出故障用户其实根本不会关心。
基于上述原因,互联网系统的架构安全目前并没有太好的设计手段来实现,更多地是依靠运营商或者云服务商强大的带宽和流量清洗的能力,较少自己来设计和实现。
## 规模
很多企业级的系统,既没有高性能要求,也没有双中心高可用要求,也不需要什么扩展性,但往往我们一说到这样的系统,很多人都会脱口而出:这个系统好复杂!为什么这样说呢?关键就在于这样的系统往往功能特别多,逻辑分支特别多。特别是有的系统,发展时间比较长,不断地往上面叠加功能,后来的人由于不熟悉整个发展历史,可能连很多功能的应用场景都不清楚,或者细节根本无法掌握,面对的就是一个黑盒系统,看不懂、改不动、不敢改、修不了,复杂度自然就感觉很高了。
**规模带来复杂度的主要原因就是“量变引起质变”**,当数量超过一定的阈值后,复杂度会发生质的变化。常见的规模带来的复杂度有:
1.功能越来越多,导致系统复杂度指数级上升
例如某个系统开始只有3大功能后来不断增加到8大功能虽然还是同一个系统但复杂度已经相差很大了具体相差多大呢
我以一个简单的抽象模型来计算一下,假设系统间的功能都是两两相关的,系统的复杂度=功能数量+功能之间的连接数量,通过计算我们可以看出:
<li>3个功能的系统复杂度= 3 + 3 = 6
</li>
<li>8个功能的系统复杂度= 8 + 28 = 36
</li>
可以看出具备8个功能的系统的复杂度不是比具备3个功能的系统的复杂度多5而是多了30基本是指数级增长的主要原因在于随着系统功能数量增多功能之间的连接呈指数级增长。下图形象地展示了功能数量的增多带来了复杂度。
<img src="https://static001.geekbang.org/resource/image/00/29/00328479c77f39c22637a3a53b535629.png" alt="">
<img src="https://static001.geekbang.org/resource/image/3f/1c/3fcdf2386bc9158899bfc6f3625df81c.png" alt="">
通过肉眼就可以很直观地看出具备8个功能的系统复杂度要高得多。
2.数据越来越多,系统复杂度发生质变
与功能类似系统数据越来越多时也会由量变带来质变最近几年火热的“大数据”就是在这种背景下诞生的。大数据单独成为了一个热门的技术领域主要原因就是数据太多以后传统的数据收集、加工、存储、分析的手段和工具已经无法适应必须应用新的技术才能解决。目前的大数据理论基础是Google发表的三篇大数据相关论文其中Google File System是大数据文件存储的技术理论Google Bigtable是列式数据存储的技术理论Google MapReduce是大数据运算的技术理论这三篇技术论文各自开创了一个新的技术领域。
即使我们的数据没有达到大数据规模数据的增长也可能给系统带来复杂性。最典型的例子莫过于使用关系数据库存储数据我以MySQL为例MySQL单表的数据因不同的业务和应用场景会有不同的最优值但不管怎样都肯定是有一定的限度的一般推荐在5000万行左右。如果因为业务的发展单表数据达到了10亿行就会产生很多问题例如
<li>添加索引会很慢,可能需要几个小时,这几个小时内数据库表是无法插入数据的,相当于业务停机了。
</li>
<li>修改表结构和添加索引存在类似的问题,耗时可能会很长。
</li>
<li>即使有索引,索引的性能也可能会很低,因为数据量太大。
</li>
<li>数据库备份耗时很长。
</li>
<li>……
</li>
因此当MySQL单表数据量太大时我们必须考虑将单表拆分为多表这个拆分过程也会引入更多复杂性例如
- 拆表的规则是什么?
以用户表为例是按照用户id拆分表还是按照用户注册时间拆表
- 拆完表后查询如何处理?
以用户表为例假设按照用户id拆表当业务需要查询学历为“本科”以上的用户时要去很多表查询才能得到最终结果怎么保证性能
还有很多类似的问题这里不一一展开,后面的专栏还会讨论。
## 小结
今天我为你分析了低成本给架构设计带来的主要复杂度体现在引入新技术或创造新技术,讨论了从功能安全和架构安全引入的复杂度,以及规模带来复杂度的主要原因是“量变引起质变”,希望对你有所帮助。
这就是今天的全部内容留一道思考题给你吧。学习了6大复杂度来源后结合你所在的业务分析一下主要的复杂度是这其中的哪些部分是否还有其他复杂度原因
欢迎你把答案写到留言区,和我一起讨论。相信经过深度思考的回答,也会让你对知识的理解更加深刻。(编辑乱入:精彩的留言有机会获得丰厚福利哦!)

View File

@@ -0,0 +1,213 @@
<audio id="audio" title="08 | 架构设计三原则" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/2c/56/2c186469ab14bbd0f0394a68bfe99056.mp3"></audio>
前面几期专栏我跟你系统的聊了架构设计的主要目的是为了解决软件系统复杂度带来的问题并分析了复杂度的来源。从今天开始我会分两期讲讲架构设计的3个原则以及架构设计原则的案例。
成为架构师是每个程序员的梦想,但并不意味着把编程做好就能够自然而然地成为一个架构师,优秀程序员和架构师之间还有一个明显的鸿沟需要跨越,这个鸿沟就是“**不确定性**”。
对于编程来说本质上是不能存在不确定的对于同样一段代码不管是谁写的不管什么时候执行执行的结果应该都是确定的注意“确定的”并不等于“正确的”有bug也是确定的。而对于架构设计来说本质上是不确定的同样的一个系统A公司和B公司做出来的架构可能差异很大但最后都能正常运转同样一个方案A设计师认为应该这样做B设计师认为应该那样做看起来好像都有道理……相比编程来说架构设计并没有像编程语言那样的语法来进行约束更多的时候是面对多种可能性时进行选择。
可是一旦涉及“选择”,就很容易让架构师陷入两难的境地,例如:
<li>是要选择业界最先进的技术,还是选择团队目前最熟悉的技术?如果选了最先进的技术后出了问题怎么办?如果选了目前最熟悉的技术,后续技术演进怎么办?
</li>
<li>是要选择Google的Angular的方案来做还是选择Facebook的React来做Angular看起来更强大但React看起来更灵活
</li>
<li>是要选MySQL还是MongoDB团队对MySQL很熟悉但是MongoDB更加适合业务场景
</li>
<li>淘宝的电商网站架构很完善,我们新做一个电商网站,是否简单地照搬淘宝就可以了?
</li>
还有很多类似的问题和困惑,关键原因在于架构设计领域并没有一套通用的规范来指导架构师进行架构设计,更多是依赖架构师的经验和直觉,因此架构设计有时候也会被看作一项比较神秘的工作。
业务千变万化技术层出不穷设计理念也是百花齐放看起来似乎很难有一套通用的规范来适用所有的架构设计场景。但是在研究了架构设计的发展历史、多个公司的架构发展过程QQ、淘宝、Facebook等、众多的互联网公司架构设计后我发现有几个共性的原则隐含其中这就是**合适原则、简单原则、演化原则**,架构设计时遵循这几个原则,有助于你做出最好的选择。
## 合适原则
**合适原则宣言:“合适优于业界领先”。**
优秀的技术人员都有很强的技术情结当他们做方案或者架构时总想不断地挑战自己想达到甚至优于业界领先水平是其中一个典型表现因为这样才能够展现自己的优秀才能在年终KPI绩效总结里面骄傲地写上“设计了XX方案达到了和Google相同的技术水平”“XX方案的性能测试结果大大优于阿里集团的YY方案”。
但现实是大部分这样想和这样做的架构最后可能都以失败告终我在互联网行业见过“亿级用户平台”的失败案例2011年的时候某个几个人规模的业务团队雄心勃勃的提出要做一个和腾讯QQ那时候微信还没起来一拼高下的“亿级用户平台”最后结果当然是不出所料的失败了。
为什么会这样呢?
再好的梦想,也需要脚踏实地实现!这里的“脚踏实地”主要体现在下面几个方面。
1.将军难打无兵之仗
大公司的分工比较细一个小系统可能就是一个小组负责比如说某个通信大厂做一个OM管理系统就有十几个人阿里的中间件团队有几十个人而大部分公司整个研发团队可能就100多人某个业务团队可能就十几个人。十几个人的团队想做几十个人的团队的事情而且还要做得更好不能说绝对不可能但难度是可想而知的。
**没那么多人,却想干那么多活,是失败的第一个主要原因。**
2.罗马不是一天建成的
业界领先的很多方案其实并不是一堆天才某个时期灵机一动然后加班加点就做出来的而是经过几年时间的发展才逐步完善和初具规模的。阿里中间件团队2008年成立发展到现在已经有十年了。我们只知道他们抗住了多少次“双11”做了多少优秀的系统但经历了什么样的挑战、踩了什么样的坑只有他们自己知道这些挑战和踩坑都是架构设计非常关键的促进因素单纯靠拍脑袋或者头脑风暴是不可能和真正实战相比的。
**没有那么多积累,却想一步登天,是失败的第二个主要原因。**
3.冰山下面才是关键
可能有人认为业界领先的方案都是天才创造出来的所以自己也要造一个业界领先的方案以此来证明自己也是天才。确实有这样的天才但更多的时候业界领先的方案其实都是“逼”出来的简单来说“业务”发展到一定阶段量变导致了质变出现了新的问题已有的方式已经不能应对这些问题需要用一种新的方案来解决通过创新和尝试才有了业界领先的方案。GFS为何在Google诞生而不是在Microsoft诞生我认为Google有那么庞大的数据是一个主要的因素而不是因为Google的工程师比Microsoft的工程师更加聪明。
**没有那么卓越的业务场景,却幻想灵光一闪成为天才,是失败的第三个主要原因。**
回到我前面提到的“亿级用户平台”失败的例子分析一下原因。没有腾讯那么多的人当然钱差得更多没有QQ那样海量用户的积累没有QQ那样的业务这个项目失败其实是在一开始就注定的。注意这里的失败不是说系统做不出来而是系统没有按照最初的目标来实现上面提到的3个失败原因也全占了。
所以真正优秀的架构都是在企业当前人力、条件、业务等各种约束下设计出来的能够合理地将资源整合在一起并发挥出最大功效并且能够快速落地。这也是很多BAT出来的架构师到了小公司或者创业团队反而做不出成绩的原因因为没有了大公司的平台、资源、积累只是生搬硬套大公司的做法失败的概率非常高。
## 简单原则
**简单原则宣言:“简单优于复杂”。**
软件架构设计是一门技术活。所谓技术活,从历史上看,无论是瑞士的钟表,还是瓦特的蒸汽机;无论是莱特兄弟发明的飞机,还是摩托罗拉发明的手机,无一不是越来越精细、越来越复杂。因此当我们进行架构设计时,会自然而然地想把架构做精美、做复杂,这样才能体现我们的技术实力,也才能够将架构做成一件艺术品。
由于软件架构和建筑架构表面上的相似性,我们也会潜意识地将对建筑的审美观点移植到软件架构上面。我们惊叹于长城的宏伟、泰姬陵的精美、悉尼歌剧院的艺术感、迪拜帆船酒店的豪华感,因此,对于我们自己亲手打造的软件架构,我们也希望它宏伟、精美、艺术、豪华……总之就是不能寒酸、不能简单。
团队的压力有时也会有意无意地促进我们走向复杂的方向因为大部分人在评价一个方案水平高低的时候复杂性是其中一个重要的参考指标。例如设计一个主备方案如果你用心跳来实现可能大家都认为这太简单了。但如果你引入ZooKeeper来做主备决策可能很多人会认为这个方案更加“高大上”一些毕竟ZooKeeper使用的是ZAB协议而ZAB协议本身就很复杂。其实真正理解ZAB协议的人很少我也不懂但并不妨碍我们都知道ZAB协议很优秀。
刚才我聊的这些原因,会在潜意识层面促使初出茅庐的架构师,不自觉地追求架构的复杂性。然而,“复杂”在制造领域代表先进,在建筑领域代表领先,但在软件领域,却恰恰相反,代表的是“问题”。
软件领域的复杂性体现在两个方面:
1.结构的复杂性
结构复杂的系统几乎毫无例外具备两个特点:
<li>组成复杂系统的组件数量更多;
</li>
<li>同时这些组件之间的关系也更加复杂。
</li>
我以图形的方式来说明复杂性:
2个组件组成的系统
<img src="https://static001.geekbang.org/resource/image/2d/9c/2dca583c9634ffcc224852adab208d9c.png" alt="">
3个组件组成的系统
<img src="https://static001.geekbang.org/resource/image/3f/74/3f4181e7166570077c3a4701129c5274.png" alt="">
4个组件组成的系统
<img src="https://static001.geekbang.org/resource/image/e5/8c/e5716355fb4a84a464e14c2fc3289a8c.png" alt="">
5个组件组成的系统
<img src="https://static001.geekbang.org/resource/image/a1/86/a14cf5f4dba6fca660dea0aa56ce5486.png" alt="">
结构上的复杂性存在的第一个问题是,**组件越多,就越有可能其中某个组件出现故障**从而导致系统故障。这个概率可以算出来假设组件的故障率是10%有10%的时间不可用那么有3个组件的系统可用性是1-10%×1-10%×1-10%= 72.9%有5个组件的系统可用性是1-10%×1-10%×1-10%×1-10%×1-10%=59%两者的可用性相差13%。
结构上的复杂性存在的第二个问题是,**某个组件改动,会影响关联的所有组件**这些被影响的组件同样会继续递归影响更多的组件。还以上面图中5个组件组成的系统为例组件A修改或者异常时会影响组件B/C/ED又会影响E。这个问题会影响整个系统的开发效率因为一旦变更涉及外部系统需要协调各方统一进行方案评估、资源协调、上线配合。
结构上的复杂性存在的第三个问题是,**定位一个复杂系统中的问题总是比简单系统更加困难**。首先是组件多,每个组件都有嫌疑,因此要逐一排查;其次组件间的关系复杂,有可能表现故障的组件并不是真正问题的根源。
2.逻辑的复杂性
意识到结构的复杂性后,我们的第一反应可能就是“降低组件数量”,毕竟组件数量越少,系统结构越简。最简单的结构当然就是整个系统只有一个组件,即系统本身,所有的功能和逻辑都在这一个组件中实现。
不幸的是,这样做是行不通的,原因在于除了结构的复杂性,还有逻辑的复杂性,即如果某个组件的逻辑太复杂,一样会带来各种问题。
逻辑复杂的组件,一个典型特征就是单个组件承担了太多的功能。以电商业务为例,常见的功能有:商品管理、商品搜索、商品展示、订单管理、用户管理、支付、发货、客服……把这些功能全部在一个组件中实现,就是典型的逻辑复杂性。
逻辑复杂几乎会导致软件工程的每个环节都有问题,假设现在淘宝将这些功能全部在单一的组件中实现,可以想象一下这个恐怖的场景:
<li>系统会很庞大可能是上百万、上千万的代码规模“clone”一次代码要30分钟。
</li>
<li>几十、上百人维护这一套代码,某个“菜鸟”不小心改了一行代码,导致整站崩溃。
</li>
<li>需求像雪片般飞来,为了应对,开几十个代码分支,然后各种分支合并、各种分支覆盖。
</li>
<li>产品、研发、测试、项目管理不停地开会讨论版本计划,协调资源,解决冲突。
</li>
<li>版本太多每天都要上线几十个版本系统每隔1个小时重启一次。
</li>
<li>线上运行出现故障,几十个人扑上去定位和处理,一间小黑屋都装不下所有人,整个办公区闹翻天。
</li>
<li>……
</li>
不用多说,肯定谁都无法忍受这样的场景。
但是,为什么复杂的电路就意味更强大的功能,而复杂的架构却有很多问题呢?根本原因在于电路一旦设计好后进入生产,就不会再变,复杂性只是在设计时带来影响;而一个软件系统在投入使用后,后续还有源源不断的需求要实现,因此要不断地修改系统,复杂性在整个系统生命周期中都有很大影响。
功能复杂的组件,另外一个典型特征就是采用了复杂的算法。复杂算法导致的问题主要是难以理解,进而导致难以实现、难以修改,并且出了问题难以快速解决。
以ZooKeeper为例ZooKeeper本身的功能主要就是选举为了实现分布式下的选举采用了ZAB协议所以ZooKeeper功能虽然相对简单但系统实现却比较复杂。相比之下etcd就要简单一些因为etcd采用的是Raft算法相比ZAB协议Raft算法更加容易理解更加容易实现。
综合前面的分析我们可以看到无论是结构的复杂性还是逻辑的复杂性都会存在各种问题所以架构设计时如果简单的方案和复杂的方案都可以满足需求最好选择简单的方案。《UNIX编程艺术》总结的KISSKeep It Simple, Stupid!)原则一样适应于架构设计。
## 演化原则
**演化原则宣言:“演化优于一步到位”。**
软件架构从字面意思理解和建筑结构非常类似,事实上“架构”这个词就是建筑领域的专业名词,维基百科对“软件架构”的定义中有一段话描述了这种相似性:
>
从和目的、主题、材料和结构的联系上来说,软件架构可以和建筑物的架构相比拟。
例如,软件架构描述的是一个软件系统的结构,包括各个模块,以及这些模块的关系;建筑架构描述的是一幢建筑的结构,包括各个部件,以及这些部件如何有机地组成成一幢完美的建筑。
然而,字面意思上的相似性却掩盖了一个本质上的差异:建筑一旦完成(甚至一旦开建)就不可再变,而软件却需要根据业务的发展不断地变化!
<li>古埃及的吉萨大金字塔4000多年前完成的到现在还是当初的架构。
</li>
<li>中国的明长城600多年前完成的现在保存下来的长城还是当年的结构。
</li>
<li>美国白宫1800年建成200年来进行了几次扩展但整体结构并无变化只是在旁边的空地扩建或者改造内部的布局。
</li>
对比一下,我们来看看软件架构。
Windows系统的发展历史
<img src="https://static001.geekbang.org/resource/image/83/bc/83a8089855470db9d2a4449bb032d8bc.png" alt="">
[http://www.abcydia.com/upload/2016/01/4-1.jpg](http://www.abcydia.com/upload/2016/01/4-1.jpg)
如果对比Windows 8的架构和Windows 1.0的架构,就会发现它们其实是两个不同的系统了!
Android的发展历史
 <img src="https://static001.geekbang.org/resource/image/67/c4/671adc5fb5ed7c4fcf89b23ac5612cc4.png" alt="">
[http://www.dappworld.com/wp-content/uploads/2015/09/Android-History-Dappworld.jpg](http://www.dappworld.com/wp-content/uploads/2015/09/Android-History-Dappworld.jpg)
同样Android 6.0和Android 1.6的差异也很大。
**对于建筑来说,永恒是主题;而对于软件来说,变化才是主题**。软件架构需要根据业务的发展而不断变化。设计Windows和Android的人都是顶尖的天才即便如此他们也不可能在1985年设计出Windows 8不可能在2009年设计出Android 6.0。
如果没有把握“软件架构需要根据业务发展不断变化”这个本质,在做架构设计的时候就很容易陷入一个误区:试图一步到位设计一个软件架构,期望不管业务如何变化,架构都稳如磐石。
为了实现这样的目标,要么照搬业界大公司公开发表的方案;要么投入庞大的资源和时间来做各种各样的预测、分析、设计。无论哪种做法,后果都很明显:投入巨大,落地遥遥无期。更让人沮丧的是,就算跌跌撞撞拼死拼活终于落地,却发现很多预测和分析都是不靠谱的。
考虑到软件架构需要根据业务发展不断变化这个本质特点,**软件架构设计其实更加类似于大自然“设计”一个生物,通过演化让生物适应环境,逐步变得更加强大:**
<li>首先,生物要适应当时的环境。
</li>
<li>其次,生物需要不断地繁殖,将有利的基因传递下去,将不利的基因剔除或者修复。
</li>
<li>第三,当环境变化时,生物要能够快速改变以适应环境变化;如果生物无法调整就被自然淘汰;新的生物会保留一部分原来被淘汰生物的基因。
</li>
软件架构设计同样是类似的过程:
<li>首先,设计出来的架构要满足当时的业务需要。
</li>
<li>其次,架构要不断地在实际应用过程中迭代,保留优秀的设计,修复有缺陷的设计,改正错误的设计,去掉无用的设计,使得架构逐渐完善。
</li>
<li>第三,当业务发生变化时,架构要扩展、重构,甚至重写;代码也许会重写,但有价值的经验、教训、逻辑、设计等(类似生物体内的基因)却可以在新架构中延续。
</li>
架构师在进行架构设计时需要牢记这个原则,时刻提醒自己不要贪大求全,或者盲目照搬大公司的做法。应该认真分析当前业务的特点,明确业务面临的主要问题,设计合理的架构,快速落地以满足业务需要,然后在运行过程中不断完善架构,不断随着业务演化架构。
即使是大公司的团队,在设计一个新系统的架构时,也需要遵循演化的原则,而不应该认为团队人员多、资源多,不管什么系统上来就要一步到位,因为业务的发展和变化是很快的,不管多牛的团队,也不可能完美预测所有的业务发展和变化路径。
## 小结
今天我为你讲了面对“不确定性”时架构设计的三原则,分别是合适优于业界领先、简单优于复杂、演化优于一步到位,希望对你有所帮助。
这就是今天的全部内容,留一道思考题给你吧。我讲的这三条架构设计原则是否每次都要全部遵循?是否有优先级?谈谈你的理解,并说说为什么。
欢迎你把答案写到留言区,和我一起讨论。相信经过深度思考的回答,也会让你对知识的理解更加深刻。(编辑乱入:精彩的留言有机会获得丰厚福利哦!)

View File

@@ -0,0 +1,218 @@
<audio id="audio" title="09 | 架构设计原则案例" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/60/87/60b1d7e351735aff7a85743284e82687.mp3"></audio>
周二我给你介绍了架构设计的三条核心原则先复习一下合适原则、简单原则和演化原则。我们在架构设计实践中应该时刻谨记这三条设计原则指导我们设计出合适的架构即使是代表中国互联网技术最顶尖水平的BAT其架构的发展历程也同样遵循这三条原则。
今天我就以大家耳熟能详的淘宝和手机QQ作为案例来简单分析一下。
## 淘宝
注:以下部分内容摘自《淘宝技术发展》。
淘宝技术发展主要经历了“个人网站”→“Oracle/支付宝/旺旺”→“Java时代1.0”→“Java时代2.0”→“Java时代3.0”→“分布式时代”。我们看看每个阶段的主要驱动力是什么。
1.个人网站
>
2003年4月7日马云提出成立淘宝2003年5月10日淘宝就上线了中间只有1个月怎么办淘宝的答案就是买一个。
>
估计大部分人很难想象如今技术牛气冲天的阿里最初的淘宝竟然是买来的,我们看看当初决策的依据:
>
当时对整个项目组来说压力最大的就是时间,怎么在最短的时间内把一个从来就没有的网站从零开始建立起来?了解淘宝历史的人知道淘宝是在 2003 年 5 月 10 日上线的,这之间只有一个月。要是你在这个团队里,你怎么做?我们的答案就是:买一个来。
淘宝当时在初创时,没有过多考虑技术是否优越、性能是否海量以及稳定性如何,主要的考虑因素就是:快!
因为此时业务要求快速上线,时间不等人,等你花几个月甚至十几个月搞出一个强大的系统出来,可能市场机会就没有了,黄花菜都凉了。
同样,在考虑如何买的时候,淘宝的决策依据主要也是“快”。
>
买一个网站显然比做一个网站要省事一些,但是他们的梦想可不是做一个小网站而已,要做大,就不是随便买个就行的,要有比较低的维护成本,要能够方便地扩展和二次开发。
>
那接下来就是第二个问题:买一个什么样的网站?答案是:轻量一点的,简单一点的。
**买一个系统是为了“快速可用”,而买一个轻量级的系统是为了“快速开发”**。因为系统上线后肯定有大量的需求需要做,这时能够快速开发就非常重要。
从这个实例我们可以看到:淘宝最开始的时候业务要求就是“快”,因此反过来要求技术同样要“快”,业务决定技术,这里架构设计和选择主要遵循的是“合适原则”和“简单原则”。
第一代的技术架构如图所示。
<img src="https://static001.geekbang.org/resource/image/36/yd/369c79010ebbdea7fbc787a9f8388yyd.jpg" alt="" />
2.Oracle/支付宝/旺旺
淘宝网推出后由于正好碰到“非典”网购很火爆加上采取了成功的市场运作流量和交易量迅速上涨业务发展很快在2003年底MySQL已经撑不住了。
一般人或者团队在这个时候,可能就开始优化系统、优化架构、分拆业务了,因为这些是大家耳熟能详也很拿手的动作。那我们来看看淘宝这个时候怎么采取的措施:
>
技术的替代方案非常简单就是换成Oracle。换Oracle的原因除了它容量大、稳定、安全、性能高还有人才方面的原因。
可以看出这个时候淘宝的策略主要还是“买”买更高配置的Oracle这个是当时情况下最快的方法。
除了购买Oracle后来为了优化又买了更强大的存储
>
后来数据量变大了本地存储不行了。买了NASNetwork Attached Storage网络附属存储NetApp的NAS存储作为了数据库的存储设备加上Oracle RACReal Application Clusters实时应用集群来实现负载均衡。
为什么淘宝在这个时候继续采取“买”的方式来快速解决问题呢?我们可以从时间上看出端倪:此时离刚上线才半年不到,业务飞速发展,最快的方式支撑业务的发展还是去买。如果说第一阶段买的是“方案”,这个阶段买的就是“性能”,这里架构设计和选择主要遵循的还是“合适原则”和“简单原则”。
换上Oracle和昂贵的存储后第二代架构如图所示。
<img src="https://static001.geekbang.org/resource/image/c7/2e/c735c053a4765c0739e2c5b3ef1b962e.jpg" alt="" />
3.脱胎换骨的Java时代1.0
>
淘宝切换到Java的原因很有趣主要因为找了一个PHP的开源连接池SQL Relay连接到Oracle而这个代理经常死锁死锁了就必须重启而数据库又必须用Oracle于是决定换个开发语言。最后淘宝挑选了Java而且当时挑选Java也是请Sun公司的人这帮人很厉害先是将淘宝网站从PHP热切换到了Java后来又做了支付宝。
这次切换的最主要原因是因为技术影响了业务的发展,频繁的死锁和重启对用户业务产生了严重的影响,从业务的角度来看这是不得不解决的技术问题。
但这次淘宝为什么没有去“买”呢我们看最初选择SQL Relay的原因
>
但对于PHP语言来说它是放在Apache上的每一个请求都会对数据库产生一个连接它没有连接池这种功能Java语言有Servlet容器可以存放连接池。那如何是好呢这帮人打探到eBay在PHP下面用了一个连接池的工具是BEA卖给他们的。我们知道BEA的东西都很贵我们买不起于是多隆在网上寻寻觅觅找到一个开源的连接池代理服务SQL Relay。
不清楚当时到底有多贵Oracle都可以买连接池买不起 所以我个人感觉这次切换语言更多是为以后业务发展做铺垫毕竟当时PHP语言远远没有Java那么火、那么好招人。淘宝选择Java语言的理由可以从侧面验证这点
>
Java是当时最成熟的网站开发语言它有比较良好的企业开发框架被世界上主流的大规模网站普遍采用另外有Java开发经验的人才也比较多后续维护成本会比较低。
综合来看,这次架构的变化没有再简单通过“买”来解决,而是通过重构来解决,架构设计和选择遵循了“演化原则”。
从PHP改为Java后第三代技术架构如图所示。
<img src="https://static001.geekbang.org/resource/image/95/2c/9558b5cbb1yyf77154e4172ceb66b92c.jpg" alt="" />
4.坚若磐石的Java时代2.0
Java时代2.0淘宝做了很多优化工作数据分库、放弃EJB、引入Spring、加入缓存、加入CDN、采用开源的JBoss。为什么在这个时候要做这些动作原文作者很好地概括了做这些动作的原因
>
这些杂七杂八的修改我们对数据分库、放弃EJB、引入Spring、加入缓存、加入CDN、采用开源的JBoss看起来没有章法可循其实都是围绕着提高容量、提高性能、节约成本来做的。
我们思考一下,为什么在前面的阶段,淘宝考虑的都是“快”,而现在开始考虑“容量、性能、成本”了呢?而且为什么这个时候不采取“买”的方式来解决容量、性能、成本问题呢?
简单来说,就是“买”也搞不定了,此时的业务发展情况是这样的:
>
随着数据量的继续增长到了2005年商品数有1663万PV有8931万注册会员有1390万这给数据和存储带来的压力依然很大数据量大性能就慢。
原有的方案存在固有缺陷随着业务的发展已经不是靠“买”就能够解决问题了此时必须从整个架构上去进行调整和优化。比如说Oracle再强大在做like类搜索的时候也不可能做到纯粹的搜索系统如Solr、Sphinx等的性能因为这是机制决定的。
另外,随着规模的增大,纯粹靠买的一个典型问题开始成为重要的考虑因素,那就是**成本**。当买一台两台Oracle的时候可能对成本并不怎么关心但如果要买100台Oracle成本就是一个关键因素了。这就是“量变带来质变”的一个典型案例业务和系统发生质变后架构设计遵循“演化原则”的思想需要再一次重构甚至重写。
Java架构经过各种优化第四代技术架构如图所示。
<img src="https://static001.geekbang.org/resource/image/84/5b/84818454a50bc4ca97fdf3d152cbb45b.jpg" alt="" />
5.Java 时代3.0和分布式时代
>
<p>Java时代3.0我个人认为是淘宝技术飞跃的开始简单来说就是淘宝技术从商用转为“自研”典型的就是去IOE化。<br />
分布式时代我认为是淘宝技术的修炼成功,到了这个阶段,自研技术已经自成一派,除了支撑本身的海量业务,也开始影响整个互联网的技术发展。</p>
到了这个阶段业务规模急剧上升后原来并不是主要复杂度的IOE成本开始成为了主要的问题因此通过自研系统来降低IOE的成本去IOE也是系统架构的再一次演化。
## 手机QQ
以下部分内容摘自《QQ 1.4亿在线背后的故事》。
手机QQ的发展历程按照用户规模可以粗略划分为4个阶段十万级、百万级、千万级、亿级不同的用户规模IM后台的架构也不同而且基本上都是用户规模先上去然后产生各种问题倒逼技术架构升级。
1.十万级IM 1.X
最开始的手机QQ后台是这样的可以说是简单得不能再简单、普通得不能再普通的一个架构了因为当时业务刚开始架构设计遵循的是“合适原则”和“简单原则”。
<img src="https://static001.geekbang.org/resource/image/4f/85/4f43b7902c343a95bbc04f2ddf44c085.jpg" alt="" />
2.百万级IM 2.X
随着业务发展到2001年QQ同时在线人数也突破了一百万。第一代架构很简单明显不可能支撑百万级的用户规模主要的问题有
<li>
以接入服务器的内存为例单个在线用户的存储量约为2KB索引和在线状态为50字节好友表400个好友 × 5字节/好友 = 2000字节大致来说2GB内存只能支持一百万在线用户。
</li>
<li>
CPU/网卡包量和流量/交换机流量等瓶颈。
</li>
<li>
单台服务器支撑不下所有在线用户/注册用户。
</li>
于是针对这些问题做架构改造按照“演化原则”的指导进行了重构重构的方案相比现在来说也还是简单得多因此当时做架构设计时也遵循了“合适原则”和“简单原则”。IM 2.X的最终架构如图所示。
<img src="https://static001.geekbang.org/resource/image/f7/14/f7286a0fd79c61cdfd55eec957276d14.jpg" alt="" />
3.千万级IM 3.X
业务发展到2005年QQ同时在线人数突破了一千万。第二代架构支撑百万级用户是没问题的但支撑千万级用户又会产生新问题表现有
<li>
同步流量太大,状态同步服务器遇到单机瓶颈。
</li>
<li>
所有在线用户的在线状态信息量太大,单台接入服务器存不下,如果在线数进一步增加,甚至单台状态同步服务器也存不下。
</li>
<li>
单台状态同步服务器支撑不下所有在线用户。
</li>
<li>
单台接入服务器支撑不下所有在线用户的在线状态信息。
</li>
针对这些问题架构需要继续改造升级再一次“演化”。IM 3.X的最终架构如下图可以看到这次的方案相比之前的方案来说并不简单了这是业务特性决定的。
<img src="https://static001.geekbang.org/resource/image/59/27/5933a11358bbeb12ab62ec18a23ff827.jpg" alt="" />
4.亿级IM 4.X
业务发展到2010年3月QQ同时在线人数过亿。第三代架构此时也不适应了主要问题有
<li>
灵活性很差比如“昵称”长度增加一半需要两个月增加“故乡”字段需要两个月最大好友数从500变成1000需要三个月。
</li>
<li>
无法支撑某些关键功能比如好友数上万、隐私权限控制、PC QQ与手机QQ不可互踢、微信与QQ互通、异地容灾。
</li>
除了不适应,还有一个更严重的问题:
>
IM后台从1.0到3.5都是在原来基础上做改造升级的但是持续打补丁已经难以支撑亿级在线IM后台4.0必须从头开始,重新设计实现!
这里再次遵循了“演化原则”,决定重新打造一个这么复杂的系统,不得不佩服当时决策人的勇气和魄力!
重新设计的IM 4.0架构如图所示,和之前的架构相比,架构本身都拆分为两个主要的架构:存储架构和通信架构。
- 存储架构
<img src="https://static001.geekbang.org/resource/image/10/a2/103006ae445b6623f6c6eaa18295e4a2.jpg" alt="" />
- 通信架构
<img src="https://static001.geekbang.org/resource/image/c9/d5/c9febc2c26c2088332c31eae451b36d5.jpg" alt="" />
## 小结
今天我给你讲了淘宝和手机QQ两个典型互联网业务的架构发展历程通过这两个案例我们可以看出即使是现在非常复杂、非常强大的架构也并不是一开始就进行了复杂设计而是首先采取了简单的方式简单原则满足了当时的业务需要合适原则随着业务的发展逐步演化而来的演化原则。罗马不是一天建成的架构也不是一开始就设计成完美的样子然后可以一劳永逸一直用下去。
这就是今天的全部内容留一道思考题给你吧。搜索一个互联网大厂BATJ、TMD等的架构发展案例分析一下其发展过程看看哪些地方体现了这三条架构设计原则。
欢迎把你的答案写到留言区,和我一起讨论。相信经过深度思考的回答,也会让你对知识的理解更加深刻。(编辑乱入:精彩的留言有机会获得丰厚福利哦!)

View File

@@ -0,0 +1,97 @@
<audio id="audio" title="10 | 架构设计流程:识别复杂度" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/48/cb/48cb03383be4cc5e4f02c985b5a59ccb.mp3"></audio>
从今天开始我将分4期结合复杂度来源和架构设计原则通过一个模拟的设计场景“前浪微博”和你一起看看在实践中究竟如何进行架构设计。今天先来看架构设计流程第1步识别复杂度。
## 架构设计第1步识别复杂度
我在前面讲过,架构设计的本质目的是为了解决软件系统的复杂性,所以在我们设计架构时,首先就要分析系统的复杂性。只有正确分析出了系统的复杂性,后续的架构设计方案才不会偏离方向;否则,如果对系统的复杂性判断错误,即使后续的架构设计方案再完美再先进,都是南辕北辙,做的越好,错的越多、越离谱。
例如如果一个系统的复杂度本来是业务逻辑太复杂功能耦合严重架构师却设计了一个TPS达到50000/秒的高性能架构,即使这个架构最终的性能再优秀也没有任何意义,因为架构没有解决正确的复杂性问题。
架构的复杂度主要来源于“高性能”“高可用”“可扩展”等几个方面,但架构师在具体判断复杂性的时候,不能生搬硬套,认为任何时候架构都必须同时满足这三方面的要求。实际上大部分场景下,复杂度只是其中的某一个,少数情况下包含其中两个,如果真的出现同时需要解决三个或者三个以上的复杂度,要么说明这个系统之前设计的有问题,要么可能就是架构师的判断出现了失误,即使真的认为要同时满足这三方面的要求,也必须要进行优先级排序。
例如专栏前面提到过的“亿级用户平台”失败的案例设计对标腾讯的QQ按照腾讯QQ的用户量级和功能复杂度进行设计高性能、高可用、可扩展、安全等技术一应俱全一开始就设计出了40多个子系统然后投入大量人力开发了将近1年时间才跌跌撞撞地正式上线。上线后发现之前的过度设计完全是多此一举而且带来很多问题
<li>系统复杂无比,运维效率低下,每次业务版本升级都需要十几个子系统同步升级,操作步骤复杂,容易出错,出错后回滚还可能带来二次问题。
</li>
<li>每次版本开发和升级都需要十几个子系统配合,开发效率低下。
</li>
<li>子系统数量太多,关系复杂,小问题不断,而且出问题后定位困难。
</li>
<li>开始设计的号称TPS 50000/秒的系统实际TPS连500都不到。
</li>
由于业务没有发展最初的设计人员陆续离开后来接手的团队无奈又花了2年时间将系统重构合并很多子系统将原来40多个子系统合并成不到20个子系统整个系统才逐步稳定下来。
如果运气真的不好,接手了一个每个复杂度都存在问题的系统,那应该怎么办呢?答案是一个个来解决问题,不要幻想一次架构重构解决所有问题。例如这个“亿级用户平台”的案例,后来接手的团队其实面临几个主要的问题:系统稳定性不高,经常出各种莫名的小问题;系统子系统数量太多,系统关系复杂,开发效率低;不支持异地多活,机房级别的故障会导致业务整体不可用。如果同时要解决这些问题,就可能会面临这些困境:
<li>要做的事情太多,反而感觉无从下手。
</li>
<li>设计方案本身太复杂,落地时间遥遥无期。
</li>
<li>同一个方案要解决不同的复杂性,有的设计点是互相矛盾的。例如,要提升系统可用性,就需要将数据及时存储到硬盘上,而硬盘刷盘反过来又会影响系统性能。
</li>
因此,正确的做法是**将主要的复杂度问题列出来,然后根据业务、技术、团队等综合情况进行排序,优先解决当前面临的最主要的复杂度问题**。“亿级用户平台”这个案例,团队就优先选择将子系统的数量降下来,后来发现子系统数量降下来后,不但开发效率提升了,原来经常发生的小问题也基本消失了,于是团队再在这个基础上做了异地多活方案,也取得了非常好的效果。
对于按照复杂度优先级解决的方式,存在一个普遍的担忧:如果按照优先级来解决复杂度,可能会出现解决了优先级排在前面的复杂度后,解决后续复杂度的方案需要将已经落地的方案推倒重来。这个担忧理论上是可能的,但现实中几乎是不可能出现的,原因在于软件系统的可塑性和易变性。对于同一个复杂度问题,软件系统的方案可以有多个,总是可以挑出综合来看性价比最高的方案。
即使架构师决定要推倒重来这个新的方案也必须能够同时解决已经被解决的复杂度问题一般来说能够达到这种理想状态的方案基本都是依靠新技术的引入。例如Hadoop能够将高可用、高性能、大容量三个大数据处理的复杂度问题同时解决。
识别复杂度对架构师来说是一项挑战,因为原始的需求中并没有哪个地方会明确地说明复杂度在哪里,需要架构师在理解需求的基础上进行分析。有经验的架构师可能一看需求就知道复杂度大概在哪里;如果经验不足,那只能采取“排查法”,从不同的角度逐一进行分析。
## 识别复杂度实战
我们假想一个创业公司,名称叫作“前浪微博”。前浪微博的业务发展很快,系统也越来越多,系统间协作的效率很低,例如:
<li>用户发一条微博后,微博子系统需要通知审核子系统进行审核,然后通知统计子系统进行统计,再通知广告子系统进行广告预测,接着通知消息子系统进行消息推送……一条微博有十几个通知,目前都是系统间通过接口调用的。每通知一个新系统,微博子系统就要设计接口、进行测试,效率很低,问题定位很麻烦,经常和其他子系统的技术人员产生分岐,微博子系统的开发人员不胜其烦。
</li>
<li>用户等级达到VIP后等级子系统要通知福利子系统进行奖品发放要通知客服子系统安排专属服务人员要通知商品子系统进行商品打折处理……等级子系统的开发人员也是不胜其烦。
</li>
新来的架构师在梳理这些问题时,结合自己的经验,敏锐地发现了这些问题背后的根源在于架构上各业务子系统强耦合,而消息队列系统正好可以完成子系统的解耦,于是提议要引入消息队列系统。经过一分析二讨论三开会四汇报五审批等一系列操作后,消息队列系统终于立项了。其他背景信息还有:
<li>中间件团队规模不大大约6人左右。
</li>
<li>中间件团队熟悉Java语言但有一个新同事C/C++很牛。
</li>
<li>开发平台是Linux数据库是MySQL。
</li>
<li>目前整个业务系统是单机房部署,没有双机房。
</li>
针对前浪微博的消息队列系统,采用“排查法”来分析复杂度,具体分析过程是:
- 这个消息队列是否需要高性能
我们假设前浪微博系统用户每天发送1000万条微博那么微博子系统一天会产生1000万条消息我们再假设平均一条消息有10个子系统读取那么其他子系统读取的消息大约是1亿次。
1000万和1亿看起来很吓人但对于架构师来说关注的不是一天的数据而是1秒的数据即TPS和QPS。我们将数据按照秒来计算一天内平均每秒写入消息数为115条每秒读取的消息数是1150条再考虑系统的读写并不是完全平均的设计的目标应该以峰值来计算。峰值一般取平均值的3倍那么消息队列系统的TPS是345QPS是3450这个量级的数据意味着并不要求高性能。
虽然根据当前业务规模计算的性能要求并不高但业务会增长因此系统设计需要考虑一定的性能余量。由于现在的基数较低为了预留一定的系统容量应对后续业务的发展我们将设计目标设定为峰值的4倍因此最终的性能要求是TPS为1380QPS为13800。TPS为1380并不高但QPS为13800已经比较高了因此高性能读取是复杂度之一。注意这里的设计目标设定为峰值的4倍是根据业务发展速度来预估的不是固定为4倍不同的业务可以是2倍也可以是8倍但一般不要设定在10倍以上更不要一上来就按照100倍预估。
- 这个消息队列是否需要高可用性
对于微博子系统来说如果消息丢了导致没有审核然后触犯了国家法律法规则是非常严重的事情对于等级子系统来说如果用户达到相应等级后系统没有给他奖品和专属服务则VIP用户会很不满意导致用户流失从而损失收入虽然也比较关键但没有审核子系统丢消息那么严重。
综合来看,消息队列需要高可用性,包括消息写入、消息存储、消息读取都需要保证高可用性。
- 这个消息队列是否需要高可扩展性
消息队列的功能很明确,基本无须扩展,因此可扩展性不是这个消息队列的复杂度关键。
为了方便理解这里我只排查“高性能”“高可用”“扩展性”这3个复杂度在实际应用中不同的公司或者团队可能还有一些其他方面的复杂度分析。例如金融系统可能需要考虑安全性有的公司会考虑成本等。
综合分析下来,消息队列的复杂性主要体现在这几个方面:高性能消息读取、高可用消息写入、高可用消息存储、高可用消息读取。
“前浪微博”的消息队列设计才刚完成第1步专栏下一期会根据今天识别的复杂度设计备选方案前面提到的场景在下一期还会用到哦。
## 小结
今天我为你讲了架构设计流程的第一个步骤“识别复杂度”,并且通过一个模拟的场景讲述了“排查法”的具体分析方式,希望对你有所帮助。
这就是今天的全部内容,留一道思考题给你吧。尝试用排查法分析一下你参与过或者研究过的系统的复杂度,然后与你以前的理解对比一下,看看是否有什么新发现?
欢迎你把答案写到留言区,和我一起讨论。相信经过深度思考的回答,也会让你对知识的理解更加深刻。(编辑乱入:精彩的留言有机会获得丰厚福利哦!)

View File

@@ -0,0 +1,120 @@
<audio id="audio" title="11 | 架构设计流程:设计备选方案" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/88/9d/8831d0d7dfb0649c61f0f61c0157749d.mp3"></audio>
上一期我讲了架构设计流程第1步识别复杂度确定了系统面临的主要复杂度问题后方案设计就有了明确的目标我们就可以开始真正进行架构方案设计了。今天我来讲讲架构设计流程第2步设计备选方案同样还会结合上期“前浪微博”的场景谈谈消息队列设计备选方案的实战。
## 架构设计第2步设计备选方案
架构师的工作并不神秘,成熟的架构师需要对已经存在的技术非常熟悉,对已经经过验证的架构模式烂熟于心,然后根据自己对业务的理解,挑选合适的架构模式进行组合,再对组合后的方案进行修改和调整。
虽然软件技术经过几十年的发展,新技术层出不穷,但是经过时间考验,已经被各种场景验证过的成熟技术其实更多。例如,高可用的主备方案、集群方案,高性能的负载均衡、多路复用,可扩展的分层、插件化等技术,绝大部分时候我们有了明确的目标后,按图索骥就能够找到可选的解决方案。
只有当这种方式完全无法满足需求的时候,才会考虑进行方案的创新,而事实上方案的创新绝大部分情况下也都是基于已有的成熟技术。
<li>NoSQLKey-Value的存储和数据库的索引其实是类似的Memcache只是把数据库的索引独立出来做成了一个缓存系统。
</li>
<li>Hadoop大文件存储方案基础其实是集群方案+ 数据复制方案。
</li>
<li>Docker虚拟化基础是LXCLinux Containers
</li>
<li>LevelDB的文件存储结构是Skip List。
</li>
在《技术的本质》一书中,对技术的组合有清晰的阐述:
>
新技术都是在现有技术的基础上发展起来的,现有技术又来源于先前的技术。将技术进行功能性分组,可以大大简化设计过程,这是技术“模块化”的首要原因。技术的“组合”和“递归”特征,将彻底改变我们对技术本质的认识。
虽说基于已有的技术或者架构模式进行组合,然后调整,大部分情况下就能够得到我们需要的方案,但并不意味着架构设计是一件很简单的事情。因为可选的模式有很多,组合的方案更多,往往一个问题的解决方案有很多个;如果再在组合的方案上进行一些创新,解决方案会更多。因此,如何设计最终的方案,并不是一件容易的事情,这个阶段也是很多架构师容易犯错的地方。
第一种常见的错误:设计最优秀的方案。
很多架构师在设计架构方案时心里会默认有一种技术情结我要设计一个优秀的架构才能体现我的技术能力例如高可用的方案中集群方案明显比主备方案要优秀和强大高性能的方案中淘宝的XX方案是业界领先的方案……
根据架构设计原则中“合适原则”和“简单原则“的要求挑选合适自己业务、团队、技术能力的方案才是好方案否则要么浪费大量资源开发了无用的系统例如之前提过的“亿级用户平台”的案例设计了TPS 50000的系统实际TPS只有500要么根本无法实现例如10个人的团队要开发现在的整个淘宝系统
第二种常见的错误:只做一个方案。
很多架构师在做方案设计时,可能心里会简单地对几个方案进行初步的设想,再简单地判断哪个最好,然后就基于这个判断开始进行详细的架构设计了。
这样做有很多弊端:
<li>心里评估过于简单,可能没有想得全面,只是因为某一个缺点就把某个方案给否决了,而实际上没有哪个方案是完美的,某个地方有缺点的方案可能是综合来看最好的方案。
</li>
<li>架构师再怎么牛,经验知识和技能也有局限,有可能某个评估的标准或者经验是不正确的,或者是老的经验不适合新的情况,甚至有的评估标准是架构师自己原来就理解错了。
</li>
<li>单一方案设计会出现过度辩护的情况,即架构评审时,针对方案存在的问题和疑问,架构师会竭尽全力去为自己的设计进行辩护,经验不足的设计人员可能会强词夺理。
</li>
因此,架构师需要设计多个备选方案,但方案的数量可以说是无穷无尽的,架构师也不可能穷举所有方案,那合理的做法应该是什么样的呢?
<li>**备选方案的数量以3 ~ 5个为最佳**。少于3个方案可能是因为思维狭隘考虑不周全多于5个则需要耗费大量的精力和时间并且方案之间的差别可能不明显。
</li>
<li>**备选方案的差异要比较明显**。例如主备方案和集群方案差异就很明显或者同样是主备方案用ZooKeeper做主备决策和用Keepalived做主备决策的差异也很明显。但是都用ZooKeeper做主备决策一个检测周期是1分钟一个检测周期是5分钟这就不是架构上的差异而是细节上的差异了不适合做成两个方案。
</li>
<li>**备选方案的技术不要只局限于已经熟悉的技术**。设计架构时架构师需要将视野放宽考虑更多可能性。很多架构师或者设计师积累了一些成功的经验出于快速完成任务和降低风险的目的可能自觉或者不自觉地倾向于使用自己已经熟悉的技术对于新的技术有一种不放心的感觉。就像那句俗语说的“如果你手里有一把锤子所有的问题在你看来都是钉子”。例如架构师对MySQL很熟悉因此不管什么存储都基于MySQL去设计方案系统性能不够了首先考虑的就是MySQL分库分表而事实上也许引入一个Memcache缓存就能够解决问题。
</li>
第三种常见的错误:备选方案过于详细。
有的架构师或者设计师在写备选方案时,错误地将备选方案等同于最终的方案,每个备选方案都写得很细。这样做的弊端显而易见:
<li>耗费了大量的时间和精力。
</li>
<li>将注意力集中到细节中,忽略了整体的技术设计,导致备选方案数量不够或者差异不大。
</li>
<li>评审的时候其他人会被很多细节给绕进去评审效果很差。例如评审的时候针对某个定时器应该是1分钟还是30秒争论得不可开交。
</li>
正确的做法是备选阶段关注的是技术选型而不是技术细节技术选型的差异要比较明显。例如采用ZooKeeper和Keepalived两种不同的技术来实现主备差异就很大而同样都采用ZooKeeper一个方案的节点设计是/service/node/master另一个方案的节点设计是/company/service/master这两个方案并无明显差异无须在备选方案设计阶段作为两个不同的备选方案至于节点路径究竟如何设计只要在最终的方案中挑选一个进行细化即可。
## 设计备选方案实战
还是回到“前浪微博”的场景上期我们通过“排查法”识别了消息队列的复杂性主要体现在高性能消息读取、高可用消息写入、高可用消息存储、高可用消息读取。接下来进行第2步设计备选方案。
1.备选方案1采用开源的Kafka
Kafka是成熟的开源消息队列方案功能强大性能非常高而且已经比较成熟很多大公司都在使用。
2.备选方案2集群 + MySQL存储
首先考虑单服务器高性能。高性能消息读取属于“计算高可用”的范畴单服务器高性能备选方案有很多种。考虑到团队的开发语言是Java虽然有人觉得C/C++语言更加适合写高性能的中间件系统但架构师综合来看认为无须为了语言的性能优势而让整个团队切换语言消息队列系统继续用Java开发。由于Netty是Java领域成熟的高性能网络库因此架构师选择基于Netty开发消息队列系统。
由于系统设计的QPS是13800即使单机采用Netty来构建高性能系统单台服务器支撑这么高的QPS还是有很大风险的因此架构师选择采取集群方式来满足高性能消息读取集群的负载均衡算法采用简单的轮询即可。
同理,“高可用写入”和“高性能读取”一样,可以采取集群的方式来满足。因为消息只要写入集群中一台服务器就算成功写入,因此“高可用写入”的集群分配算法和“高性能读取”也一样采用轮询,即正常情况下,客户端将消息依次写入不同的服务器;某台服务器异常的情况下,客户端直接将消息写入下一台正常的服务器即可。
整个系统中最复杂的是“高可用存储”和“高可用读取”“高可用存储”要求已经写入的消息在单台服务器宕机的情况下不丢失“高可用读取”要求已经写入的消息在单台服务器宕机的情况下可以继续读取。架构师第一时间想到的就是可以利用MySQL的主备复制功能来达到“高可用存储“的目的通过服务器的主备方案来达到“高可用读取”的目的。
具体方案:
<img src="https://static001.geekbang.org/resource/image/7b/8a/7b224715dc8efe67faa2af94922f948a.png" alt="">
简单描述一下方案:
<li>采用数据分散集群的架构,集群中的服务器进行分组,每个分组存储一部分消息数据。
</li>
<li>每个分组包含一台主MySQL和一台备MySQL分组内主备数据复制分组间数据不同步。
</li>
<li>正常情况下,分组内的主服务器对外提供消息写入和消息读取服务,备服务器不对外提供服务;主服务器宕机的情况下,备服务器对外提供消息读取的服务。
</li>
<li>客户端采取轮询的策略写入和读取消息。
</li>
3.备选方案3集群 + 自研存储方案
在备选方案2的基础上将MySQL存储替换为自研实现存储方案因为MySQL的关系型数据库的特点并不是很契合消息队列的数据特点参考Kafka的做法可以自己实现一套文件存储和复制方案此处省略具体的方案描述实际设计时需要给出方案
可以看出高性能消息读取单机系统设计这部分时并没有多个备选方案可选备选方案2和备选方案3都采取基于Netty的网络库用Java语言开发原因就在于团队的Java背景约束了备选的范围。通常情况下成熟的团队不会轻易改变技术栈反而是新成立的技术团队更加倾向于采用新技术。
上面简单地给出了3个备选方案用来示范如何操作实践中要比上述方案复杂一些。架构师的技术储备越丰富、经验越多备选方案也会更多从而才能更好地设计备选方案。例如开源方案选择可能就包括Kafka、ActiveMQ、RabbitMQ集群方案的存储既可以考虑用MySQL也可以考虑用HBase还可以考虑用Redis与MySQL结合等自研文件系统也可以有多个可以参考Kafka也可以参考LevelDB还可以参考HBase等。限于篇幅这里就不一一展开了。
## 小结
今天我为你讲了架构设计流程的第二个步骤:设计备选方案,基于我们模拟的“前浪微博”消息系统,给出了备选方案的设计样例,希望对你有所帮助。
这就是今天的全部内容,留一道思考题给你吧,除了这三个备选方案,如果让你来设计第四个备选方案,你的方案是什么?
欢迎你把答案写到留言区,和我一起讨论。相信经过深度思考的回答,也会让你对知识的理解更加深刻。(编辑乱入:精彩的留言有机会获得丰厚福利哦!)

View File

@@ -0,0 +1,167 @@
<audio id="audio" title="12 | 架构设计流程:评估和选择备选方案" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4c/0c/4c480783f4a8ca9d8dee8b1f38b0a60c.mp3"></audio>
上一期我讲了设计备选方案,在完成备选方案设计后,如何挑选出最终的方案也是一个很大的挑战,主要原因有:
<li>
每个方案都是可行的,如果方案不可行就根本不应该作为备选方案。
</li>
<li>
没有哪个方案是完美的。例如A方案有性能的缺点B方案有成本的缺点C方案有新技术不成熟的风险。
</li>
<li>
评价标准主观性比较强比如设计师说A方案比B方案复杂但另外一个设计师可能会认为差不多因为比较难将“复杂”一词进行量化。因此方案评审的时候我们经常会遇到几个设计师针对某个方案或者某个技术点争论得面红耳赤。
</li>
正因为选择备选方案存在这些困难,所以实践中很多设计师或者架构师就采取了下面几种指导思想:
- 最简派
设计师挑选一个看起来最简单的方案。例如我们要做全文搜索功能方案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++来做运维管理系统。
- 领导派
领导派就更加聪明了,列出备选方案,设计师自己拿捏不定,然后就让领导来定夺,反正最后方案选的对那是领导厉害,方案选的不对怎么办?那也是领导“背锅”。
其实这些不同的做法本身并不存在绝对的正确或者绝对的错误关键是不同的场景应该采取不同的方式。也就是说有时候我们要挑选最简单的方案有时候要挑选最优秀的方案有时候要挑选最熟悉的方案甚至有时候真的要领导拍板。因此关键问题是这里的“有时候”到底应该怎么判断今天我就来讲讲架构设计流程的第3步评估和选择备选方案。
## 架构设计第3步评估和选择备选方案
前面提到了那么多指导思想,真正应该选择哪种方法来评估和选择备选方案呢?我的答案就是“**360度环评**”!具体的操作方式为:**列出我们需要关注的质量属性点,然后分别从这些质量属性的维度去评估每个方案,再综合挑选适合当时情况的最优方案**。
常见的方案质量属性点有性能、可用性、硬件成本、项目投入、复杂度、安全性、可扩展性等。在评估这些质量属性时需要遵循架构设计原则1“合适原则”和原则2“简单原则”避免贪大求全基本上某个质量属性能够满足一定时期内业务发展就可以了。
假如我们做一个购物网站现在的TPS是1000如果我们预期1年内能够发展到TPS 2000业务一年翻倍已经是很好的情况了在评估方案的性能时只要能超过2000的都是合适的方案而不是说淘宝的网站TPS是每秒10万我们的购物网站就要按照淘宝的标准也实现TPS 10万。
有的设计师会有这样的担心如果我们运气真的很好业务直接一年翻了10倍TPS从1000上升到10000那岂不是按照TPS 2000做的方案不合适了又要重新做方案
这种情况确实有可能存在但概率很小如果每次做方案都考虑这种小概率事件我们的方案会出现过度设计导致投入浪费。考虑这个问题的时候需要遵循架构设计原则3“演化原则”避免过度设计、一步到位的想法。按照原则3的思想即使真的出现这种情况那就算是重新做方案代价也是可以接受的因为业务如此迅猛发展钱和人都不是问题。例如淘宝和微信的发展历程中有过多次这样大规模重构系统的经历。
通常情况下如果某个质量属性评估和业务发展有关系例如性能、硬件成本等需要评估未来业务发展的规模时一种简单的方式是将当前的业务规模乘以2 ~4即可如果现在的基数较低可以乘以4如果现在基数较高可以乘以2。例如现在的TPS是1000则按照TPS 4000来设计方案如果现在TPS是10000则按照TPS 20000来设计方案。
当然最理想的情况是设计一个方案能够简单地扩容就能够跟上业务的发展。例如我们设计一个方案TPS 2000的时候只要2台机器TPS 20000的时候只需要简单地将机器扩展到20台即可。但现实往往没那么理想因为量变会引起质变具体哪些地方质变是很难提前很长时间能预判到的。举一个最简单的例子一个开发团队5个人开发了一套系统能够从TPS 2000平滑扩容到TPS 20000但是当业务规模真的达到TPS 20000的时候团队规模已经扩大到了20个人此时系统发生了两个质变
<li>
首先是团队规模扩大20个人的团队在同一个系统上开发开发效率变将很低系统迭代速度很慢经常出现某个功能开发完了要等另外的功能开发完成才能一起测试上线此时如果要解决问题就需要将系统拆分为更多子系统。
</li>
<li>
其次是原来单机房的集群设计不满足业务需求了,需要升级为异地多活的架构。
</li>
如果团队一开始就预测到这两个问题系统架构提前就拆分为多个子系统并且支持异地多活呢这种“事后诸葛亮”也是不行的因为最开始的时候团队只有5个人5个人在有限的时间内要完成后来20个人才能完成的高性能、异地多活、可扩展的架构项目时间会遥遥无期业务很难等待那么长的时间。
完成方案的360度环评后我们可以基于评估结果整理出360度环评表一目了然地看到各个方案的优劣点。但是360度环评表也只能帮助我们分析各个备选方案还是没有告诉我们具体选哪个方案原因就在于没有哪个方案是完美的极少出现某个方案在所有对比维度上都是最优的。例如引入开源方案工作量小但是可运维性和可扩展性差自研工作量大但是可运维和可维护性好使用C语言开发性能高但是目前团队C语言技术积累少使用Java技术积累多但是性能没有C语言开发高成本会高一些……诸如此类。
面临这种选择上的困难,有几种看似正确但实际错误的做法。
- 数量对比法简单地看哪个方案的优点多就选哪个。例如总共5个质量属性的对比其中A方案占优的有3个B方案占优的有2个所以就挑选A方案。
这种方案主要的问题在于把所有质量属性的重要性等同而没有考虑质量属性的优先级。例如对于BAT这类公司来说方案的成本都不是问题可用性和可扩展性比成本要更重要得多但对于创业公司来说成本可能就会变得很重要。
其次有时候会出现两个方案的优点数量是一样的情况。例如我们对比6个质量属性很可能出现两个方案各有3个优点这种情况下也没法选如果为了数量上的不对称强行再增加一个质量属性进行对比这个最后增加的不重要的属性反而成了影响方案选择的关键因素这又犯了没有区分质量属性的优先级的问题。
- 加权法每个质量属性给一个权重。例如性能的权重高中低分别得10分、5分、3分成本权重高中低分别是5分、3分、1分然后将每个方案的权重得分加起来最后看哪个方案的权重得分最高就选哪个。
这种方案主要的问题是无法客观地给出每个质量属性的权重得分。例如性能权重得分为何是10分、5分、3分而不是5分、3分、1分或者是100分、80分、60分这个分数是很难确定的没有明确的标准甚至会出现为了选某个方案设计师故意将某些权重分值调高而降低另外一些权重分值最后方案的选择就变成了一个数字游戏了。
正确的做法是**按优先级选择**,即架构师综合当前的业务发展情况、团队人员规模和技能、业务发展预测等因素,将质量属性按照优先级排序,首先挑选满足第一优先级的,如果方案都满足,那就再看第二优先级……以此类推。那会不会出现两个或者多个方案,每个质量属性的优缺点都一样的情况呢?理论上是可能的,但实际上是不可能的。前面我提到,在做备选方案设计时,不同的备选方案之间的差异要比较明显,差异明显的备选方案不可能所有的优缺点都是一样的。
## 评估和选择备选方案实战
再回到我们设计的场景“前浪微博”。针对上期提出的3个备选方案架构师组织了备选方案评审会议参加的人有研发、测试、运维、还有几个核心业务的主管。
1.备选方案1采用开源Kafka方案
<li>
业务主管倾向于采用Kafka方案因为Kafka已经比较成熟各个业务团队或多或少都了解过Kafka。
</li>
<li>
中间件团队部分研发人员也支持使用Kafka因为使用Kafka能节省大量的开发投入但部分人员认为Kafka可能并不适合我们的业务场景因为Kafka的设计目的是为了支撑大容量的日志消息传输而我们的消息队列是为了业务数据的可靠传输。
</li>
<li>
运维代表提出了强烈的反对意见首先Kafka是Scala语言编写的运维团队没有维护Scala语言开发的系统的经验出问题后很难快速处理其次目前运维团队已经有一套成熟的运维体系包括部署、监控、应急等使用Kafka无法融入这套体系需要单独投入运维人力。
</li>
<li>
测试代表也倾向于引入Kafka因为Kafka比较成熟无须太多测试投入。
</li>
2.备选方案2集群 + MySQL存储
<li>
中间件团队的研发人员认为这个方案比较简单但部分研发人员对于这个方案的性能持怀疑态度毕竟使用MySQL来存储消息数据性能肯定不如使用文件系统并且有的研发人员担心做这样的方案是否会影响中间件团队的技术声誉毕竟用MySQL来做消息队列看起来比较“土”、比较另类。
</li>
<li>
运维代表赞同这个方案因为这个方案可以融入到现有的运维体系中而且使用MySQL存储数据可靠性有保证运维团队也有丰富的MySQL运维经验但运维团队认为这个方案的成本比较高一个数据分组就需要4台机器2台服务器 + 2台数据库
</li>
<li>
测试代表认为这个方案测试人力投入较大,包括功能测试、性能测试、可靠性测试等都需要大量地投入人力。
</li>
<li>
业务主管对这个方案既不肯定也不否定,因为反正都不是业务团队来投入人力来开发,系统维护也是中间件团队负责,对业务团队来说,只要保证消息队列系统稳定和可靠即可。
</li>
3.备选方案3集群 + 自研存储系统
<li>
中间件团队部分研发人员认为这是一个很好的方案既能够展现中间件团队的技术实力性能上相比MySQL也要高但另外的研发人员认为这个方案复杂度太高按照目前的团队人力和技术实力要做到稳定可靠的存储系统需要耗时较长的迭代这个过程中消息队列系统可能因为存储出现严重问题例如文件损坏导致丢失大量数据。
</li>
<li>
运维代表不太赞成这个方案因为运维之前遇到过几次类似的存储系统故障导致数据丢失的问题损失惨重。例如MongoDB丢数据、Tokyo Tyrant丢数据无法恢复等。运维团队并不相信目前的中间件团队的技术实力足以支撑自己研发一个存储系统这让中间件团队的人员感觉有点不爽
</li>
<li>
测试代表赞同运维代表的意见,并且自研存储系统的测试难度也很高,投入也很大。
</li>
<li>
业务主管对自研存储系统也持保留意见因为从历史经验来看新系统上线肯定有bug而存储系统出bug是最严重的一旦出bug导致大量消息丢失对系统的影响会严重。
</li>
针对3个备选方案的讨论初步完成后架构师列出了3个方案的360度环评表
<img src="https://static001.geekbang.org/resource/image/7d/8c/7de80a7501627b02ba0288f8f725a68c.jpg" alt="" />
列出这个表格后,无法一眼看出具体哪个方案更合适,于是大家都把目光投向架构师,决策的压力现在集中在架构师身上了。
架构师经过思考后给出了最终选择备选方案2原因有
<li>
排除备选方案1的主要原因是可运维性因为再成熟的系统上线后都可能出问题如果出问题无法快速解决则无法满足业务的需求并且Kafka的主要设计目标是高性能日志传输而我们的消息队列设计的主要目标是业务消息的可靠传输。
</li>
<li>
排除备选方案3的主要原因是复杂度目前团队技术实力和人员规模总共6人还有其他中间件系统需要开发和维护无法支撑自研存储系统参考架构设计原则2简单原则
</li>
<li>
备选方案2的优点就是复杂度不高也可以很好地融入现有运维体系可靠性也有保障。
</li>
针对备选方案2的缺点架构师解释是
<li>
备选方案2的第一个缺点是性能业务目前需要的性能并不是非常高方案2能够满足即使后面性能需求增加方案2的数据分组方案也能够平行扩展进行支撑参考架构设计原则3演化原则
</li>
<li>
备选方案2的第二个缺点是成本一个分组就需要4台机器支撑目前的业务需求可能需要12台服务器但实际上备机包括服务器和数据库主要用作备份可以和其他系统并行部署在同一台机器上。
</li>
<li>
备选方案2的第三个缺点是技术上看起来并不很优越但我们的设计目的不是为了证明自己参考架构设计原则1合适原则而是更快更好地满足业务需求。
</li>
最后大家针对一些细节再次讨论后确定了选择备选方案2。
通过“前浪微博”这个案例我们可以看出备选方案的选择和很多因素相关并不单单考虑性能高低、技术是否优越这些纯技术因素。业务的需求特点、运维团队的经验、已有的技术体系、团队人员的技术水平都会影响备选方案的选择。因此同样是上述3个备选方案有的团队会选择引入Kafka例如很多创业公司的初创团队人手不够需要快速上线支撑业务有的会选择自研存储系统例如阿里开发了RocketMQ人多力量大业务复杂是主要原因
## 小结
今天我为你讲了架构设计流程的第三个步骤:评估和选择备选方案,并且基于模拟的“前浪微博”消息队列系统,给出了具体的评估和选择示例,希望对你有所帮助。
这就是今天的全部内容留一道思考题给你吧RocketMQ和Kafka有什么区别阿里为何选择了自己开发RocketMQ
欢迎你把答案写到留言区,和我一起讨论。相信经过深度思考的回答,也会让你对知识的理解更加深刻。(编辑乱入:精彩的留言有机会获得丰厚福利哦!)

View File

@@ -0,0 +1,112 @@
<audio id="audio" title="13 | 架构设计流程:详细方案设计" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/f2/1c/f2fbad0aec754675ddc25adfba16321c.mp3"></audio>
完成备选方案的设计和选择后我们终于可以长出一口气因为整个架构设计最难的一步已经完成了但整体方案尚未完成架构师还需继续努力。接下来我们需要再接再励将最终确定的备选方案进行细化使得备选方案变成一个可以落地的设计方案。所以今天我来讲讲架构设计流程第4步详细方案设计。
## 架构设计第4步详细方案设计
简单来说,详细方案设计就是将方案涉及的关键技术细节给确定下来。
<li>假如我们确定使用Elasticsearch来做全文搜索那么就需要确定Elasticsearch的索引是按照业务划分还是一个大索引就可以了副本数量是2个、3个还是4个集群节点数量是3个还是6个等。
</li>
<li>假如我们确定使用MySQL分库分表那么就需要确定哪些表要分库分表按照什么维度来分库分表分库分表后联合查询怎么处理等。
</li>
<li>假如我们确定引入Nginx来做负载均衡那么Nginx的主备怎么做Nginx的负载均衡策略用哪个权重分配轮询ip_hash等。
</li>
可以看到详细设计方案里面其实也有一些技术点和备选方案类似。例如Nginx的负载均衡策略备选有轮询、权重分配、ip_hash、fair、url_hash五个具体选哪个呢看起来和备选方案阶段面临的问题类似但实际上这里的技术方案选择是**很轻量级的**,我们无须像备选方案阶段那样操作,而只需要简单根据这些技术的适用场景选择就可以了。
例如Nginx的负载均衡策略简单按照下面的规则选择就可以了。
- 轮询(默认)
每个请求按时间顺序逐一分配到不同的后端服务器后端服务器分配的请求数基本一致如果后端服务器“down掉”能自动剔除。
- 加权轮询
根据权重来进行轮询,权重高的服务器分配的请求更多,主要适应于后端服务器性能不均的情况,如新老服务器混用。
- ip_hash
每个请求按访问IP的hash结果分配这样每个访客固定访问一个后端服务器主要用于解决session的问题如购物车类的应用。
- fair
按后端服务器的响应时间来分配请求,响应时间短的优先分配,能够最大化地平衡各后端服务器的压力,可以适用于后端服务器性能不均衡的情况,也可以防止某台后端服务器性能不足的情况下还继续接收同样多的请求从而造成雪崩效应。
- url_hash
按访问URL的hash结果来分配请求每个URL定向到同一个后端服务器适用于后端服务器能够将URL的响应结果缓存的情况。
这几个策略的适用场景区别还是比较明显的根据我们的业务需要挑选一个合适的即可。例如比如一个电商架构由于和session比较强相关因此如果用Nginx来做集群负载均衡那么选择ip_hash策略是比较合适的。
**详细设计方案阶段可能遇到的一种极端情况就是在详细设计阶段发现备选方案不可行,一般情况下主要的原因是备选方案设计时遗漏了某个关键技术点或者关键的质量属性。**例如我曾经参与过一个项目在备选方案阶段确定是可行的但在详细方案设计阶段发现由于细节点太多方案非常庞大整个项目可能要开发长达1年时间最后只得废弃原来的备选方案重新调整项目目标、计划和方案。这个项目的主要失误就是在备选方案评估时忽略了开发周期这个质量属性。
幸运的是,这种情况可以通过下面方式有效地避免:
<li>**架构师不但要进行备选方案设计和选型,还需要对备选方案的关键细节有较深入的理解。**例如架构师选择了Elasticsearch作为全文搜索解决方案前提必须是架构师自己对Elasticsearch的设计原理有深入的理解比如索引、副本、集群等技术点而不能道听途说Elasticsearch很牛所以选择它更不能成为把“细节我们不讨论”这句话挂在嘴边的“PPT架构师”。
</li>
<li>**通过分步骤、分阶段、分系统等方式,尽量降低方案复杂度**,方案本身的复杂度越高,某个细节推翻整个方案的可能性就越高,适当降低复杂性,可以减少这种风险。
</li>
<li>如果方案本身就很复杂,那就采取**设计团队**的方式来进行设计博采众长汇集大家的智慧和经验防止只有1~2个架构师可能出现的思维盲点或者经验盲区。
</li>
## 详细方案设计实战
虽然我们上期在“前浪微博”消息队列的架构设计挑选了备选方案2作为最终方案但备选方案设计阶段的方案粒度还比较粗无法真正指导开发人员进行后续的设计和开发因此需要在备选方案的基础上进一步细化。
下面我列出一些备选方案2典型的需要细化的点供参考有兴趣的同学可以自己尝试细化更多的设计点。
1.细化设计点1数据库表如何设计
<li>数据库设计两类表一类是日志表用于消息写入时快速存储到MySQL中另一类是消息表每个消息队列一张表。
</li>
<li>业务系统发布消息时,首先写入到日志表,日志表写入成功就代表消息写入成功;后台线程再从日志表中读取消息写入记录,将消息内容写入到消息表中。
</li>
<li>业务系统读取消息时,从消息表中读取。
</li>
<li>日志表表名为MQ_LOG包含的字段日志ID、发布者信息、发布时间、队列名称、消息内容。
</li>
<li>消息表表名就是队列名称包含的字段消息ID递增生成、消息内容、消息发布时间、消息发布者。
</li>
<li>日志表需要及时清除已经写入消息表的日志数据消息表最多保存30天的消息数据。
</li>
2.细化设计点2数据如何复制
直接采用MySQL主从复制即可只复制消息存储表不复制日志表。
3.细化设计点3主备服务器如何倒换
采用ZooKeeper来做主备决策主备服务器都连接到ZooKeeper建立自己的节点主服务器的路径规则为“/MQ/server/分区编号/master”备机为“/MQ/server/分区编号/slave”节点类型为EPHEMERAL。
备机监听主机的节点消息,当发现主服务器节点断连后,备服务器修改自己的状态,对外提供消息读取服务。
4.细化设计点4业务服务器如何写入消息
<li>消息队列系统设计两个角色:生产者和消费者,每个角色都有唯一的名称。
</li>
<li>消息队列系统提供SDK供各业务系统调用SDK从配置中读取所有消息队列系统的服务器信息SDK采取轮询算法发起消息写入请求给主服务器。如果某个主服务器无响应或者返回错误SDK将发起请求发送到下一台服务器。
</li>
5.细化设计点5业务服务器如何读取消息
<li>消息队列系统提供SDK供各业务系统调用SDK从配置中读取所有消息队列系统的服务器信息轮流向所有服务器发起消息读取请求。
</li>
<li>消息队列服务器需要记录每个消费者的消费状态,即当前消费者已经读取到了哪条消息,当收到消息读取请求时,返回下一条未被读取的消息给消费者。
</li>
6.细化设计点6业务服务器和消息队列服务器之间的通信协议如何设计
考虑到消息队列系统后续可能会对接多种不同编程语言编写的系统为了提升兼容性传输协议用TCP数据格式为ProtocolBuffer。
当然还有更多设计细节就不再一一列举,因此这还不是一个完整的设计方案,我希望可以通过这些具体实例来说明细化方案具体如何去做。
## 小结
今天我为你讲了架构设计流程的第四个步骤:详细方案设计,并且基于模拟的“前浪微博”消息队列系统,给出了具体的详细设计示例,希望对你有所帮助。这个示例并不完整,有兴趣的同学可以自己再详细思考一下还有哪些细节可以继续完善。
这就是今天的全部内容留一道思考题给你吧你见过“PPT架构师”么他们一般都具备什么特点
欢迎你把答案写到留言区,和我一起讨论。相信经过深度思考的回答,也会让你对知识的理解更加深刻。(编辑乱入:精彩的留言有机会获得丰厚福利哦!)