极客时间专栏《从零开始学架构》学习笔记
极客时间专栏《从零开始学架构》学习笔记
Last edited 2022-6-18
password
created time
May 25, 2022 01:00 PM
type
Post
status
Published
date
May 25, 2022
slug
summary
迈向架构师的第一步
tags
开发
知识付费
架构师
category
学习思考
icon

开篇词 | 照着做,你也能成为架构师!

摘录:
1. 架构设计的思维和程序设计的思维差异很大。 架构设计的关键思维是判断和取舍,程序设计的关键思维是逻辑和实现。很多程序员在转换为架构师后,很难一开始就意识到这个差异,还是按照写代码的方式去思考架构,会导致很多困惑。
💡
判断和取舍是什么意思呢? 也许说的是判断如何设计以及使用什么样的技术选型。
这个专栏涵盖了我的整套架构设计方法论和架构实践,主要包括以下内容。 架构基础:我会先介绍架构设计的本质、历史背景和目的,然后从复杂度来源以及架构设计的原则和流程来详细介绍架构基础。 高性能架构模式:我会从存储高性能、计算高性能方面,介绍几种设计方案的典型特征和应用场景。 高可用架构模式:我会介绍 CAP 原理、FMEA 分析方法,分析常见的高可用存储架构和高可用计算架构,并给出一些设计方法和技巧。 可扩展架构模式:我会介绍可扩展模式及其基本思想,分析一些常见架构模式。 架构实战:我会将理论和案例结合,帮助你落地前面提到的架构原则、架构流程和架构模式。
💡
架构基础、高性能、高可用、可扩展,最后是实战,结构看起来很合理,希望内容也同样精彩!

01 | 架构到底是指什么?

系统泛指由一群有关联的个体组成,根据某种规则运作,能完成个别元件不能单独完成的工作的群体。它的意思是“总体”“整体”或“联盟”。
子系统也是由一群有关联的个体所组成的系统,多半会是更大系统中的一部分。
按照这个定义,系统和子系统比较容易理解。我们以微信为例来做一个分析。
💡
感觉子系统跟模块的概念没有明显的区分度,我也可以说微信这个系统是由几个模块组成
软件模块(Module)一套一致而互相有紧密关连的软件组织。它分别包含了程序和数据结构两部分。现代软件开发往往利用模块作为合成的单位。模块的接口表达了由该模块提供的功能和调用它时所需的元素。模块是可能分开被编写的单位。这使它们可再用和允许人员同时协作、编写及研究不同的模块。
模块和组件都是系统的组成部分,只是从不同的角度拆分系统而已。
从逻辑的角度来拆分系统后,得到的单元就是“模块”;从物理的角度来拆分系统后,得到的单元就是“组件”。划分模块的主要目的是职责分离;划分组件的主要目的是单元复用。其实,“组件”的英文 component 也可翻译成中文的“零件”一词,“零件”更容易理解一些,“零件”是一个物理的概念,并且具备“独立且可替换”的特点。
假设我们要做一个学生信息管理系统,这个系统从逻辑的角度来拆分,可以分为“登录注册模块”“个人信息模块”“个人成绩模块”;从物理的角度来拆分,可以拆分为 Nginx、Web 服务器、MySQL。
💡
这样说就清楚多了,模块是从逻辑角度来拆分系统,组件是从物理角度拆分系统。
💡
我觉得模块是从业务角度来说的,而组件更关注的是可复用。
软件框架(Software framework)通常指的为了实现某个业界标准或完成特定基本任务的软件组件规范也指为了实现某个软件组件规范时,提供规范所要求之基础功能的软件产品
1. 框架是组件规范:例如,MVC 就是一种最常见的开发规范,类似的还有 MVP、MVVM、J2EE 等框架。 2. 框架提供基础功能的产品:例如,Spring MVC 是 MVC 的开发框架,除了满足 MVC 的规范,Spring 提供了很多基础功能来帮助我们实现功能,包括注解(@Controller 等)、Spring Security、Spring JPA 等很多基础功能。
💡
软件框架既可以指组件规范,也可以指提供规范所需基础功能的软件产品
软件架构指软件系统的“基础结构”,创造这些基础结构的准则,以及对这些结构的描述
框架关注的是“规范”,架构关注的是“结构”。
参考维基百科的定义,我将架构重新定义为:软件架构指软件系统的顶层结构。
💡
框架关注的是“规范”,架构关注的是“结构”。 如果把软件开发比作建设大楼的话,架构便是设计图纸,框架便是施工条例。
用人来比喻,架构是骨头,搭起整个软件。框架是大脑,约束软件应该怎么做。模块是吃喝拉撒走,负责不同的功能。组件是躯干四肢头,组成一个人。而子系统就是呼吸系统运动系统,属于软件的一部分,独立工作却又遵从指挥。
💡
评论区的比喻比我的生动多了,哈哈哈哈。

02 | 架构设计的历史背景

