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,118 @@
<audio id="audio" title="01 | 高并发系统:它的通用设计方法是什么?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7a/00/7a70665cb5e21dc0efcc243c5bb58100.mp3"></audio>
我们知道,高并发代表着大流量,高并发系统设计的魅力就在于我们能够凭借自己的聪明才智设计巧妙的方案,从而抵抗巨大流量的冲击,带给用户更好的使用体验。这些方案好似能操纵流量,让流量更加平稳地被系统中的服务和组件处理。
来做个简单的比喻吧。
从古至今,长江和黄河流域水患不断,远古时期大禹曾拓宽河道,清除淤沙让流水更加顺畅;都江堰作为史上最成功的治水案例之一,用引流将岷江之水分流到多个支流中,以分担水流压力;三门峡和葛洲坝通过建造水库将水引入水库先存储起来,然后再想办法把水库中的水缓缓地排出去,以此提高下游的抗洪能力。
而我们在应对高并发大流量时也会采用类似“抵御洪水”的方案,归纳起来共有三种方法。
<li>
Scale-out横向扩展分而治之是一种常见的高并发系统设计方法采用分布式部署的方式把流量分流开让每个服务器都承担一部分并发和流量。
</li>
<li>
缓存:使用缓存来提高系统的性能,就好比用“拓宽河道”的方式抵抗高并发大流量的冲击。
</li>
<li>
异步:在某些场景下,未处理完成之前我们可以让请求先返回,在数据准备好之后再通知请求方,这样可以在单位时间内处理更多的请求。
</li>
简单介绍了这三种方法之后,我再详细地带你了解一下,这样当你在设计高并发系统时就可以有考虑的方向了。当然了,这三种方法会细化出更多的内容,我会在后面的课程中深入讲解。
首先,我们先来了解第一种方法:**Scale-out。**
## Scale-up vs Scale-out
著名的“摩尔定律”是由Intel的创始人之一戈登·摩尔于1965年提出的。这个定律提到集成电路上可容纳的晶体管的数量约每隔两年会增加一倍。
后来Intel首席执行官大卫·豪斯提出“18个月”的说法即预计18个月会将芯片的性能提升一倍这个说法广为流传。
摩尔定律虽然描述的是芯片的发展速度但我们可以延伸为整体的硬件性能从20世纪后半叶开始计算机硬件的性能是指数级演进的。
直到现在摩尔定律依然生效在半个世纪以来的CPU发展过程中芯片厂商靠着在有限面积上做更小的晶体管的黑科技大幅度地提升着芯片的性能。从第一代集成电路上只有十几个晶体管到现在一个芯片上动辄几十亿晶体管的数量摩尔定律指引着芯片厂商完成了技术上的飞跃。
但是有专家预测摩尔定律可能在未来几年之内不再生效原因是目前的芯片技术已经做到了5nm级别在工艺上可以突破的空间不大可能达不到摩尔定律提到的每18个月翻一番的速度了。此时双核和多核技术的产生拯救了摩尔定律这些技术的思路是将多个CPU核心压在一个芯片上从而大大提升CPU的并行处理能力。
我们在高并发系统设计上也沿用了同样的思路将类似追逐摩尔定律不断提升CPU性能的方案叫做Scale-up纵向扩展把类似CPU多核心的方案叫做Scale-out这两种思路在实现方式上是完全不同的。
<li>
Scale-up通过购买性能更好的硬件来提升系统的并发处理能力比方说目前系统4核4G每秒可以处理200次请求那么如果要处理400次请求呢很简单我们把机器的硬件提升到8核8G硬件资源的提升可能不是线性的这里仅为参考
</li>
<li>
Scale-out则是另外一个思路它通过将多个低性能的机器组成一个分布式集群来共同抵御高并发流量的冲击。沿用刚才的例子我们可以使用两台4核4G的机器来处理那400次请求。
</li>
**那么什么时候选择Scale-up什么时候选择Scale-out呢**一般来讲在我们系统设计初期会考虑使用Scale-up的方式因为这种方案足够简单所谓能用堆砌硬件解决的问题就用硬件来解决但是当系统并发超过了单机的极限时我们就要使用Scale-out的方式。
Scale-out虽然能够突破单机的限制但也会引入一些复杂问题。比如如果某个节点出现故障如何保证整体可用性当多个节点有状态需要同步时如何保证状态信息在不同节点的一致性如何做到使用方无感知的增加和删除节点其中每一个问题都涉及很多的知识点我会在后面的课程中深入地讲解这里暂时不展开了。
说完了Scale-out我们再来看看高并发系统设计的另一种方法**缓存。**
## 使用缓存提升性能
Web 2.0是缓存的时代,这一点毋庸置疑。缓存遍布在系统设计的每个角落,从操作系统到浏览器,从数据库到消息队列,任何略微复杂的服务和组件中你都可以看到缓存的影子。我们使用缓存的主要作用是提升系统的访问性能,在高并发的场景下就可以支撑更多用户的同时访问。
那么为什么缓存可以大幅度提升系统的性能呢?我们知道数据是放在持久化存储中的,一般的持久化存储都是使用磁盘作为存储介质的,而普通磁盘数据由机械手臂、磁头、转轴、盘片组成,盘片又分为磁道、柱面和扇区,盘片构造图我放在下面了。
盘片是存储介质,每个盘片被划分为多个同心圆,信息都被存储在同心圆之中,这些同心圆就是磁道。在磁盘工作时盘片是在高速旋转的,机械手臂驱动磁头沿着径向移动,在磁道上读取所需要的数据。我们把磁头寻找信息花费的时间叫做寻道时间。
<img src="https://static001.geekbang.org/resource/image/88/02/88a27d1b7a0f55917e59486137ab3002.jpg" alt="">
普通磁盘的寻道时间是10ms左右而相比于磁盘寻道花费的时间CPU执行指令和内存寻址的时间都是在ns纳秒级别从千兆网卡上读取数据的时间是在μs微秒级别。所以在整个计算机体系中磁盘是最慢的一环甚至比其它的组件要慢几个数量级。因此我们通常使用以内存作为存储介质的缓存以此提升性能。
当然缓存的语义已经丰富了很多我们可以将任何降低响应时间的中间存储都称为缓存。缓存的思想遍布很多设计领域比如在操作系统中CPU有多级缓存文件有Page Cache缓存你应该有所了解。
## 异步处理
**异步**也是一种常见的高并发设计方法我们在很多文章和演讲中都能听到这个名词与之共同出现的还有它的反义词同步。比如分布式服务框架Dubbo中有同步方法调用和异步方法调用IO模型中有同步IO和异步IO。
**那么什么是同步,什么是异步呢?**以方法调用为例,同步调用代表调用方要阻塞等待被调用方法中的逻辑执行完成。这种方式下,当被调用方法响应时间较长时,会造成调用方长久的阻塞,在高并发下会造成整体系统性能下降甚至发生雪崩。
异步调用恰恰相反,调用方不需要等待方法逻辑执行完成就可以返回执行其他的逻辑,在被调用方法执行完毕后再通过回调、事件通知等方式将结果反馈给调用方。
异步调用在大规模高并发系统中被大量使用,**比如我们熟知的12306网站。**当我们订票时页面会显示系统正在排队这个提示就代表着系统在异步处理我们的订票请求。在12306系统中查询余票、下单和更改余票状态都是比较耗时的操作可能涉及多个内部系统的互相调用如果是同步调用就会像12306刚刚上线时那样高峰期永远不可能下单成功。
而采用异步的方式,后端处理时会把请求丢到消息队列中,同时快速响应用户,告诉用户我们正在排队处理,然后释放出资源来处理更多的请求。订票请求处理完之后,再通知用户订票成功或者失败。
处理逻辑后移到异步处理程序中Web服务的压力小了资源占用的少了自然就能接收更多的用户订票请求系统承受高并发的能力也就提升了。
<img src="https://static001.geekbang.org/resource/image/07/09/0756d48f746590894b6e96ae4e4f7609.jpg" alt="">
既然我们了解了这三种方法,那么是不是意味着在高并发系统设计中,开发一个系统时要把这些方法都用上呢?当然不是,系统的设计是不断演进的。
**罗马不是一天建成的,系统的设计也是如此。**不同量级的系统有不同的痛点,也就有不同的架构设计的侧重点。**如果都按照百万、千万并发来设计系统电商一律向淘宝看齐IM全都学习微信和QQ那么这些系统的命运一定是灭亡。**
因为淘宝、微信的系统虽然能够解决同时百万、千万人同时在线的需求,但其内部的复杂程度也远非我们能够想象的。盲目地追从只能让我们的架构复杂不堪,最终难以维护。就拿从单体架构往服务化演进来说,淘宝也是在经历了多年的发展后,发现系统整体的扩展能力出现问题时,开始启动服务化改造项目的。
**我之前也踩过一些坑,**参与的一个创业项目在初始阶段就采用了服务化的架构,但由于当时人力有限,团队技术积累不足,因此在实际项目开发过程中,发现无法驾驭如此复杂的架构,也出现了问题难以定位、系统整体性能下降等多方面的问题,甚至连系统宕机了都很难追查到根本原因,最后不得不把服务做整合,回归到简单的单体架构中。
所以我建议一般系统的演进过程应该遵循下面的思路:
<li>
最简单的系统设计满足业务需求和流量现状,选择最熟悉的技术体系。
</li>
<li>
随着流量的增加和业务的变化修正架构中存在问题的点,如单点问题、横向扩展问题、性能无法满足需求的组件。在这个过程中,选择社区成熟的、团队熟悉的组件帮助我们解决问题,在社区没有合适解决方案的前提下才会自己造轮子。
</li>
<li>
当对架构的小修小补无法满足需求时,考虑重构、重写等大的调整方式以解决现有的问题。
</li>
**以淘宝为例,**当时在业务从0到1的阶段是通过购买的方式快速搭建了系统。而后随着流量的增长淘宝做了一系列的技术改造来提升高并发处理能力比如数据库存储引擎从MyISAM迁移到InnoDB数据库做分库分表增加缓存启动中间件研发等。
当这些都无法满足时就考虑对整体架构做大规模重构比如说著名的“五彩石”项目让淘宝的架构从单体演进为服务化架构。正是通过逐步的技术演进淘宝才进化出如今承担过亿QPS的技术架构。
归根结底一句话:**高并发系统的演进应该是循序渐进,以解决系统中存在的问题为目的和驱动力的。**
## 课程小结
在今天的课程中,我带着你了解了高并发系统设计的三种通用方法:**Scale-out、缓存和异步。**这三种方法可以在做方案设计时灵活地运用,但它不是具体实施的方案,而是三种思想,在实际运用中会千变万化。
就拿Scale-out来说数据库一主多从、分库分表、存储分片都是它的实际应用方案。而我们需要注意的是在应对高并发大流量的时候系统是可以通过增加机器来承担流量冲击的至于要采用什么样的方案还是要具体问题具体分析。
## 一课一思
高并发系统演进是一个渐进的过程,并非一蹴而就的,那么你在系统演进过程中积累了哪些经验又踩到了哪些坑呢?欢迎在留言区与我一同交流。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

