如何构建一个高性能的服务器

目录

技术介绍

内存池

线程池

数据库连接池

协程

异步非阻塞I/O

根据需求选择服务器的模型架构

技术选择

同步/异步

内存池/线程池/数据库连接池

结语


关键词:内存池,线程池,协程,系统代价,异步I/O,I/O多路复用,非阻塞I/O

既然都要考虑高性能的服务器了,我认为在服务器硬件方面的CPU,内存,高速SSD,高速网络连接也需要进行配置,如此性能才能得到最大的提升。

未考虑:安全性相关功能,负载均衡,冗余备份,分布式技术

技术介绍

内存池

通常我们在编写程序的时候会使用 new delete 这些关键字来向操作系统申请内存,而这样造成的后果就是每次申请内存和释放内存的时候,都需要和操作系统的系统调用打交道,从堆中分配所需的内存。如果这样的操作太过频繁,就会造成大量的内存碎片进而降低内存的分配性能,甚至出现内存分配失败的情况。而内存池就是为了解决这个问题而产生的一种技术。

每一次进行内存分配,就会消耗一次分配内存的时间,设这个时间为 T,那么进行 n 次分配总共消耗的时间就是 nT;如果我们一开始就确定好我们可能需要多少内存,那么在最初的时候就分配好这样的一块内存区域,当我们需要内存的时候,直接从这块已经分配好的内存中使用即可,那么总共需要的分配时间仅仅只有 T。当 n 越大时,节约的时间就越多。

内存池技术其实便是由用户自己去进行内存的管理,而不是由操作系统来分配,避免了复杂的操作系统调度环境,转而只需要专心管好自己的一片小空间(相对而言)。

我们可以通过重载/替换new来完成基于内存池的内存分配,这样所有的小的频繁的内存申请都可以通过一个较小的内存池来统一管理。

在内存池中的申请空间在使用完毕以后(通过delete释放),内存池可以将这片使用完毕的内存初始化(也可以不初始化,交给用户来进行),然后放回未使用内存的链表中。

这样便可以通过两个链表来对内存池进行管理(一个已使用内存链表,一个未使用内存链表)。

在申请内存时,如果发现未使用内存链表内的内存长度不满足,也可以申请新的内存空间来放在未使用内存链表后。

或者拒绝内存的分配,重新申请一个池然后返回分配空间,这取决于内存池是否是可变长度/大小的。

不过内存池会有一个在多线程环境下的线程安全问题需要注意,不过也有较简单的解决方法,那便是给每一个线程一个自己的内存池来操作。如果需要两个线程都访问一个内存池,那便只能通过信号量/无锁/共享内存等方式来保证线程安全。

实现:

初始化:在内存池创建时,会预先分配一定数量的内存块,并将其放入池中。这些内存块可以是固定大小的,也可以是不同大小的。

分配内存:当需要使用内存时,直接从内存池中获取一个空闲的内存块。如果内存池中没有可用的内存块,可以根据需要进行扩容或者等待其他内存块释放。

使用内存:获取到内存块后,可以用于存储数据或者执行其他操作。

归还内存:使用完毕后,将内存块归还给内存池,以便重新使用。

内存池的好处是减少了内存分配和释放的次数,避免了内存碎片的产生,提高了内存分配和释放的效率,同时也减少了系统的负担。

线程池

在高性能的服务器中,多线程是必须的选项,我们需要动员服务器中的所有资源,不能浪费服务器的性能。

既然我们需要使用多线程,那么在服务器中便会不断的创建并销毁进程,这时,我们便会需要一个技术,帮助我们减小在创建/销毁线程时的性能消耗,同时对这些线程进行管理,合理的控制这些线程的数量,防止多余的性能消耗。

线程通过于预先创建一定数量的线程,并将其放入池中。当有任务或请求到达时,直接从线程池中获取线程执行,执行完毕后再归还给池。通过使用线程池,可以避免频繁的线程创建和销毁过程,提高了服务器的并发处理能力和响应速度。

线程池的主要优势在于以下几点:

减少线程创建和销毁开销:线程的创建和销毁是一项开销较大的操作。在高性能服务器中,如果每个请求都创建一个新的线程来处理,会消耗大量的系统资源和时间。而使用线程池,可以预先创建一定数量的线程,并将其放入池中,这样可以避免频繁的线程创建和销毁,减少了系统开销。