机器语言(1940 年之前) 机器语言的主要问题是三难:太难写、太难读、太难改
汇编语言(20 世纪 40 年代) 除了编写本身复杂,还有另外一个复杂的地方在于:不同 CPU 的汇编指令和结构是不同的。例如,Intel 的 CPU 和 Motorola 的 CPU 指令不同,同样一个程序,为 Intel 的 CPU 写一次,还要为 Motorola 的 CPU 再写一次,而且指令完全不同
高级语言(20 世纪 50 年代) 为什么称这些语言为“高级语言”呢?原因在于这些语言让程序员不需要关注机器底层的低级结构和逻辑,而只要关注具体的问题和业务即可。
第一次软件危机与结构化程序设计(20 世纪 60 年代~20 世纪 70 年代) 随着软件的规模和复杂度的大大增加,20 世纪 60 年代中期开始爆发了第一次软件危机,典型表现有软件质量低下、项目无法如期完成、项目严重超支等,因为软件而导致的重大事故时有发生。
第二次软件危机与面向对象(20 世纪 80 年代) 第二次软件危机的根本原因还是在于软件生产力远远跟不上硬件和业务的发展。第一次软件危机的根源在于软件的“逻辑”变得非常复杂,而第二次软件危机主要体现在软件的“扩展”变得非常复杂。结构化程序设计虽然能够解决(也许用“缓解”更合适)软件逻辑的复杂性,但是对于业务变化带来的软件扩展却无能为力,软件领域迫切希望找到新的银弹来解决软件危机,在这种背景下,面向对象的思想开始流行起来。
软件架构的出现有其历史必然性。20 世纪 60 年代第一次软件危机引出了“结构化编程”,创造了“模块”概念;20 世纪 80 年代第二次软件危机引出了“面向对象编程”,创造了“对象”概念;到了 20 世纪 90 年代“软件架构”开始流行,创造了“组件”概念。我们可以看到,“模块”“对象”“组件”本质上都是对达到一定规模的软件进行拆分,差别只是在于随着软件的复杂度不断增加,拆分的粒度越来越粗,拆分的层次越来越高。
在古代的狼人传说中,只有用银质子弹(银弹)才能制服这些异常凶残的怪兽。在软件开发活动中,“银弹”特指人们渴望找到用于制服软件项目这头难缠的“怪兽”的“万能钥匙”。
💡
原来银弹是这么个意思,之前一直没有仔细了解过。

03 | 架构设计的目的

架构设计的误区
不是每个系统都要做架构设计吗? 这其实是知其然不知其所以然,系统确实要做架构设计,但还是不知道为何要做架构设计,反正大家都要做架构设计,所以做架构设计肯定没错。 这样的架构师或者设计师很容易走入生搬硬套业界其他公司已有架构的歧路,美其名曰“参考”“微改进”。一旦强行引入其他公司架构后,很可能会发现架构水土不服,或者运行起来很别扭等各种情况,最后往往不得不削足适履,或者不断重构,甚至无奈推倒重来。
💡
确实需要较强的分析能力,根据眼前的情况,选用最合适的方案来解决问题
架构设计的真正目的
从周二与你分享的架构设计的历史背景,可以看到,整个软件技术发展的历史,其实就是一部与“复杂度”斗争的历史,架构的出现也不例外。简而言之,架构也是为了应对软件系统复杂度而提出的一个解决方案,通过回顾架构产生的历史背景和原因,我们可以基本推导出答案:架构设计的主要目的是为了解决软件系统复杂度带来的问题.
💡
架构设计的主要目的是解决系统复杂度带来的问题,简单来说就是要将复杂度减少到可控范围
首先,遵循这条准则能够让“新手”架构师心中有数,而不是一头雾水.
💡
说得真好,架构师不是为了设计架构而设计架构,而是为了管理系统的复杂度,解决业务问题而设计。
其次,遵循这条准则能够让“老鸟”架构师有的放矢,而不是贪大求全
💡
做架构设计不是为了装 X,最终方案也许朴实无华,但决策过程才是体现架构师价值所在的地方。为什么这样做?还有哪些可能的方案?为什么不那样做?只有掌握了足够的技术知识和对业务有足够的了解后,才能做出真正合适的选择。
架构是为了应对软件系统复杂度而提出的一个解决方案。个人感悟是:架构即(重要)决策,是在一个有约束的盒子里去求解或接近最合适的解。这个有约束的盒子是团队经验、成本、资源、进度、业务所处阶段等所编织、掺杂在一起的综合体(人,财,物,时间,事情等)。架构无优劣,但是存在恰当的架构用在合适的软件系统中,而这些就是决策的结果。
💡
说的真好

04 | 复杂度来源:高性能

软件系统中高性能带来的复杂度主要体现在两方面,一方面是单台计算机内部为了高性能带来的复杂度;另一方面是多台计算机集群为了高性能带来的复杂度
💡
高性能带来的复杂度分为单机复杂度和集群复杂度
操作系统和性能最相关的就是进程线程。最早的计算机其实是没有操作系统的,只有输入、计算和输出功能,用户输入一个指令,计算机完成操作,大部分时候计算机都在等待用户输入指令,这样的处理性能很显然是很低效的,因为人的输入速度是远远比不上计算机的运算速度的。
💡
这让我想到了费曼先生在研究原子弹期间给计算机喂卡片的情景,一堆人在屋子里给计算机喂不同的卡片,而一旦有人弄错了,一切就都得重来。
多进程多线程虽然让多任务并行处理的性能大大提升,但本质上还是分时系统,并不能做到时间上真正的并行。解决这个问题的方式显而易见,就是让多个 CPU 能够同时执行计算任务,从而实现真正意义上的多任务并行。目前这样的解决方案有 3 种:SMP(Symmetric Multi-Processor,对称多处理器结构)、NUMA(Non-Uniform Memory Access,非一致存储访问结构)、MPP(Massive Parallel Processing,海量并行处理结构)。其中 SMP 是我们最常见的,目前流行的多核处理器就是 SMP 方案。
💡
还记得上次天昊老哥分享时提到过对称多处理器结构和非一致存储访问结构的区别,但已经忘记是怎么一回事了。
我从最简单的一台服务器变两台服务器开始,来讲任务分配带来的复杂性,整体架构示意图如下。
💡
确实,从单机变多机后,集群管理的复杂度其实很高,比如负载均衡、流量调度、单实例故障处理等等
通过这种任务分解的方式,能够把原来大一统但复杂的业务系统,拆分成小而简单但需要多个系统配合的业务系统。从业务的角度来看,任务分解既不会减少功能,也不会减少代码量(事实上代码量可能还会增加,因为从代码内部调用改为通过服务器之间的接口调用),那为何通过任务分解就能够提升性能呢?
💡
于是就有了微服务。
notion image

05 | 复杂度来源:高可用

