高性能
【⚠️未整理】
来源:https://cloud.tencent.com/developer/article/2230715
前言
高性能和高并发,听着就有点类似,并且他们还经常一起提及,比如提高我们的并发性能,显然,高性能可以提高我们的并发,但是细化来看,他们是有区别的,他们的考量点的维度不同。高性能需要我们从单机维度到整体维度去考虑,更多的是先从编码角度、架构使用角度去让我们的单机(单实例)有更好的性能,然后再从整个系统层面来拥有更好的性能;高并发则直接是全局角度来让我们的系统在全链路下都能够抗住更多的并发请求。高性能架构设计主要集中在单机优化、服务集群优化、编码优化三方面。但架构层面的设计是高性能的基础,如果架构层面的设计没有做到高性能,仅依靠优化编码,对整体系统的提升是有限的。我们从一个全局角度来看高性能的系统设计,需要整体考虑的包括如下几个层面:● • 前端层面。后端优化的再好,如果前端(客户端)的性能不 ok,那么对用户而言,他们的体感还是很差的,因此前端层也是有必要考虑的,只是不在我们本文的设计范围之内,在实际工作中是需要进行探讨的。● • 编码实现层面:代码逻辑的分层、分模块、协程、资源复用(对象池,线程池等)、异步、IO 多路复用(异步非阻塞)、并发、无锁设计、设计模式等。● • 单机架构设计层面:IO 多路复用、Reactor 和 Proactor 架构模式● • 系统架构设计层面:架构分层、业务分模块、集群(集中式、分布式)、缓存(多级缓存、本地缓存)、消息队列(异步、削峰)● • 基础建设层面:机房、机器、资源分配● • 运维部署层面:容器化部署、弹性伸缩● • 性能测试优化层面:性能压测、性能分析、性能优化
1. 前端层面
后端优化的再好,如果前端(客户端)的性能不 ok,那么对用户而言,他们的体感还是很差的,因此前端层也是有必要考虑的,只是不在我们本文的设计范围之内,在实际工作中是需要进行探讨的。这里简单说明下,从我个人工作的经历来看,前端(客户端)这里可以优化的点包括但不限于:数据预加载、数据本地缓存、业务逻辑前置处理、CDN 加速、请求压缩、异步处理、合并请求、长连接、静态资源等
CDN加速
当我们的系统中存在着大量的静态资源请求:对于移动 APP 来说,这些静态资源主要是图片、视频和流媒体信息;对于 Web 网站来说,则包括了 JavaScript 文件、CSS 文件、静态 HTML 文件等等。最常用的静态资源加速手段就是采用CDN。CDN加速是一种网络技术,旨在通过分布式网络节点将内容快速、高效地分发给用户。CDN加速的核心价值在于提高用户访问网站或应用时的速度和体验。它通过全球分布的服务器网络来缓存内容,将数据存储在用户附近的地理位置,从而缩短数据传输距离和时间。使用CDN可以减少网站的加载时间,提高用户的满意度和留存率。
2. 编码实现层面
批量处理
无论是读请求或者写请求,如果接口支持批量查询或者批量写操作,都可以减少RPC调用的次数,减少网络开销,从而优化整体的性能,所以在提供接口的时候一般要考虑提供可以批量查询或者批量写入的接口。
无锁设计(lock free)
在多线程、多协程的框架下,如果我们并发的线程(协程)之间访问共享资源,那么需要特别注意,要么通过加锁、要么通过无锁化设计,否则没有任何处理的访问共享资源会产生意想不到的结果。而加锁的设计,在并发较大的时候,如果锁的力度不合适,或者频繁的加锁解锁,又会使我们的性能严重下降。为此,在追求高性能的时候,大家就比较推崇无锁化的设计。目前很多后台底层设计,为了避免共享资源的竞争,都采用了无锁化设计,特别是在底层框架上。无锁化主要有两种实现,无锁队列和原子操作。● • 无锁队列。可以通过 链表或者 RingBuffer(循环数组)来实现无锁队列。● • 原子操作。利用硬件同步原语 CAS 来实现各种无锁的数据结构。比如 Go 语言中的 atomic 包、C++11 语言中的 atomic 库。
数据序列化
为什么要说 数据序列化协议?因为我们的系统,要么就是各个后端微服务之间通过 RPC 做交互,要么就是通过 HTTP/TCP 协议和前端(终端)做交互,因此不可避免的需要我们进行网络数据传输。而数据,只有序列化后,才方便进行网络传输。序列化就是将数据结构或对象转换成二进制串的过程,也就是编码的过程,序列化后,会把数据转换为二进制串,然后可以进行网络传输;反序列化就是在序列化过程中所生成的二进制串转换成数据结构或者对象的过程,将二进制转换为对象后业务才好进行后续的逻辑处理。常见的序列化协议如下● • Protocol Buffer(PB)● • JSON● • XML● • 内置类型(如 java 语言就有 java.io.Serializable)常见的序列化协议的对比在网上有各种性能的对比,这里就不在贴相关截图了,只说结论:从性能上和使用广泛度上来看,后端服务之间现在一般推荐使用 PB。如果和前端交互,由于 HTTP 协议只能支持 JSON,因此一般只能 JSON。
池化技术(资源复用)
池化技术是非常常见的一个提高性能的技术,池化的核心思想就是对资源进行复用,减少重复创建销毁所带来的开销。复用就是创建一个池子,然后再在这个池子里面对各种资源进行统一分配和调度,不是创建后就释放,而是统一放到池子里面来复用,这样可以减少重复创建和销毁,从而提高性能。而这个资源就包括我们编程中常见到如 线程资源、网络连接资源、内存资源,具体到对应的池化技术层面就是 线程池(协程池)、连接池、内存池等。● • 线程池(协程池)。本质都是进程、线程、协程这些维度的一个池子,先创建合适数量的线程(协程)并且初始处于休眠状态,然后当需要用到的时候,从池子里面唤醒一个,然后执行业务逻辑,处理完业务逻辑后,资源并不释放,而是直接放回池子里面休眠,等待后续的请求被唤醒,这样重复利用。○ • 创建线程的开销是很大的,因此如果来一个请求就频创建一个线程、进程,那么请求的性能肯定不会太高。● • 连接池。这个是最常用的,一般我们都要操作 MySQL、Redis 等存储资源,同样的,我们并不是每次请求 MySQL、Redis 等存储的时候就新创建一个连接去访问数据,而是初始化的时候就创建合适数量的连接放到池子里面,当需要连接去访问数据的时候,从池子里面获取一个空闲的连接去访问数据,访问完了之后不释放连接,而是放回池子里面。○ • 连接池需要保证连接的可用性,就是这个连接和 MySQL、Redis 等存储是必须要定期发送数据来保证连接的,要不然会被断开。同时我们要针对已经失效(断开)的连接进行检测和摘除。● • 内存池。常规的情况下,我们都是直接调用 new、malloc 等 Linux 操作系统的 API 来申请分配内存,而每次申请的内存块的大小不定,所以,当我们频繁 分配内存、回收内存的时候,会造成大量的内存碎片,同时每次使用内存都要重新分配也会降低性能。内存池就是先预先分配足够大的一块内存,当做我们的内存池,然后每次用户请求分配内存的时候,就会返回内存池中的一块空闲的内存,并将这块内存的标志置为已使用,当内存使用完毕释放内存的时候,也不是真正地调用 free 或 delete 来释放内存,而是把这块内存直接放回内存池内并且同时把标志置为空闲。一般业内都有相关的套件来帮我们来做这个事情,比如在 C/C++ 语言里面,都有相关库去封装原生的 malloc,glibc 实现了一个 ptmalloc 库,Google 实现了一个 tcmalloc 库。● • 对象池。其实前面几种类型的池化技术,其实都可以作为对象池的各种应用,因为各种资源都可以当做一个对象。对象池就是避免大量创建同一个类型的对象,从而进行池化,保证对象的可复用性。
异步化
对于处理耗时的任务,如果采用同步等待的方式,会严重降低系统的吞吐量,可以通过异步化进行解决。特别是对非关键依赖模块进行异步化处理,可以大大的提升系统的并行处理能力和系统吞吐量。而异步化最常用的方案是采用消息队列,无论是本地的消息队列或者消息队列组件都是异步化处理的常用方案,特别对于提高写入性能具有比较明显的作用。
并发流程
如果一个任务需要处理多个子任务,可以将没有依赖关系的子任务并发化,这种场景在后台开发很常见。如一个请求需要查询3个数据,分别耗时T1、T2、T3,如果串行调用总耗时T=T1+T2+T3。对三个任务执行并发,总耗时T=max(T1,T 2,T3)。该方案无论读写请求都适用。
3. 单机架构设计层面
单机优化层面就是要尽量提升单机的性能,将单机的性能发挥到极致的其中一个关键点就是我们服务器采取的并发模型,然后在这个模型下,去设计好我们的服务器对连接的管理、对请求的处理流程。而这些就涉及到我们的多协程、多线程的进程模型和异步非阻塞、同步非阻塞的 IO 模型。在具体实现细节上,针对连接的管理,要想提高性能,那么就要采用 IO 多路复用技术,可以参考I/O Multiplexing查看,I/O 多路复用技术的两个关键点在于:● • 当多条连接共用一个阻塞对象后,进程只需要在一个阻塞对象上等待,而无须再轮询所有连接,常见的实现方式有 select、epoll、kqueue 等。● • 当某条连接有新的数据可以处理时,操作系统会通知进程,进程从阻塞状态返回,开始进行业务处理。
IO 多路复用(epoll 模型)
基本上来说,异步 I/O 模型的发展技术是:select -> poll -> epoll -> aio -> libevent -> libuv。而且现在大家比较熟悉和使用的最多的恐怕就是 epoll 和 aio ,尤其是 epoll 模型,基本是 Linux 后端系统下的大部分框架和软件都是采用 epoll 模型。但是,需要特别强调的是,仅仅依靠 epoll 不是万能的,连接数太多的时候单进程的 epoll 也是不行的。
Reactor 和 Proactor 架构模式
epoll 只是一个 IO 多路复用的模型,在后端系统设计里面,要想实现单机的高性能,那在 IO 多路复用基础之上,我们的整个网络框架,还需要配合池化技术来提高我们的性能。因此,业界一般都是采用 I/O 多路复用 + 线程池(协程池、进程池)的方式来提高性能。与之对应的,在业界常用的两个单机高性能的架构模式就是Reactor 和 Proactor 模式。Reactor 模式属于非阻塞同步网络模型,Proactor 模式属于非阻塞异步网络模型。在业内开源软件里面,Redis 采用的是 单 Reactor 单进程的方式,Memcache 采用的是 多 Reactor 多线程的方式,Nginx 采用的是多 Reactor 多进程的方式。关于 的详细介绍,可以查看The Design and Implementation of the Reactor。Redis 可以用单进程 Reactor 模式的是因为 Redis 的应用场景是内部访问,并发数一般不会超过 1w,而 Nginx 必须用多进程 Reactor 模式是因为 Nginx 是外网访问,并发数很容易超过 1w,因此我们的网络架构模式,必须要通过 I/O 多路复用 + 线程池(协程池、进程池)来配合。可以看到,单机优化层面其实和编码层面上的多协程、异步 IO、 池化技术都是有强关联的。这里也是一个知识相通的典型,我们所学的一些基础层面的知识点,在架构层面、模型层面都是有用武之地的。
4. 系统架构设计层面
架构设计层面:架构分层、业务分模块、集群(集中式、分布式)、缓存(多级缓存、本地缓存)、消息队列(异步、削峰)
架构和模块划分的设计
整个系统想要有一个高性能,那么首先就需要有个合理的架构设计,这里需要根据一些架构设计原则,比如高内聚低耦合,职责单一等来去构建我们的架构。最有效的方式包括架构分层设计、业务分模块设计。这么设计之后,在整体的系统性能优化上,后面就会有比较大的优化空间,从而不至于后面想要优化就根本无从下手,只能重构系统。
服务化框架的设计
目前的互联网时代,我们基本上都是采用微服务来搭建我们的系统,而微服务化的必要条件就是要有一套服务化框架,这个服务化框架最核心的功能包括 RPC 请求和最基础的服务治理策略(服务注册和发现、负载均衡等)。为此,这里服务化框架的性能就尤为重要,这里主要包括这个服务化框架里面实现:● • 数据处理。○ • 数据序列化协议,一般有些采用 PB 协议,不管是从性能还是维护都是最优的。○ • 数据压缩,一般采用 gzip 压缩,压缩后可以减少网络上的数据传输。● • 网络模型。○ • 同步还是异步流程,如果是 Go 语言,那么可以来一个请求 go 一个协程来处理。○ • 是否有相关连接池的能力。○ • 其他的一些优化。
计算高性能
算性能的优化可以先从单机性能优化开始,多进程、多线程、IO多路复用、异步IO等都存在很多可以优化的地方,但基本系统或框架已经提供了基本的优化能力,只需使用即可。
负载均衡
如果单机的性能优化已经到了瓶颈,无法应对业务的增长,就会开始增加服务器,构建集群。对于计算来说,每一台服务器接到同样的输入,都应该返回同样的输出,当服务器从单台变成多台之后,就会面临请求来了要由哪一台服务器处理的问题,我们当然希望由当前比较空闲的服务器去处理新的请求,这里对请求任务的处理分配问题,就叫负载均衡。负载均衡系统是水平扩展的关键技术,通过负载均衡,相当于可以把流量分散到不同的机器的不同的服务实例里面,这样每个服务实例都可以承担一部分请求,从而可以提高我们的整体系统的性能。对于负载均衡的方式,大都是在客户端发现模式(client-side) 来实现服务路和负载均衡,一般也都会支持常见的负载均衡策略,如随机,轮训,hash,权重,连接数【连接数越少,优先级越高】。
消息队列
在后端系统设计里面,很多流程和请求并不要求实时处理,更不需要做到强一致,大部分情况下,我们只需要实现最终一致性就可以了。故而,我们通过队列,就可以使我们的系统能够实现异步处理逻辑、流程削峰、业务模块解耦、柔性事务等多种效果,从而可以完成最终一致性,并且能够极大的提高我们系统的性能。我们常见的队列包括● • 消息队列:使用的最为广泛的队列之一,代表作有 RabbitMQ、RocketMQ、Kafka 等。可以用来实现异步逻辑、削峰、解耦等多种效果。从而可以极大的提高我们的性能● • 延迟队列:延时队列相比于普通队列最大的区别就体现在其延时的属性上,普通队列的元素是先进先出,按入队顺序进行处理,而延时队列中的元素在入队时会指定一个延迟时间,表示其希望能够在经过该指定时间后处理。延迟队列的目的是为了异步处理。延迟队列的应用场景其实也非常的广泛,比如说以下的场景:○ • 到期后自动执行指定操作。○ • 在指定时间之前自动执行某些动作○ • 查询某个任务是否完成,未完成等待一定时间再次查询○ • 回调通知,当回调失败时,等待后重试● • 任务队列:将任务提交到队列中异步执行,最常见的就是线程池的任务队列。
各级缓存的设计
分布式缓存
分布式缓存的代表作有 Redis、Memcache。通过分布式缓存,我们可以不直接读数据库,而是读取缓存来获取数据,可以极大的提高我们读数据的性能。而一般的业务都是读多写少,因此,对我们的整体性能的提高是非常有效的手段,而且是必须的手段。
本地缓存
本地缓存可以从几个维度来看:● • 客户端的本地缓存:针对一些不常改变的数据,客户端也可以缓存,这样就可以避免请求后端,从而可以改善性能● • 后端服务的本地缓存:后端服务中,一般都会采用分布式缓存,但是,有些场景下,如果我们的数据量比较小,那么可以直接将这些数据缓存到进程里面,这样直接通过内存读取,而不用网络耗时,性能会更高。但是本地缓存一般只会缓存少量数据。数据量太大就不合适。
多级缓存
多级缓存的出现是为了解决单一缓存层级无法同时满足快速访问和大容量存储需求的问题。基于本地缓存和分布式缓存各自的优缺点,多级缓存通常包含一级本地缓存和二级分布式缓存。当用户获取数据时,先从一级缓存中获取数据,如果未命中则从二级缓存获取,最后才会访问数据库。可以极大提高性能。举个例子,电商系统里面,我们做一个活动页,活动页的前面 10 个商品是特卖商品,然后后面的其他商品就是常规商品,因为是活动页面,那么这个页面的访问肯定就会非常大。而活动页面的前 10 个商品,必然是用户首先进来页面就一定会看到的,而用户想要继续看其他商品,那么就需要在手机上手动上滑刷新一下。这个场景下,前面 10 个商品的访问量无疑是最大的,而用户手动上滑刷新后的请求就会少很多。为此,我们可以把全量商品都缓存在分布式缓存如 redis 里面,然后再在这个基础之上,把前面 10 个商品的信息缓存到本地,这样,当活动开始后,拉取的第一页 10 个商品数据,都是从本地缓存拉取的,本地读取性能会非常高,因为内存读取就行,完全不需要网络交互。其他的模式,可以 本地缓存 + 二级分布式缓存 + 一级分布式缓存,也就是针对分布式缓存再做一层分级,这样每一级的缓存都能抗一部分的量,因此整体来看,能够对外提供的性能就足够高。
缓存预热
通过异步任务提前将接下来要大量访问的数据预热到我们缓存里面。这样当有请求的突峰的时候,可以从容应对。
NoSQL
除了 Redis、本地缓存这些,其他的一些 NoSQL 中,MongoDB、Elasticserach 也是常见的性能很高的组件,我们可以根据适用场景,合理选用。比如我们在电商系统里面,我们针对商品的搜索、推荐都是采用 Elasticserach 来实现。关系型数据库也有其无法规避的缺点,比如无法直接存储某种结构化的数据、扩展表结构时会锁表影响线上性能、大数据场景下I/O较高、全文搜索的功能比较弱等。基于这些缺点,也有很多新的数据库框架被创造出来,解决其某方面的问题。比如以Redis为代表的的KV存储,可以解决无法存储结构化数据的问题;以MongoDB为代表的的文档数据库可以解决扩展表结构被强Schema约束的问题;以HBase为代表的的列式数据库可以解决大数据场景下的I/O问题;以ES为代表的的全文搜索引擎可以解决全文检索效率的问题等。这些数据库统称为NoSQL数据库,但NoSQL并不是全都不能写SQL,而是Not Only SQL的意思。
存储的设计(存储高性能)
数据分区
数据分区是把数据按一定的方式分成多个区(比如通过地理位置),不同的数据区来分担不同区的流量,这需要一个数据路由的中间件,但会导致跨库的 Join 和跨库的事务非常复杂。将数据分布到多个分区有两种比较典型的方案:● • 根据键做哈希,根据哈希值选择对应的数据节点。● • 根据范围分区,某一段连续的键都保存在一个数据节点上。
分库分表
一般来说,影响数据库最大的性能问题有两个,一个是对数据库的操作,一个是数据库中数据的大小。对于前者,我们需要从业务上来优化。一方面,简化业务,不要在数据库上做太多的关联查询,而对于一些更为复杂的用于做报表或是搜索的数据库操作,应该把其移到更适合的地方。比如,用 ElasticSearch 来做查询,用 Hadoop 或别的数据分析软件来做报表分析。对于后者,一般就是拆分。分库分表技术,有些地方也称为 Sharding、分片,通过分库分表可以提高我们的读写性能,分库分表有垂直切分和水平切分两种:● • 垂直切分(分库),一般按照业务功能模块来划分,分库后分表部署到不同的库上。分库是为了提高并发能力,比如读写请求量大就需要分库。● • 水平切分(分表),当一个表中的数据量过大时,我们可以把该表的数据通过各种 ID 的 hash 散列来划分,比如 用户 ID、订单 ID 的 hash。分表更多的是应对性能问题,比如查询慢的问题。单表一般情况下,千万级别后各种性能就开始下降了,就要考虑开始分表了。分表包括垂直切分和水平切分,而分区只能起到水平切分的作用。
读写分离
互联网系统大多数都是读多写少,因此读写分离可以帮助主库抗量,读写分离就是将读的请求量改为从库承担,写还是主库来承担。一般我们都是一主多从的架构,既可以抗量,又可以保证数据不丢。
冷热分离
针对业务场景而言,如果数据有冷热之分的话,可以将历史冷数据与当前热数据分开存储,这样可以减轻当前热数据的存储量,可以提高性能。我们常见的存储系统比如 MySQL、Elasticserach 等都可以支持。
分布式数据库
分布式数据库的基本思想是将原来集中式数据库中的数据分散存储到多个通过网络连接的数据存储节点上,以获取更大的存储容量和更高的并发访问量,从而提高我们的性能。现在传统的关系型数据库已经开始从集中式模型向分布式架构发展了。一般云服务厂商,都会提供分布式数据库的解决方案,比如腾讯云的 TDSQL MySQL 版,TDSQL for MySQL 是腾讯打造的一款分布式数据库产品,具备强一致高可用、全球部署架构、分布式水平扩展、高性能、企业级安全等特性,同时提供智能 DBA、自动化运营、监控告警等配套设施,为客户提供完整的分布式数据库解决方案。
5. 基础建设层面
基础建设层面,大体分为 3 大块:● 机房层面,主要关注机房的网络出口带宽、入口带宽。一般这个对我们业务开发来说,都接触不到,但是这里还是需要注意,如果机房带宽不够,那么我们的服务就支撑不了大的并发,从而也没法让我们的系统有一个好的性能。● 机器配置层面,服务器本身的性能要足够好,包括 CPU、内存、磁盘(SSD)等资源。同理,一般这个对我们业务开发来说,都接触不到,但是如果机器配置较差,那么我们的服务部署在这样的机器上面,也无法充分发挥,从而使得我们的业系统也无法拥有一个好的性能。● 资源使用层面,我们要合理的分配 CPU 和内存等相关资源,一般 CPU 的使用率不要超过 70%-80%,超过这个阈值后,我们服务的性能就会开始下降,因此一般我们在 70% 的时候就要开始执行扩容。如果是 K8s 容器部署的话,我们可以设置 CPU 使用率超过指定阈值后就自动扩容。当然,如果是物理机部署,或者其他方式,可以同样的进行监控和及时扩容。也就是说,要保证我们所需的各种资源(CPU、内存、磁盘、带宽)都在一个合理的范围。
6. 运维部署层面
在运维部署层面做好相关建设,是有助于提高我们系统的整体性能的。比如,我们可以通过容器化部署做到弹性伸缩,通过弹性伸缩的能力,可以使得我们的服务,在资源分配使用上,一直保持合理的 CPU、内存等资源的使用率。\
最后更新于