提高并发处理能力:线程池可以管理和控制线程的数量。通过限制线程池中的线程数量,可以控制系统的并发度,防止系统负载过高,保持系统的稳定性。同时,线程池可以根据系统负载情况动态调整线程的数量,以适应不同的并发压力。

提高响应速度:线程池中的线程可以立即响应任务或请求,无需等待线程创建的时间。这样可以减少任务或请求的等待时间,提高服务器的响应速度。

提高资源利用率:线程池中的线程可以重复利用,避免了频繁的线程创建和销毁过程。这样可以减少系统资源的浪费,提高资源的利用率。

优化系统负载:通过合理配置线程池的大小和线程数量,可以避免系统负载过高或过低的情况。在高性能服务器中,合理的负载均衡是非常重要的,可以提高系统的整体性能和效率。

实现通常包括以下几个关键组件:

任务队列:用于存储待执行的任务。当线程池中的线程空闲时,会从任务队列中取出任务进行执行。

线程管理器:用于创建和销毁线程,以及监控线程池的状态。线程管理器会根据需要动态地创建新线程或销毁空闲的线程,以保持线程池中的线程数量在合适的范围内。

线程工厂:用于创建线程的工厂类。线程工厂可以自定义线程的属性,如线程名、优先级等。

执行器:用于提交任务到线程池。执行器可以根据需要将任务添加到任务队列中,并通知线程管理器有新任务可执行。

线程池的实现一般遵循以下步骤:

初始化线程池:根据需求设置线程池的属性,如核心线程数、最大线程数、任务队列大小等。

创建线程管理器:根据需求创建线程管理器,用于创建和销毁线程,并监控线程池的状态。

创建任务队列:创建一个用于存储待执行任务的任务队列。

创建线程工厂:根据需求创建线程工厂,用于创建线程。

创建执行器:创建一个执行器,用于提交任务到线程池。

提交任务:使用执行器将任务提交到线程池。执行器会将任务添加到任务队列中,并通知线程管理器有新任务可执行。

线程执行任务:当线程池中有空闲线程时,线程管理器会从任务队列中取出任务,并将任务分配给空闲线程执行。

线程返回结果:线程执行完任务后,可以返回任务的执行结果给调用者。

销毁线程池:当不再需要线程池时,可以销毁线程池,释放资源。

以上是线程池的基本实现步骤,具体的实现方式可以根据编程语言和框架的不同而有所差异。

注:多线程池只能便于管理,对于性能的提升没有大的帮助。

数据库连接池

通过预先创建一定数量的数据库连接,并将其放入池中。当需要与数据库交互时,直接从连接池中获取连接,使用完毕后再归还给池。通过使用数据库连接池,可以避免频繁的数据库连接和断开操作,提高了数据库访问的效率和响应速度。

数据库连接池的主要优点如下:

资源管理:数据库连接池可以管理数据库连接的数量和资源的分配,避免了资源浪费和系统过载的问题。

提高响应速度:连接池中的连接可以立即响应数据库操作,无需等待连接创建的时间,提高了数据库访问的响应速度。

控制并发度:连接池可以限制并发执行的连接数量,防止数据库负载过高,保持数据库的稳定性。

重复利用:连接池中的连接可以重复使用,避免了频繁的连接和断开操作,提高了数据库访问的效率。

协程

协程是一种轻量级的线程,也被称为用户级线程或纤程。与传统的线程相比,协程更加高效和灵活。

在传统的多线程编程中,线程的切换由操作系统控制,切换的开销较大。而协程则是由程序员自己控制,切换的开销很小。这是因为协程在执行过程中可以主动让出CPU,而不是等待操作系统的调度。

协程可以看作是一种特殊的函数,它可以在执行过程中暂停,并在需要时恢复执行。这使得程序可以更加方便地处理并发任务和异步操作。通过使用协程,程序可以将复杂的异步操作拆分成多个简单的协程,使得代码更加清晰和易于维护。

协程通常使用一个调度器来管理多个协程的执行。调度器负责调度协程的执行顺序,以及在协程暂停时将控制权转移到其他协程。调度器可以根据需要进行协程的切换,以实现并发执行。

总结起来,协程是一种高效且灵活的并发编程模型,通过自主控制执行顺序和切换时机,可以简化复杂的异步操作,提高程序的性能和可维护性。