系统无中断地执行其功能的能力,代表系统的可用性程度,是进行系统设计时的准则之一。
系统的高可用方案五花八门,但万变不离其宗,本质上都是通过“冗余”来实现高可用。通俗点来讲,就是一台机器不够就两台,两台不够就四台;一个机房可能断电,那就部署两个机房;一条通道可能故障,那就用两条,两条不够那就用三条(移动、电信、联通一起上)。高可用的“冗余”解决方案,单纯从形式上来看,和之前讲的高性能是一样的,都是通过增加更多机器来达到目的,但其实本质上是有根本区别的:高性能增加机器目的在于“扩展”处理性能;高可用增加机器目的在于“冗余”处理单元.
💡
高性能靠扩展,高可用靠冗余。
我们再看一个复杂一点的高可用集群架构。
💡
为实现高可用,通常会使用主备服务器来进行冗余,不同主备方案各有优劣,需要根据实际情况进行选择
notion image
存储高可用的难点不在于如何备份数据,而在于如何减少或者规避数据不一致对业务造成的影响.
💡
这一点也是在架构选型中需要考虑的。
高可用状态决策
💡
高可用状态决策的三种方案: 1. 独裁式:优点:管理和决策简单,缺点:容易出现单点故障。 2. 协商式:优点:可以部分解决单点故障,缺点:复杂度较高,不同异常情况下的考虑需要兼备。 3. 民主式:优点:能很好的解决单点故障,缺点:算法十分复杂,且存在脑裂风险。
高可用的解决方法不是解决,而是减少或者规避,而规避某个问题的时候,一般都会引发另一个问题,只是这个问题比之前的小,高可用的设计过程其实也是一个取舍的过程。这也就是为什么系统可用性永远只是说几个九,永远缺少那个一。
💡
现在似乎有点明白开篇中所说的取舍的含义了。

06 | 复杂度来源:可扩展性

设计具备良好可扩展性的系统,有两个基本条件:正确预测变化完美封装变化.
对于架构师来说,如何把握预测的程度和提升预测结果的准确性,是一件很复杂的事情,而且没有通用的标准可以简单套上去,更多是靠自己的经验、直觉,所以架构设计评审的时候经常会出现两个设计师对某个判断争得面红耳赤的情况,原因就在于没有明确标准,不同的人理解和判断有偏差,而最终又只能选择一个判断。
💡
确实,在做设计的时候,如果完全不考虑可扩展性,后续新需求的成本将会越来越高,而具备可扩展性的系统,则能很好的承接新的变动。
第一种应对变化的常见方案是将“变化”封装在一个“变化层”,将不变的部分封装在一个独立的“稳定层”
💡
将通用的逻辑和流程提炼出来,组成业务框架,不知道这叫不叫稳定层。
第二种常见的应对变化的方案是提炼出一个“抽象层”和一个“实现层”。抽象层是稳定的,实现层可以根据具体业务需要定制开发,当加入新的功能时,只需要增加新的实现,无须修改抽象层。这种方案典型的实践就是设计模式和规则引擎。考虑到绝大部分技术人员对设计模式都非常熟悉,我以设计模式为例来说明这种方案的复杂性。
notion image
💡
没错,就是这样

07 | 复杂度来源:低成本、安全、规模

低成本给架构设计带来的主要复杂度体现在,往往只有“创新”才能达到低成本目标
无论是引入新技术,还是自己创造新技术,都是一件复杂的事情。引入新技术的主要复杂度在于需要去熟悉新技术,并且将新技术与已有技术结合起来;创造新技术的主要复杂度在于需要自己去创造全新的理念和技术,并且新技术跟旧技术相比,需要有质的飞跃。
💡
如果不考虑成本,架构设计就简单多了,但这是不现实的。
相比来说,创造新技术复杂度更高,因此一般中小公司基本都是靠引入新技术来达到低成本的目标;而大公司更有可能自己去创造新的技术来达到低成本的目标,因为大公司才有足够的资源、技术和时间去创造新技术。
💡
但大公司喜欢创造技术也带来了一些负面效应,大家都喜欢自己造轮子,可却很少有长期维护的轮子,导致常见的情况是大部分造出来的轮子生命力只有一两年就结束了,主要技术负责人离职几乎就意味着这个技术的流产。
从技术的角度来讲,安全可以分为两类:一类是功能上的安全,一类是架构上的安全。
💡
防小偷与防强盗,这个比喻真是妙极了。
很多企业级的系统,既没有高性能要求,也没有双中心高可用要求,也不需要什么扩展性,但往往我们一说到这样的系统,很多人都会脱口而出:这个系统好复杂!为什么这样说呢?关键就在于这样的系统往往功能特别多,逻辑分支特别多。特别是有的系统,发展时间比较长,不断地往上面叠加功能,后来的人由于不熟悉整个发展历史,可能连很多功能的应用场景都不清楚,或者细节根本无法掌握,面对的就是一个黑盒系统,看不懂、改不动、不敢改、修不了,复杂度自然就感觉很高了。
💡
确实如此,前人留下的历史代码往往包含了大量的业务逻辑信息,而这隐藏在糟糕的代码设计之中,很难直接通过阅读代码来理解,如果相关的负责人离职了,那就基本上无法维护,只能尽量不动它或者绕过它。
常见的规模带来的复杂度有:
💡
深感赞同,功能的增加确实会导致复杂程度大幅增加,特别是在糟糕的模块分层和业务架构下,表现就更加离谱了。另外数据量级发生变化时,往往意味着需要重构或者引入新的复杂度来承接流量。

08 | 架构设计三原则