View File

@@ -0,0 +1,117 @@
<audio id="audio" title="02 | 架构分层:我们为什么一定要这么做?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c8/e4/c8e95224dfa638dac1c983f497bbe1e4.mp3"></audio>
在系统从0到1的阶段为了让系统快速上线我们通常是不考虑分层的。但是随着业务越来越复杂大量的代码纠缠在一起会出现逻辑不清晰、各模块相互依赖、代码扩展性差、改动一处就牵一发而动全身等问题。
这时,对系统进行分层就会被提上日程,那么我们要如何对架构进行分层?架构分层和高并发架构设计又有什么关系呢?本节课,我将带你寻找答案。
## 什么是分层架构
软件架构分层在软件工程中是一种常见的设计方式它是将整体系统拆分成N个层次每个层次有独立的职责多个层次协同提供完整的功能。
我们在刚刚成为程序员的时候会被“教育”说系统的设计要是“MVC”Model-View-Controller架构。它将整体的系统分成了Model模型View视图和Controller控制器三个层次也就是将用户视图和业务处理隔离开并且通过控制器连接起来很好地实现了表现和逻辑的解耦是一种标准的软件分层架构。
<img src="https://static001.geekbang.org/resource/image/a3/a3/a3fbea25c3d39cab31826ad7e3c300a3.jpg" alt="">
另外一种常见的分层方式是将整体架构分为表现层、逻辑层和数据访问层:
- 表现层,顾名思义嘛,就是展示数据结果和接受用户指令的,是最靠近用户的一层;
- 逻辑层里面有复杂业务的具体实现;
- 数据访问层则是主要处理和存储之间的交互。
这是在架构上最简单的一种分层方式。其实我们在不经意间已经按照三层架构来做系统分层设计了比如在构建项目的时候我们通常会建立三个目录Web、Service和Dao它们分别对应了表现层、逻辑层还有数据访问层。
<img src="https://static001.geekbang.org/resource/image/86/25/86aa315ef6b6752dc58db69f44f82725.jpg" alt="">
除此之外如果我们稍加留意就可以发现很多的分层的例子。比如我们在大学中学到的OSI网络模型它把整个网络分成了七层自下而上分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。
工作中经常能用到TCP/IP协议它把网络简化成了四层即链路层、网络层、传输层和应用层。每一层各司其职又互相帮助网络层负责端到端的寻址和建立连接传输层负责端到端的数据传输等同时相邻两层还会有数据的交互。这样可以隔离关注点让不同的层专注做不同的事情。
<img src="https://static001.geekbang.org/resource/image/40/2a/408c9e360c55765bd00b1aff80de382a.jpg" alt="">
Linux文件系统也是分层设计的从下图你可以清晰地看出文件系统的层次。在文件系统的最上层是虚拟文件系统VFS用来屏蔽不同的文件系统之间的差异提供统一的系统调用接口。虚拟文件系统的下层是Ext3、Ext4等各种文件系统再向下是为了屏蔽不同硬件设备的实现细节我们抽象出来的单独的一层——通用块设备层然后就是不同类型的磁盘了。
我们可以看到某些层次负责的是对下层不同实现的抽象从而对上层屏蔽实现细节。比方说VFS对上层系统调用层来说提供了统一的调用接口同时对下层中不同的文件系统规约了实现模型当新增一种文件系统实现的时候只需要按照这种模型来设计就可以无缝插入到Linux文件系统中。
<img src="https://static001.geekbang.org/resource/image/c7/88/c76a7f98b633939cd47fec24670fb188.jpg" alt="">
那么,为什么这么多系统一定要做分层的设计呢?答案是分层设计存在一定的优势。
## 分层有什么好处
**分层的设计可以简化系统设计,让不同的人专注做某一层次的事情。**想象一下,如果你要设计一款网络程序却没有分层,该是一件多么痛苦的事情。
因为你必须是一个通晓网络的全才,要知道各种网络设备的接口是什么样的,以便可以将数据包发送给它。你还要关注数据传输的细节,并且需要处理类似网络拥塞,数据超时重传这样的复杂问题。当然了,你更需要关注数据如何在网络上安全传输,不会被别人窥探和篡改。
而有了分层的设计,你只需要专注设计应用层的程序就可以了,其他都可以交给下面几层来完成。
**再有,分层之后可以做到很高的复用。**比如我们在设计系统A的时候发现某一层具有一定的通用性那么我们可以把它抽取独立出来在设计系统B的时候使用起来这样可以减少研发周期提升研发的效率。
**最后一点,分层架构可以让我们更容易做横向扩展。**如果系统没有分层,当流量增加时我们需要针对整体系统来做扩展。但是,如果我们按照上面提到的三层架构将系统分层后,就可以针对具体的问题来做细致的扩展。
比如说业务逻辑里面包含有比较复杂的计算导致CPU成为性能的瓶颈那这样就可以把逻辑层单独抽取出来独立部署然后只对逻辑层来做扩展这相比于针对整体系统扩展所付出的代价就要小得多了。
这一点也可以解释我们课程开始时提出的问题:架构分层究竟和高并发设计的关系是怎样的?从“[01 | 高并发系统:它的通用设计方法是什么?](https://time.geekbang.org/column/article/137323)”中我们了解到,横向扩展是高并发系统设计的常用方法之一,既然分层的架构可以为横向扩展提供便捷, 那么支撑高并发的系统一定是分层的系统。
## 如何来做系统分层
说了这么多分层的优点,那么当我们要做分层设计的时候,需要考虑哪些关键因素呢?
在我看来,最主要的一点就是你需要理清楚每个层次的边界是什么。你也许会问:“如果按照三层架构来分层的话,每一层的边界不是很容易就界定吗?”
没错,当业务逻辑简单时,层次之间的边界的确清晰,开发新的功能时也知道哪些代码要往哪儿写。但是当业务逻辑变得越来越复杂时,边界就会变得越来越模糊,给你举个例子。
任何一个系统中都有用户系统最基本的接口是返回用户信息的接口它调用逻辑层的GetUser方法GetUser方法又和User DB交互获取数据就像下图左边展示的样子。
这时产品提出一个需求在APP中展示用户信息的时候如果用户不存在那么要自动给用户创建一个用户。同时要做一个HTML5的页面HTML5页面要保留之前的逻辑也就是不需要创建用户。这时逻辑层的边界就变得不清晰表现层也承担了一部分的业务逻辑将获取用户和创建用户接口编排起来
<img src="https://static001.geekbang.org/resource/image/92/17/921ab923ef3f7097450458b7d1d6a617.jpg" alt="">
那我们要如何做呢?参照阿里发布的[《阿里巴巴Java开发手册v1.4.0(详尽版)》](https://yq.aliyun.com/articles/69327),我们可以将原先的三层架构细化成下面的样子:
<img src="https://static001.geekbang.org/resource/image/45/b1/45e6640e70d3e1eae4b45a45fefa32b1.jpg" alt="">
我来解释一下这个分层架构中的每一层的作用。
- 终端显示层:各端模板渲染并执行显示的层。当前主要是 Velocity 渲染JS 渲染, JSP 渲染,移动端展示等。
- 开放接口层将Service层方法封装成开放接口同时进行网关安全控制和流量控制等。
- Web层主要是对访问控制进行转发各类基本参数校验或者不复用的业务简单处理等。
- Service层业务逻辑层。
- Manager 层通用业务处理层。这一层主要有两个作用其一你可以将原先Service层的一些通用能力下沉到这一层比如与缓存和存储交互策略中间件的接入其二你也可以在这一层封装对第三方接口的调用比如调用支付服务调用审核服务等。
- DAO层数据访问层与底层 MySQL、Oracle、HBase 等进行数据交互。
- 外部接口或第三方平台:包括其它部门 RPC 开放接口,基础平台,其它公司的 HTTP 接口。
在这个分层架构中主要增加了Manager层它与Service层的关系是Manager层提供原子的服务接口Service层负责依据业务逻辑来编排原子接口。
以上面的例子来说Manager层提供创建用户和获取用户信息的接口而Service层负责将这两个接口组装起来。这样就把原先散布在表现层的业务逻辑都统一到了Service层每一层的边界就非常清晰了。
除此之外,分层架构需要考虑层次之间一定是相邻层互相依赖,数据的流转也只能在相邻的两层之间流转。
我们还是以三层架构为例,数据从表示层进入之后一定要流转到逻辑层,做业务逻辑处理,然后流转到数据访问层来和数据库交互。那么你可能会问:“如果业务逻辑很简单的话可不可以从表示层直接到数据访问层,甚至直接读数据库呢?”
其实从功能上是可以的,但是从长远的架构设计考虑,这样会造成层级调用的混乱,比方说如果表示层或者业务层可以直接操作数据库,那么一旦数据库地址发生变更,你就需要在多个层次做更改,这样就失去了分层的意义,并且对于后面的维护或者重构都会是灾难性的。
## 分层架构的不足
任何事物都不可能是尽善尽美的,分层架构虽有优势也会有缺陷,它最主要的一个缺陷就是增加了代码的复杂度。
这是显而易见的嘛,明明可以在接收到请求后就可以直接查询数据库获得结果,却偏偏要在中间插入多个层次,并且有可能每个层次只是简单地做数据的传递。有时增加一个小小的需求也需要更改所有层次上的代码,看起来增加了开发的成本,并且从调试上来看也增加了复杂度,原本如果直接访问数据库我只需要调试一个方法,现在我却要调试多个层次的多个方法。
另外一个可能的缺陷是,如果我们把每个层次独立部署,层次间通过网络来交互,那么多层的架构在性能上会有损耗。这也是为什么服务化架构性能要比单体架构略差的原因,也就是所谓的“多一跳”问题。
那我们是否要选择分层的架构呢?**答案当然是肯定的。**
你要知道,任何的方案架构都是有优势有缺陷的,天地尚且不全何况我们的架构呢?分层架构固然会增加系统复杂度,也可能会有性能的损耗,但是相比于它能带给我们的好处来说,这些都是可以接受的,或者可以通过其它的方案解决的。**我们在做决策的时候切不可以偏概全,因噎废食。**
## 课程小结
今天我带着你了解了分层架构的优势和不足,以及我们在实际工作中如何来对架构做分层。我想让你了解的是,分层架构是软件设计思想的外在体现,是一种实现方式。我们熟知的一些软件设计原则都在分层架构中有所体现。
比方说,单一职责原则规定每个类只有单一的功能,在这里可以引申为每一层拥有单一职责,且层与层之间边界清晰;迪米特法则原意是一个对象应当对其它对象有尽可能少的了解,在分层架构的体现是数据的交互不能跨层,只能在相邻层之间进行;而开闭原则要求软件对扩展开放,对修改关闭。它的含义其实就是将抽象层和实现层分离,抽象层是对实现层共有特征的归纳总结,不可以修改,但是具体的实现是可以无限扩展,随意替换的。
掌握这些设计思想会自然而然地明白分层架构设计的妙处,同时也能帮助我们做出更好的设计方案。
## 一课一思
课程中我们提到了分层架构的多种模型,比如三层架构模型,阿里巴巴提出的分层架构模型,那么在你日常开发的过程中,会如何来做架构分层呢?你觉得如此分层的优势是什么呢?欢迎在留言区与我一同交流。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

View File

@@ -0,0 +1,137 @@
<audio id="audio" title="03 | 系统设计目标(一):如何提升系统性能?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/77/c2/772ea501e250c7980093abc5e5eb2bc2.mp3"></audio>
提到互联网系统设计,你可能听到最多的词儿就是“三高”,也就是“高并发”“高性能”“高可用”,它们是互联网系统架构设计永恒的主题。在前两节课中,我带你了解了高并发系统设计的含义,意义以及分层设计原则,接下来,我想带你整体了解一下高并发系统设计的目标,然后在此基础上,进入我们今天的话题:如何提升系统的性能?
## 高并发系统设计的三大目标:高性能、高可用、可扩展
**高并发,**是指运用设计手段让系统能够处理更多的用户并发请求也就是承担更大的流量。它是一切架构设计的背景和前提脱离了它去谈性能和可用性是没有意义的。很显然嘛你在每秒一次请求和每秒一万次请求两种不同的场景下分别做到毫秒级响应时间和五个九99.999%)的可用性,无论是设计难度还是方案的复杂度,都不是一个级别的。
**而性能和可用性,**是我们实现高并发系统设计必须考虑的因素。
性能反映了系统的使用体验,想象一下,同样承担每秒一万次请求的两个系统,一个响应时间是毫秒级,一个响应时间在秒级别,它们带给用户的体验肯定是不同的。
可用性则表示系统可以正常服务用户的时间。我们再类比一下,还是两个承担每秒一万次的系统,一个可以做到全年不停机、无故障,一个隔三差五宕机维护,如果你是用户,你会选择使用哪一个系统呢?答案不言而喻。
另一个耳熟能详的名词叫**“可扩展性”,**它同样是高并发系统设计需要考虑的因素。为什么呢?我来举一个具体的例子。
流量分为平时流量和峰值流量两种,峰值流量可能会是平时流量的几倍甚至几十倍,在应对峰值流量的时候,我们通常需要在架构和方案上做更多的准备。**这就是淘宝会花费大半年的时间准备双十一,也是在面对“明星离婚”等热点事件时,看起来无懈可击的微博系统还是会出现服务不可用的原因。**而易于扩展的系统能在短时间内迅速完成扩容,更加平稳地承担峰值流量。
高性能、高可用和可扩展,是我们在做高并发系统设计时追求的三个目标,我会用三节课的时间,带你了解在高并发大流量下如何设计高性能、高可用和易于扩展的系统。
了解完这些内容之后,我们正式进入今天的话题:如何提升系统的性能?
## 性能优化原则
“天下武功,唯快不破”。性能是系统设计成功与否的关键,实现高性能也是对程序员个人能力的挑战。不过在了解实现高性能的方法之前,我们先明确一下性能优化的原则。
**首先,性能优化一定不能盲目,一定是问题导向的。**脱离了问题,盲目地提早优化会增加系统的复杂度,浪费开发人员的时间,也因为某些优化可能会对业务上有些折中的考虑,所以也会损伤业务。
**其次,性能优化也遵循“八二原则”,**即你可以用20%的精力解决80%的性能问题。所以我们在优化过程中一定要抓住主要矛盾,优先优化主要的性能瓶颈点。
**再次,性能优化也要有数据支撑。**在优化过程中,你要时刻了解你的优化让响应时间减少了多少,提升了多少的吞吐量。
**最后,性能优化的过程是持续的。**高并发的系统通常是业务逻辑相对复杂的系统那么在这类系统中出现的性能问题通常也会有多方面的原因。因此我们在做性能优化的时候要明确目标比方说支撑每秒1万次请求的吞吐量下响应时间在10ms那么我们就需要持续不断地寻找性能瓶颈制定优化方案直到达到目标为止。
在以上四个原则的指引下,掌握常见性能问题的排查方式和优化手段,就一定能让你在设计高并发系统时更加游刃有余。
## 性能的度量指标
性能优化的第三点原则中提到,对于性能我们需要有度量的标准,有了数据才能明确目前存在的性能问题,也能够用数据来评估性能优化的效果。**所以明确性能的度量指标十分重要。**
一般来说,度量性能的指标是系统接口的响应时间,但是单次的响应时间是没有意义的,你需要知道一段时间的性能情况是什么样的。所以,我们需要收集这段时间的响应时间数据,然后依据一些统计方法计算出特征值,这些特征值就能够代表这段时间的性能情况。我们常见的特征值有以下几类。
- 平均值
顾名思义,平均值是把这段时间所有请求的响应时间数据相加,再除以总请求数。平均值可以在一定程度上反应这段时间的性能,但它敏感度比较差,如果这段时间有少量慢请求时,在平均值上并不能如实地反应。
举个例子假设我们在30s内有10000次请求每次请求的响应时间都是1ms那么这段时间响应时间平均值也是1ms。这时当其中100次请求的响应时间变成了100ms那么整体的响应时间是(100 * 100 + 9900 * 1) / 10000 = 1.99ms。你看虽然从平均值上来看仅仅增加了不到1ms但是实际情况是有1%的请求100/10000的响应时间已经增加了100倍。**所以,平均值对于度量性能来说只能作为一个参考。**
- 最大值
这个更好理解,就是这段时间内所有请求响应时间最长的值,但它的问题又在于过于敏感了。
还拿上面的例子来说如果10000次请求中只有一次请求的响应时间达到100ms那么这段时间请求的响应耗时的最大值就是100ms性能损耗为原先的百分之一这种说法明显是不准确的。
- 分位值
分位值有很多种比如90分位、95分位、75分位。以90分位为例我们把这段时间请求的响应时间从小到大排序假如一共有100个请求那么排在第90位的响应时间就是90分位值。分位值排除了偶发极慢请求对于数据的影响能够很好地反应这段时间的性能情况分位值越大对于慢请求的影响就越敏感。
<img src="https://static001.geekbang.org/resource/image/1e/1e/1eb73981dfb18cfde5088c96d0c5cf1e.jpg" alt="">
在我来看,分位值是最适合作为时间段内,响应时间统计值来使用的,在实际工作中也应用最多。除此之外,平均值也可以作为一个参考值来使用。
我在上面提到,脱离了并发来谈性能是没有意义的,我们通常使用**吞吐量**或者**响应时间**来度量并发和流量,使用吞吐量的情况会更多一些。但是你要知道,这两个指标是呈倒数关系的。
这很好理解响应时间1s时吞吐量是每秒1次响应时间缩短到10ms那么吞吐量就上升到每秒100次。所以一般我们度量性能时都会同时兼顾吞吐量和响应时间比如我们设立性能优化的目标时通常会这样表述在每秒1万次的请求量下响应时间99分位值在10ms以下。
那么,响应时间究竟控制在多长时间比较合适呢?这个不能一概而论。
从用户使用体验的角度来看200ms是第一个分界点接口的响应时间在200ms之内用户是感觉不到延迟的就像是瞬时发生的一样。而1s是另外一个分界点接口的响应时间在1s之内时虽然用户可以感受到一些延迟但却是可以接受的超过1s之后用户就会有明显等待的感觉等待时间越长用户的使用体验就越差。所以健康系统的99分位值的响应时间通常需要控制在200ms之内而不超过1s的请求占比要在99.99%以上。
现在你了解了性能的度量指标,那我们再来看一看,随着并发的增长我们实现高性能的思路是怎样的。
## 高并发下的性能优化
假如说你现在有一个系统这个系统中处理核心只有一个执行的任务的响应时间都在10ms它的吞吐量是在每秒100次。那么我们如何来优化性能从而提高系统的并发能力呢主要有两种思路一种是提高系统的处理核心数另一种是减少单次任务的响应时间。
**1.提高系统的处理核心数**
提高系统的处理核心数就是增加系统的并行处理能力,这个思路是优化性能最简单的途径。拿上一个例子来说,你可以把系统的处理核心数增加为两个,并且增加一个进程,让这两个进程跑在不同的核心上。这样从理论上,你系统的吞吐量可以增加一倍。当然了,在这种情况下,吞吐量和响应时间就不是倒数关系了,而是:吞吐量=并发进程数/响应时间。
计算机领域的阿姆达尔定律Amdahls law是吉恩·阿姆达尔在1967年提出的。它描述了并发进程数与响应时间之间的关系含义是在固定负载下并行计算的加速比也就是并行化之后效率提升情况可以用下面公式来表示
>
(Ws + Wp) / (Ws + Wp/s)
其中Ws表示任务中的串行计算量Wp表示任务中的并行计算量s表示并行进程数。从这个公式我们可以推导出另外一个公式
>
1/(1-p+p/s)
其中s还是表示并行进程数p表示任务中并行部分的占比。当p为1时也就是完全并行时加速比与并行进程数相等当p为0时即完全串行时加速比为1也就是说完全无加速当s趋近于无穷大的时候加速比就等于1/(1-p)你可以看到它完全和p成正比。特别是当p为1时加速比趋近于无穷大。
以上公式的推导过程有些复杂,你只需要记住结论就好了。
我们似乎找到了解决问题的银弹,是不是无限制地增加处理核心数就能无限制地提升性能,从而提升系统处理高并发的能力呢?很遗憾,随着并发进程数的增加,并行的任务对于系统资源的争抢也会愈发严重。在某一个临界点上继续增加并发进程数,反而会造成系统性能的下降,这就是性能测试中的拐点模型。
<img src="https://static001.geekbang.org/resource/image/23/3f/2379fce36fa3453a0326e62e4d5a333f.jpg" alt="">
从图中你可以发现,并发用户数处于轻压力区时,响应时间平稳,吞吐量和并发用户数线性相关。而当并发用户数处于重压力区时,系统资源利用率到达极限,吞吐量开始有下降的趋势,响应时间也会略有上升。这个时候,再对系统增加压力,系统就进入拐点区,处于超负荷状态,吞吐量下降,响应时间大幅度上升。
所以我们在评估系统性能时通常需要做压力测试,目的就是找到系统的“拐点”,从而知道系统的承载能力,也便于找到系统的瓶颈,持续优化系统性能。
说完了提升并行能力,我们再看看优化性能的另一种方式:减少单次任务响应时间。
**2.减少单次任务响应时间**
想要减少任务的响应时间首先要看你的系统是CPU密集型还是IO密集型的因为不同类型的系统性能优化方式不尽相同。
CPU密集型系统中需要处理大量的CPU运算那么选用更高效的算法或者减少运算次数就是这类系统重要的优化手段。比方说如果系统的主要任务是计算Hash值那么这时选用更高性能的Hash算法就可以大大提升系统的性能。发现这类问题的主要方式是通过一些Profile工具来找到消耗CPU时间最多的方法或者模块比如Linux的perf、eBPF等。
IO密集型系统指的是系统的大部分操作是在等待IO完成这里IO指的是磁盘IO和网络IO。我们熟知的系统大部分都属于IO密集型比如数据库系统、缓存系统、Web系统。这类系统的性能瓶颈可能出在系统内部也可能是依赖的其他系统而发现这类性能瓶颈的手段主要有两类。
第一类是采用工具Linux的工具集很丰富完全可以满足你的优化需要比如网络协议栈、网卡、磁盘、文件系统、内存等等。这些工具的用法很多你可以在排查问题的过程中逐渐积累。除此之外呢一些开发语言还有针对语言特性的分析工具比如说Java语言就有其专属的内存分析工具。
另外一类手段就是可以通过监控来发现性能问题。在监控中我们可以对任务的每一个步骤做分时的统计,从而找到任务的哪一步消耗了更多的时间。这一部分在演进篇中会有专门的介绍,这里就不再展开了。
那么找到了系统的瓶颈点我们要如何优化呢优化方案会随着问题的不同而不同。比方说如果是数据库访问慢那么就要看是不是有锁表的情况、是不是有全表扫描、索引加的是否合适、是否有JOIN操作、需不需要加缓存等等如果是网络的问题就要看网络的参数是否有优化的空间抓包来看是否有大量的超时重传网卡是否有大量丢包等。
总而言之,“兵来将挡水来土掩”,我们需要制定不同的性能优化方案来应对不同的性能问题。
## 课程小结
今天,我带你了解了性能的原则、度量指标,以及在高并发下优化性能的基本思路。性能优化是一个很大的话题,只用短短一讲是完全不够的,所以我会在后面的课程中详细介绍其中的某些方面,比方说我们如何用缓存优化系统的读取性能,如何使用消息队列优化系统的写入性能等等。
有时候你在遇到性能问题的时候会束手无策,从今天的课程中你可以得到一些启示,在这里我给你总结出几点:
- 数据优先,你做一个新的系统在上线之前一定要把性能监控系统做好;
- 掌握一些性能优化工具和方法,这就需要在工作中不断地积累;
- 计算机基础知识很重要,比如说网络知识、操作系统知识等等,掌握了基础知识才能让你在优化过程中抓住性能问题的关键,也能在性能优化过程中游刃有余。
## 一课一思
在课程中我们提到了一些性能优化的原则和基本的思考点,那么你在日常工作中有哪些性能优化的手段和经验呢?欢迎在留言区与我分享你的经验。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

View File

@@ -0,0 +1,147 @@
<audio id="audio" title="04 | 系统设计目标(二):系统怎样做到高可用?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/df/62/dfcb63e0f5f97f5b26d7d8497cafda62.mp3"></audio>
你好,我是唐扬。
开课之后有同学反馈说课程中偏理论知识的讲解比较多希望看到实例。我一直关注这些声音也感谢你提出的建议在04讲的开篇我想对此作出一些回应。
在课程设计时,我主要想用基础篇中的前五讲内容带你了解一些关于高并发系统设计的基本概念,期望能帮你建立一个整体的框架,这样方便在后面的演进篇和实战篇中对涉及的知识点做逐一的展开和延伸。比方说,本节课提到了降级,那我会在运维篇中以案例的方式详细介绍降级方案的种类以及适用的场景,之所以这么设计是期望通过前面少量的篇幅把课程先串起来,以点带面,逐步展开。
当然,不同的声音是我后续不断优化课程内容的动力,我会认真对待每一条建议,不断优化课程,与你一起努力、进步。
接下来,让我们正式进入课程。
本节课,我会继续带你了解高并发系统设计的第二个目标——高可用性。你需要在本节课对提升系统可用性的思路和方法有一个直观的了解,这样,当后续对点讲解这些内容时,你能马上反应过来,你的系统在遇到可用性的问题时,也能参考这些方法进行优化。
**高可用性High AvailabilityHA**是你在系统设计时经常会听到的一个名词,它指的是系统具备较高的无故障运行的能力。
我们在很多开源组件的文档中看到的HA方案就是提升组件可用性让系统免于宕机无法服务的方案。比如你知道Hadoop 1.0中的NameNode是单点的一旦发生故障则整个集群就会不可用而在Hadoop2中提出的NameNode HA方案就是同时启动两个NameNode一个处于Active状态另一个处于Standby状态两者共享存储一旦Active NameNode发生故障则可以将Standby NameNode切换成Active状态继续提供服务这样就增强了Hadoop的持续无故障运行的能力也就是提升了它的可用性。
通常来讲,一个高并发大流量的系统,系统出现故障比系统性能低更损伤用户的使用体验。想象一下,一个日活用户过百万的系统,一分钟的故障可能会影响到上千的用户。而且随着系统日活的增加,一分钟的故障时间影响到的用户数也随之增加,系统对于可用性的要求也会更高。所以今天,我就带你了解一下在高并发下,我们如何来保证系统的高可用性,以便给你的系统设计提供一些思路。
## 可用性的度量
可用性是一个抽象的概念,你需要知道要如何来度量它,与之相关的概念是:**MTBF和MTTR。**
**MTBFMean Time Between Failure**是平均故障间隔的意思,代表两次故障的间隔时间,也就是系统正常运转的平均时间。这个时间越长,系统稳定性越高。
**MTTRMean Time To Repair**表示故障的平均恢复时间,也可以理解为平均故障时间。这个值越小,故障对于用户的影响越小。
可用性与MTBF和MTTR的值息息相关我们可以用下面的公式表示它们之间的关系
>
Availability = MTBF / (MTBF + MTTR)
这个公式计算出的结果是一个比例,而这个比例代表着系统的可用性。一般来说,我们会使用几个九来描述系统的可用性。
<img src="https://static001.geekbang.org/resource/image/73/75/73a87a9bc14a27c9ec9dfda1b72e1e75.jpg" alt="">
其实通过这张图你可以发现,一个九和两个九的可用性是很容易达到的,只要没有蓝翔技校的铲车搞破坏,基本上可以通过人肉运维的方式实现。
三个九之后系统的年故障时间从3天锐减到8小时。到了四个九之后年故障时间缩减到1小时之内。在这个级别的可用性下你可能需要建立完善的运维值班体系、故障处理流程和业务变更流程。你可能还需要在系统设计上有更多的考虑。比如在开发中你要考虑如果发生故障是否不用人工介入就能自动恢复。当然了在工具建设方面你也需要多加完善以便快速排查故障原因让系统快速恢复。
到达五个九之后,故障就不能靠人力恢复了。想象一下,从故障发生到你接收报警,再到你打开电脑登录服务器处理问题,时间可能早就过了十分钟了。所以这个级别的可用性考察的是系统的容灾和自动恢复的能力,让机器来处理故障,才会让可用性指标提升一个档次。
一般来说,我们的核心业务系统的可用性,需要达到四个九,非核心系统的可用性最多容忍到三个九。在实际工作中,你可能听到过类似的说法,只是不同级别,不同业务场景的系统对于可用性要求是不一样的。
目前,你已经对可用性的评估指标有了一定程度的了解了,接下来,我们来看一看高可用的系统设计需要考虑哪些因素。
## 高可用系统设计的思路
一个成熟系统的可用性需要从系统设计和系统运维两方面来做保障,两者共同作用,缺一不可。那么如何从这两方面入手,解决系统高可用的问题呢?
#### 1.系统设计
“Design for failure”是我们做高可用系统设计时秉持的第一原则。在承担百万QPS的高并发系统中集群中机器的数量成百上千台单机的故障是常态几乎每一天都有发生故障的可能。
未雨绸缪才能决胜千里。我们在做系统设计的时候,要把发生故障作为一个重要的考虑点,预先考虑如何自动化地发现故障,发生故障之后要如何解决。当然了,除了要有未雨绸缪的思维之外,我们还需要掌握一些具体的优化方法,比如**failover故障转移、超时控制以及降级和限流。**
一般来说发生failover的节点可能有两种情况
1.是在完全对等的节点之间做failover。<br>
2.是在不对等的节点之间,即系统中存在主节点也存在备节点。
在对等节点之间做failover相对来说简单些。在这类系统中所有节点都承担读写流量并且节点中不保存状态每个节点都可以作为另一个节点的镜像。在这种情况下如果访问某一个节点失败那么简单地随机访问另一个节点就好了。
举个例子Nginx可以配置当某一个Tomcat出现大于500的请求的时候重试请求另一个Tomcat节点就像下面这样
<img src="https://static001.geekbang.org/resource/image/e1/0a/e167ec1db28e977e37455d746e9d1d0a.jpg" alt="">
针对不对等节点的failover机制会复杂很多。比方说我们有一个主节点有多台备用节点这些备用节点可以是热备同样在线提供服务的备用节点也可以是冷备只作为备份使用那么我们就需要在代码中控制如何检测主备机器是否故障以及如何做主备切换。
使用最广泛的故障检测机制是“心跳”。你可以在客户端上定期地向主节点发送心跳包,也可以从备份节点上定期发送心跳包。当一段时间内未收到心跳包,就可以认为主节点已经发生故障,可以触发选主的操作。
选主的结果需要在多个备份节点上达成一致所以会使用某一种分布式一致性算法比方说PaxosRaft。
除了故障转移以外,对于系统间调用超时的控制也是高可用系统设计的一个重要考虑方面。
复杂的高并发系统通常会有很多的系统模块组成,同时也会依赖很多的组件和服务,比如说缓存组件,队列服务等等。它们之间的调用最怕的就是延迟而非失败,因为失败通常是瞬时的,可以通过重试的方式解决。而一旦调用某一个模块或者服务发生比较大的延迟,调用方就会阻塞在这次调用上,它已经占用的资源得不到释放。当存在大量这种阻塞请求时,调用方就会因为用尽资源而挂掉。
在系统开发的初期,超时控制通常不被重视,或者是没有方式来确定正确的超时时间。
**我之前经历过一个项目,**模块之间通过RPC框架来调用超时时间是默认的30秒。平时系统运行得非常稳定可是一旦遇到比较大的流量RPC服务端出现一定数量慢请求的时候RPC客户端线程就会大量阻塞在这些慢请求上长达30秒造成RPC客户端用尽调用线程而挂掉。后面我们在故障复盘的时候发现这个问题后调整了RPC数据库缓存以及调用第三方服务的超时时间这样在出现慢请求的时候可以触发超时就不会造成整体系统雪崩。
既然要做超时控制,那么我们怎么来确定超时时间呢?这是一个比较困难的问题。
超时时间短了,会造成大量的超时错误,对用户体验产生影响;超时时间长了,又起不到作用。**我建议你通过收集系统之间的调用日志统计比如说99%的响应时间是怎样的,然后依据这个时间来指定超时时间。**如果没有调用的日志,那么你只能按照经验值来指定超时时间。不过,无论你使用哪种方式,超时时间都不是一成不变的,需要在后面的系统维护过程中不断地修改。
超时控制实际上就是不让请求一直保持,而是在经过一定时间之后让请求失败,释放资源给接下来的请求使用。这对于用户来说是有损的,但是却是必要的,因为它牺牲了少量的请求却保证了整体系统的可用性。而我们还有另外两种有损的方案能保证系统的高可用,它们就是降级和限流。
**降级是为了保证核心服务的稳定而牺牲非核心服务的做法。**比方说我们发一条微博会先经过反垃圾服务检测,检测内容是否是广告,通过后才会完成诸如写数据库等逻辑。
反垃圾的检测是一个相对比较重的操作,因为涉及到非常多的策略匹配,在日常流量下虽然会比较耗时却还能正常响应。但是当并发较高的情况下,它就有可能成为瓶颈,而且它也不是发布微博的主体流程,所以我们可以暂时关闭反垃圾服务检测,这样就可以保证主体的流程更加稳定。
**限流完全是另外一种思路,**它通过对并发的请求进行限速来保护系统。
比如对于Web应用我限制单机只能处理每秒1000次的请求超过的部分直接返回错误给客户端。虽然这种做法损害了用户的使用体验但是它是在极端并发下的无奈之举是短暂的行为因此是可以接受的。
实际上,无论是降级还是限流,在细节上还有很多可供探讨的地方,我会在后面的课程中,随着系统的不断演进深入地剖析,在基础篇里就不多说了。
#### 2.系统运维
在系统设计阶段为了保证系统的可用性可以采取上面的几种方法,那在系统运维的层面又能做哪些事情呢?其实,我们可以从**灰度发布、故障演练**两个方面来考虑如何提升系统的可用性。
你应该知道在业务平稳运行过程中系统是很少发生故障的90%的故障是发生在上线变更阶段的。比方说,你上了一个新的功能,由于设计方案的问题,数据库的慢请求数翻了一倍,导致系统请求被拖慢而产生故障。
如果没有变更,数据库怎么会无缘无故地产生那么多的慢请求呢?因此,为了提升系统的可用性,重视变更管理尤为重要。而除了提供必要回滚方案,以便在出现问题时快速回滚恢复之外,**另一个主要的手段就是灰度发布。**
灰度发布指的是系统的变更不是一次性地推到线上的而是按照一定比例逐步推进的。一般情况下灰度发布是以机器维度进行的。比方说我们先在10%的机器上进行变更同时观察Dashboard上的系统性能指标以及错误日志。如果运行了一段时间之后系统指标比较平稳并且没有出现大量的错误日志那么再推动全量变更。
灰度发布给了开发和运维同学绝佳的机会,让他们能在线上流量上观察变更带来的影响,是保证系统高可用的重要关卡。
灰度发布是在系统正常运行条件下,保证系统高可用的运维手段,那么我们如何知道发生故障时系统的表现呢?这里就要依靠另外一个手段:**故障演练。**
故障演练指的是对系统进行一些破坏性的手段,观察在出现局部故障时,整体的系统表现是怎样的,从而发现系统中存在的,潜在的可用性问题。
一个复杂的高并发系统依赖了太多的组件,比方说磁盘,数据库,网卡等,这些组件随时随地都可能会发生故障,而一旦它们发生故障,会不会如蝴蝶效应一般造成整体服务不可用呢?我们并不知道,因此,故障演练尤为重要。
在我来看,**故障演练和时下比较流行的“混沌工程”的思路如出一辙,**作为混沌工程的鼻祖Netfix在2010年推出的“Chaos Monkey”工具就是故障演练绝佳的工具。它通过在线上系统上随机地关闭线上节点来模拟故障让工程师可以了解在出现此类故障时会有什么样的影响。
当然,这一切是以你的系统可以抵御一些异常情况为前提的。如果你的系统还没有做到这一点,那么**我建议你**另外搭建一套和线上部署结构一模一样的线下系统,然后在这套系统上做故障演练,从而避免对生产系统造成影响。
## 课程小结
本节课我带你了解了如何度量系统的可用性,以及在做高并发系统设计时如何来保证高可用。
说了这么多,你可以看到从开发和运维角度上来看,提升可用性的方法是不同的:
<li>
**开发**注重的是如何处理故障,关键词是冗余和取舍。冗余指的是有备用节点,集群来顶替出故障的服务,比如文中提到的故障转移,还有多活架构等等;取舍指的是丢卒保车,保障主体服务的安全。
</li>
<li>
从**运维角度**来看则更偏保守,注重的是如何避免故障的发生,比如更关注变更管理以及如何做故障的演练。
</li>
两者结合起来才能组成一套完善的高可用体系。
**你还需要注意的是,**提高系统的可用性有时候是以牺牲用户体验或者是牺牲系统性能为前提的,也需要大量人力来建设相应的系统,完善机制。所以我们要把握一个度,不该做过度的优化。就像我在文中提到的,核心系统四个九的可用性已经可以满足需求,就没有必要一味地追求五个九甚至六个九的可用性。
另外一般的系统或者组件都是追求极致的性能的那么有没有不追求性能只追求极致的可用性的呢答案是有的。比如配置下发的系统它只需要在其它系统启动时提供一份配置即可所以秒级返回也可十秒钟也OK无非就是增加了其它系统的启动时间而已。但是它对可用性的要求是极高的甚至会到六个九原因是配置可以获取的慢但是不能获取不到。**我给你举这个例子是想让你了解,**可用性和性能有时候是需要做取舍的,但如何取舍就要视不同的系统而定,不能一概而论了。
## 一课一思
在今天的课程中,我提到了很多保证高可用的手段,那么你在工作中会有哪些保证系统高可用的设计技巧呢?欢迎在留言区写下你的思考,我会跟你一起讨论这些问题。
**另外,期待你在评论区留下更多的声音,你的建议,我尤为珍重,我会和你一起,努力将课程做好。**
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

View File

@@ -0,0 +1,87 @@
<audio id="audio" title="05 | 系统设计目标(三):如何让系统易于扩展?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a6/dc/a6b9f5c7f55946b4dfda97142639d4dc.mp3"></audio>
从架构设计上来说,高可扩展性是一个设计的指标,它表示可以通过增加机器的方式来线性提高系统的处理能力,从而承担更高的流量和并发。
你可能会问:“在架构设计之初,为什么不预先考虑好使用多少台机器,支持现有的并发呢?”这个问题我在“[03 | 系统设计目标(一):如何提升系统性能?](https://time.geekbang.org/column/article/139474)”一课中提到过,答案是峰值的流量不可控。
一般来说基于成本考虑在业务平稳期我们会预留30%50%的冗余以应对运营活动或者推广可能带来的峰值流量但是当有一个突发事件发生时流量可能瞬间提升到23倍甚至更高我们还是以微博为例。
鹿晗和关晓彤互圈公布恋情,大家会在两个人的微博下面,或围观,或互动,微博的流量短时间内增长迅速,微博信息流也短暂出现无法刷出新的消息的情况。
那我们要如何应对突发的流量呢?架构的改造已经来不及了,最快的方式就是堆机器。不过我们需要保证,扩容了三倍的机器之后,相应的我们的系统也能支撑三倍的流量。有的人可能会产生疑问:“这不是显而易见的吗?很简单啊。”真的是这样吗?我们来看看做这件事儿难在哪儿。
## 为什么提升扩展性会很复杂
在上一讲中,我提到可以在单机系统中通过增加处理核心的方式,来增加系统的并行处理能力,但这个方式并不总奏效。因为当并行的任务数较多时,系统会因为争抢资源而达到性能上的拐点,系统处理能力不升反降。
而对于由多台机器组成的集群系统来说也是如此。集群系统中,不同的系统分层上可能存在一些“瓶颈点”,这些瓶颈点制约着系统的横向扩展能力。这句话比较抽象,我举个例子你就明白了。
比方说你系统的流量是每秒1000次请求对数据库的请求量也是每秒1000次。如果流量增加10倍虽然系统可以通过扩容正常服务数据库却成了瓶颈。再比方说单机网络带宽是50Mbps那么如果扩容到30台机器前端负载均衡的带宽就超过了千兆带宽的限制也会成为瓶颈点。那么我们的系统中存在哪些服务会成为制约系统扩展的重要因素呢
其实无状态的服务和组件更易于扩展而像MySQL这种存储服务是有状态的就比较难以扩展。因为向存储集群中增加或者减少机器时会涉及大量数据的迁移而一般传统的关系型数据库都不支持。这就是为什么提升系统扩展性会很复杂的主要原因。
除此之外,从例子中你可以看到,我们需要站在整体架构的角度,而不仅仅是业务服务器的角度来考虑系统的扩展性 。**所以说,数据库、缓存、依赖的第三方、负载均衡、交换机带宽等等**都是系统扩展时需要考虑的因素。我们要知道系统并发到了某一个量级之后,哪一个因素会成为我们的瓶颈点,从而针对性地进行扩展。
针对这些复杂的扩展性问题,我提炼了一些系统设计思路,供你了解。
## 高可扩展性的设计思路
**拆分**是提升系统扩展性最重要的一个思路,它会把庞杂的系统拆分成独立的,有单一职责的模块。相对于大系统来说,考虑一个一个小模块的扩展性当然会简单一些。**将复杂的问题简单化,这就是我们的思路。**
但对于不同类型的模块我们在拆分上遵循的原则是不一样的。我给你举一个简单的例子假如你要设计一个社区那么社区会有几个模块呢可能有5个模块。
- 用户:负责维护社区用户信息,注册,登陆等;
- 关系:用户之间关注、好友、拉黑等关系的维护;
- 内容:社区发的内容,就像朋友圈或者微博的内容;
- 评论、赞:用户可能会有的两种常规互动操作;
- 搜索:用户的搜索,内容的搜索。
而部署方式按照最简单的三层部署架构,负载均衡负责请求的分发,应用服务器负责业务逻辑的处理,数据库负责数据的存储落地。这时,所有模块的业务代码都混合在一起了,数据也都存储在一个库里。
<img src="https://static001.geekbang.org/resource/image/58/a6/5803451931917e0806c37c39802410a6.jpg" alt="">
#### 1.存储层的扩展性
无论是存储的数据量,还是并发访问量,不同的业务模块之间的量级相差很大,比如说成熟社区中,关系的数据量是远远大于用户数据量的,但是用户数据的访问量却远比关系数据要大。所以假如存储目前的瓶颈点是容量,那么我们只需要针对关系模块的数据做拆分就好了,而不需要拆分用户模块的数据。**所以存储拆分首先考虑的维度是业务维度。**
拆分之后,这个简单的社区系统就有了用户库、内容库、评论库、点赞库和关系库。这么做还能隔离故障,某一个库“挂了”不会影响到其它的数据库。
<img src="https://static001.geekbang.org/resource/image/5e/b6/5ee6e1350e2d4d5514a05032b10bd3b6.jpg" alt="">
按照业务拆分,在一定程度上提升了系统的扩展性,但系统运行时间长了之后,单一的业务数据库在容量和并发请求量上仍然会超过单机的限制。**这时,我们就需要针对数据库做第二次拆分。**
这次拆分是按照数据特征做水平的拆分,比如说我们可以给用户库增加两个节点,然后按照某些算法将用户的数据拆分到这三个库里面,具体的算法我会在后面讲述数据库分库分表时和你细说。
水平拆分之后,我们就可以让数据库突破单机的限制了。但这里要注意,我们不能随意地增加节点,因为一旦增加节点就需要手动地迁移数据,成本还是很高的。所以基于长远的考虑,我们最好一次性增加足够的节点以避免频繁的扩容。
当数据库按照业务和数据维度拆分之后,我们尽量不要使用事务。因为当一个事务中同时更新不同的数据库时,需要使用二阶段提交,来协调所有数据库要么全部更新成功,要么全部更新失败。这个协调的成本会随着资源的扩展不断升高,最终达到无法承受的程度。
说完了存储层的扩展性,我们来看看业务层是如何做到易于扩展的。
#### 2.业务层的扩展性
我们一般会从三个维度考虑业务层的拆分方案,它们分别是:业务维度,重要性维度和请求来源维度。
首先,我们需要把相同业务的服务拆分成单独的业务池,比方说上面的社区系统中,我们可以按照业务的维度拆分成用户池、内容池、关系池、评论池、点赞池和搜索池。
每个业务依赖独自的数据库资源,不会依赖其它业务的数据库资源。这样当某一个业务的接口成为瓶颈时,我们只需要扩展业务的池子,以及确认上下游的依赖方就可以了,这样就大大减少了扩容的复杂度。
<img src="https://static001.geekbang.org/resource/image/a6/b5/a62e9add7797fc8e55c06fa8a21065b5.jpg" alt="">
除此之外,我们还可以根据业务接口的重要程度,把业务分为核心池和非核心池。打个比方,就关系池而言,关注、取消关注接口相对重要一些,可以放在核心池里面;拉黑和取消拉黑的操作就相对不那么重要,可以放在非核心池里面。这样,我们可以优先保证核心池的性能,当整体流量上升时优先扩容核心池,降级部分非核心池的接口,从而保证整体系统的稳定性。
<img src="https://static001.geekbang.org/resource/image/ce/28/ce6e856238d8af7059c44b3a47eced28.jpg" alt="">
最后你还可以根据接入客户端类型的不同做业务池的拆分。比如说服务于客户端接口的业务可以定义为外网池服务于小程序或者HTML5页面的业务可以定义为H5池服务于内部其它部门的业务可以定义为内网池等等。
## 课程小结
本节课我带你了解了提升系统扩展性的复杂度以及系统拆分的思路。拆分看起来比较简单,可是什么时候做拆分,如何做拆分还是有很多细节考虑的。
未做拆分的系统虽然可扩展性不强,但是却足够简单,无论是系统开发还是运行维护都不需要投入很大的精力。拆分之后,需求开发需要横跨多个系统多个小团队,排查问题也需要涉及多个系统,运行维护上,可能每个子系统都需要有专人来负责,对于团队是一个比较大的考验。这个考验是我们必须要经历的一个大坎,需要我们做好准备。
## 一课一思
在今天的课程中我们谈到了传统关系型数据库的可扩展性是很差的那么在你看来常见的NoSQL数据库是如何解决扩展性的问题呢欢迎在留言区和我一起讨论。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

View File

@@ -0,0 +1,3 @@
<audio id="audio" title="06 | 面试现场第一期:当问到组件实现原理时,面试官是在刁难你吗?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/6a/0c/6a89b1be8d3b75687ced59c143370c0c.mp3"></audio>
<img src="https://static001.geekbang.org/resource/image/0c/e7/0c5237f2bfd702bbb19423d36fafe4e7.jpg" alt="unpreview"><img src="https://static001.geekbang.org/resource/image/3d/08/3d6b93bdf72be3826c9be626b19f0308.jpg" alt="unpreview"><img src="https://static001.geekbang.org/resource/image/a0/db/a04a123eebb754cb04c3c055c47400db.jpg" alt="unpreview"><img src="https://static001.geekbang.org/resource/image/d6/7a/d6a832cc3fe1284e6274c1d43f44067a.jpg" alt="unpreview"><img src="https://static001.geekbang.org/resource/image/b9/da/b963b7e55dd6c3cf1fb3340ffb233fda.jpg" alt="unpreview"><img src="https://static001.geekbang.org/resource/image/69/fe/69f89c4662d71b1e434a85ba89ff87fe.jpg" alt="unpreview">