高性能和高并发,听着就有点类似,并且他们还经常一起提及,比如提高我们的并发性能,显然,高性能可以提高我们的并发,但是细化来看,他们是有区别的,他们的考量点的维度不同。高性能需要我们从单机维度到整体维度去考虑,更多的是先从编码角度、架构使用角度去让我们的单机(单实例)有更好的性能,然后再从整个系统层面来拥有更好的性能;高并发则直接是全局角度来让我们的系统在全链路下都能够抗住更多的并发请求。
高性能架构设计主要集中在单机优化、服务集群优化、编码优化三方面。但架构层面的设计是高性能的基础,如果架构层面的设计没有做到高性能,仅依靠优化编码,对整体系统的提升是有限的。我们从一个全局角度来看高性能的系统设计,需要整体考虑的包括如下几个层面:
后端优化的再好,如果前端(客户端)的性能不 ok,那么对用户而言,他们的体感还是很差的,因此前端层也是有必要考虑的,只是不在我们本文的设计范围之内,在实际工作中是需要进行探讨的。
这里简单说明下,从我个人工作的经历来看,前端(客户端)这里可以优化的点包括但不限于:数据预加载、数据本地缓存、业务逻辑前置处理、CDN 加速、请求压缩、异步处理、合并请求、长连接、静态资源等
编码实现层面:代码逻辑的分层、分模块、协程、资源复用(对象池,线程池等)、异步、IO 多路复用(异步非阻塞)、并发、无锁设计、设计模式等。
大多数情况下,多进程、多线程、多协程都可以大大提高我们的并发性能,尤其是是多协程。
在网络框架层面,现在一般成熟的后端系统框架(服务化框架)都是支持多线程、多协程的,因此对于网络框架这点,我们只要是引用相对成熟的服务化框架来实现我们的业务,基本上可以不用过多考虑和设计。
在业务层面,如果是 Go 语言,天然支持大量并发,并且创建 Go 的协程非常容易,一个 go 关键字就搞定,因此多协程那就非常容易了,Go 里面可以创建大量协程来提高我们的并发性能。如果是其他语言,我们尽可能的使用多协程、多线程去执行我们的业务逻辑。
在多线程、多协程的框架下,如果我们并发的线程(协程)之间访问共享资源,那么需要特别注意,要么通过加锁、要么通过无锁化设计,否则没有任何处理的访问共享资源会产生意想不到的结果。而加锁的设计,在并发较大的时候,如果锁的力度不合适,或者频繁的加锁解锁,又会使我们的性能严重下降。
为此,在追求高性能的时候,大家就比较推崇无锁化的设计。目前很多后台底层设计,为了避免共享资源的竞争,都采用了无锁化设计,特别是在底层框架上。无锁化主要有两种实现,无锁队列和原子操作。
为什么要说 数据序列化协议?因为我们的系统,要么就是各个后端微服务之间通过 RPC 做交互,要么就是通过 HTTP/TCP 协议和前端(终端)做交互,因此不可避免的需要我们进行网络数据传输。而数据,只有序列化后,才方便进行网络传输。
序列化就是将数据结构或对象转换成二进制串的过程,也就是编码的过程,序列化后,会把数据转换为二进制串,然后可以进行网络传输;反序列化就是在序列化过程中所生成的二进制串转换成数据结构或者对象的过程,将二进制转换为对象后业务才好进行后续的逻辑处理。
常见的序列化协议如下
常见的序列化协议的对比在网上有各种性能的对比,这里就不在贴相关截图了,只说结论:从性能上和使用广泛度上来看,后端服务之间现在一般推荐使用 PB。如果和前端交互,由于 HTTP 协议只能支持 JSON,因此一般只能 JSON。
池化技术是非常常见的一个提高性能的技术,池化的核心思想就是对资源进行复用,减少重复创建销毁所带来的开销。复用就是创建一个池子,然后再在这个池子里面对各种资源进行统一分配和调度,不是创建后就释放,而是统一放到池子里面来复用,这样可以减少重复创建和销毁,从而提高性能。而这个资源就包括我们编程中常见到如 线程资源、网络连接资源、内存资源,具体到对应的池化技术层面就是 线程池(协程池)、连接池、内存池等。
创建线程的开销是很大的,因此如果来一个请求就频创建一个线程、进程,那么请求的性能肯定不会太高。
连接池需要保证连接的可用性,就是这个连接和 MySQL、Redis 等存储是必须要定期发送数据来保证连接的,要不然会被断开。同时我们要针对已经失效(断开)的连接进行检测和摘除。
异步有两个层面的意思:
异步是相对同步而言的,同步就是要等待前面一个事情执行完毕才能继续执行,异步就是可以不用等待,可想而知,异步的性能要比同步好很多。
针对 IO 层面的异步调用,就是我们常说的 I/O 模型,有 阻塞、非阻塞、同步、异步这几种类型。在 Linux 操作系统内核中,内置了 5 种不同的 IO 交互模式,分别是阻塞 IO、非阻塞 IO、多路复用 IO、信号驱动 IO、异步 IO。
针对网络 IO 模型而言,Linux 下,使用最多性能较好的是同步非阻塞模型,具体代表是 AIO,而 windows 下的代表作 IOCP 则实现了真正的异步非阻塞 I/O。
业务逻辑层面的异步流程,就是指让我们的应用程序在业务逻辑上可以异步的执行。通常比较复杂的业务,都会有很多步骤流程,如果所有步骤都是同步的话,那么当这些步骤中有一步卡住,那么整个流程都会卡住,这样的流程显然性能不会很高。为此,在业内,我们如果想要提高性能,提高并发,那么基本上都会采用异步流程的方式。
举个实际的应用案例,针对 IM 系统的发送消息的这个场景,比如微信发送消息,那么当客户端发送的消息,服务端收到后,这个消息肯定要落地存储,这个发送的流程才能算完毕,但是,如果每条消息,服务端都真正存储到 DB 后再返回给客户端说已经正确收到,那么这个性能显然会很低,因为我们知道,写 DB 的性能是很低的,尤其是像微信这种每天有大量消息的 App。那么这个流程就可以异步化,服务端收到消息后,先把消息写入消息队列,写入队列成功就返回给客户端,然后异步流程去从消息队列里面消费数据然后落地存储到 DB 里面,这样性能就非常高了,因为消息队列的性能会很高。而比较低性能的操作都是异步处理。
并发流程,同样是针对我们上层的应用程序而言的,我们在处理业务逻辑的时候,尤其是相对负责的业务逻辑,一般下游都可能会有多个请求,或者说多个流程,如果依赖的下游多个请求之间没有强依赖关系,那么我们可以将这些请求的流程并发处理,这个是后端系统设计里面非常常见的优化手段。
通过并发的处理流程,可以将串行的叠加处理耗时优化为单个处理耗时,这样就大大的降低了整体耗时,举个例子,一个商品活动页面,渲染的数据包括 用户基本信息、用户活动积分、用户推荐商品列表。那么当收到这个用户的请求的时候,我们需要 查询用户的基本信息、用户的活动积分,还有用户的商品推荐,而这 3 个步骤完全是没有相互依赖关系的,因此,我们可以并发去分别查询,这样可以极大的减少耗时,从而提高我们的性能。
单机优化层面就是要尽量提升单机的性能,将单机的性能发挥到极致的其中一个关键点就是我们服务器采取的并发模型,然后在这个模型下,去设计好我们的服务器对连接的管理、对请求的处理流程。而这些就涉及到我们的多协程、多线程的进程模型和异步非阻塞、同步非阻塞的 IO 模型。
在具体实现细节上,针对连接的管理,要想提高性能,那么就要采用 IO 多路复用技术,可以参考I/O Multiplexing查看,I/O 多路复用技术的两个关键点在于:
基本上来说,异步 I/O 模型的发展技术是:select -> poll -> epoll -> aio -> libevent -> libuv。
而且现在大家比较熟悉和使用的最多的恐怕就是 epoll 和 aio ,尤其是 epoll 模型,基本是 Linux 后端系统下的大部分框架和软件都是采用 epoll 模型。
但是,需要特别强调的是,仅仅依靠 epoll 不是万能的,连接数太多的时候单进程的 epoll 也是不行的。
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、 池化技术都是有强关联的。这里也是一个知识相通的典型,我们所学的一些基础层面的知识点,在架构层面、模型层面都是有用武之地的。
架构设计层面:架构分层、业务分模块、集群(集中式、分布式)、缓存(多级缓存、本地缓存)、消息队列(异步、削峰)
整个系统想要有一个高性能,那么首先就需要有个合理的架构设计,这里需要根据一些架构设计原则,比如高内聚低耦合,职责单一等来去构建我们的架构。最有效的方式包括架构分层设计、业务分模块设计。
这么设计之后,在整体的系统性能优化上,后面就会有比较大的优化空间,从而不至于后面想要优化就根本无从下手,只能重构系统。
目前的互联网时代,我们基本上都是采用微服务来搭建我们的系统,而微服务化的必要条件就是要有一套服务化框架,这个服务化框架最核心的功能包括 RPC 请求和最基础的服务治理策略(服务注册和发现、负载均衡等)。
为此,这里服务化框架的性能就尤为重要,这里主要包括这个服务化框架里面实现:
数据序列化协议,一般有些采用 PB 协议,不管是从性能还是维护都是最优的。
数据压缩,一般采用 gzip 压缩,压缩后可以减少网络上的数据传输。
同步还是异步流程,如果是 Go 语言,那么可以来一个请求 go 一个协程来处理。
是否有相关连接池的能力。
其他的一些优化。
负载均衡系统是水平扩展的关键技术,通过负载均衡,相当于可以把流量分散到不同的机器的不同的服务实例里面,这样每个服务实例都可以承担一部分请求,从而可以提高我们的整体系统的性能。
对于负载均衡的方式,大都是在客户端发现模式(client-side) 来实现服务路和负载均衡,一般也都会支持常见的负载均衡策略,如随机,轮训,hash,权重,连接数【连接数越少,优先级越高】。
在后端系统设计里面,很多流程和请求并不要求实时处理,更不需要做到强一致,大部分情况下,我们只需要实现最终一致性就可以了。故而,我们通过队列,就可以使我们的系统能够实现异步处理逻辑、流程削峰、业务模块解耦、柔性事务等多种效果,从而可以完成最终一致性,并且能够极大的提高我们系统的性能。
我们常见的队列包括
到期后自动执行指定操作。
在指定时间之前自动执行某些动作
查询某个任务是否完成,未完成等待一定时间再次查询
回调通知,当回调失败时,等待后重试
分布式缓存的代表作有 Redis、Memcache。通过分布式缓存,我们可以不直接读数据库,而是读取缓存来获取数据,可以极大的提高我们读数据的性能。而一般的业务都是读多写少,因此,对我们的整体性能的提高是非常有效的手段,而且是必须的手段。
本地缓存可以从几个维度来看:
多级缓存是一个更为高级的缓存架构设计,比如最简单的模式可以是 本地缓存 + 分布式缓存这样形成一个多级缓存架构。
我们把全量要缓存的数据都放到分布式缓存里面,然后把一些热点的少量缓存放到本地缓存里面,这样大部分热点数据都能够从本地直接读取,而其他非热点的数据还是通过分布式缓存读取,这样可以极大的提高我们的性能,提高并发能力。
举个例子,电商系统里面,我们做一个活动页,活动页的前面 10 个商品是特卖商品,然后后面的其他商品就是常规商品,因为是活动页面,那么这个页面的访问肯定就会非常大。而活动页面的前 10 个商品,必然是用户首先进来页面就一定会看到的,而用户想要继续看其他商品,那么就需要在手机上手动上滑刷新一下。这个场景下,前面 10 个商品的访问量无疑是最大的,而用户手动上滑刷新后的请求就会少很多。为此,我们可以把全量商品都缓存在分布式缓存如 redis 里面,然后再在这个基础之上,把前面 10 个商品的信息缓存到本地,这样,当活动开始后,拉取的第一页 10 个商品数据,都是从本地缓存拉取的,本地读取性能会非常高,因为内存读取就行,完全不需要网络交互。
其他的模式,可以 本地缓存 + 二级分布式缓存 + 一级分布式缓存,也就是针对分布式缓存再做一层分级,这样每一级的缓存都能抗一部分的量,因此整体来看,能够对外提供的性能就足够高。
通过异步任务提前将接下来要大量访问的数据预热到我们缓存里面。这样当有请求的突峰的时候,可以从容应对。
除了 Redis、本地缓存这些,其他的一些 NoSQL 中,MongoDB、Elasticserach 也是常见的性能很高的组件,我们可以根据适用场景,合理选用。
比如我们在电商系统里面,我们针对商品的搜索、推荐都是采用 Elasticserach 来实现。
数据分区是把数据按一定的方式分成多个区(比如通过地理位置),不同的数据区来分担不同区的流量,这需要一个数据路由的中间件,但会导致跨库的 Join 和跨库的事务非常复杂。
将数据分布到多个分区有两种比较典型的方案:
一般来说,影响数据库最大的性能问题有两个,一个是对数据库的操作,一个是数据库中数据的大小。对于前者,我们需要从业务上来优化。一方面,简化业务,不要在数据库上做太多的关联查询,而对于一些更为复杂的用于做报表或是搜索的数据库操作,应该把其移到更适合的地方。比如,用 ElasticSearch 来做查询,用 Hadoop 或别的数据分析软件来做报表分析。对于后者,一般就是拆分。分库分表技术,有些地方也称为 Sharding、分片,通过分库分表可以提高我们的读写性能,
分库分表有垂直切分和水平切分两种:
分表包括垂直切分和水平切分,而分区只能起到水平切分的作用。
互联网系统大多数都是读多写少,因此读写分离可以帮助主库抗量,读写分离就是将读的请求量改为从库承担,写还是主库来承担。一般我们都是一主多从的架构,既可以抗量,又可以保证数据不丢。
针对业务场景而言,如果数据有冷热之分的话,可以将历史冷数据与当前热数据分开存储,这样可以减轻当前热数据的存储量,可以提高性能。
我们常见的存储系统比如 MySQL、Elasticserach 等都可以支持。
分布式数据库的基本思想是将原来集中式数据库中的数据分散存储到多个通过网络连接的数据存储节点上,以获取更大的存储容量和更高的并发访问量,从而提高我们的性能。现在传统的关系型数据库已经开始从集中式模型向分布式架构发展了。一般云服务厂商,都会提供分布式数据库的解决方案,比如腾讯云的 TDSQL MySQL 版,TDSQL for MySQL 是腾讯打造的一款分布式数据库产品,具备强一致高可用、全球部署架构、分布式水平扩展、高性能、企业级安全等特性,同时提供智能 DBA、自动化运营、监控告警等配套设施,为客户提供完整的分布式数据库解决方案。
基础建设层面,大体分为 3 大块:
在运维部署层面做好相关建设,是有助于提高我们系统的整体性能的。比如,我们可以通过容器化部署做到弹性伸缩,通过弹性伸缩的能力,可以使得我们的服务,在资源分配使用上,一直保持合理的 CPU、内存等资源的使用率。
我们从架构设计层面、编码实现层面按照高性能的解决方案和思路实现了我们系统之后,理论上,我们的系统性能不会太差,但是,具体我们的系统性能如何?是否存在可优化点?代码的实现是否有性能问题?我们的依赖服务是否存在性能问题?等等,这些对我们大部分人来说,如果没有一个合理的性能压测和分析,那么可能还是黑盒的。
因此,针对我们研发人员而言,在高性能架构设计方面的最后一个环节,就是进行性能测试优化,具体包括三个环节:
本文转载自微信公众号「 后端系统和架构」,作者「 AllenWu」