合适原则宣言:“合适优于业界领先”。
💡
技术人员通常会有在技术上追求更快更新更好的执念
简单原则宣言:“简单优于复杂”。
💡
简单带来的好处是显而易见的,易于理解,易于管理和维护
演化原则宣言:“演化优于一步到位”。
💡
一步到位是一种不切实际的幻想,业务总是处于变化之中
如果没有把握“软件架构需要根据业务发展不断变化”这个本质,在做架构设计的时候就很容易陷入一个误区:试图一步到位设计一个软件架构,期望不管业务如何变化,架构都稳如磐石。
考虑到软件架构需要根据业务发展不断变化这个本质特点,软件架构设计其实更加类似于大自然“设计”一个生物,通过演化让生物适应环境,逐步变得更加强大:
💡
架构设计类似于大自然设计一个生物,真是一个有趣的比喻
💡
优胜劣汰,适者生存,不适者会灭亡,但糟糕的架构设计却会遗臭万年。
架构师在进行架构设计时需要牢记这个原则,时刻提醒自己不要贪大求全,或者盲目照搬大公司的做法。应该认真分析当前业务的特点,明确业务面临的主要问题,设计合理的架构,快速落地以满足业务需要,然后在运行过程中不断完善架构,不断随着业务演化架构。
架构即决策。架构需要面向业务需求,并在各种资源(人、财、物、时、事)约束条件下去做权衡、取舍。而决策就会存在不确定性。采用一些高屋建瓴的设计原则有助于去消除不确定,去逼近解决问题的最优解。

09 | 架构设计原则案例

2003 年 4 月 7 日马云提出成立淘宝,2003 年 5 月 10 日淘宝就上线了,中间只有 1 个月,怎么办?淘宝的答案就是:买一个。
估计大部分人很难想象如今技术牛气冲天的阿里最初的淘宝竟然是买来的,我们看看当初决策的依据:
当时对整个项目组来说压力最大的就是时间,怎么在最短的时间内把一个从来就没有的网站从零开始建立起来?了解淘宝历史的人知道淘宝是在 2003 年 5 月 10 日上线的,这之间只有一个月。要是你在这个团队里,你怎么做?我们的答案就是:买一个来。
💡
哈哈,确实没想到,原来淘宝一开始是买来的网站
第一代的技术架构如图所示。
notion image
换上 Oracle 和昂贵的存储后,第二代架构如图所示。
notion image
从 PHP 改为 Java 后,第三代技术架构如图所示。
notion image
Java 架构经过各种优化,第四代技术架构如图所示。
notion image
💡
真是让人印象深刻,淘宝的架构演进之路确实有很多可以借鉴的地方,业务之初最重要的是快速落地和验证,所以通过买入现有的成熟方案是最稳妥的,业务快速发展之时,也可以通过堆设备和堆人力来解决,直到业务成熟和稳定,技术成为了发展瓶颈时,再着手进行大刀阔斧的改革。

10 | 架构设计流程:识别复杂度

架构设计的本质目的是为了解决软件系统的复杂性,所以在我们设计架构时,首先就要分析系统的复杂性。只有正确分析出了系统的复杂性,后续的架构设计方案才不会偏离方向;否则,如果对系统的复杂性判断错误,即使后续的架构设计方案再完美再先进,都是南辕北辙,做的越好,错的越多、越离谱。
💡
架构设计的本质是管理复杂度,在一定约束条件下,设计出满足需求的方案。
架构的复杂度主要来源于“高性能”“高可用”“可扩展”等几个方面,但架构师在具体判断复杂性的时候,不能生搬硬套,认为任何时候架构都必须同时满足这三方面的要求。实际上大部分场景下,复杂度只是其中的某一个,少数情况下包含其中两个,如果真的出现同时需要解决三个或者三个以上的复杂度,要么说明这个系统之前设计的有问题,要么可能就是架构师的判断出现了失误,即使真的认为要同时满足这三方面的要求,也必须要进行优先级排序。
💡
架构师要做的是判断和取舍。
如果运气真的不好,接手了一个每个复杂度都存在问题的系统,那应该怎么办呢?答案是一个个来解决问题,不要幻想一次架构重构解决所有问题。
💡
确实,不能妄想一次重构就解决所有问题,弄清楚真正的问题所在,逐步优化才是王道。之前接手过一个遗留系统,也是在开发过程中逐步重构和完善后,才让它重新焕发生机。后面工作重心有调整,系统交接给了别人,交接过程甚至都不需要过多讲解,依靠代码和文档就能很好理解。
正确的做法是将主要的复杂度问题列出来,然后根据业务、技术、团队等综合情况进行排序,优先解决当前面临的最主要的复杂度问题。“亿级用户平台”这个案例,团队就优先选择将子系统的数量降下来,后来发现子系统数量降下来后,不但开发效率提升了,原来经常发生的小问题也基本消失了,于是团队再在这个基础上做了异地多活方案,也取得了非常好的效果。
💡
先找到主要问题,然后一个个解决。

11 | 架构设计流程:设计备选方案

架构师的工作并不神秘,成熟的架构师需要对已经存在的技术非常熟悉,对已经经过验证的架构模式烂熟于心,然后根据自己对业务的理解,挑选合适的架构模式进行组合,再对组合后的方案进行修改和调整。
💡
需要对现有技术十分熟悉,所以技术储备还是很有必要的。
新技术都是在现有技术的基础上发展起来的,现有技术又来源于先前的技术。将技术进行功能性分组,可以大大简化设计过程,这是技术“模块化”的首要原因。技术的“组合”和“递归”特征,将彻底改变我们对技术本质的认识。
💡
将技术进行功能性分组,确实有道理,但具体怎么做呢?
第一种常见的错误:设计最优秀的方案。
💡
相比优秀,合适和简单更重要。
第二种常见的错误:只做一个方案。
💡
架构设计最好是能有多套方案,毕竟个人的视角是有限的,有自己的盲区和犯错的可能性。 其实平时的技术设计也是如此,尝试从多个角度来设计方案,然后跟大家一起讨论,才能得到真正的成长,不然就会只是在自己的舒适圈里,对自己本就熟悉的技术和模式更加熟练而已。
架构师也不可能穷举所有方案,那合理的做法应该是什么样的呢?
💡
数量以3~5个为最佳 差异要明显 备选方案不能只局限于已熟悉的技术
第三种常见的错误:备选方案过于详细。
💡
备选方案应该关注技术选型而不是技术细节。

12 | 架构设计流程:评估和选择备选方案