异步非阻塞I/O

异步I/O是一种支持数据传输完成前可以允许其他计算的I/O,换句话说调用该I/O的方法不会阻塞线程,从而方便线程去执行其他计算操作。

采用异步非阻塞I/O的原因:

如果某线程使用同步阻塞I/O,该线程会因为阻塞而挂起,在挂起的期间无法执行其他CPU计算,假如一个服务处理请求的线程池的数量是N,那么极端情况下N个线程因为同步阻塞I/O而挂起(通常N不会很大,否则线程切换成本较高),该服务网卡内核队列堆积的请求就会越来越多最终服务需要处理的其他请求都会因为客户端判断超时而失败(这是一个典型的同步I/O事故现场)。

服务器的CPU资源是非常宝贵的,如果线程都因为I/O等待而挂起,最终可执行资源必然减少,CPU利用率上不去,实际上就是提高了处理请求的成本。只有充分异步化的I/O才能发挥CPU的价值,在不升级硬件的前提下提高系统吞吐量,可以充分的利用CPU的资源。

根据需求选择服务器的模型架构

在设计和搭建服务器架构时,选择适合的模型是至关重要的。不同的需求和应用场景需要不同的服务器模型架构来满足性能、可扩展性和可维护性等方面的要求。

分析需求:首先,需要对需求进行全面的分析。了解应用程序的性质和目标,例如高并发、I/O密集型还是计算密集型。还需要考虑预期的用户量、数据量、请求类型和响应时间等因素。这些信息将有助于确定所需的性能和可扩展性。

了解不同的服务器模型架构:根据需求,了解各种常见的服务器模型架构。例如,Reactor模型、Proactor模型、Actor模型。了解每个模型的工作原理、适用场景和优缺点:

  1. Reactor模型:基于事件驱动的模型,适合高并发的场景。通过一个事件循环监听和分发IO事件,使用回调函数处理事件的读写操作。适用于大量的并发连接和请求,如网络服务器、消息队列等。
  2. Proactor模型:适用于IO密集型的场景,提高IO操作的效率。通过异步IO操作的主动性,将IO操作的完成和结果通知给应用程序。适合处理大量的IO操作,如数据库访问、文件操作等。
  3. Actor模型:适用于分布式和并发系统,能够实现高度并发和可扩展性。将系统中的实体抽象为Actor,通过消息传递进行通信和协作。适合复杂的并发和分布式系统,如游戏引擎、通信系统等。

考虑并发性和可扩展性:对于高并发场景,Reactor模型和Actor模型可能是更好的选择,它们能够处理大量的并发连接和请求。这些模型可以通过使用多线程或线程池来实现并发处理,从而提高系统的吞吐量和响应能力。而对于I/O密集型场景,Proactor模型可能更适合,它能够提高I/O操作的效率,通过异步I/O操作避免阻塞。

考虑异步操作和事件驱动:如果应用程序需要处理大量的异步操作和事件,可以考虑使用支持异步I/O和事件驱动的服务器模型。这样可以提高系统的响应性和吞吐量。异步操作能够充分利用资源,并减少等待时间。事件驱动的模型可以将多个并发操作整合在一个事件循环中,从而更高效地处理请求。

考虑可维护性和开发复杂度:除了性能和可扩展性,还需要考虑服务器模型的可维护性和开发复杂度。选择一个易于理解、测试和维护的模型,能够降低开发和维护的难度。一些模型可能需要更多的编程技巧和复杂的代码结构,而另一些模型可能提供更简单和直观的开发体验。

综合考虑并选择最合适的模型:根据需求、性能、可扩展性、可维护性和开发复杂度等因素,综合考虑并选择最合适的服务器模型架构。在做出决策之前,可以进行原型实验或模拟测试来验证选择的模型是否能够满足需求。

不断优化和调整:一旦选择了服务器模型架构,需要持续进行优化和调整。根据实际情况调整参数、改进代码和架构,以进一步提高性能和可扩展性。监控和分析系统的性能指标,及时进行调整和优化,确保服务器能够在实际应用中发挥最佳性能。

经过这些步骤的思考,我们便可以确定我们需要使用的技术栈与服务器模型,选择适合的服务器模型架构是设计和搭建高性能、可扩展和可维护的服务器系统的关键。通过分析需求、研究不同模型、综合考虑各种因素,并进行实验和测试,可以选择最合适的服务器模型架构。持续的优化和调整将确保服务器能够在实际应用中发挥最佳性能。

