单服务器高性能模式:PPC与TPC

2024年 4月 12日 46.2k 0

高性能是每位程序员的追求。无论我们设计系统还是编写代码,都渴望达到最佳性能。但实现高性能是极为复杂的,因为诸如磁盘、操作系统、CPU、内存、缓存、网络、编程语言和架构等因素都可能影响系统性能。一个不当的 debug 日志,甚至可能将服务器的性能从每秒处理 30000 个事务降低到 8000 个;一个 tcp_nodelay 参数的设置,可能将响应时间从 2 毫秒延长到 40 毫秒。因此,实现高性能是一项极具挑战性的任务。软件系统开发的不同阶段都会对最终的性能产生影响。

站在架构师的角度,特别关注高性能架构的设计是至关重要的。高性能架构设计主要集中在两个方面:

  • 尽量提升单个服务器的性能,将其性能发挥到极致。
  • 如果单服务器无法满足性能需求,则设计服务器集群方案。
  • 除了上述两点,系统最终能否实现高性能还与具体的实现和编码有关。但架构设计是实现高性能的基础。如果架构设计不能达到高性能要求,那么后续的实现和编码优化也只能在有限的空间内发挥作用。可以形象地说,架构设计决定了系统性能的上限,而实现细节则决定了系统性能的下限。

    实现单服务器高性能的关键之一是选择合适的并发模型。并发模型涉及以下两个关键设计点:

  • 服务器如何管理连接。
  • 服务器如何处理请求。
  • 这两个设计点最终都与操作系统的 I/O 模型和进程模型相关。常见的 I/O 模型包括阻塞、非阻塞、同步和异步;而进程模型可以是单进程、多进程或多线程。

    PPC

    PPC,即 Process Per Connection,意味着每次有新连接时就会创建一个新的进程来专门处理该连接的请求。这是传统 UNIX 网络服务器常采用的模型。

    图片图片

    在这种模式下,父进程负责接受连接,并在接受到连接后通过“fork”创建一个子进程来处理连接的读写请求。子进程处理完请求后关闭连接。需要注意的是,在“fork”创建子进程后,父进程直接调用 close,看起来好像是关闭了连接,但实际上只是减少了连接的文件描述符引用计数。真正的关闭连接是在子进程调用 close 后,连接的文件描述符引用计数变为 0,操作系统才会真正关闭连接。

    PPC 模式实现简单,适用于连接数不多的情况,比如数据库服务器。在互联网兴起之前,对于普通的业务服务器,由于访问量和并发量相对较低,这种模式运作良好。世界上第一个 web 服务器 CERN httpd 就采用了这种模式。

    然而,随着互联网的发展,服务器的并发和访问量激增,PPC 模式的弊端也显现出来:

    fork 代价高:创建一个进程的代价很高,需要分配大量内核资源,将内存映像从父进程复制到子进程。即使现在的操作系统采用了 Copy on Write 技术,总体上创建进程的代价仍然较高。

    父子进程通信复杂:父进程“fork”子进程后,父子进程之间通信复杂,需要采用 IPC 进程通信方案。例如,子进程需要在关闭连接之前告知父进程处理了多少个请求,以支持父进程进行全局统计。

    并发连接数量有限:如果每个连接存活时间较长且新连接不断进来,进程数量会不断增加,导致操作系统进程调度和切换频繁,系统压力增大。因此,一般情况下,PPC 方案最多能处理的并发连接数量只有几百个。

    TPC

    TPC,即 Thread Per Connection,意味着每次有新连接时都会创建一个新线程专门处理该连接的请求。相比进程,线程更轻量级,创建线程的开销更小;同时,线程共享进程内存空间,线程间通信相对简单。因此,TPC 实际上是解决了或者减轻了 PPC 中 fork 代价高和父子进程通信复杂的问题。

    图片图片

    在TPC模式下,父进程负责接受连接,然后创建子线程来处理连接的读写请求,最后子线程关闭连接。与 PPC 不同的是,主进程无需手动关闭连接,因为子线程共享主进程的进程空间,连接的文件描述符没有被复制,只需一次 close 即可。

    尽管TPC解决了 fork 代价高和进程通信复杂的问题,但也带来了新的挑战:

    创建线程虽然比创建进程代价低,但在高并发情况下(如每秒上万连接),仍存在性能问题。

    虽然无需进程间通信,但线程间的互斥和共享带来了复杂性,容易导致死锁问题。

    多线程会出现互相影响的情况,某个线程异常可能导致整个进程退出(如内存越界)。

    除了引入新的问题,TPC 仍然面临 CPU 线程调度和切换的代价。因此,在并发连接几百个的场景下,更倾向于使用 PPC,因为它没有死锁风险,也不会受多进程相互影响,具有更高的稳定性。

    prethread

    TPC模式中,只有在连接进来时才创建新的线程来处理连接请求。尽管创建线程比创建进程更轻量级,但仍然存在一定的代价。为了解决这个问题,出现了prethread模式。

    图片图片

    类似于prefork,prethread模式会预先创建线程,然后开始接受用户请求。这样,当新连接进来时,就无需再创建线程,从而提升用户感知的速度和体验。

    由于多线程之间数据共享和通信更方便,prethread的实现方式比prefork更灵活。常见的实现方式包括:

    主进程accept连接,然后将连接交给某个线程处理。

    多个子线程尝试accept连接,只有一个线程accept成功。

    Apache服务器的MPM worker模式本质上就是一种prethread方案,但进行了改进。Apache首先创建多个进程,每个进程再创建多个线程。这样做的主要考虑是为了提高稳定性,即使某个子进程的某个线程异常退出,仍会有其他子进程继续提供服务,不会导致整个服务器崩溃。

    prethread理论上可以支持比prefork更多的并发连接。例如,Apache服务器的MPM worker模式默认支持400个并发处理线程(16个进程 × 25个线程)。

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论