在架构设计系列1:什么是架构设计中,我们讲了架构设计的主要目的,是为了解决软件系统复杂度带来的问题,今天我们来聊聊软件系统复杂度的来源之一高性能。
要搞清楚什么是高性能架构,我们需先弄明白高性能是什么。
高性能定义
首先,性能是什么,怎么理解它?
简单来说,性能,指的是一件事务的处理能力。
那么,什么是高性能呢?
高性能,指的是处理一件事务的速度更快,所消耗的资源更少。
高性能架构
那么,什么是高性能架构呢?
高性能架构,是指在有限的资源投入下,通过采用适当的技术和策略,让系统具备了优秀的性能。
对于技术人员来说,在有限的资源投入下,如何提高系统性能,这是一个挑战,更是一个机遇。
可以想象一下,同为架构师,你做的架构,在性能相同的情况下,成本更低,那是不是你的优势呢。
在进入互联网时代后,业务的发展速度是远远超乎你的想象的。例如:
我们提炼下关键信息,每秒12万笔支付,每秒收发76万个红包。这两个数字意味着,有成千上万,甚至上亿的用户,在同时使用系统。
在用户体量这么庞大的情况下,可以想象系统的压力有多大,尤其还是支付和红包这种复杂的业务,这要求整个链路都保证高性能。而一个复杂系统的链路往往是很长的,要保证链路上的每个环节配合起来达到高性能的目的,是一件个复杂且具有挑战性的任务。
如此,高性能架构就派上了用场。
我们可以通过高性能架构设计,最大限度地提高系统的处理速度、吞吐量和效率,从而提供稳定和可靠的系统服务,以满足大规模、高并发和复杂的业务需求。
高性能系统,一般具备如下特点:
软件系统中高性能带来的复杂度主要体现在两个方面,一方面是单台计算机内部为了高性能带来的复杂度;另一方面是多台计算机集群为了高性能带来的复杂度。
因此,我们可以从单机高性能和集群高性能两个方面来理解软件系统的高性能。
接下来,我们一起来探讨一下有哪些常用技术可以提升单机性能和集群性能。
单机高性能最关键的地方就是操作系统。
计算机性能的发展本质上是硬件发展驱动的,尤其是CPU的性能发展。著名的“摩尔定律”表明了 CPU 的处理能力每隔 18 个月就翻一番;而将硬件性能充分发挥出来的关键就是操作系统。
所以操作系统本身其实也是跟随硬件的发展而发展的,操作系统是软件系统的运行环境,操作系统的复杂度直接决定了软件系统的复杂度。
操作系统和性能最相关的就是进程和线程。
计算机如何并行运行任务?
在早期计算机一次只能执行一个任务,如果某个任务需要从 I/O 设备(例如磁带)读取大量的数据,在 I/O 操作的过程中,CPU 其实是空闲的,而这个空闲时间本来是可以进行其他计算的。
为了提升性能,用进程来对应一个任务,每个任务都有自己独立的内存空间,进程间互不相关,由操作系统来进行调度。为了达到多进程并行运行的目的,采取了分时的方式,即把 CPU 的时间分成很多片段,每个片段只能执行某个进程中的指令。
虽然从操作系统和 CPU 的角度来说还是串行处理的,但是由于 CPU 的处理速度很快,从用户的角度来看,感觉是多进程在并行处理。
进程间如何通信?
多进程虽然要求每个任务都有独立的内存空间,进程间互不相关,但从用户的角度来看,如果两个任务之间能够在运行过程中就进行通信,会让任务设计变得更加灵活高效。
否则如果两个任务运行过程中不能通信,只能是 A 任务将结果写到存储,B 任务再从存储读取进行处理,不仅效率低,而且任务设计更加复杂。
为了解决这个问题,进程间通信的各种方式被设计出来了,包括管道、消息队列、信号量、共享存储等。
进程内如何并行运行任务?
多进程让多任务能够并行处理,但本身还有缺点,单个进程内部只能串行处理,而实际上很多进程内部的子任务并不要求是严格按照时间顺序来执行的,也需要并行处理。
为了解决这个问题,人们又发明了线程,线程是进程内部的子任务,但这些子任务都共享同一份进程数据。为了保证数据的正确性,又发明了互斥锁机制。
有了多线程后,操作系统调度的最小单位就变成了线程,而进程变成了操作系统分配资源的最小单位。
要提升单机的性能,其中一个关键点就是服务器采取的并发模型,而这就涉及到多进程、多线程和异步非阻塞、同步非阻塞的IO模型。
IO 多路复用
IO 多路复用技术的两个关键点:
Reactor 和 Proactor 架构模式
在后端系统设计里面,要想实现单机的高性能,那在 IO 多路复用基础之上,我们的整个网络框架,还需要配合池化技术来提高我们的性能。
因此,业界一般都是采用 I/O 多路复用 + 线程池 的方式来提高性能。与之对应的,在业界常用的两个单机高性能的架构模式就是Reactor 和 Proactor 模式。Reactor 模式属于同步非阻塞网络模型,Proactor 模式属于异步非阻塞网络模型。
在业内开源软件里面,Redis 采用的是 单 Reactor 单进程的方式,Memcache 采用的是 多 Reactor 多线程的方式,Nginx 采用的是多 Reactor 多进程的方式。
如果我们要完成一个高性能的软件系统,需要考虑如多进程、多线程、进程间通信、多线程并发等技术点。
多进程和多线程虽然让多任务并行处理的性能大大提升,但本质上还是分时系统,并不能做到同一时刻真正的并行。解决这个问题的方式,就是让多个 CPU 能够同时执行计算任务,从而实现真正意义上的多任务并行。
目前最常见的多核处理器就是SMP方案。SMP,全称 Symmetric Multi-Processor,对称多处理器结构。
虽然计算机硬件的性能快速发展,但和业务的发展速度相比,还是小巫见大巫了,尤其是进入互联网时代后,业务的发展速度远远超过了硬件的发展速度。
就像前面提到的,支付和红包这两种复杂的业务,单机的性能无论如何是无法支撑的,必须采用集群的方式来达到高性能。例如,支付宝和微信这种规模的业务系统,后台系统的机器数量都是万台级别的。
通过大量机器来提升性能,并不仅仅是增加机器这么简单,让多台机器配合起来达到高性能的目的,是一个复杂的任务。常见的方式有:
任务分配是指每台机器都可以处理完整的业务任务,将不同的任务分配到不同的机器上执行。
高性能集群设计的复杂性主要体现在需要增加一个任务分配器,以及选择一个合适的任务分配算法。
对于任务分配器,更流行的叫法是负载均衡器。但这个名称会让人潜意识里觉得,任务分配的目的是要保持各个计算单元的负载达到均衡状态。而实际上任务分配并不仅限于考虑计算单元的负载均衡,不同的任务分配算法有不一样的目标,有的基于负载考虑,有的基于性能(吞吐量、响应时间)考虑,有的基于业务考虑。
选择合适的任务分配器也是一件复杂的事情,需要综合考虑性能、成本、可维护性、可用性等各方面的因素。
常见的任务分配器的分类有:
DNS负载均衡
这是最简单最常见的负载均衡方式,经常用来实现地理级别的均衡。
DNS负载均衡的本质是不同地理位置的用户访问时,DNS解析同一个域名可以返回不同的IP地址。比如,北方用户访问位于北京的机房,南方的用户则访问深圳机房。以www.baidu.com来说,北方用户获取的地址是61.135.165.224,南方用户获取的是14.215.177.38。
硬件负载均衡
通过单独的硬件设备实现负载均衡的功能,此类设备和路由器、交换机类似,可以理解为一个用于负载均衡的基础网络设备。当前典型的主要有两款:F5和A10。
软件负载均衡
通过提供负载均衡软件来实现负载均衡功能,比较常见的有LVS和Nginx,其中LVS是Linux内核的4层负载均衡,Nginx是软件的7层负载均衡。
除了使用开源的系统进行负载均衡,如果业务比较特殊,也有可能基于开源系统进行定制(例如,Nginx插件),甚至进行自研。
软件负载均衡和硬件负载均衡的最主要区别就在于性能上,硬件负载均衡性能远远高于软件。比如Nginx的性能是万级,一般的Linux服务器上安装Nginx后大概能达到5万/秒;LVS的性能是十万级,据说可达到80万/秒;而F5性能是百万级,从 200万/秒到800万/秒都有。
负载均衡典型架构
一般情况下,我们会对着三种负载均衡方式基于各自的优缺点进行组合使用。组合的基本原则为:DNS负载均衡实现地理级别的负载均衡;硬件负载均衡实现集群级别的负载均衡;软件负载均衡实现机器级别的负载均衡。
通过任务分配的方式,我们能够突破单台机器处理性能的瓶颈,通过增加更多的机器来满足业务的性能需求,但如果业务本身也越来越复杂,单纯只通过任务分配的方式来扩展性能,收益会越来越低。
例如,业务简单的时候 1 台机器扩展到 10 台机器,性能能够提升 8 倍(需要扣除机器群带来的部分性能损耗,因此无法达到理论上的 10 倍那么高),但如果业务越来越复杂,1 台机器扩展到 10 台,性能可能只能提升 5 倍。
造成这种现象的主要原因是业务越来越复杂,单台机器处理的性能会越来越低。为了能够继续提升性能,我们需要采用新的方式:任务分解。
微服务架构就采用了这种思路,通过这种任务分解的方式,能够把原来大一统但复杂的业务系统,拆分成小而简单但需要多个系统配合的业务系统。
从业务的角度来看,任务分解既不会减少功能,也不会减少代码量(事实上代码量可能还会增加,因为从代码内部调用改为通过服务器之间的接口调用),那为何通过任务分解就能够提升性能呢?主要有如下几方面的因素:
简单的系统更容易做到高性能
系统的功能越简单,影响性能的点就越少,就更加容易进行有针对性的优化。
可以针对单个任务进行扩展
当各个逻辑任务分解到独立的子系统后,整个系统的性能瓶颈更加容易发现,而且发现后只需要针对有瓶颈的子系统进行性能优化或者提升,不需要改动整个系统,风险会小很多。
既然将一个大一统的系统分解为多个子系统能够提升性能,那是不是划分得越细越好呢?
其实不然,这样做性能不仅不会提升,反而还会下降,最主要的原因是如果系统拆分得太细,为了完成某个业务,系统间的调用次数会呈指数级别上升,而系统间的调用通道目前都是通过网络传输的方式,性能远比系统内的函数调用要低得多。
因此,任务分解带来的性能收益是有一个度的,并不是任务分解越细越好,而对于架构设计来说,如何把握这个粒度就非常关键了。
具体如何做任务分解,可以参考读 架构设计系列3:如何设计可扩展架构 中的拆分 这个章节。
接下来,我们来简单分析一下提升性能的几种常见手段。
服务化是指通过任务分解的方式,把一个复杂的业务系统,拆分成多个小而简单且需要互相配合的业务系统。
大型复杂系统服务化的必然结果,是业务中台化。另外,单体架构并不一定是坏的架构,这取决于应用的复杂度。例如,一个初创的公司,要在互联网上开展业务,由于业务规模不大,业务复杂性有限,研发数量也不多,这个时候,单体架构就是最合适的。
服务化的目的,是将可重用的服务灵活组合在一起,来快速响应多变的业务需求,以支撑业务快速试错。
如何判断一个系统需不需要服务化呢?通常我们主要需要考虑以下几个因素:
如果一个系统存在以上几个问题,那么通过服务化来对系统进行整体技术升级和业务重构,是一个较好的途径。
组织架构调整到位
为了将系统服务化更好的落地,组织架构的调整,是非常关键的一步。
因为系统服务化以后,会延伸出来一些团队问题,例如团队的分工、协作等等。因此只有将组织架构调整到位,才能将服务化带来的好处最大化。
服务化的基础建设
服务化以后,其核心强调的是不同服务之间的通信,而这会衍生出一系列的复杂问题需要我们去解决,例如服务注册、服务发布、服务调用、负载均衡,监控等等,这需要一套完整的服务治理方案。
因此,服务化的必要条件就是要有一套服务化框架,这个服务化框架要能解决这些复杂问题,并且服务化框架的性能尤为重要。
目前主流的两种服务化框架:SpringCloud 和 Dubbo。
服务化的重要手段
一句话总结:业务解耦,能力复用,高效交付。
异步是一种设计理念,是相对同步而言的。
同步指发出一个调用时,调用方得等待这个调用返回结果才能继续往后执行。
异步指发出一个调用时,调用者不会立刻得到结果,而是可继续执行后续操作,直到被调用者处理完成后,通过状态、通知或回调来通知调用者。
通过异步可以降低时延,提升系统的整体性能,改善用户体验。
1)IO 层面的异步
针对 IO 层面的异步调用,就是我们常说的 I/O 模型,有阻塞、非阻塞和同步、异步这几种类型。
在 Linux 操作系统内核中,内置了 5 种不同的 IO 交互模式,分别是阻塞 IO、非阻塞 IO、多路复用 IO、信号驱动 IO、异步 IO。针对网络 IO 模型而言,Linux 下,使用最多性能较好的是同步非阻塞模型。
异步调用的常用技术
2)业务逻辑层面的异步
业务逻辑层面的异步流程,就是指让我们的应用程序在业务逻辑上可以异步的执行。
通常比较复杂的业务,都会有很多步骤流程,如果所有步骤都是同步的话,那么当这些步骤中有一步卡住,那么整个流程都会卡住,这样的流程显然性能不会很高。
为此,在业内,我们如果想要提高性能,提高并发,那么基本上都会采用异步流程的方式。
异步流程的常用技术
一句话总结总结:虽然异步的执行效率高,但是复杂性和编程难度也高,所以切勿滥用。
池化技术是一种常见的提高性能的技术,它将 “昂贵的”、“费时的” 的资源维护在一个特定的 “池子” 中,减少重复创建和销毁资源,方便统一管理和复用,从而提高系统性能。
通过池化技术来减少重复创建销毁所带来的系统开销,提高系统性能。
线程池
连接池
一句话总结:统一管理和复用资源,提高性能和资源利用率。
缓存是一种提高资源访问速度的技术。其特性是:一次写入,无数次读取。
缓存的本质是用空间换时间。其牺牲了数据的实时性,以内存中的缓存数据,代替从目标服务器(如DB)读取最新的数据,可减轻服务器压力和减少网络延迟。
使用缓存的目的很明显就是为了提升系统性能(高性能、高并发)。
使用缓存的优缺点是什么?
优点
缺点
1)怎么提高资源访问速度呢?
将资源存放在离用户较近或访问速度更快的系统中。
2)哪些位置可以使用缓存?
缓存分类 | 缓存维度 | 描述 |
---|---|---|
客户端缓存 | 浏览器缓存 | 1、离用户最近的缓存点,借助用户的终端设备存储网络资源,性价比最高。 2、一般用于缓存图片、JS、CSS等,可通过消息头中的Expires和Cache-Control等属性来控制。 |
服务端缓存 | CDN缓存 | 1、存放HTML,CSS,JS等静态资源。 2、起分流作用,减轻源服务器的负载。 |
服务端缓存 | 反向代理缓存 | 1、动静分离,一般缓存静态资源,动态资源转发到应用服务器处理。 |
服务端缓存 | 本地缓存 | 1、内存缓存,访问速度快,适合缓存少量数据的场景。 2、硬盘缓存,数据缓存到文件,访问速度比通过网络获取数据更快,适合缓存大量数据的场景。 |
服务端缓存 | 分布式缓存 | 1、大型网站架构中必备的架构要素。 2、缓存热点数据,减轻数据库压力。 |
3)哪种类型的缓存引入成本更高? 为什么引入成本更高?
本地缓存 和 分布式缓存的引入成本会更高。因为这两种类型缓存的资源与业务相关,需要经过业务逻辑的计算,所以对缓存与原数据之间的数据一致性要求更高。
4)1个核心指标:缓存命中率
缓存命中率越高,性能越好 。其计算公式为:缓存命中率 = 命中的次数 / (命中的次数 + 未命中的次数)。
如何提高缓存命中率?常见的策略如下:
一句话总结:如何提高缓存命中率?就是让数据更长时间的驻留在缓存中。
5)1个核心问题:缓存一致性
缓存一致性指缓存与源数据之间的一致性,要保障缓存一致性,事情将变得复杂起来。
如何实现缓存一致性呢?常用的缓存策略如下:
在Cache/DB 架构中,缓存策略就是如何从 Cache 和 DB 读取、写入数据。
1、过期缓存模式:Cache Expiry Pattern
2、旁路缓存模式:Cache Aside Pattern
3、异步写入模式:Write Behind Pattern
一句话总结:如何实现缓存一致性?就是让每个读操作,能够获得最新的写操作数据。
一句话总结:缓存是应对高并发的王者(缓存为王)。
数据存储通常是指数据以某种格式记录在计算机内部或外部存储介质上。
常见的存储介质有:磁带、磁盘等。数据存储方式因存储介质而不同。在磁带上数据仅按顺序文件方式存取;在磁盘上则可按使用要求采用顺序存取或直接存取方式。数据存储方式与数据文件组织密切相关,其关键在于建立记录的逻辑与物理顺序间对应关系,确定存储地址,以提高数据存取速度。
常见的数据存储管理系统有:数据库(MySQL)、搜索引擎(Elasticserach)、缓存系统(Redis)、消息队列(Kafka)等。这也是我们接下来探讨的重点。
在互联网时代,当系统并发量达到一定阶段时,数据存储往往会成为性能瓶颈。如果不在一开始就进行良好的设计,则后期的横向扩展,分库分表都会遇到困难。
为什么性能瓶颈往往是数据存储,而不是应用服务呢?
因为应用服务基本是无状态的,可以较为方便的进行水平扩展,因此应用服务的高性能会相对简单。但是对于数据存储的高性能,相对来说会复杂很多,因为数据是有状态的。
常见的解决存储高性能的方案有如下几种,业界大多是围绕这些来构建,或者是做相关衍生和扩展。
1)读写分离
互联网系统往往都是读多写少,因此性能优化的第一步就是读写分离。
读写分离就是将读操作与写操作分离的一种优化手段,我们可以通过这一技术来解决数据存储的性能瓶颈问题。
目前业界流行的读写分离方案,通常是基于主从模式的架构,通过引入数据访问代理层,来实现访问动作的读写分离。具体有如下两种方式:
通过独立Proxy实现读写分离
引入数据访问代理的好处是源程序不需要做任何改动就可以实现读写分离,坏处是由于多了一层中间件做中转代理,性能上会有所下降,数据访问代理也容易成为性能瓶颈,并且还存在一定维护成本。典型产品有 MyCAT、阿里云-RDS数据库代理等。
通过内嵌SDK实现读写分离
还有另一种方式,是将数据访问代理层前置到应用侧,通过SDK方式与应用集成在一起,可避免独立一层所带来的性能损耗和维护成本高的问题。但这种方式对开发语言有一定要求,存在适用性问题。典型产品有 ShardingSphere 等。
2)数据分区
“分区”是指以物理方式将数据分割成独立的数据进行存储的过程。将数据分割成分区,可以对这些分区进行单独进行管理和访问。 分区可以改善可伸缩性、减少争用,以及优化性能。 另外,它还能提供一种按使用模式来分割数据的机制。
为何要将数据分区?
如何设计分区?
数据分区的三个典型策略:
3)分库分表
对于分库分表的概念大家应该并不会陌生,其拆开来讲就是分库和分表两个手段:
分库分表的两种典型方案:
垂直拆分
水平拆分
常见的水平拆分手段
关于分库分表的更多信息见文章:开发设计实践:分库分表实现方案,此处不再赘述。
4)冷热分离
冷热分离是指将历史冷数据与当前热数据分开存储,冷库只存放那那些走到终态的数据,热库存放还需要去修改字段的数据,这样可以减轻当前热数据的存储量,可以提高性能。
如果判断数据到底是冷数据还是热数据?或者说,什么情况下可以使用冷热分离?
一句话总结:通过拆分手段,分散读写压力,分散存储压力,从而提高性能。
在追求系统高性能的同时,千万不要忽略了成本这个因素。因为,高性能往往意味着高成本。
因此在设计高性能系统时,要特别注意,成本最小化,收益最大化。
最后,作为一名技术人员,我们应该要有一个技术追求:学会用同样的资源做更多的工作。