前面提到了那么多指导思想,真正应该选择哪种方法来评估和选择备选方案呢?我的答案就是“360 度环评”!具体的操作方式为:列出我们需要关注的质量属性点,然后分别从这些质量属性的维度去评估每个方案,再综合挑选适合当时情况的最优方案.
💡
哈哈哈哈,让我想到了公司的360评估。
正确的做法是按优先级选择,即架构师综合当前的业务发展情况、团队人员规模和技能、业务发展预测等因素,将质量属性按照优先级排序,首先挑选满足第一优先级的,如果方案都满足,那就再看第二优先级……以此类推。那会不会出现两个或者多个方案,每个质量属性的优缺点都一样的情况呢?理论上是可能的,但实际上是不可能的。前面我提到,在做备选方案设计时,不同的备选方案之间的差异要比较明显,差异明显的备选方案不可能所有的优缺点都是一样的。
💡
按优先级选择,所以排列优先级就显得很重要了。

13 | 架构设计流程:详细方案设计

简单来说,详细方案设计就是将方案涉及的关键技术细节给确定下来。
💡
但这些决策也需要大量的技术储备和经验做支持,平时在做技术决策和看别人的技术方案时,也需要多加关注。
Nginx 的负载均衡策略,简单按照下面的规则选择就可以了。
💡
不同负载均衡方案适用于不同场景,这是需要熟知的。
详细设计方案阶段可能遇到的一种极端情况就是在详细设计阶段发现备选方案不可行,一般情况下主要的原因是备选方案设计时遗漏了某个关键技术点或者关键的质量属性。
💡
架构师的知识储备和经验确实是决定架构好坏的关键因素。

14 | 高性能数据库集群:读写分离

读写分离的基本原理是将数据库读写操作分散到不同的节点上,下面是其基本架构图。
读写分离的实现逻辑并不复杂,但有两个细节点将引入设计复杂度:主从复制延迟分配机制.
💡
我们现在用的就是主从复制,读写分离的方案,一个主库下面挂两个从库。
以 MySQL 为例,主从复制延迟可能达到 1 秒,如果有大量数据同步,延迟 1 分钟也是有可能的。主从复制延迟会带来一个问题:如果业务服务器将数据写入到数据库主服务器后立刻(1 秒内)进行读取,此时读操作访问的是从机,主机还没有将数据复制过来,到从机读取数据是读不到最新数据的,业务上就可能出现问题。例如,用户刚注册完后立刻登录,业务服务器会提示他“你还没有注册”,而用户明明刚才已经注册成功了。
解决主从复制延迟有几种常见的方法:
💡
我们的解决方案是特定场景指定主库进行读写。
将读写操作区分开来,然后访问不同的数据库服务器,一般有两种方式:程序代码封装中间件封装
程序代码封装的方式具备几个特点:
中间件封装指的是独立一套系统出来,实现读写操作分离和数据库服务器连接的管理。中间件对业务服务器提供 SQL 兼容的协议,业务服务器无须自己进行读写分离。对于业务服务器来说,访问中间件和访问数据库没有区别,事实上在业务服务器看来,中间件就是一个数据库服务器。其基本架构是:
💡
我们的方案是在代码上进行读写分离,使用时每个库都会对应一个 Reader 和 Writer ,DAL 层也会进行 Reader 和 Writer 的抽象。

15 | 高性能数据库集群:分库分表

读写分离分散了数据库读写操作的压力,但没有分散存储压力,当数据量达到千万甚至上亿条的时候,单台数据库服务器的存储能力会成为系统的瓶颈,主要体现在这几个方面:
💡
读写分离分散的是读写操作的压力,分库分表分散的是存储压力,一个是对外,一个是对内。
业务分库指的是按照业务模块将数据分散到不同的数据库服务器。
虽然业务分库能够分散存储和访问压力,但同时也带来了新的问题,接下来我进行详细分析。
💡
确实,初创业务还是先不要考虑分库分表了,等业务得到验证之后,再逐步演化也不迟。
💡
目前我们是按模块进行微服务和人员划分,比如用户组就会负责用户相关的微服务设计与维护,也有单独的库。
单表数据拆分有两种方式:垂直分表水平分表。示意图如下:
💡
画个图确实形象许多。
分表能够有效地分散存储压力和带来性能提升,但和分库一样,也会引入各种复杂性。
💡
垂直分表适合将不常用但占用大量空间的字段拆分出去,水平分表则适合将行数特别大的表进行拆分。
水平分表相比垂直分表,会引入更多的复杂性,主要表现在下面几个方面:
💡
水平分表真的太复杂了,在之前公司的时候,在记录订单表时使用过分表,带来的复杂度真的太高了。
💡
所以轻易不要进行分表,先通过硬件升级、SQL优化、缓存等方式来优化现有业务,然后再是垂直分表,水平分表是下下之选。

16 | 高性能NoSQL

关系数据库存在如下缺点。
💡
关系型数据库的缺点也正式非关系型数据库的优点,或者说非关系型数据库就是为了解决关系型数据库的缺点而产生的。
针对上述问题,分别诞生了不同的 NoSQL 解决方案,这些方案与关系数据库相比,在某些应用场景下表现更好。但世上没有免费的午餐,NoSQL 方案带来的优势,本质上是牺牲 ACID 中的某个或者某几个特性,因此我们不能盲目地迷信 NoSQL 是银弹,而应该将 NoSQL 作为 SQL 的一个有力补充,NoSQL != No SQL,而是 NoSQL = Not Only SQL。
💡
永远没有银弹。
常见的 NoSQL 方案分为 4 类。
Redis 是 K-V 存储的典型代表,它是一款开源(基于 BSD 许可)的高性能 K-V 缓存和存储系统。Redis 的 Value 是具体的数据结构,包括 string、hash、list、set、sorted set、bitmap 和 hyperloglog,所以常常被称为数据结构服务器。
Redis 的缺点主要体现在并不支持完整的 ACID 事务,Redis 虽然提供事务功能,但 Redis 的事务和关系数据库的事务不可同日而语,Redis 的事务只能保证隔离性和一致性(I 和 C),无法保证原子性和持久性(A 和 D)。
虽然 Redis 并没有严格遵循 ACID 原则,但实际上大部分业务也不需要严格遵循 ACID 原则。以上面的微博关注操作为例,即使系统没有将 A 加入 B 的粉丝列表,其实业务影响也非常小,因此我们在设计方案时,需要根据业务特性和要求来确定是否可以用 Redis,而不能因为 Redis 不遵循 ACID 原则就直接放弃。
文档数据库的 no-schema 特性,给业务开发带来了几个明显的优势。
顾名思义,列式数据库就是按照列来存储数据的数据库,与之对应的传统关系数据库被称为“行式数据库”,因为关系数据库是按照行来存储数据的。
全文搜索基本原理
💡
KV数据库、文档数据库、列数据库、全文搜索数据库,不同的类型有着不同的优缺点,可以与关系型数据库互为补充。
💡
目前来说,我们一般使用Redis做缓存和计数,使用Hbase存储大数据量的内容,ES则分为在线和离线场景,在线的场景有搜索团队管理,离线场景主要为CMS服务,MongoDB用的比较少。