通过理解需求、选择合适的服务器模型架构,我们可以构建出满足性能、可扩展性和可维护性要求的服务器系统,为应用程序的成功提供坚实的基础。

在设计和搭建服务器架构时,我们必须始终关注需求,并根据实际情况进行调整和优化,以确保服务器能够稳定高效地运行。(这时候需要从软件工程/软件需求的角度进行思考)

技术选择

同步/异步

并发编程是为了“同时”执行多个程序,如果是计算密集的任务,并发反而没有优势,会因为任务切换降低效率。而对于I/O密集的任务,比如经常读写文件,访问数据库等由于I/O操作远没有CPU速度快,让程序阻塞于I/O操作将浪费CPU资源。

并发模式指的是I/O处理单元和多个逻辑单元协调完成任务的方法。主要有两种:

半同步/半异步模式

同步指的是由应用程序完成读写,异步是内核直接完成,然后告诉应用程序这一完成事件。

而在并发模式中,同步指的是程序完全按照代码序列的顺序执行,异步指的是程序执行需要由系统事件驱动,系统事件包括中断,信号等。

显然,异步进程执行效率高,实时性强,但是结构复杂,不适合调试扩展,且不适合大量的并发。而服务器这种既需要实时性,又需要满足多个用户请求,所以我们需要结合二者的优点:

同步线程用来处理客户逻辑,异步线程用来处理I/O事件

领导者/追随者模式

这是多个工作线程轮流获得事件源集合,轮流监听、分发并处理事件的一种模式。

在任意时刻,程序只有一个领导者线程,用来监听I/O事件,其他线程都是追随者,休眠在线程池中等待成为新的领导者。如果当前领导者监听到了I/O事件,则首先从线程池中选出新的领导者,自己来处理I/O事件,这样就实现了并发。(我第一次看到这个设计时是非常佩服的)。

内存池/线程池/数据库连接池

我认为一个高效的服务器,这三者是一定会使用的技术且十分重要的部分,只是在具体模型实现的过程中,这几个技术使用的位置或者说实现的方式会有些许的变化罢了。

内存池

在高并发的服务器应用中,频繁的内存分配和释放会导致性能问题。内存池通过预分配一块连续的内存区域,将内存管理的开销降到最低,减少内存碎片,提高内存分配的效率。(具体实现可以使用QUEUE的中控器思想或者直接使用链表管理)

线程池

在高并发的服务器应用中,频繁地创建和销毁线程会消耗大量的系统资源,并且线程的创建和销毁也会带来一定的开销。线程池通过预先创建一组线程,并重复利用它们来处理请求,减少线程创建和销毁的开销,提高线程的复用性,从而提高服务器的响应能力和性能。(实现较为简单,但是如果需要进行模板编程难度将会提升不少)

数据库连接池

服务器应用中,频繁地创建和关闭数据库连接会导致性能问题。数据库连接的创建和关闭是一项昂贵的操作,会消耗大量的系统资源。数据库连接池通过预先创建一组数据库连接,并重复利用它们来处理数据库操作,减少连接的创建和关闭开销,提高数据库连接的复用性,从而提高服务器与数据库之间的性能和响应能力。

它们可以提高服务器的资源利用率,减少资源的浪费和开销,提高服务器的性能和响应能力。通过合理使用这些池化技术,可以有效地管理和复用内存、线程和数据库连接,从而构建出高效的服务器系统。

结语

这些便是我认为构建一个高性能的服务器所必须的东西了,当然这其中一个优秀的程序员是必不可少的,低效的代码很可能会将好不容易提升的性能浪费掉。不够既然选择了高效的服务器来实现,这其中的提升,想必也够一个程序员提升不少了。

不过笔者还没有实实在在的编写完成过一个高性能的服务器,也只有使用线程池/内存池加上阻塞IO来实现了一部分。

说来很不好意思,在写服务器的时候,不清楚有各种IO多路复用,epoll,反应堆模型这种网络编程模型,蒙头瞎写代码,花了不少精力,但是我个人还是比较庆幸的,在看到各种编程模型后可以更好的理解其中的原理与概念,很多原理在没有实际动手前,我想我是不会知其所以然的。

有错误还请指正,谢谢。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

nameless_233

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值