当前位置: 首页 > 科技观察

一篇关于高性能架构和系统设计经验的文章

时间:2023-03-12 04:44:27 科技观察

高性能和高并发听起来有点像,经常会一起提到,比如提升我们的并发性能。很明显,高性能可以提高我们的并发度,但是在细节上,它们是不一样的,它们考虑的维度是不一样的。高性能需要我们从单机维度考虑到整体维度,更重要的是从编码和架构使用的角度让我们的单机(单实例)有更好的性能,进而从整个系统层面。高性能;高并发是直接从全局的角度让我们的系统在全链路下抗住更多的并发请求。1.高性能架构和系统设计的几个层次高性能架构设计主要集中在三个方面:单机优化、服务集群优化和代码优化。然而,架构层面的设计是高性能的基础。如果架构层面的设计没有做到高性能,仅仅依靠优化编码会限制整个系统的提升。我们从全局的角度来看高性能的系统设计,我们需要从以下几个方面来整体考虑:前端层面。后端优化的再好,如果前端(客户端)的性能不行,那么对于用户来说,物理体验还是很差的,所以前端层也是需要考虑的,但这不在我们文章的设计范围内,需要在实践中讨论。编码实现层面:代码逻辑分层、子模块、协程、资源复用(对象池、线程池等)、异步、IO多路复用(异步非阻塞)、并发、无锁设计、设计模式等..单机架构设计层次:IO多路复用、Reactor和Proactor架构模式系统架构设计层次:架构分层、业务分模块、集群(集中式、分布式)、缓存(多级缓存、本地缓存)、消息队列(异步)、调峰)基础设施层面:机房、机器、资源分配运维部署层面:容器化部署、弹性伸缩性能测试优化层面:性能压力测试、性能分析、性能优化好吧,如果前端的性能(client)不行,那么对于用户来说,他们的体感还是很差的,所以前端层也是需要考虑的,但是不在我们本文的设计范围内,在实际工作中需要进行讨论。这是一个简短的解释。从我个人的工作经验来看,前端(客户端)可以在这里进行优化,包括但不限于:数据预加载、数据本地缓存、业务逻辑预处理、CDN加速、请求压缩、异步处理、合并请求、longconnections,staticresources等3.编码实现层编码实现层:代码逻辑分层,子模块,协程,资源复用(对象池,线程池等),异步,IO复用(异步非阻塞))、并发、无锁设计、设计模式等。多线程、多协程大多数情况下,多进程、多线程、多协程可以极大的提升我们的并发性能,尤其是多协程。在网络框架层面,现在成熟的后端系统框架(服务框架)普遍支持多线程、多协同。所以对于网络框架,我们只需要参考比较成熟的服务框架就可以实现我们的业务,基本上不需要过多的考虑和设计。在业务层面,如果是Go语言,自然支持大并发,创建Go协程非常容易。一个go关键字就可以处理它。因此,很容易创建多个协程。Go可以创建大量的协程来提高我们的性能。并发性能。对于其他语言,我们尽量使用多协程、多线程来执行我们的业务逻辑。无锁设计(lockfree)在多线程和多协程的框架下,如果我们访问并发线程(协程)之间的共享资源,需要特别注意,要么通过加锁,要么通过无锁设计,否则,未经任何处理就访问共享资源会产生意想不到的结果。至于锁的设计,当并发量大的时候,如果锁的强度不合适,或者频繁的加锁解锁,我们的性能会严重下降。为此,在追求高性能的时候,大家都推崇无锁设计。目前,为了避免共享资源的竞争,很多后端底层设计都采用了无锁设计,尤其是在底层框架上。无锁的实现主要有两种,无锁队列和原子操作。无锁队列。无锁队列可以通过链表或者RingBuffers(循环数组)来实现。原子操作。硬件同步原语CAS用于实现各种无锁数据结构。比如Go语言中的atomic包,C++11语言中的atomic库。数据序列化为什么要说数据序列化协议呢?因为我们的系统要么通过RPC在各个后端微服务之间进行交互,要么通过HTTP/TCP协议与前端(终端)进行交互,所以不可避免的需要进行网络数据传输。而数据,只有经过序列化,才能方便网络传输。序列化是将数据结构或对象转换成二进制字符串的过程,也就是编码的过程。序列化后,数据将被转换成二进制字符串,然后可以通过网络传输;反序列化是在序列化过程中产生的。将二进制字符串转换为数据结构或对象的过程,二进制转换为对象后,业务可以进行后续的逻辑处理。常见的序列化协议如下ProtocolBuffer(PB)JSONXML内置类型(如java语言有java.io.Serializable)常见序列化协议比较网上有各种性能对比,相关截图就不贴了这里。只是结论:从性能和广泛使用的角度来看,一般推荐后端服务之间使用PB。如果和前端交互,由于HTTP协议只能支持JSON,所以一般只能使用JSON。池化技术(资源重用)池化技术是一种非常常用的提高性能的技术。池化的核心思想是重用资源,减少重复创建和销毁带来的开销。复用就是创建一个pool,然后在这个pool中统一分配和调度各种资源。他们没有在创建后释放它们,而是将它们放入池中以供重用。这样可以减少重复创建和销毁,从而提高性能。而这个资源包括了我们编程中常见的线程资源、网络连接资源、内存资源,而对应的池化技术层次就是线程池(coroutinepool)、连接池、内存池等线程池(coroutinepool)。它本质上是一个进程、线程和协程的池。首先,创建适当数量的线程(协程)并开始睡眠。然后,在需要时,从池中唤醒一个并执行业务逻辑。业务逻辑处理完后,资源并没有释放,而是放回池中休眠,等待后续的请求被唤醒,这样才能被复用。创建线程的开销是非常大的,所以如果频繁的为一个请求创建一个线程或者进程,这个请求的性能肯定不会太高。连接池。这是最常用的。一般我们要操作MySQL、Redis等存储资源。同样,我们并不是每次请求MySQL、Redis等存储时都新建一个连接来访问数据,而是在初始化时创建。池中放置了适当数量的连接。当需要连接访问数据时,从池中获取一个空闲连接来访问数据。访问完成后,连接并没有被释放,而是放回池中。连接池需要保证连接的可用性,即MySQL、Redis等连接和存储必须定时发送数据来保证连接,否则会断开连接。同时,我们需要检测并移除无效(断开连接)的连接。内存池。一般情况下,我们直接调用Linux操作系统的new、malloc等API申请内存分配,每次申请的内存块大小是可变的。因此,当我们频繁分配内存和回收内存时,会造成大量的内存碎片,而且每次使用内存都需要重新分配内存,也会降低性能。内存池就是预先分配一块足够大的内存作为我们的内存池,然后每次用户请求分配内存时,都会返回内存池中的一块空闲内存,并将这块内存的flag设置为YesUse,当内存用完释放内存时,实际上并没有调用free或者delete来释放内存,而是直接把这块内存放回内存池中,同时设置flag为free。一般业界都有相关的包来帮我们做这件事。比如在C/C++语言中,有相关的库来封装原生的malloc,glibc实现了一个ptmalloc库,Google实现了一个tcmalloc库。对象池。其实前面几类池化技术其实都可以作为对象池的各种应用,因为各种资源都可以看成是一个对象。对象池是为了避免创建大量相同类型的对象,从而进行池化,保证对象的可重用性。异步IO和异步流程异步有两个层面的意思:IO层面的异步调用和业务逻辑层面的异步流程,可以想象异步性能比同步好很多。IO层面的异步调用IO层面的异步调用就是我们常说的I/O模型,包括阻塞、非阻塞、同步、异步。在Linux操作系统内核中,内置了五种不同的IO交互方式,分别是阻塞IO、非阻塞IO、多路复用IO、信号驱动IO和异步IO。就网络IO模型而言,Linux下使用最多的是同步非阻塞模型,性能较好,具体以AIO为代表,而Windows下的代表作IOCP,实现了真正的异步非阻塞I/O.业务逻辑层的异步流程业务逻辑层的异步流程是指我们的应用在业务逻辑上可以异步执行。通常一个更复杂的业务在这个过程中会有很多步骤。如果所有步骤都是同步的,那么当其中一个步骤卡住时,整个流程就会卡住。显然,这样的过程不会执行得很好。为此,在业界,如果要提高性能和并发,基本上都会使用异步流程。举一个实际的应用案例,对于IM系统发送消息的场景,比如微信发送消息,当客户端发送的消息被服务端接收到时,消息必须存储在地面上,发送过程可以完成,但是如果服务端真的把每条消息都存到DB里,然后返回给客户端说接收正确了,那么这个性能显然会很低,因为我们知道写入DB的性能很low,尤其是像微信这样每天都有很多新闻的App。那么这个过程可以是异步的。服务端收到消息后,先将消息写入消息队列,写入成功后返回给客户端。然后异步进程从消息队列中消费数据并将其存储在数据库中。这样一来性能就非常高了,因为消息队列的性能会非常高。性能相对较低的操作被异步处理。并发进程并发进程也是针对我们上层应用的。我们在处理业务逻辑,尤其是相对负责的业务逻辑的时候,一般下游可能会有多个请求,或者多个流程。如果多个依赖的下游请求之间没有强依赖关系,那么我们可以并发处理这些请求的流程。这是后端系统设计中非常常见的优化方法。通过并发处理流程,可以将耗时的串行叠加处理优化为单次处理耗时,大大降低了整体耗时。例如,对于商品活动页面,渲染的数据包括用户基本信息、用户活动积分、用户推荐商品列表。那么当我们收到用户的请求时,我们需要查询用户的基本信息、用户的活动积分、用户的产品推荐,而这三个步骤是完全独立的,所以我们可以并发发送。单独查询,可以大大减少耗时,提高我们的性能。4.单机架构设计层面的单机优化要点单机优化层面是尽可能提高单机的性能。单机性能最大化的一个关键点就是我们的服务器采用的并发模型,然后在这个模型下,设计好我们的服务器来管理连接和处理请求。而这些涉及到我们的多协程、多线程进程模型和异步非阻塞、同步非阻塞IO模型。在具体的实现细节上,对于连接管理,如果要提高性能,就必须使用IO多路复用技术。可以参考I/OMultiplexing查看。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多路复用,我们整个网络框架也需要配合pooling技术来提升我们的性能。因此业界普遍采用I/O多路复用+线程池(协程池、进程池)来提升性能。相应的,业界常用的两种单机高性能架构模式是Reactor和Proactor模式。Reactor模式属于非阻塞同步网络模型,Proactor模式属于非阻塞异步网络模型。在业界的开源软件中,Redis采用的是单反应器单进程的方式,Memcache采用的是多反应器多线程的方式,Nginx采用的是多反应器多进程的方式。详细介绍见Reactor的设计与实现。Redis可以使用单进程Reactor模式,因为Redis的应用场景是内部访问,并发数一般不超过1w,而Nginx必须使用多进程Reactor模式,因为Nginx是从外网访问的,并发数并发量很容易超过1w,所以我们的网络架构模型必须通过I/O多路复用+线程池(协程池、进程池)来协调。可见,单机优化层面其实与编码层面的多协程、异步IO、池化技术强相关。这也是知识共享的典型例子。我们学习的一些基础知识点在架构层面和模型层面都有用。五、系统架构设计层面架构设计层面:架构分层、业务分模块、集群(集中式、分布式)、缓存(多级缓存、本地缓存)、消息队列(异步、调峰)架构及模块划分设计整个系统要有高性能,首先要有合理的架构设计。这里我们需要按照一些架构设计原则来构建我们的架构,比如高内聚低耦合,单一职责等。最有效的方法包括架构分层设计和业务子模块设计。这样设计之后,在系统整体性能优化方面,后面会有比较大的优化空间,这样后面想优化,就无从下手,只能重构系统。面向服务框架的设计在现在的互联网时代,我们基本上都是用微服务来搭建我们的系统,微服务的必要条件就是有一套面向服务的框架。这个面向服务的框架的核心功能包括RPC请求和最基本的服务治理策略(服务注册与发现、负载均衡等)。为此,这里面向服务框架的性能就显得尤为重要,主要包括在这个面向服务框架中的实现:数据处理。数据序列化协议一般使用PB协议,在性能和维护方面都是最优的。数据压缩,一般采用gzip压缩,压缩后可以减少数据在网络上的传输。网络模型。不管是同步过程还是异步过程,如果是Go语言,那么一个请求就可以用协程来处理。是否有连接池能力。其他一些优化。负载均衡负载均衡系统是横向扩展的关键技术。通过负载均衡,相当于把流量分配到不同机器上的不同服务实例,让每个服务实例可以承担一部分请求,这样可以提高我们的整体性能。系统性能。对于负载均衡的方式,大部分都是通过客户端发现方式(client-side)来实现服务路径和负载均衡,一般都支持常见的负载均衡策略,比如random,round-robin,hash,weight,连接数【连接数越少,优先级越高】。各种队列的合理使用在后端系统的设计中,很多流程和请求都不需要实时处理,更不用说强一致性了。大多数情况下,我们只需要实现最终一致性即可。因此,通过队列,我们??可以让我们的系统实现异步处理逻辑、流程调峰、业务模块解耦、灵活事务等各种效果,从而达到最终一致性,大大提高我们系统的性能。表现。我们常见的队列包括消息队列:使用最广泛的队列之一,代表作有RabbitMQ、RocketMQ、Kafka等,可以用来实现异步逻辑、削峰、解耦等各种效果。这样可以大大提高我们延迟队列的性能:延迟队列与普通队列最大的区别就体现在它的delay属性上。延迟队列中的元素在入队时会指定一个延迟时间,表示希望在指定的时间过后被处理。延迟队列的目的是为了异步处理。延迟队列的应用场景其实非常广泛,比如以下场景:到期后自动执行指定的操作。在指定时间之前自动执行一些动作,检查任务是否完成,如果没有完成,等待一定时间再次查询回调通知。回调失败时等待重试任务队列:将任务提交到队列中异步执行,最常见的是线程池的任务队列。各级缓存的设计分布式缓存分布式缓存的代表作有Redis和Memcache。通过分布式缓存,我们可以不直接读取数据库,而是读取缓存获取数据,这样可以大大提高我们读取数据的性能。总的来说就是多读少写。因此,它是提高我们整体绩效的一种非常有效的手段,是一种必要的手段。本地缓存本地缓存可以从几个维度来看:客户端本地缓存:对于一些不经常变化的数据,客户端也可以缓存起来,这样就可以避免请求后端,可以提高本地缓存的性能后端服务:在后端服务中,一般使用分布式缓存。但是在某些场景下,如果我们的数据量比较小,我们可以直接在进程中缓存数据,这样就可以直接通过内存来读取,而不是耗时的网络。性能会更高。但是本地缓存一般只会缓存少量的数据。数据太多不合适。多级缓存多级缓存是一种更高级的缓存架构设计。比如最简单的模式可以是本地缓存+分布式缓存,组成多级缓存架构。我们把所有需要缓存的数据放到分布式缓存中,然后将少量的热点缓存放到本地缓存中,这样大部分热点数据可以直接从本地读取,而其他非热点数据仍然通过分布式缓存进行分发。缓存读取,可以大大提高我们的性能,提高并发性。比如在电商系统中,我们做一个活动页面。活动页面前10个商品为特价商品,后面的其他商品为普通商品。因为是活动页面,所以这个页面的访问量肯定会很大。.活动页面的前10个商品必须是用户先进入页面才能看到的,如果用户想继续查看其他商品,需要在手机上手动向上滑动刷新。这种场景下,前10个商品无疑是访问量最大的,用户手动上滑刷新后的请求会少很多。为此,我们可以将全量的商品缓存在redis等分布式缓存中,然后在此基础上,将前10个商品的信息缓存到本地,这样活动启动时,第一页拉取第10个商品数据都是从本地缓存拉取的,本地读取性能会很高,因为内存读取就可以了,完全不需要网络交互。其他模式可以是本地缓存+二级分布式缓存+一级分布式缓存,也就是对分布式缓存多一层分级,让每一级缓存都可以抵挡一部分的量,所以总体来说,可以提供的外部是足够高的。缓存预热是通过异步任务,将需要大量访问的数据提前预热到我们的缓存中。这样当突然出现请求高峰时,可以从容应对。除了Redis和本地缓存,其他的高性能NoSQL,MongoDB和Elasticserach也是NoSQL中常见的高性能组件。我们可以根据适用场景合理选择。比如在我们的电商系统中,我们使用Elasticserach来实现商品的搜索和推荐。存储设计数据分区数据分区就是将数据按照一定的方式(比如按地理位置)划分到多个区域,不同的数据区域共享不同区域的流量。这需要一个中间件进行数据路由,但是会导致跨库的Join和跨库的事务非常复杂。将数据分布到多个分区有两种典型的方案:基于键进行散列,以及基于散列值选择相应的数据节点。根据范围划分,某个连续的键存储在一个数据节点上。一般来说,影响数据库最大的性能问题有两个,一个是数据库的操作,另一个是数据库中数据的大小。对于前者,我们需要从业务角度进行优化。一方面,为了简化业务,不要对数据库做太多的关联查询,对于一些比较复杂的数据库操作,比如报表或者搜索,应该移到更合适的地方。比如用ElasticSearch做查询,用Hadoop或者其他数据分析软件做报表分析。对于后者,一般是拆分的。Sharding技术,有些地方也叫Sharding或sharding,可以通过分片来提高我们的读写性能。),一般按业务功能模块划分,分库后部署到不同的库中。分库是为了提高并发能力。比如大量的读写请求,需要分库。水平切分(分表),当一个表的数据量太大时,我们可以通过各种ID的hash来对表中的数据进行分割,比如用户ID和订单ID的hash。分表更多的是处理性能问题,比如查询慢的问题。一般情况下,单表的各项性能会在千万级之后开始下降,所以需要考虑开始分表。表拆分包括垂直拆分和水平拆分,而分区只能起到水平拆分的作用。读写分离大部分互联网系统都是读多写少,所以读写分离可以帮助主库抗负载。读写分离,就是把读请求卷改到从库,写还是由主库来承担。一般我们都是一主多从的架构,这样既能抗体积,又能保证数据不丢失。冷热分离对于业务场景,如果将数据进行冷热分离,可以将历史冷数据与当前热数据分开存储,可以减少当前热数据的存储量,提高性能。我们常用的存储系统MySQL、Elasticserach等都可以支持。分布式数据库分布式数据库的基本思想是将原来集中式数据库中的数据分散存储在通过网络连接的多个数据存储节点上,以获得更大的存储容量和更高的并发访问,从而提高我们的性能。现在传统的关系型数据库已经开始从集中式模型向分布式架构发展。一般的云服务厂商都会提供分布式数据库解决方案,比如腾讯云的TDSQLMySQL版,TDSQLforMySQL是腾讯打造的分布式数据库产品,具有强一致性高可用、全球部署架构、分布式水平扩展、高性能、企业级级安全等特性,同时提供智能DBA、自动化运维、监控告警等配套设施,为客户提供完整的分布式数据库解决方案。六、基础设施层面基础设施层面大致分为3大部分:机房层面,主要关注机房的网络出口带宽和入口带宽。这个一般我们业务开发是接触不到的,但是这里还是需要注意一下。如果机房的带宽不够,那么我们的服务就无法支持大并发,我们的系统也就无法有很好的性能。在机器配置层面,服务器本身的性能必须足够好,包括CPU、内存、磁盘(SSD)等资源。同样的,一般我们的业务开发是接触不到的,但是如果机器配置不好,那么我们部署在这样的机器上的服务就不能得到充分的利用,这样我们的业务系统就无法有很好的性能。在资源使用方面,我们需要合理分配CPU和内存等相关资源。一般CPU使用率不应超过70%-80%。超过这个阈值后,我们的服务性能就会开始下降。是时候开始扩大规模了。如果是K8s容器部署,我们可以设置CPU使用率达到指定阈值后自动扩容。当然,如果是部署在物理机上或者其他方式,也可以及时监控和扩容。也就是说,我们必须保证我们所需要的各种资源(CPU、内存、磁盘、带宽)都在一个合理的范围内。7、运维部署层面。做好运维部署层面,有助于提升我们系统的整体性能。比如我们可以通过容器化部署实现弹性伸缩。通过弹性伸缩的能力,我们的服务可以在资源分配和使用上始终保持合理的CPU、内存等资源利用率。8.性能测试优化层面当我们按照高性能方案和思路从架构设计和编码实现层面实现我们的系统后,理论上我们的系统性能不会太差,但是我们的系统性能怎么样呢?有优化点吗?代码的实现是否存在性能问题?我们的依赖服务是否存在性能问题?等等,对于我们大多数人来说,如果没有合理的性能压测和分析,那么它可能还是一个黑盒子。所以对于我们研发人员来说,高性能架构设计的最后一个环节就是进行性能测试优化,具体包括三个环节:性能压力测试。先对系统的各个环节进行压力测试,如果条件允许,最好做全链路压力测试。性能分析。压测之后,优化的分析方法是用火焰图分析,看哪里性能最差,有没有可以优化的点。我们首先要找到最差的性能,然后对其进行优化,这样才能事半功倍。性能优化。找到最优点后,对其进行优化。然后重复这三个步骤,直到您认为性能完全符合预期。本文转载自微信公众号《后端系统与架构》,作者“AllenWu”,可通过以下二维码关注。转载本文请联系“后端系统与架构”公众号。