17 | 高性能缓存架构

虽然我们可以通过各种手段来提升存储系统的性能,但在某些复杂的业务场景下,单纯依靠存储系统的性能提升不够的,典型的场景有:
缓存就是为了弥补存储系统在这些复杂业务场景下的不足,其基本原理是将可能重复使用的数据放到内存中,一次生成、多次使用,避免每次使用都去访问存储系统。
缓存穿透是指缓存没有发挥作用,业务系统虽然去缓存查询数据,但缓存中没有数据,业务系统需要再次去存储系统查询数据。通常情况下有两种情况: 1. 存储数据不存在 这种情况的解决办法比较简单,如果查询存储系统的数据没有找到,则直接设置一个默认值(可以是空值,也可以是具体的值)存到缓存中,这样第二次读取缓存时就会获取到默认值,而不会继续访问存储系统。 2. 缓存数据生成耗费大量时间或者资源 典型的就是电商的商品分页,假设我们在某个电商平台上选择“手机”这个类别查看,由于数据巨大,不能把所有数据都缓存起来,只能按照分页来进行缓存,由于难以预测用户到底会访问哪些分页,因此业务上最简单的就是每次点击分页的时候按分页计算和生成缓存。通常情况下这样实现是基本满足要求的,但是如果被竞争对手用爬虫来遍历的时候,系统性能就可能出现问题。 这种情况并没有太好的解决方案,因为爬虫会遍历所有的数据,而且什么时候来爬取也是不确定的,可能是每天都来,也可能是每周,也可能是一个月来一次,我们也不可能为了应对爬虫而将所有数据永久缓存。通常的应对方案要么就是识别爬虫然后禁止访问,但这可能会影响 SEO 和推广;要么就是做好监控,发现问题后及时处理,因为爬虫不是攻击,不会进行暴力破坏,对系统的影响是逐步的,监控发现问题后有时间进行处理。
💡
我们对于缓存穿透的处理也是设置空值
缓存雪崩是指当缓存失效(过期)后引起系统性能急剧下降的情况。当缓存过期被清除后,业务系统需要重新生成缓存,因此需要再次访问存储系统,再次进行运算,这个处理步骤耗时几十毫秒甚至上百毫秒。而对于一个高并发的业务系统来说,几百毫秒内可能会接到几百上千个请求。由于旧的缓存已经被清除,新的缓存还未生成,并且处理这些请求的线程都不知道另外有一个线程正在生成缓存,因此所有的请求都会去重新生成缓存,都会去访问存储系统,从而对存储系统造成巨大的性能压力。这些压力又会拖慢整个系统,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃。 缓存雪崩的常见解决方法有两种:更新锁机制后台更新机制
💡
我们的处理方案是后台定时更新缓存,比较简单粗暴
缓存热点的解决方案就是复制多份缓存副本,将请求分散到多个缓存服务器上,减轻缓存热点导致的单台缓存服务器压力以微博为例,对于粉丝数超过 100 万的明星,每条微博都可以生成 100 份缓存,缓存的数据是一样的,通过在缓存的 key 里面加上编号进行区分,每次读缓存时都随机读取其中某份缓存。 缓存副本设计有一个细节需要注意,就是不同的缓存副本不要设置统一的过期时间,否则就会出现所有缓存副本同时生成同时失效的情况,从而引发缓存雪崩效应。正确的做法是设定一个过期时间范围,不同的缓存副本的过期时间是指定范围内的随机值。
💡
目前我们还没有遇到这样的问题。 过期时间通常是固定值+一个小范围的随机数。
💡
Redis缓存需要注意的三个要点: 1. 缓存穿透 2. 缓存雪崩 3. 热点数据
💡
缓存穿透的解决办法是为不存在的key设置默认值。
💡
缓存雪崩的解决办法有: 1. 加更新锁,保证只有一个或少数几个线程会更新锁 2. 后台更新,通过后台线程不断把新数据刷进去
💡
热点数据的解决办法是复制多份缓存副本,分散读缓存数据的压力

18 | 单服务器高性能模式:PPC与TPC

单服务器高性能的关键之一就是服务器采取的并发模型,并发模型有如下两个关键设计点:
PPC 是 Process Per Connection 的缩写,其含义是指每次有新的连接就新建一个进程去专门处理这个连接的请求,这是传统的 UNIX 网络服务器所采用的模型。基本的流程图是:
PPC 的缺点:
💡
PPC 即每个请求都 fork 一个子进程来处理,实现模式比较简单,但只适用于并发量很小的系统。
prefork 就是提前创建进程(pre-fork)。系统在启动的时候就预先创建好进程,然后才开始接受用户的请求,当有新的连接进来的时候,就可以省去 fork 进程的操作,让用户访问更快、体验更好。prefork 的基本示意图是:
notion image
💡
prefork 可以解决 fork 进程慢导致的用户访问速度下降问题,但其它PPC的问题仍旧存在。
TPC 是 Thread Per Connection 的缩写,其含义是指每次有新的连接就新建一个线程去专门处理这个连接的请求。与进程相比,线程更轻量级,创建线程的消耗比进程要少得多;同时多线程是共享进程内存空间的,线程通信相比进程通信更简单。因此,TPC 实际上是解决或者弱化了 PPC fork 代价高的问题和父子进程通信复杂的问题。
TPC 虽然解决了 fork 代价高和进程通信复杂的问题,但是也引入了新的问题,具体表现在:
💡
多线程方案解决了 fork 子进程的成本问题,但也引入了新的复杂度,如线程间通信和资源占用于共享。
和 prefork 类似,prethread 模式会预先创建线程,然后才开始接受用户的请求,当有新的连接进来的时候,就可以省去创建线程的操作,让用户感觉更快、体验更好。
Apache 服务器的 MPM worker 模式本质上就是一种 prethread 方案,但稍微做了改进。Apache 服务器会首先创建多个进程,每个进程里面再创建多个线程,这样做主要是为了考虑稳定性,即:即使某个子进程里面的某个线程异常导致整个子进程退出,还会有其他子进程继续提供服务,不会导致整个服务器全部挂掉。
💡
prethread 方案解决了高并发时创建线程的延时问题
高并发需要根据两个条件划分:连接数量,请求数量。

19 | 单服务器高性能模式:Reactor与Proactor

PPC 模式最主要的问题就是每个连接都要创建进程(为了描述简洁,这里只以 PPC 和进程为例,实际上换成 TPC 和线程,原理是一样的),连接结束后进程就销毁了,这样做其实是很大的浪费。为了解决这个问题,一个自然而然的想法就是资源复用,即不再单独为每个连接创建进程,而是创建一个进程池,将连接分配给进程,一个进程可以处理多个连接的业务。
💡
PPC 和 TPC 方案的主要问题是资源复用问题,可以使用资源池的方式进行优化,但同时会引入新的问题,在高并发连接场景下,如何高效处理。于是就需要对场景做进一步的拆解,然后对 read write 操作进行优化。
为了能够更好地解决上述问题,很容易可以想到,只有当连接上有数据的时候进程才去处理,这就是 I/O 多路复用技术的来源。
💡
相当于中间多了一个接线员,一个接线员可以同时负责监听多个电话,有消息的时候再转接给具体的后台线程或进程。
I/O 多路复用结合线程池,完美地解决了 PPC 和 TPC 的问题,而且“大神们”给它取了一个很牛的名字:Reactor,中文是“反应堆”。联想到“核反应堆”,听起来就很吓人,实际上这里的“反应”不是聚变、裂变反应的意思,而是“事件反应”的意思,可以通俗地理解为“来了一个事件我就有相应的反应”,这里的“我”就是 Reactor,具体的反应就是我们写的代码,Reactor 会根据事件类型来调用相应的代码进行处理。Reactor 模式也叫 Dispatcher 模式(在很多开源的系统里面会看到这个名称的类,其实就是实现 Reactor 模式的),更加贴近模式本身的含义,即 I/O 多路复用统一监听事件,收到事件后分配(Dispatch)给某个进程。
💡
线程池解决的是线程需要不断创建和销毁的开销问题,I/O多路复用解决的是阻塞式读写操作的低效问题。
初看 Reactor 的实现是比较简单的,但实际上结合不同的业务场景,Reactor 模式的具体实现方案灵活多变,主要体现在:
💡
真是可以不断的往下挖,不断的优化。
1. 单 Reactor 单进程 / 线程
💡
单 Reactor 单进程/线程的方式主要适用于业务处理非常快速的场景,如:Redis
2. 单 Reactor 多线程
💡
相当于将业务处理这部分单独抽出来让子线程来完成,让可通用的handler承担更少的职责。
单 Reator 多线程方案能够充分利用多核多 CPU 的处理能力,但同时也存在下面的问题:
💡
单 Reactor 单进程方案的瓶颈在于: 1. 只有单个 Reactor 2. 只有单个handler来处理业务逻辑 这两个地方都可能造成性能瓶颈。 单 Reactor 多线程方案解决了第二个问题,但第一个问题仍旧存在。
你可能会发现,我只列出了“单 Reactor 多线程”方案,没有列出“单 Reactor 多进程”方案,这是什么原因呢?主要原因在于如果采用多进程,子进程完成业务处理后,将结果返回给父进程,并通知父进程发送给哪个 client,这是很麻烦的事情。因为父进程只是通过 Reactor 监听各个连接上的事件然后进行分配,子进程与父进程通信时并不是一个连接。如果要将父进程和子进程之间的通信模拟为一个连接,并加入 Reactor 进行监听,则是比较复杂的。而采用多线程时,因为多线程是共享数据的,因此线程间通信是非常方便的。虽然要额外考虑线程间共享数据时的同步问题,但这个复杂度比进程间通信的复杂度要低很多。
💡
单 Reactor 多进程方案十分复杂,可能得不偿失。
3. 多 Reactor 多进程 / 线程
💡
多 Reactor 多进程/线程方案,看起来就像是在接线员中增加了一个主接线员的角色,主接线员要做的事情就是把接收到的事件转发给其它接线员。
目前著名的开源系统 Nginx 采用的是多 Reactor 多进程,采用多 Reactor 多线程的实现有 Memcache 和 Netty
💡
这个得记下来
Reactor 是非阻塞同步网络模型,因为真正的 read 和 send 操作都需要用户进程同步操作这里的“同步”指用户进程在执行 read 和 send 这类 I/O 操作的时候是同步的,如果把 I/O 操作改为异步就能够进一步提升性能,这就是异步网络模型 Proactor
💡
继续拆解,找出性能瓶颈,然后进行优化。
Proactor 中文翻译为“前摄器”比较难理解,与其类似的单词是 proactive,含义为“主动的”,因此我们照猫画虎翻译为“主动器”反而更好理解。Reactor 可以理解为“来了事件我通知你,你来处理”,而 Proactor 可以理解为“来了事件我来处理,处理完了我通知你。这里的“我”就是操作系统内核,“事件”就是有新连接、有数据可读、有数据可写的这些 I/O 事件,“你”就是我们的程序代码。
💡
Proactor 相当于把所有的 I/O 操作单独抽出来交给 Asynchronous 处理,这样就能进一步减少handler的处理时长。
💡
看完这篇感觉收获颇发,知其然还需要知其所以然,不能仅仅知道几个名词就完事了,要知道它产生的背景、原理和适用的场景,才能真正掌握它。

20 | 高性能负载均衡:分类及架构

高性能集群的复杂性主要体现在需要增加一个任务分配器,以及为任务选择一个合适的任务分配算法。对于任务分配器,现在更流行的通用叫法是“负载均衡器”。
负载均衡不只是为了计算单元的负载达到均衡状态
负载均衡分类:常见的负载均衡系统包括 3 种:DNS 负载均衡硬件负载均衡软件负载均衡
DNS 是最简单也是最常见的负载均衡方式,一般用来实现地理级别的均衡。例如,北方的用户访问北京的机房,南方的用户访问深圳的机房。DNS 负载均衡的本质是 DNS 解析同一个域名可以返回不同的 IP 地址。例如,同样是 www.baidu.com,北方用户解析后获取的地址是 61.135.165.224(这是北京机房的 IP),南方用户解析后获取的地址是 14.215.177.38(这是深圳机房的 IP)。
硬件负载均衡是通过单独的硬件设备来实现负载均衡功能,这类设备和路由器、交换机类似,可以理解为一个用于负载均衡的基础网络设备。目前业界典型的硬件负载均衡设备有两款:F5 和 A10。这类设备性能强劲、功能强大,但价格都不便宜,一般只有“土豪”公司才会考虑使用此类设备。普通业务量级的公司一是负担不起,二是业务量没那么大,用这些设备也是浪费。
软件负载均衡通过负载均衡软件来实现负载均衡功能,常见的有 Nginx 和 LVS,其中 Nginx 是软件的 7 层负载均衡,LVS 是 Linux 内核的 4 层负载均衡4 层和 7 层的区别就在于协议灵活性,Nginx 支持 HTTP、E-mail 协议;而 LVS 是 4 层负载均衡,和协议无关,几乎所有应用都可以做,例如,聊天、数据库等
软件和硬件的最主要区别就在于性能,硬件负载均衡性能远远高于软件负载均衡性能。Ngxin 的性能是万级,一般的 Linux 服务器上装一个 Nginx 大概能到 5 万 / 秒;LVS 的性能是十万级,据说可达到 80 万 / 秒;而 F5 性能是百万级,从 200 万 / 秒到 800 万 / 秒都有(数据来源网络,仅供参考,如需采用请根据实际业务场景进行性能测试)。当然,软件负载均衡的最大优势是便宜,一台普通的 Linux 服务器批发价大概就是 1 万元左右,相比 F5 的价格,那就是自行车和宝马的区别了。
软件负载均衡的优点:
前面我们介绍了 3 种常见的负载均衡机制:DNS 负载均衡、硬件负载均衡、软件负载均衡,每种方式都有一些优缺点,但并不意味着在实际应用中只能基于它们的优缺点进行非此即彼的选择,反而是基于它们的优缺点进行组合使用。具体来说,组合的基本原则为:DNS 负载均衡用于实现地理级别的负载均衡;硬件负载均衡用于实现集群级别的负载均衡;软件负载均衡用于实现机器级别的负载均衡
💡
单机性能优化总有极限,这个时候就需要集群来实现多机性能的扩展,但引入了一个新的复杂度,那就是如何对请求流量进行调度。 通常的解决方案有三个:DNS、硬件负载均衡、软件负载均衡。 DNS 适用于地理级别的负载均衡。 硬件负载均衡可以承接上百万的并发,适用于同机房内多集群的负载均衡。 软件负载均衡简单便宜灵活,适用于同集群内的负载均衡。

21 | 高性能负载均衡:算法

负载均衡算法数量较多,而且可以根据一些业务特性进行定制开发,抛开细节上的差异,根据算法期望达到的目的,大体上可以分为下面几类。
轮询:负载均衡系统收到请求后,按照顺序轮流分配到服务器上。 轮询是最简单的一个策略,无须关注服务器本身的状态. 只要服务器在运行,运行状态是不关注的。但如果服务器直接宕机了,或者服务器和负载均衡系统断连了,这时负载均衡系统是能够感知的,也需要做出相应的处理。
加权轮询:负载均衡系统根据服务器权重进行任务分配,这里的权重一般是根据硬件配置进行静态配置的,采用动态的方式计算会更加契合业务,但复杂度也会更高。 加权轮询是轮询的一种特殊形式,其主要目的就是为了解决不同服务器处理能力有差异的问题例如,集群中有新的机器是 32 核的,老的机器是 16 核的,那么理论上我们可以假设新机器的处理能力是老机器的 2 倍,负载均衡系统就可以按照 2:1 的比例分配更多的任务给新机器,从而充分利用新机器的性能。 加权轮询解决了轮询算法中无法根据服务器的配置差异进行任务分配的问题,但同样存在无法根据服务器的状态差异进行任务分配的问题。
负载最低优先:负载均衡系统将任务分配给当前负载最低的服务器这里的负载根据不同的任务类型和业务场景,可以用不同的指标来衡量。例如:
💡
负载均衡算法可以分为四类:任务平分、负载均衡、性能最优、hash。
💡
任务平分主要有简单轮询和加权轮询,特点是简单高效。缺点是不感知服务器状态,不能根据服务器当前情况进行调整。
💡
负载均衡主要根据系统负载情况来调节服务器请求分配情况,而负载的指标则可以根据业务情况来进行定制化处理。如:LVS可以根据服务器连接数指标处理,Nginx可以根据HTTP请求数进行处理。
  • 开发
  • 知识付费
  • 架构师
  • 【区块链入门】什么是区块链《费曼学习法7天特战营》学习笔记